Compare commits

..

2 Commits

Author SHA1 Message Date
dimgigov 2d3f413de3 docs: add deficiency audit and ID generators plan
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
2026-05-16 02:58:11 +03:00
dimgigov 652ed1b477 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
2026-05-16 02:57:30 +03:00
9 changed files with 841 additions and 21 deletions
+179
View File
@@ -0,0 +1,179 @@
# BaraDB Deficiencies Fixed During NimForum Migration
This document summarizes the bugs and design deficiencies discovered in BaraDB while porting NimForum from SQLite to BaraDB's TCP wire protocol.
---
## 1. Comma Join Not Supported in Parser
**Problem:** The BaraQL parser did not recognize comma-separated table lists in the `FROM` clause.
```sql
SELECT * FROM thread t, category c WHERE t.category = c.id
```
This would fail with "unknown tokens" after the first comma.
**Root Cause:** `parseSelect()` only parsed a single table after `FROM` and ignored everything after a comma.
**Fix:** Modified `database/src/barabadb/query/parser.nim` to loop over comma-separated tables and treat each additional table as an implicit `CROSS JOIN`.
```nim
while p.peek().kind == tkComma:
discard p.advance()
let nextTableTok = p.expect(tkIdent)
# ... build joinNode with jkCross and add to result.selJoins
```
---
## 2. DEFAULT Constraints Not Evaluated at Schema Creation Time
**Problem:** `CREATE TABLE` with `DEFAULT <expr>` stored the raw AST node instead of the evaluated string value. During `INSERT`, `applyDefaultValues()` checked `colDef.defaultVal.len > 0`, but `defaultVal` was empty because the AST was never evaluated to a string.
**Result:** Explicitly omitted columns in `INSERT` raised `NOT NULL constraint violation` even when a `DEFAULT` was defined.
**Fix:** Added `evalNodeToString()` in `database/src/barabadb/query/executor.nim` to evaluate `DEFAULT` AST nodes to their string representation during schema restoration.
```nim
proc evalNodeToString(node: Node): string =
let ir = lowerExpr(node)
return evalExpr(ir, initTable[string, string](), nil)
```
---
## 3. Join Column Resolution Overwrites Duplicate Column Names
**Problem:** `Row` is defined as `Table[string, string]`. When a query selects columns with identical names from joined tables:
```sql
SELECT t.id, c.id FROM thread t INNER JOIN category c ON t.category = c.id
```
The `Project` operator in `executePlan()` builds `newRow` as a `Table`. The second `id` overwrites the first, so both columns end up with the value from the right-hand table.
**Result:** `t.id` returns `0` (category id) instead of `1` (thread id).
**Fix:** Two changes in `database/src/barabadb/query/executor.nim`:
1. `lowerSelect()` — for `nkPath` expressions, use the full path (`t.id`) as the alias instead of just the last segment (`id`).
2. Added uniqueness logic for all aliases: if a duplicate alias is detected, append `_1`, `_2`, etc. This also fixes `strftime()` appearing multiple times in the same select list.
```nim
# Before
projectPlan.projectAliases.add(e.pathParts[^1]) # -> "id"
# After
projectPlan.projectAliases.add(e.pathParts.join(".")) # -> "t.id"
```
---
## 4. Empty Result Sets Do Not Send Column Metadata
**Problem:** In `database/src/barabadb/core/server.nim`, the server only sent the `mkData` message when `result.rows.len > 0`:
```nim
if result.rows.len > 0:
let dataMsg = serializeResult(result, header.requestId)
await client.send(...)
```
For queries with zero matching rows (e.g. `WHERE id IN (SELECT ...)` with no subquery matches), the client received only `mkComplete` and no `mkData`.
**Result:** The client saw `columns: @[]` and `rows: @[]`. When `getRow()` fell back to `newSeq[string](qr.columns.len)`, it returned an empty seq, causing "index out of bounds" errors when the application tried to access `row[0]`.
**Fix:** Removed the `if result.rows.len > 0` guard. `serializeResult()` is now always sent, including the correct column names even when `rowCount = 0`.
---
## 5. GROUP BY Returns Empty Values for Non-Aggregated Columns
**Problem:** Unlike SQLite, BaraDB does not automatically pick an arbitrary row value for columns that are neither in `GROUP BY` nor inside an aggregate function.
```sql
SELECT u.id, u.name, count(*)
FROM person u, post p
WHERE p.author = u.id AND p.thread = ?
GROUP BY name
```
**Result:** `u.id`, `u.email`, `u.usrStatus`, etc. return empty strings, while `name` and `count(*)` are correct.
**Fix:** (Workaround in forum code) Replaced `GROUP BY` queries with `DISTINCT` + separate subqueries where ordering by count is not critical:
```sql
SELECT DISTINCT u.id, u.name, u.email, ...
FROM person u, post p
WHERE p.author = u.id AND p.thread = ?
LIMIT 5
```
---
## 6. Inconsistent Aggregate Column Names
**Problem:** Aggregate functions produce column names that omit the argument expression:
- `SELECT count(*)` → column name `count()`
- `SELECT max(id)` → column name `max()`
- `SELECT min(creation)` → column name `min()`
Code that relies on exact column names (e.g. `getValue` looking up `count(*)`) can be confused.
**Fix:** (Workaround in forum code) Avoided name-dependent lookup and rewrote queries to use positional access via `getRow()` / `getAllRows()`.
---
## 7. Async Client + waitFor in Async Context = Connection Instability
**Problem:** The original `baradb_client.nim` used `AsyncSocket` wrapped in `SyncClient` via `waitFor`. When called from inside Jester's async handlers, nested `waitFor` + `poll()` created race conditions on the single socket. `recv(12)` occasionally returned 0 bytes, causing "Connection closed" exceptions.
**Result:** Login and other routes crashed intermittently with 502 Bad Gateway.
**Fix:** Built a new synchronous client (`forum/src/baradb_sync_client.nim`) using blocking `net.Socket` from Nim's standard library. This eliminates all async event loop interactions.
Key differences from the async client:
- Uses `net.Socket` instead of `asyncnet.AsyncSocket`
- Uses blocking `recv()` with an explicit `recvExact()` helper
- No dependency on `asyncdispatch` or `waitFor`
- Fully compatible with the existing wire protocol
---
## 8. Lack of Thread Safety in the Adapter
**Problem:** A single `SyncClient` instance was shared across all HTTP requests. NimForum runs on Jester's async event loop, which can interleave request handlers in the same thread. Without synchronization, two handlers could send queries over the same socket simultaneously, corrupting the wire protocol stream.
**Fix:** Added a global `Lock` and `withDbLock` template in `forum/src/baradb_sqlite.nim`:
```nim
var dbLock: Lock
initLock(dbLock)
template withDbLock(body: untyped) =
acquire(dbLock)
try: body
finally: release(dbLock)
```
All DB operations (`query`, `getRow`, `exec`, etc.) are wrapped in this lock.
---
## Summary Table
| # | Issue | Location | Type |
|---|-------|----------|------|
| 1 | Comma join parsing | `query/parser.nim` | Parser bug |
| 2 | DEFAULT not evaluated | `query/executor.nim` | Schema bug |
| 3 | Duplicate column overwrite | `query/executor.nim` | Data structure bug |
| 4 | Empty result = no columns | `core/server.nim` | Protocol bug |
| 5 | GROUP BY empty values | N/A (engine behavior) | Semantic difference |
| 6 | Inconsistent agg names | N/A (engine behavior) | Naming convention |
| 7 | Async client unstable | `forum/src/baradb_client.nim` | Client design |
| 8 | No thread safety | `forum/src/baradb_sqlite.nim` | Adapter design |
---
*Document version: 2026-05-15*
+71
View File
@@ -0,0 +1,71 @@
# Plan: ID Generators & Sequence System
## Goal
Add auto-generated ID support to BaraDB so users don't need to manually supply IDs on INSERT.
## Phase 1: ID Generators
### 1.1 AUTO_INCREMENT on INTEGER columns
- Add `AUTO_INCREMENT` keyword to lexer
- Parse in CREATE TABLE: `id INTEGER PRIMARY KEY AUTO_INCREMENT`
- Store auto-increment state per table in ExecutionContext (counter)
- On INSERT without explicit ID → auto-populate with next value
- Thread-safe counter (atomic increment)
### 1.2 SERIAL / BIGSERIAL as syntactic sugar
- `SERIAL` = `INTEGER AUTO_INCREMENT`
- `BIGSERIAL` = `BIGINT AUTO_INCREMENT`
- Already partially parsed — wire to auto-increment logic
### 1.3 UUID generation
- Add `gen_random_uuid()` or `uuid()` as built-in function
- Can be used in INSERT: `INSERT INTO t (id) VALUES (uuid())`
- Also usable as DEFAULT: `id UUID DEFAULT uuid()`
- Use Nim's std/oids or crypto random
### 1.4 RETURNING clause
- After INSERT, return generated values
- `INSERT INTO t (name) VALUES ('x') RETURNING id`
- Already partially parsed — wire to execution
### 1.5 CREATE SEQUENCE / nextval / currval
- `CREATE SEQUENCE seq_name START 1 INCREMENT 1`
- `nextval('seq_name')` → returns next value
- `currval('seq_name')` → returns current value
- Store sequences in ExecutionContext
### 1.6 Snowflake ID (distributed)
- 64-bit ID = timestamp(41) + node_id(10) + sequence(12)
- `snowflake_id(node_id)` function
- For future distributed use
## Phase 2: JOIN Optimizations (future)
### 2.1 Hash Join
- For equi-join ON a.col = b.col
- Build hash table on smaller side, probe with larger
- O(N+M) instead of O(N*M)
### 2.2 Index Nested Loop Join
- If index exists on join column → probe index per left row
- O(N * log M) instead of O(N*M)
### 2.3 Merge Join
- For sorted inputs
- Two-pointer sweep O(N+M)
## Phase 3: Foreign Key Enforcement (future)
### 3.1 CASCADE DELETE
### 3.2 SET NULL on delete
### 3.3 RESTRICT on delete
### 3.4 ON UPDATE CASCADE
### 3.5 FK check on UPDATE (not just INSERT)
## Implementation Order
1. AUTO_INCREMENT (lexer + parser + executor)
2. SERIAL/BIGSERIAL sugar
3. UUID function
4. RETURNING clause
5. Sequences (CREATE SEQUENCE / nextval / currval)
6. Snowflake ID function
-2
View File
@@ -478,7 +478,6 @@ 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 completeMsg = serializeComplete(result.affectedRows, header.requestId)
@@ -499,7 +498,6 @@ 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 completeMsg = serializeComplete(result.affectedRows, header.requestId)
+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 =
+28 -1
View File
@@ -528,6 +528,22 @@ proc parseSelect(p: var Parser): Node =
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value,
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:
discard p.advance()
@@ -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"]