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
This commit is contained in:
2026-05-16 02:57:30 +03:00
parent 406dcf4f4e
commit 652ed1b477
7 changed files with 591 additions and 21 deletions
+4 -6
View File
@@ -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:
+1
View File
@@ -250,6 +250,7 @@ type
cdName*: string
cdType*: string
cdConstraints*: seq[Node]
cdAutoIncrement*: bool
of nkConstraintDef:
cstName*: string
cstType*: string
+141 -7
View File
@@ -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..<mutableValues.len:
mutableValues[i].insert($(nextVal + int64(i)), 0)
elif col.autoIncrement and col.name in mutableFields:
# User provided value — update counter to max
let idx = mutableFields.find(col.name)
if idx >= 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..<count:
ctx.onChange(ChangeEvent(table: stmt.insTarget, kind: ckInsert, key: "", data: ""))
# RETURNING clause
if stmt.insReturning.len > 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 & "'")
+8
View File
@@ -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 =
+29 -2
View File
@@ -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"
+6 -6
View File
@@ -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
+402
View File
@@ -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"]