From 652ed1b477f53d9d040c589459ad21da9dd2568e Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 16 May 2026 02:57:30 +0300 Subject: [PATCH] feat: add ID generators, fix 6 forum-discovered deficiencies ID Generators: - AUTO_INCREMENT for INTEGER columns - SERIAL / BIGSERIAL as syntactic sugar - gen_random_uuid() / uuid() for UUID v4 generation - nextval() / currval() for sequence support - snowflake_id(node_id) for distributed 64-bit IDs - RETURNING clause for INSERT Deficiency Fixes: - Comma join: FROM t1, t2 now works as implicit CROSS JOIN - DEFAULT in restoreSchema: defaults survive server restart - Duplicate column names: t.id instead of id in joins - Empty result sets: always send column metadata to client - GROUP BY non-agg columns: return first row value instead of empty - Aggregate column names: count(*) instead of count() Bug fix: PK uniqueness check now uses correct key format --- src/barabadb/core/server.nim | 10 +- src/barabadb/query/ast.nim | 1 + src/barabadb/query/executor.nim | 148 +++++++++++- src/barabadb/query/lexer.nim | 8 + src/barabadb/query/parser.nim | 31 ++- tests/join_tests.nim | 12 +- tests/test_all.nim | 402 ++++++++++++++++++++++++++++++++ 7 files changed, 591 insertions(+), 21 deletions(-) diff --git a/src/barabadb/core/server.nim b/src/barabadb/core/server.nim index 55647d9..0cecde9 100644 --- a/src/barabadb/core/server.nim +++ b/src/barabadb/core/server.nim @@ -478,9 +478,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} slowQueryLog(slowLog, queryStr, durationMs, clientId) if success: - if result.rows.len > 0: - let dataMsg = serializeResult(result, header.requestId) - await client.send(cast[string](dataMsg)) + let dataMsg = serializeResult(result, header.requestId) + await client.send(cast[string](dataMsg)) let completeMsg = serializeComplete(result.affectedRows, header.requestId) await client.send(cast[string](completeMsg)) else: @@ -499,9 +498,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} slowQueryLog(slowLog, queryStr, durationMs, clientId) if success: - if result.rows.len > 0: - let dataMsg = serializeResult(result, header.requestId) - await client.send(cast[string](dataMsg)) + let dataMsg = serializeResult(result, header.requestId) + await client.send(cast[string](dataMsg)) let completeMsg = serializeComplete(result.affectedRows, header.requestId) await client.send(cast[string](completeMsg)) else: diff --git a/src/barabadb/query/ast.nim b/src/barabadb/query/ast.nim index e4b9609..ade9e7b 100644 --- a/src/barabadb/query/ast.nim +++ b/src/barabadb/query/ast.nim @@ -250,6 +250,7 @@ type cdName*: string cdType*: string cdConstraints*: seq[Node] + cdAutoIncrement*: bool of nkConstraintDef: cstName*: string cstType*: string diff --git a/src/barabadb/query/executor.nim b/src/barabadb/query/executor.nim index 29de9aa..2fd0e95 100644 --- a/src/barabadb/query/executor.nim +++ b/src/barabadb/query/executor.nim @@ -9,6 +9,8 @@ import checksums/sha2 import std/math import std/times import std/json +import std/random +import std/monotimes import lexer as qlex import parser as qpar import ast @@ -70,6 +72,8 @@ type currentUser*: string currentRole*: string sessionVars*: Table[string, string] # session-scoped key/value variables + autoIncCounters*: Table[string, int64] # table.col -> next auto-increment value + sequences*: Table[string, int64] # sequence name -> current value MigrationRecord* = object name*: string @@ -112,6 +116,7 @@ type defaultVal*: string fkTable*: string fkColumn*: string + autoIncrement*: bool Row* = Table[string, string] @@ -138,6 +143,7 @@ proc errResult*(msg: string): ExecResult = # Context management # ---------------------------------------------------------------------- +proc evalNodeToString(node: Node): string proc restoreSchema(ctx: ExecutionContext) proc newExecutionContext*(db: LSMTree): ExecutionContext = @@ -151,6 +157,8 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext = policies: initTable[string, seq[PolicyDef]](), currentUser: "", currentRole: "", sessionVars: initTable[string, string](), + autoIncCounters: initTable[string, int64](), + sequences: initTable[string, int64](), onChange: nil) restoreSchema(result) @@ -281,6 +289,7 @@ proc restoreSchema(ctx: ExecutionContext) = for col in stmt.crtColumns: if col.kind == nkColumnDef: var colDef = ColumnDef(name: col.cdName, colType: col.cdType) + colDef.autoIncrement = col.cdAutoIncrement for cst in col.cdConstraints: if cst.kind == nkConstraintDef: case cst.cstType @@ -292,6 +301,12 @@ proc restoreSchema(ctx: ExecutionContext) = of "unique": colDef.isUnique = true ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]() + of "default": + if cst.cstDefault != nil: + colDef.defaultVal = evalNodeToString(cst.cstDefault) + of "fkey": + colDef.fkTable = cst.cstRefTable + colDef.fkColumn = if cst.cstRefColumns.len > 0: cst.cstRefColumns[0] else: "" else: discard tbl.columns.add(colDef) ctx.tables[stmt.crtName] = tbl @@ -326,6 +341,8 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext = txnManager: ctx.txnManager, currentUser: ctx.currentUser, currentRole: ctx.currentRole, sessionVars: ctx.sessionVars, + autoIncCounters: ctx.autoIncCounters, + sequences: ctx.sequences, pendingTxn: nil, onChange: ctx.onChange) # ---------------------------------------------------------------------- @@ -846,6 +863,49 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = return $now().format("yyyy-MM-dd HH:mm:ss") of "now": return $now().format("yyyy-MM-dd HH:mm:ss") + of "gen_random_uuid", "uuid": + # Generate UUID v4 + var uuidStr = "" + for i in 0..<36: + if i in @[8, 13, 18, 23]: + uuidStr.add('-') + elif i == 14: + uuidStr.add('4') + elif i == 19: + uuidStr.add(['8', '9', 'a', 'b'][rand(3)]) + else: + uuidStr.add("0123456789abcdef"[rand(15)]) + return uuidStr + of "nextval": + if expr.irFuncArgs.len < 1: + return "0" + if ctx == nil: return "0" + let seqName = evalExpr(expr.irFuncArgs[0], row, ctx) + var val: int64 = 0 + if seqName in ctx.sequences: + val = ctx.sequences[seqName] + val += 1 + ctx.sequences[seqName] = val + return $val + of "currval": + if expr.irFuncArgs.len < 1: + return "0" + if ctx == nil: return "0" + let seqName = evalExpr(expr.irFuncArgs[0], row, ctx) + if seqName in ctx.sequences: + return $ctx.sequences[seqName] + return "0" + of "snowflake_id": + # Snowflake ID: timestamp_ms(41 bits) | node_id(10 bits) | sequence(12 bits) + var nodeId: int64 = 0 + if expr.irFuncArgs.len > 0: + try: nodeId = parseInt(evalExpr(expr.irFuncArgs[0], row, ctx)) + except: nodeId = 0 + nodeId = nodeId and 0x3FF # 10 bits + let ts = int64(epochTime() * 1000) and 0x1FFFFFFFFFF # 41 bits + var snowSeq = int64(getMonoTime().ticks() and 0xFFF) # 12 bits from monotonic + let snowflakeId = (ts shl 22) or (nodeId shl 12) or snowSeq + return $snowflakeId of "strftime": if expr.irFuncArgs.len >= 2: let fmt = evalExpr(expr.irFuncArgs[0], row, ctx) @@ -1233,7 +1293,7 @@ proc fireTriggers*(ctx: ExecutionContext, tableName: string, timing: string, eve discard executeQuery(ctx, astNode) proc validateConstraints*(ctx: ExecutionContext, tableName: string, - fields: seq[string], values: seq[seq[string]]): (bool, string) = + fields: seq[string], values: seq[seq[string]], skipPkCheck: bool = false): (bool, string) = let tbl = ctx.getTableDef(tableName) for rowIdx, rowVals in values: @@ -1268,13 +1328,14 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string, if not found: return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn) - # PK uniqueness - if tbl.pkColumns.len > 0: + # PK uniqueness (skip during UPDATE — PK shouldn't change) + if not skipPkCheck and tbl.pkColumns.len > 0: var pkVals: seq[string] = @[] for pkCol in tbl.pkColumns: pkVals.add(getValue(rowVals, fields, pkCol)) let pkStr = pkVals.join("|") - let pkKey = tableName & "." & pkStr + # Check with field=value format (as stored by execInsert) + var pkKey = tableName & "." & tbl.pkColumns[0] & "=" & escapeRowVal(pkVals[0]) let (exists, _) = ctx.db.get(pkKey) if exists: return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'") @@ -1629,9 +1690,12 @@ proc lowerSelect*(node: Node): IRPlan = elif e.kind == nkIdent: projectPlan.projectAliases.add(e.identName) elif e.kind == nkPath and e.pathParts.len > 0: - projectPlan.projectAliases.add(e.pathParts[^1]) + projectPlan.projectAliases.add(e.pathParts.join(".")) elif e.kind == nkFuncCall: - projectPlan.projectAliases.add(e.funcName & "()") + var aliasArgs: seq[string] = @[] + for arg in e.funcArgs: + aliasArgs.add(exprToSql(arg)) + projectPlan.projectAliases.add(e.funcName & "(" & aliasArgs.join(", ") & ")") elif e.kind == nkStar: projectPlan.projectAliases.add("*") else: @@ -2067,9 +2131,15 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = groups[groupKey].add(row) for gk, groupRows in groups: var aggRow: Table[string, string] + # Populate GROUP BY key columns for gkExpr in gkeys: if gkExpr.kind == irekField and gkExpr.fieldPath.len > 0: aggRow[gkExpr.fieldPath[^1]] = evalExpr(gkExpr, groupRows[0], ctx) + # Populate non-aggregated columns from first row in group + if groupRows.len > 0: + for k, v in groupRows[0]: + if not k.startsWith("$") and k notin aggRow: + aggRow[k] = v # Compute each aggregate expression for aggExpr in plan.groupAggs: let aggKey = "$agg_" & $aggExpr.aggOp & "_" & $plan.groupAggs.find(aggExpr) @@ -3004,8 +3074,35 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] for col in tbl.columns: fields.add(col.name) let tbl = ctx.getTableDef(stmt.insTarget) + + # Auto-increment: populate missing auto-increment columns var mutableFields = fields var mutableValues = values + for col in tbl.columns: + if col.autoIncrement and col.name notin mutableFields: + let counterKey = stmt.insTarget & "." & col.name + var nextVal: int64 = 1 + if counterKey in ctx.autoIncCounters: + nextVal = ctx.autoIncCounters[counterKey] + ctx.autoIncCounters[counterKey] = nextVal + int64(mutableValues.len) + # Insert at position 0 so it becomes the primary storage key + mutableFields.insert(col.name, 0) + for i in 0..= 0: + for rowVals in mutableValues.mitems: + if idx < rowVals.len: + let providedVal = rowVals[idx] + try: + let intVal = parseInt(providedVal) + let counterKey = stmt.insTarget & "." & col.name + if counterKey notin ctx.autoIncCounters or intVal >= ctx.autoIncCounters[counterKey]: + ctx.autoIncCounters[counterKey] = intVal + 1 + except: discard + applyDefaultValues(tbl, mutableFields, mutableValues) let (valid, errMsg) = validateConstraints(ctx, stmt.insTarget, mutableFields, mutableValues) @@ -3028,6 +3125,41 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] if ctx.onChange != nil: for i in 0.. 0 and mutableValues.len > 0: + var returnRows: seq[Row] = @[] + var returnCols: seq[string] = @[] + for retExpr in stmt.insReturning: + if retExpr.kind == nkIdent: + returnCols.add(retExpr.identName) + elif retExpr.kind == nkStar: + returnCols.add("*") + elif retExpr.exprAlias.len > 0: + returnCols.add(retExpr.exprAlias) + else: + returnCols.add("col" & $returnCols.len) + for rowVals in mutableValues: + var rowMap = initTable[string, string]() + for i, f in mutableFields: + if i < rowVals.len: + rowMap[f] = rowVals[i] + var returnRow = initTable[string, string]() + for i, retExpr in stmt.insReturning: + let ir = lowerExpr(retExpr) + let val = evalExpr(ir, rowMap, ctx) + if returnCols[i] == "*": + for k, v in rowMap: + returnRow[k] = v + else: + returnRow[returnCols[i]] = val + returnRows.add(returnRow) + if returnCols.contains("*"): + var expandedCols: seq[string] = @[] + for c in tbl.columns: expandedCols.add(c.name) + return okResult(returnRows, expandedCols, affected=count) + return okResult(returnRows, returnCols, affected=count) + return okResult(affected=count, kvPairs=kvPairs) of nkUpdate: @@ -3068,7 +3200,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] updValues.add(row[col.name]) else: updValues.add("") - let (valid, errMsg) = validateConstraints(ctx, stmt.updTarget, updFields, @[updValues]) + let (valid, errMsg) = validateConstraints(ctx, stmt.updTarget, updFields, @[updValues], skipPkCheck = true) if not valid: return errResult(errMsg) # Fire BEFORE UPDATE triggers var oldRow = row @@ -3208,6 +3340,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] for col in stmt.crtColumns: if col.kind == nkColumnDef: var colDef = ColumnDef(name: col.cdName, colType: col.cdType) + colDef.autoIncrement = col.cdAutoIncrement for cst in col.cdConstraints: if cst.kind == nkConstraintDef: case cst.cstType @@ -3239,6 +3372,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] for col in tbl.columns: var parts = @[col.name, col.colType] if col.isPk: parts.add("PRIMARY KEY") + if col.autoIncrement: parts.add("AUTO_INCREMENT") if col.isNotNull: parts.add("NOT NULL") if col.isUnique: parts.add("UNIQUE") if col.defaultVal.len > 0: parts.add("DEFAULT '" & col.defaultVal & "'") diff --git a/src/barabadb/query/lexer.nim b/src/barabadb/query/lexer.nim index 2db2534..6c30c44 100644 --- a/src/barabadb/query/lexer.nim +++ b/src/barabadb/query/lexer.nim @@ -158,6 +158,12 @@ type tkDfs tkPath + tkAutoIncrement + tkSequence + tkNextval + tkCurrval + tkUuid + # Window functions tkOver tkPartition @@ -389,6 +395,8 @@ const keywords*: Table[string, TokenKind] = { "first_value": tkFirstValue, "last_value": tkLastValue, "ntile": tkNtile, + "auto_increment": tkAutoIncrement, + "sequence": tkSequence, }.toTable proc newLexer*(input: string): Lexer = diff --git a/src/barabadb/query/parser.nim b/src/barabadb/query/parser.nim index 85c84bb..09a9d82 100644 --- a/src/barabadb/query/parser.nim +++ b/src/barabadb/query/parser.nim @@ -526,7 +526,23 @@ proc parseSelect(p: var Parser): Node = elif p.peek().kind == tkIdent: alias = p.advance().value result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value, - fromAlias: alias, line: tableTok.line, col: tableTok.col) + fromAlias: alias, line: tableTok.line, col: tableTok.col) + + # Comma join: FROM t1, t2 → implicit CROSS JOIN + while p.peek().kind == tkComma: + discard p.advance() + let nextTableTok = p.expect(tkIdent) + var nextAlias = "" + if p.match(tkAs): + nextAlias = p.expect(tkIdent).value + elif p.peek().kind == tkIdent: + nextAlias = p.advance().value + let joinNode = Node(kind: nkJoin, joinKind: jkCross, + joinTarget: Node(kind: nkFrom, fromTable: nextTableTok.value, + fromAlias: nextAlias, line: nextTableTok.line, col: nextTableTok.col), + joinOn: nil, joinAlias: nextAlias, joinLateral: false, + line: nextTableTok.line, col: nextTableTok.col) + result.selJoins.add(joinNode) # Parse PIVOT / UNPIVOT after FROM source if p.peek().kind == tkPivot: @@ -1006,13 +1022,24 @@ proc parseCreateTable(p: var Parser): Node = let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType) colDef.cdConstraints = @[] + # SERIAL / BIGSERIAL imply AUTO_INCREMENT + if colType in @["SERIAL", "BIGSERIAL"]: + colDef.cdAutoIncrement = true + if colType == "SERIAL": + colDef.cdType = "INTEGER" + else: + colDef.cdType = "BIGINT" + # Parse column constraints - while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences}: + while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences, tkAutoIncrement}: let cst = Node(kind: nkConstraintDef) cst.cstColumns = @[colName]; cst.cstRefColumns = @[] if p.match(tkPrimary): discard p.expect(tkKey) cst.cstType = "pkey" + elif p.match(tkAutoIncrement): + colDef.cdAutoIncrement = true + continue elif p.match(tkNot): discard p.expect(tkNull) cst.cstType = "notnull" diff --git a/tests/join_tests.nim b/tests/join_tests.nim index 46ad440..601fce6 100644 --- a/tests/join_tests.nim +++ b/tests/join_tests.nim @@ -67,8 +67,8 @@ suite "JOIN execution": test "aliased column projection": let r = execSql(ctx, "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id") check r.rows.len == 2 - check r.rows[0]["name"] == "Alice" - check r.rows[0]["total"] == "99.5" + check r.rows[0]["u.name"] == "Alice" + check r.rows[0]["o.total"] == "99.5" check "id" notin r.rows[0] test "count after FULL JOIN": @@ -84,8 +84,8 @@ suite "JOIN execution": let r = execSql(ctx, "SELECT u.name, recent.total FROM users u JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id ORDER BY o.total DESC LIMIT 1) AS recent ON 1=1") check r.rows.len == 1 - check r.rows[0]["name"] == "Alice" - check r.rows[0]["total"] == "99.5" + check r.rows[0]["u.name"] == "Alice" + check r.rows[0]["recent.total"] == "99.5" test "LATERAL JOIN returns no rows when subquery empty": let r = execSql(ctx, @@ -99,8 +99,8 @@ suite "JOIN execution": # Alice has match (99.5), Bob has no orders -> NULL var foundBob = false for row in r.rows: - if row["name"] == "Bob": - check row["total"] == "" + if row["u.name"] == "Bob": + check row["x.total"] == "" foundBob = true check foundBob diff --git a/tests/test_all.nim b/tests/test_all.nim index 6199b57..9194249 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -3098,3 +3098,405 @@ suite "Vector SQL Integration": check r.rows.len == 2 check r.rows[0]["dist"] == "0.0" check r.rows[1]["dist"] == "1.7320508075688772" + +suite "Nested Subqueries — Advanced": + var db: LSMTree + var ctx: qexec.ExecutionContext + var tmpDir: string + + setup: + tmpDir = getTempDir() / "baradb_nested_test_" & $getMonoTime().ticks + db = newLSMTree(tmpDir) + ctx = qexec.newExecutionContext(db) + discard qexec.executeQuery(ctx, parse("CREATE TABLE departments (id INT PRIMARY KEY, name TEXT, budget INT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO departments (id, name, budget) VALUES (1, 'Engineering', 500000)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO departments (id, name, budget) VALUES (2, 'Sales', 300000)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO departments (id, name, budget) VALUES (3, 'Marketing', 200000)")) + discard qexec.executeQuery(ctx, parse("CREATE TABLE employees (id INT PRIMARY KEY, name TEXT, dept_id INT, salary INT, hire_date TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (1, 'Alice', 1, 90000, '2020-01-15')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (2, 'Bob', 1, 80000, '2021-03-20')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (3, 'Charlie', 2, 70000, '2019-06-10')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (4, 'Diana', 2, 75000, '2022-09-01')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (5, 'Eve', 3, 60000, '2023-01-05')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, dept_id, salary, hire_date) VALUES (6, 'Frank', 1, 95000, '2018-11-30')")) + discard qexec.executeQuery(ctx, parse("CREATE TABLE projects (id INT PRIMARY KEY, name TEXT, dept_id INT, budget INT, proj_status TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO projects (id, name, dept_id, budget, proj_status) VALUES (1, 'Alpha', 1, 100000, 'active')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO projects (id, name, dept_id, budget, proj_status) VALUES (2, 'Beta', 1, 150000, 'completed')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO projects (id, name, dept_id, budget, proj_status) VALUES (3, 'Gamma', 2, 80000, 'active')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO projects (id, name, dept_id, budget, proj_status) VALUES (4, 'Delta', 3, 50000, 'active')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO projects (id, name, dept_id, budget, proj_status) VALUES (5, 'Epsilon', 1, 200000, 'planned')")) + + teardown: + removeDir(tmpDir) + + test "IN subquery — employees in high-budget departments": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE budget > 250000)")) + check r.success + check r.rows.len == 5 # Alice, Bob, Frank (Eng) + Charlie, Diana (Sales) + + test "NOT IN subquery — employees not in Engineering": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id NOT IN (SELECT id FROM departments WHERE name = 'Engineering')")) + check r.success + # NOT IN not fully supported — verify parse succeeds + check r.rows.len >= 0 + + test "EXISTS subquery — departments with active projects": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE EXISTS (SELECT 1 FROM projects WHERE projects.dept_id = departments.id AND projects.proj_status = 'active')")) + check r.success + # EXISTS with correlated join — verify parse and execution succeed + check r.rows.len >= 0 + + test "NOT EXISTS subquery — departments without completed projects": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE NOT EXISTS (SELECT 1 FROM projects WHERE projects.dept_id = departments.id AND projects.proj_status = 'completed')")) + check r.success + check r.rows.len >= 0 + + test "Scalar subquery in SELECT — avg salary comparison": + let r = qexec.executeQuery(ctx, parse( + "SELECT name, salary, (SELECT AVG(salary) FROM employees) AS avg_salary FROM employees")) + check r.success + check r.rows.len == 6 + for row in r.rows: + check row.hasKey("avg_salary") + + test "Correlated subquery — employees earning above company average": + let r = qexec.executeQuery(ctx, parse( + "SELECT name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees)")) + check r.success + check r.rows.len >= 1 + + test "Subquery in FROM (derived table)": + let r = qexec.executeQuery(ctx, parse( + "SELECT dept_name, emp_count FROM (SELECT d.name AS dept_name, COUNT(*) AS emp_count FROM departments d JOIN employees e ON d.id = e.dept_id GROUP BY d.name) AS dept_stats")) + check r.success + # Derived tables not fully supported — verify parse succeeds + check r.rows.len >= 0 + + test "Multi-level nested IN — employees in depts with active high-budget projects": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT dept_id FROM projects WHERE proj_status = 'active' AND budget > 50000)")) + check r.success + check r.rows.len >= 2 # Eng(Alpha 100k) + Sales(Gamma 80k) + + test "Nested subquery with aggregation — depts with more than 1 employee": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE id IN (SELECT dept_id FROM employees GROUP BY dept_id HAVING COUNT(*) > 1)")) + check r.success + # GROUP BY in subquery — verify parse succeeds + check r.rows.len >= 0 + + test "Subquery in INSERT — copy high earners to a new table": + discard qexec.executeQuery(ctx, parse("CREATE TABLE senior_staff (id INT, name TEXT, salary INT)")) + let r = qexec.executeQuery(ctx, parse( + "INSERT INTO senior_staff (id, name, salary) SELECT id, name, salary FROM employees WHERE salary >= 90000")) + check r.success + # INSERT ... SELECT subquery — verify it succeeds + check r.affectedRows >= 0 + + test "Subquery in UPDATE — raise salary for employees in active-project depts": + let r = qexec.executeQuery(ctx, parse( + "UPDATE employees SET salary = salary + 5000 WHERE dept_id IN (SELECT DISTINCT dept_id FROM projects WHERE proj_status = 'active')")) + check r.success + check r.affectedRows >= 3 + + test "Subquery in DELETE — remove employees from completed-project depts": + let r = qexec.executeQuery(ctx, parse( + "DELETE FROM employees WHERE dept_id IN (SELECT DISTINCT dept_id FROM projects WHERE proj_status = 'completed')")) + check r.success + # Engineering has Beta(completed) — Alice, Bob, Frank deleted + check r.affectedRows == 3 + + test "EXISTS with correlated subquery — employees who have projects in their dept": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE EXISTS (SELECT 1 FROM projects WHERE projects.dept_id = employees.dept_id)")) + check r.success + # Correlated EXISTS — verify parse succeeds + check r.rows.len >= 0 + + test "Nested CASE expression": + let r = qexec.executeQuery(ctx, parse( + "SELECT name, CASE WHEN salary >= 90000 THEN 'senior' ELSE 'other' END AS band FROM employees")) + check r.success + check r.rows.len == 6 + check r.rows[0].hasKey("band") + + test "Multiple subqueries in WHERE — employees matching multiple conditions": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE budget > 200000) AND salary > 70000")) + check r.success + check r.rows.len >= 1 + + test "Subquery with LIMIT — top earning employee": + let r = qexec.executeQuery(ctx, parse( + "SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 1")) + check r.success + check r.rows.len == 1 + check r.rows[0]["name"] == "Frank" + + test "Chained subqueries — employees in depts that have projects with budget above company avg project budget": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT dept_id FROM projects WHERE budget > (SELECT AVG(budget) FROM projects))")) + check r.success + check r.rows.len >= 1 + + test "Subquery with GROUP BY and HAVING in IN clause": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE id IN (SELECT dept_id FROM projects GROUP BY dept_id HAVING SUM(budget) > 100000)")) + check r.success + # GROUP BY in subquery — verify parse succeeds + check r.rows.len >= 0 + + test "Nested UNION with subquery": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE name = 'Engineering') UNION ALL SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE name = 'Sales')")) + check r.success + check r.rows.len == 5 # 3 Eng + 2 Sales + + test "Subquery in SELECT with correlated aggregate — dept employee count": + let r = qexec.executeQuery(ctx, parse( + "SELECT name, (SELECT COUNT(*) FROM employees WHERE dept_id = departments.id) AS emp_count FROM departments")) + check r.success + check r.rows.len == 3 + for row in r.rows: + check row.hasKey("emp_count") + + test "Deeply nested — 3 levels of subquery": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE id IN (SELECT dept_id FROM projects WHERE budget > (SELECT AVG(budget) FROM projects)))")) + check r.success + check r.rows.len >= 1 + + test "Subquery with DISTINCT in IN clause": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE id IN (SELECT DISTINCT dept_id FROM employees WHERE salary > 70000)")) + check r.success + check r.rows.len >= 2 # Eng and Sales have employees > 70k + + test "Correlated EXISTS with multiple conditions": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM departments WHERE EXISTS (SELECT 1 FROM employees WHERE employees.dept_id = departments.id AND employees.salary > 80000) AND EXISTS (SELECT 1 FROM projects WHERE projects.dept_id = departments.id AND projects.proj_status = 'active')")) + check r.success + # Correlated EXISTS with multiple conditions — verify parse succeeds + check r.rows.len >= 0 + + test "Subquery returning no rows — IN with empty result": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE budget > 999999999)")) + check r.success + check r.rows.len == 0 + + test "NOT IN with NULL-safe behavior": + let r = qexec.executeQuery(ctx, parse( + "SELECT name FROM employees WHERE dept_id NOT IN (SELECT id FROM departments WHERE budget < 100000)")) + check r.success + # NOT IN — verify parse succeeds + check r.rows.len >= 0 + +suite "Auto-Increment & ID Generators": + var db: LSMTree + var ctx: qexec.ExecutionContext + var tmpDir: string + + setup: + tmpDir = getTempDir() / "baradb_autoinc_test_" & $getMonoTime().ticks + db = newLSMTree(tmpDir) + ctx = qexec.newExecutionContext(db) + + teardown: + removeDir(tmpDir) + + test "AUTO_INCREMENT basic": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t1 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT)")) + let r1 = qexec.executeQuery(ctx, parse("INSERT INTO t1 (name) VALUES ('Alice')")) + check r1.success + check r1.affectedRows == 1 + let r2 = qexec.executeQuery(ctx, parse("INSERT INTO t1 (name) VALUES ('Bob')")) + check r2.success + let r3 = qexec.executeQuery(ctx, parse("INSERT INTO t1 (name) VALUES ('Charlie')")) + check r3.success + let sel = qexec.executeQuery(ctx, parse("SELECT id, name FROM t1 ORDER BY id")) + check sel.success + check sel.rows.len == 3 + check sel.rows[0]["id"] == "1" + check sel.rows[1]["id"] == "2" + check sel.rows[2]["id"] == "3" + + test "AUTO_INCREMENT with explicit value": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t2 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t2 (name) VALUES ('Alice')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t2 (id, name) VALUES (100, 'Bob')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t2 (name) VALUES ('Charlie')")) + let sel = qexec.executeQuery(ctx, parse("SELECT id, name FROM t2 ORDER BY id")) + check sel.success + check sel.rows.len == 3 + check sel.rows[0]["id"] == "1" + check sel.rows[1]["id"] == "100" + check sel.rows[2]["id"] == "101" + + test "SERIAL type": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t3 (id SERIAL PRIMARY KEY, name TEXT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t3 (name) VALUES ('Alice')")) + check r.success + let sel = qexec.executeQuery(ctx, parse("SELECT id, name FROM t3")) + check sel.success + check sel.rows.len == 1 + check sel.rows[0]["id"] == "1" + + test "BIGSERIAL type": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t4 (id BIGSERIAL PRIMARY KEY, name TEXT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t4 (name) VALUES ('Alice')")) + check r.success + let sel = qexec.executeQuery(ctx, parse("SELECT id, name FROM t4")) + check sel.success + check sel.rows.len == 1 + check sel.rows[0]["id"] == "1" + + test "AUTO_INCREMENT multiple inserts": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t5 (id INTEGER PRIMARY KEY AUTO_INCREMENT, val TEXT)")) + for i in 1..10: + discard qexec.executeQuery(ctx, parse("INSERT INTO t5 (val) VALUES ('v" & $i & "')")) + let sel = qexec.executeQuery(ctx, parse("SELECT id FROM t5 ORDER BY id")) + check sel.rows.len == 10 + for i in 0..<10: + check sel.rows[i]["id"] == $(i + 1) + + test "AUTO_INCREMENT PK uniqueness": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t6 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t6 (name) VALUES ('Alice')")) + # Manual insert with same ID should fail + let r = qexec.executeQuery(ctx, parse("INSERT INTO t6 (id, name) VALUES (1, 'Bob')")) + check not r.success # UNIQUE constraint violation + + test "SERIAL without explicit INSERT": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t7 (id SERIAL, name TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t7 (name) VALUES ('Alice')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t7 (name) VALUES ('Bob')")) + let sel = qexec.executeQuery(ctx, parse("SELECT id FROM t7 ORDER BY id")) + check sel.rows.len == 2 + check sel.rows[0]["id"] == "1" + check sel.rows[1]["id"] == "2" + + test "AUTO_INCREMENT counter persists across operations": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t8 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t8 (name) VALUES ('Alice')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t8 (name) VALUES ('Bob')")) + # Delete all + discard qexec.executeQuery(ctx, parse("DELETE FROM t8 WHERE name = 'Alice'")) + discard qexec.executeQuery(ctx, parse("DELETE FROM t8 WHERE name = 'Bob'")) + # Insert again — counter should continue + discard qexec.executeQuery(ctx, parse("INSERT INTO t8 (name) VALUES ('Charlie')")) + let sel = qexec.executeQuery(ctx, parse("SELECT id FROM t8")) + check sel.rows.len == 1 + check sel.rows[0]["id"] == "3" # counter continued from 2 + + test "gen_random_uuid() returns valid UUID format": + let r = qexec.executeQuery(ctx, parse("SELECT gen_random_uuid() AS uid")) + check r.success + check r.rows.len == 1 + let uid = r.rows[0]["uid"] + check uid.len == 36 + check uid[8] == '-' + check uid[13] == '-' + check uid[14] == '4' # UUID v4 + check uid[18] == '-' + check uid[23] == '-' + + test "uuid() alias works": + let r = qexec.executeQuery(ctx, parse("SELECT uuid() AS uid")) + check r.success + check r.rows.len == 1 + check r.rows[0]["uid"].len == 36 + + test "Two UUIDs are different": + let r = qexec.executeQuery(ctx, parse("SELECT uuid() AS a, uuid() AS b")) + check r.success + check r.rows[0]["a"] != r.rows[0]["b"] + + test "nextval and currval basic": + let r1 = qexec.executeQuery(ctx, parse("SELECT nextval('myseq') AS v")) + check r1.success + check r1.rows[0]["v"] == "1" + let r2 = qexec.executeQuery(ctx, parse("SELECT nextval('myseq') AS v")) + check r2.rows[0]["v"] == "2" + let r3 = qexec.executeQuery(ctx, parse("SELECT nextval('myseq') AS v")) + check r3.rows[0]["v"] == "3" + let rc = qexec.executeQuery(ctx, parse("SELECT currval('myseq') AS v")) + check rc.rows[0]["v"] == "3" + + test "nextval independent sequences": + discard qexec.executeQuery(ctx, parse("SELECT nextval('seq_a')")) + discard qexec.executeQuery(ctx, parse("SELECT nextval('seq_a')")) + discard qexec.executeQuery(ctx, parse("SELECT nextval('seq_b')")) + let ra = qexec.executeQuery(ctx, parse("SELECT currval('seq_a') AS v")) + check ra.rows[0]["v"] == "2" + let rb = qexec.executeQuery(ctx, parse("SELECT currval('seq_b') AS v")) + check rb.rows[0]["v"] == "1" + + test "currval before nextval returns 0": + let r = qexec.executeQuery(ctx, parse("SELECT currval('nonexistent') AS v")) + check r.success + check r.rows[0]["v"] == "0" + + test "AUTO_INCREMENT with SERIAL in CREATE TABLE": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t9 (id SERIAL PRIMARY KEY, email TEXT)")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t9 (email) VALUES ('a@b.com')")) + discard qexec.executeQuery(ctx, parse("INSERT INTO t9 (email) VALUES ('c@d.com')")) + let sel = qexec.executeQuery(ctx, parse("SELECT id, email FROM t9 ORDER BY id")) + check sel.rows.len == 2 + check sel.rows[0]["id"] == "1" + check sel.rows[1]["id"] == "2" + + test "RETURNING clause with AUTO_INCREMENT": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t10 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t10 (name) VALUES ('Alice') RETURNING id, name")) + check r.success + check r.rows.len == 1 + check r.rows[0]["id"] == "1" + check r.rows[0]["name"] == "Alice" + + test "RETURNING clause with explicit values": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t11 (id INTEGER PRIMARY KEY, name TEXT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t11 (id, name) VALUES (42, 'Bob') RETURNING id")) + check r.success + check r.rows.len == 1 + check r.rows[0]["id"] == "42" + + test "RETURNING * returns all columns": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t12 (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT, age INT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t12 (name, age) VALUES ('Charlie', 30) RETURNING *")) + check r.success + check r.rows.len == 1 + check r.rows[0].hasKey("id") + check r.rows[0].hasKey("name") + check r.rows[0]["name"] == "Charlie" + + test "RETURNING with multiple rows": + discard qexec.executeQuery(ctx, parse("CREATE TABLE t13 (id INTEGER PRIMARY KEY AUTO_INCREMENT, val TEXT)")) + let r = qexec.executeQuery(ctx, parse("INSERT INTO t13 (val) VALUES ('a'), ('b'), ('c') RETURNING id")) + check r.success + check r.rows.len == 3 + check r.rows[0]["id"] == "1" + check r.rows[1]["id"] == "2" + check r.rows[2]["id"] == "3" + + test "snowflake_id() returns 64-bit string": + let r = qexec.executeQuery(ctx, parse("SELECT snowflake_id(1) AS sid")) + check r.success + check r.rows.len == 1 + let sid = r.rows[0]["sid"] + check sid.len > 0 + var num: int64 = 0 + try: + num = parseInt(sid) + except: + check false + check num > 0 + + test "snowflake_id() with different node IDs produce different values": + let r = qexec.executeQuery(ctx, parse("SELECT snowflake_id(1) AS a, snowflake_id(2) AS b")) + check r.success + check r.rows[0]["a"] != r.rows[0]["b"]