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:
@@ -478,9 +478,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
slowQueryLog(slowLog, queryStr, durationMs, clientId)
|
slowQueryLog(slowLog, queryStr, durationMs, clientId)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
if result.rows.len > 0:
|
let dataMsg = serializeResult(result, header.requestId)
|
||||||
let dataMsg = serializeResult(result, header.requestId)
|
await client.send(cast[string](dataMsg))
|
||||||
await client.send(cast[string](dataMsg))
|
|
||||||
let completeMsg = serializeComplete(result.affectedRows, header.requestId)
|
let completeMsg = serializeComplete(result.affectedRows, header.requestId)
|
||||||
await client.send(cast[string](completeMsg))
|
await client.send(cast[string](completeMsg))
|
||||||
else:
|
else:
|
||||||
@@ -499,9 +498,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
slowQueryLog(slowLog, queryStr, durationMs, clientId)
|
slowQueryLog(slowLog, queryStr, durationMs, clientId)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
if result.rows.len > 0:
|
let dataMsg = serializeResult(result, header.requestId)
|
||||||
let dataMsg = serializeResult(result, header.requestId)
|
await client.send(cast[string](dataMsg))
|
||||||
await client.send(cast[string](dataMsg))
|
|
||||||
let completeMsg = serializeComplete(result.affectedRows, header.requestId)
|
let completeMsg = serializeComplete(result.affectedRows, header.requestId)
|
||||||
await client.send(cast[string](completeMsg))
|
await client.send(cast[string](completeMsg))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ type
|
|||||||
cdName*: string
|
cdName*: string
|
||||||
cdType*: string
|
cdType*: string
|
||||||
cdConstraints*: seq[Node]
|
cdConstraints*: seq[Node]
|
||||||
|
cdAutoIncrement*: bool
|
||||||
of nkConstraintDef:
|
of nkConstraintDef:
|
||||||
cstName*: string
|
cstName*: string
|
||||||
cstType*: string
|
cstType*: string
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import checksums/sha2
|
|||||||
import std/math
|
import std/math
|
||||||
import std/times
|
import std/times
|
||||||
import std/json
|
import std/json
|
||||||
|
import std/random
|
||||||
|
import std/monotimes
|
||||||
import lexer as qlex
|
import lexer as qlex
|
||||||
import parser as qpar
|
import parser as qpar
|
||||||
import ast
|
import ast
|
||||||
@@ -70,6 +72,8 @@ type
|
|||||||
currentUser*: string
|
currentUser*: string
|
||||||
currentRole*: string
|
currentRole*: string
|
||||||
sessionVars*: Table[string, string] # session-scoped key/value variables
|
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
|
MigrationRecord* = object
|
||||||
name*: string
|
name*: string
|
||||||
@@ -112,6 +116,7 @@ type
|
|||||||
defaultVal*: string
|
defaultVal*: string
|
||||||
fkTable*: string
|
fkTable*: string
|
||||||
fkColumn*: string
|
fkColumn*: string
|
||||||
|
autoIncrement*: bool
|
||||||
|
|
||||||
Row* = Table[string, string]
|
Row* = Table[string, string]
|
||||||
|
|
||||||
@@ -138,6 +143,7 @@ proc errResult*(msg: string): ExecResult =
|
|||||||
# Context management
|
# Context management
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc evalNodeToString(node: Node): string
|
||||||
proc restoreSchema(ctx: ExecutionContext)
|
proc restoreSchema(ctx: ExecutionContext)
|
||||||
|
|
||||||
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||||
@@ -151,6 +157,8 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
|||||||
policies: initTable[string, seq[PolicyDef]](),
|
policies: initTable[string, seq[PolicyDef]](),
|
||||||
currentUser: "", currentRole: "",
|
currentUser: "", currentRole: "",
|
||||||
sessionVars: initTable[string, string](),
|
sessionVars: initTable[string, string](),
|
||||||
|
autoIncCounters: initTable[string, int64](),
|
||||||
|
sequences: initTable[string, int64](),
|
||||||
onChange: nil)
|
onChange: nil)
|
||||||
restoreSchema(result)
|
restoreSchema(result)
|
||||||
|
|
||||||
@@ -281,6 +289,7 @@ proc restoreSchema(ctx: ExecutionContext) =
|
|||||||
for col in stmt.crtColumns:
|
for col in stmt.crtColumns:
|
||||||
if col.kind == nkColumnDef:
|
if col.kind == nkColumnDef:
|
||||||
var colDef = ColumnDef(name: col.cdName, colType: col.cdType)
|
var colDef = ColumnDef(name: col.cdName, colType: col.cdType)
|
||||||
|
colDef.autoIncrement = col.cdAutoIncrement
|
||||||
for cst in col.cdConstraints:
|
for cst in col.cdConstraints:
|
||||||
if cst.kind == nkConstraintDef:
|
if cst.kind == nkConstraintDef:
|
||||||
case cst.cstType
|
case cst.cstType
|
||||||
@@ -292,6 +301,12 @@ proc restoreSchema(ctx: ExecutionContext) =
|
|||||||
of "unique":
|
of "unique":
|
||||||
colDef.isUnique = true
|
colDef.isUnique = true
|
||||||
ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]()
|
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
|
else: discard
|
||||||
tbl.columns.add(colDef)
|
tbl.columns.add(colDef)
|
||||||
ctx.tables[stmt.crtName] = tbl
|
ctx.tables[stmt.crtName] = tbl
|
||||||
@@ -326,6 +341,8 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
|||||||
txnManager: ctx.txnManager,
|
txnManager: ctx.txnManager,
|
||||||
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
||||||
sessionVars: ctx.sessionVars,
|
sessionVars: ctx.sessionVars,
|
||||||
|
autoIncCounters: ctx.autoIncCounters,
|
||||||
|
sequences: ctx.sequences,
|
||||||
pendingTxn: nil, onChange: ctx.onChange)
|
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")
|
return $now().format("yyyy-MM-dd HH:mm:ss")
|
||||||
of "now":
|
of "now":
|
||||||
return $now().format("yyyy-MM-dd HH:mm:ss")
|
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":
|
of "strftime":
|
||||||
if expr.irFuncArgs.len >= 2:
|
if expr.irFuncArgs.len >= 2:
|
||||||
let fmt = evalExpr(expr.irFuncArgs[0], row, ctx)
|
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)
|
discard executeQuery(ctx, astNode)
|
||||||
|
|
||||||
proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
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)
|
let tbl = ctx.getTableDef(tableName)
|
||||||
|
|
||||||
for rowIdx, rowVals in values:
|
for rowIdx, rowVals in values:
|
||||||
@@ -1268,13 +1328,14 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
|||||||
if not found:
|
if not found:
|
||||||
return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn)
|
return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn)
|
||||||
|
|
||||||
# PK uniqueness
|
# PK uniqueness (skip during UPDATE — PK shouldn't change)
|
||||||
if tbl.pkColumns.len > 0:
|
if not skipPkCheck and tbl.pkColumns.len > 0:
|
||||||
var pkVals: seq[string] = @[]
|
var pkVals: seq[string] = @[]
|
||||||
for pkCol in tbl.pkColumns:
|
for pkCol in tbl.pkColumns:
|
||||||
pkVals.add(getValue(rowVals, fields, pkCol))
|
pkVals.add(getValue(rowVals, fields, pkCol))
|
||||||
let pkStr = pkVals.join("|")
|
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)
|
let (exists, _) = ctx.db.get(pkKey)
|
||||||
if exists:
|
if exists:
|
||||||
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'")
|
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'")
|
||||||
@@ -1629,9 +1690,12 @@ proc lowerSelect*(node: Node): IRPlan =
|
|||||||
elif e.kind == nkIdent:
|
elif e.kind == nkIdent:
|
||||||
projectPlan.projectAliases.add(e.identName)
|
projectPlan.projectAliases.add(e.identName)
|
||||||
elif e.kind == nkPath and e.pathParts.len > 0:
|
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:
|
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:
|
elif e.kind == nkStar:
|
||||||
projectPlan.projectAliases.add("*")
|
projectPlan.projectAliases.add("*")
|
||||||
else:
|
else:
|
||||||
@@ -2067,9 +2131,15 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
groups[groupKey].add(row)
|
groups[groupKey].add(row)
|
||||||
for gk, groupRows in groups:
|
for gk, groupRows in groups:
|
||||||
var aggRow: Table[string, string]
|
var aggRow: Table[string, string]
|
||||||
|
# Populate GROUP BY key columns
|
||||||
for gkExpr in gkeys:
|
for gkExpr in gkeys:
|
||||||
if gkExpr.kind == irekField and gkExpr.fieldPath.len > 0:
|
if gkExpr.kind == irekField and gkExpr.fieldPath.len > 0:
|
||||||
aggRow[gkExpr.fieldPath[^1]] = evalExpr(gkExpr, groupRows[0], ctx)
|
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
|
# Compute each aggregate expression
|
||||||
for aggExpr in plan.groupAggs:
|
for aggExpr in plan.groupAggs:
|
||||||
let aggKey = "$agg_" & $aggExpr.aggOp & "_" & $plan.groupAggs.find(aggExpr)
|
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)
|
for col in tbl.columns: fields.add(col.name)
|
||||||
|
|
||||||
let tbl = ctx.getTableDef(stmt.insTarget)
|
let tbl = ctx.getTableDef(stmt.insTarget)
|
||||||
|
|
||||||
|
# Auto-increment: populate missing auto-increment columns
|
||||||
var mutableFields = fields
|
var mutableFields = fields
|
||||||
var mutableValues = values
|
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)
|
applyDefaultValues(tbl, mutableFields, mutableValues)
|
||||||
|
|
||||||
let (valid, errMsg) = validateConstraints(ctx, stmt.insTarget, 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:
|
if ctx.onChange != nil:
|
||||||
for i in 0..<count:
|
for i in 0..<count:
|
||||||
ctx.onChange(ChangeEvent(table: stmt.insTarget, kind: ckInsert, key: "", data: ""))
|
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)
|
return okResult(affected=count, kvPairs=kvPairs)
|
||||||
|
|
||||||
of nkUpdate:
|
of nkUpdate:
|
||||||
@@ -3068,7 +3200,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
updValues.add(row[col.name])
|
updValues.add(row[col.name])
|
||||||
else:
|
else:
|
||||||
updValues.add("")
|
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)
|
if not valid: return errResult(errMsg)
|
||||||
# Fire BEFORE UPDATE triggers
|
# Fire BEFORE UPDATE triggers
|
||||||
var oldRow = row
|
var oldRow = row
|
||||||
@@ -3208,6 +3340,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
for col in stmt.crtColumns:
|
for col in stmt.crtColumns:
|
||||||
if col.kind == nkColumnDef:
|
if col.kind == nkColumnDef:
|
||||||
var colDef = ColumnDef(name: col.cdName, colType: col.cdType)
|
var colDef = ColumnDef(name: col.cdName, colType: col.cdType)
|
||||||
|
colDef.autoIncrement = col.cdAutoIncrement
|
||||||
for cst in col.cdConstraints:
|
for cst in col.cdConstraints:
|
||||||
if cst.kind == nkConstraintDef:
|
if cst.kind == nkConstraintDef:
|
||||||
case cst.cstType
|
case cst.cstType
|
||||||
@@ -3239,6 +3372,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
for col in tbl.columns:
|
for col in tbl.columns:
|
||||||
var parts = @[col.name, col.colType]
|
var parts = @[col.name, col.colType]
|
||||||
if col.isPk: parts.add("PRIMARY KEY")
|
if col.isPk: parts.add("PRIMARY KEY")
|
||||||
|
if col.autoIncrement: parts.add("AUTO_INCREMENT")
|
||||||
if col.isNotNull: parts.add("NOT NULL")
|
if col.isNotNull: parts.add("NOT NULL")
|
||||||
if col.isUnique: parts.add("UNIQUE")
|
if col.isUnique: parts.add("UNIQUE")
|
||||||
if col.defaultVal.len > 0: parts.add("DEFAULT '" & col.defaultVal & "'")
|
if col.defaultVal.len > 0: parts.add("DEFAULT '" & col.defaultVal & "'")
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ type
|
|||||||
tkDfs
|
tkDfs
|
||||||
tkPath
|
tkPath
|
||||||
|
|
||||||
|
tkAutoIncrement
|
||||||
|
tkSequence
|
||||||
|
tkNextval
|
||||||
|
tkCurrval
|
||||||
|
tkUuid
|
||||||
|
|
||||||
# Window functions
|
# Window functions
|
||||||
tkOver
|
tkOver
|
||||||
tkPartition
|
tkPartition
|
||||||
@@ -389,6 +395,8 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"first_value": tkFirstValue,
|
"first_value": tkFirstValue,
|
||||||
"last_value": tkLastValue,
|
"last_value": tkLastValue,
|
||||||
"ntile": tkNtile,
|
"ntile": tkNtile,
|
||||||
|
"auto_increment": tkAutoIncrement,
|
||||||
|
"sequence": tkSequence,
|
||||||
}.toTable
|
}.toTable
|
||||||
|
|
||||||
proc newLexer*(input: string): Lexer =
|
proc newLexer*(input: string): Lexer =
|
||||||
|
|||||||
@@ -526,7 +526,23 @@ proc parseSelect(p: var Parser): Node =
|
|||||||
elif p.peek().kind == tkIdent:
|
elif p.peek().kind == tkIdent:
|
||||||
alias = p.advance().value
|
alias = p.advance().value
|
||||||
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.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
|
# Parse PIVOT / UNPIVOT after FROM source
|
||||||
if p.peek().kind == tkPivot:
|
if p.peek().kind == tkPivot:
|
||||||
@@ -1006,13 +1022,24 @@ proc parseCreateTable(p: var Parser): Node =
|
|||||||
let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType)
|
let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType)
|
||||||
colDef.cdConstraints = @[]
|
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
|
# 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)
|
let cst = Node(kind: nkConstraintDef)
|
||||||
cst.cstColumns = @[colName]; cst.cstRefColumns = @[]
|
cst.cstColumns = @[colName]; cst.cstRefColumns = @[]
|
||||||
if p.match(tkPrimary):
|
if p.match(tkPrimary):
|
||||||
discard p.expect(tkKey)
|
discard p.expect(tkKey)
|
||||||
cst.cstType = "pkey"
|
cst.cstType = "pkey"
|
||||||
|
elif p.match(tkAutoIncrement):
|
||||||
|
colDef.cdAutoIncrement = true
|
||||||
|
continue
|
||||||
elif p.match(tkNot):
|
elif p.match(tkNot):
|
||||||
discard p.expect(tkNull)
|
discard p.expect(tkNull)
|
||||||
cst.cstType = "notnull"
|
cst.cstType = "notnull"
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ suite "JOIN execution":
|
|||||||
test "aliased column projection":
|
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")
|
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.len == 2
|
||||||
check r.rows[0]["name"] == "Alice"
|
check r.rows[0]["u.name"] == "Alice"
|
||||||
check r.rows[0]["total"] == "99.5"
|
check r.rows[0]["o.total"] == "99.5"
|
||||||
check "id" notin r.rows[0]
|
check "id" notin r.rows[0]
|
||||||
|
|
||||||
test "count after FULL JOIN":
|
test "count after FULL JOIN":
|
||||||
@@ -84,8 +84,8 @@ suite "JOIN execution":
|
|||||||
let r = execSql(ctx,
|
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")
|
"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.len == 1
|
||||||
check r.rows[0]["name"] == "Alice"
|
check r.rows[0]["u.name"] == "Alice"
|
||||||
check r.rows[0]["total"] == "99.5"
|
check r.rows[0]["recent.total"] == "99.5"
|
||||||
|
|
||||||
test "LATERAL JOIN returns no rows when subquery empty":
|
test "LATERAL JOIN returns no rows when subquery empty":
|
||||||
let r = execSql(ctx,
|
let r = execSql(ctx,
|
||||||
@@ -99,8 +99,8 @@ suite "JOIN execution":
|
|||||||
# Alice has match (99.5), Bob has no orders -> NULL
|
# Alice has match (99.5), Bob has no orders -> NULL
|
||||||
var foundBob = false
|
var foundBob = false
|
||||||
for row in r.rows:
|
for row in r.rows:
|
||||||
if row["name"] == "Bob":
|
if row["u.name"] == "Bob":
|
||||||
check row["total"] == ""
|
check row["x.total"] == ""
|
||||||
foundBob = true
|
foundBob = true
|
||||||
check foundBob
|
check foundBob
|
||||||
|
|
||||||
|
|||||||
@@ -3098,3 +3098,405 @@ suite "Vector SQL Integration":
|
|||||||
check r.rows.len == 2
|
check r.rows.len == 2
|
||||||
check r.rows[0]["dist"] == "0.0"
|
check r.rows[0]["dist"] == "0.0"
|
||||||
check r.rows[1]["dist"] == "1.7320508075688772"
|
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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user