fix: major bug audit + fixes — honest PLAN.md
Critical bugs fixed: - SELECT now returns actual row data (was returning empty arrays) - WHERE filter evaluation now works (was pass-through stub) - ORDER BY sorting now works (was no-op) - UPDATE execution implemented (was no-op stub) - DELETE uses WHERE filter (was key-match only) - B-Tree point reads return actual row data (was returning count only) - EXPLAIN returns plan string (was computed then discarded) - UNIQUE constraint uses B-Tree index (was memtable scan only) - DEFAULT values work for int/bool/float (was string-only) - HTTP /query returns real JSON rows with columns - Docker healthcheck uses wget (Alpine has no curl) Updated PLAN.md with honest status: - Marked what's truly done vs stub vs not implemented - Honest score: 8/10 (not 9.5/10) - Clear list of what actually works in production All 216 tests pass
This commit is contained in:
+380
-276
@@ -3,6 +3,7 @@ import std/strutils
|
||||
import std/tables
|
||||
import std/hashes
|
||||
import std/sequtils
|
||||
import std/algorithm
|
||||
import lexer as qlex
|
||||
import parser as qpar
|
||||
import ast
|
||||
@@ -13,16 +14,16 @@ import ../storage/btree
|
||||
import ../core/mvcc
|
||||
|
||||
type
|
||||
IndexEntry = ref object
|
||||
IndexEntry* = ref object
|
||||
lsmKey*: string
|
||||
rowValue*: string
|
||||
|
||||
ExecutionContext* = ref object
|
||||
db*: LSMTree
|
||||
tables*: Table[string, TableDef] # table name -> definition
|
||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]] # index name -> btree
|
||||
tables*: Table[string, TableDef]
|
||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
||||
txnManager*: TxnManager
|
||||
pendingTxn*: Transaction # active transaction for this context
|
||||
pendingTxn*: Transaction
|
||||
|
||||
TableDef* = object
|
||||
name*: string
|
||||
@@ -37,7 +38,24 @@ type
|
||||
isUnique*: bool
|
||||
defaultVal*: string
|
||||
|
||||
Row = Table[string, string]
|
||||
Row* = Table[string, string]
|
||||
|
||||
ExecResult* = object
|
||||
success*: bool
|
||||
columns*: seq[string]
|
||||
rows*: seq[Row]
|
||||
affectedRows*: int
|
||||
message*: string
|
||||
|
||||
proc okResult*(rows: seq[Row] = @[], cols: seq[string] = @[], affected: int = 0, msg: string = ""): ExecResult =
|
||||
ExecResult(success: true, columns: cols, rows: rows, affectedRows: affected, message: msg)
|
||||
|
||||
proc errResult*(msg: string): ExecResult =
|
||||
ExecResult(success: false, columns: @[], rows: @[], affectedRows: 0, message: msg)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Context management
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||
result = ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
||||
@@ -45,14 +63,12 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||
restoreSchema(result)
|
||||
|
||||
proc restoreSchema(ctx: ExecutionContext) =
|
||||
## Replay persisted migrations on startup
|
||||
let prefix = "_schema:migrations:"
|
||||
for entry in ctx.db.scanMemTable():
|
||||
if entry.deleted: continue
|
||||
if not entry.key.startsWith(prefix): continue
|
||||
let ddl = cast[string](entry.value)
|
||||
if ddl.len == 0: continue
|
||||
# Replay DDL
|
||||
let tokens = qlex.tokenize(ddl)
|
||||
let astNode = qpar.parse(tokens)
|
||||
if astNode.stmts.len > 0:
|
||||
@@ -69,36 +85,28 @@ proc restoreSchema(ctx: ExecutionContext) =
|
||||
of "pkey":
|
||||
colDef.isPk = true
|
||||
tbl.pkColumns.add(col.cdName)
|
||||
let idxName = stmt.crtName & "." & col.cdName
|
||||
var bt = newBTreeIndex[string, IndexEntry]()
|
||||
ctx.btrees[idxName] = bt
|
||||
ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]()
|
||||
of "notnull": colDef.isNotNull = true
|
||||
of "unique":
|
||||
colDef.isUnique = true
|
||||
let idxName2 = stmt.crtName & "." & col.cdName
|
||||
var bt2 = newBTreeIndex[string, IndexEntry]()
|
||||
ctx.btrees[idxName2] = bt2
|
||||
ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]()
|
||||
else: discard
|
||||
tbl.columns.add(colDef)
|
||||
ctx.tables[stmt.crtName] = tbl
|
||||
else: discard
|
||||
|
||||
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||
## Create a per-connection context that shares the same data but has its own transaction.
|
||||
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
||||
btrees: ctx.btrees, txnManager: ctx.txnManager,
|
||||
pendingTxn: nil)
|
||||
|
||||
proc getTableDef(ctx: ExecutionContext, tableName: string): TableDef =
|
||||
if tableName in ctx.tables:
|
||||
return ctx.tables[tableName]
|
||||
var tbl = TableDef(name: tableName, columns: @[], pkColumns: @[])
|
||||
return tbl
|
||||
# ----------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc getColumnDef(tbl: TableDef, colName: string): ColumnDef =
|
||||
for col in tbl.columns:
|
||||
if col.name.toLower() == colName.toLower():
|
||||
return col
|
||||
proc getTableDef(ctx: ExecutionContext, tableName: string): TableDef =
|
||||
if tableName in ctx.tables: return ctx.tables[tableName]
|
||||
return TableDef(name: tableName, columns: @[], pkColumns: @[])
|
||||
|
||||
proc getValue(values: seq[string], fields: seq[string], colName: string): string =
|
||||
for i, f in fields:
|
||||
@@ -106,38 +114,152 @@ proc getValue(values: seq[string], fields: seq[string], colName: string): string
|
||||
return values[i]
|
||||
return ""
|
||||
|
||||
proc isNull(value: string): bool =
|
||||
result = value.len == 0 or value.toLower() == "null"
|
||||
proc isNull*(value: string): bool =
|
||||
value.len == 0 or value.toLower() == "null"
|
||||
|
||||
proc parseRowData(valStr: string): Table[string, string] =
|
||||
## Parse "col1=val1,col2=val2" into a table
|
||||
result = initTable[string, string]()
|
||||
for part in valStr.split(","):
|
||||
let eqPos = part.find('=')
|
||||
if eqPos >= 0:
|
||||
let k = part[0..<eqPos].strip()
|
||||
let v = part[eqPos+1..^1].strip()
|
||||
result[k] = v
|
||||
|
||||
proc evalExpr(expr: IRExpr, row: Table[string, string]): string =
|
||||
if expr == nil: return ""
|
||||
case expr.kind
|
||||
of irekLiteral:
|
||||
case expr.literal.kind
|
||||
of vkString: return expr.literal.strVal
|
||||
of vkInt64: return $expr.literal.int64Val
|
||||
of vkFloat64: return $expr.literal.float64Val
|
||||
of vkBool: return $expr.literal.boolVal
|
||||
of vkNull: return ""
|
||||
else: return ""
|
||||
of irekField:
|
||||
if expr.fieldPath.len > 0:
|
||||
let colName = expr.fieldPath[^1]
|
||||
if colName in row: return row[colName]
|
||||
if "$key" in row and row["$key"].startsWith(colName & "="):
|
||||
return row["$key"][colName.len+1..^1]
|
||||
if "$value" in row:
|
||||
let parsed = parseRowData(row["$value"])
|
||||
if colName in parsed: return parsed[colName]
|
||||
return ""
|
||||
of irekBinary:
|
||||
let left = evalExpr(expr.binLeft, row)
|
||||
let right = evalExpr(expr.binRight, row)
|
||||
case expr.binOp
|
||||
of irEq:
|
||||
if left == right: return "true"
|
||||
# Try numeric comparison
|
||||
try:
|
||||
if parseFloat(left) == parseFloat(right): return "true"
|
||||
except: discard
|
||||
return "false"
|
||||
of irNeq: return if left != right: "true" else: "false"
|
||||
of irLt:
|
||||
try:
|
||||
return if parseFloat(left) < parseFloat(right): "true" else: "false"
|
||||
except: return if left < right: "true" else: "false"
|
||||
of irLte:
|
||||
try:
|
||||
return if parseFloat(left) <= parseFloat(right): "true" else: "false"
|
||||
except: return if left <= right: "true" else: "false"
|
||||
of irGt:
|
||||
try:
|
||||
return if parseFloat(left) > parseFloat(right): "true" else: "false"
|
||||
except: return if left > right: "true" else: "false"
|
||||
of irGte:
|
||||
try:
|
||||
return if parseFloat(left) >= parseFloat(right): "true" else: "false"
|
||||
except: return if left >= right: "true" else: "false"
|
||||
of irAnd:
|
||||
if left == "true" and right == "true": return "true"
|
||||
return "false"
|
||||
of irOr:
|
||||
if left == "true" or right == "true": return "true"
|
||||
return "false"
|
||||
of irAdd:
|
||||
try: return $(parseFloat(left) + parseFloat(right))
|
||||
except: return left & right
|
||||
of irSub:
|
||||
try: return $(parseFloat(left) - parseFloat(right))
|
||||
except: return "0"
|
||||
of irMul:
|
||||
try: return $(parseFloat(left) * parseFloat(right))
|
||||
except: return "0"
|
||||
of irDiv:
|
||||
try:
|
||||
let r = parseFloat(right)
|
||||
if r != 0: return $(parseFloat(left) / r)
|
||||
return "0"
|
||||
except: return "0"
|
||||
of irLike:
|
||||
let pattern = right.replace("%", ".*").replace("_", ".")
|
||||
try:
|
||||
if left.match(pattern): return "true"
|
||||
except: discard
|
||||
return "false"
|
||||
else: return "false"
|
||||
of irekUnary:
|
||||
case expr.unOp
|
||||
of irNot:
|
||||
let v = evalExpr(expr.unExpr, row)
|
||||
return if v == "true": "false" else: "true"
|
||||
of irIsNull:
|
||||
let v = evalExpr(expr.unExpr, row)
|
||||
return if isNull(v): "true" else: "false"
|
||||
of irIsNotNull:
|
||||
let v = evalExpr(expr.unExpr, row)
|
||||
return if not isNull(v): "true" else: "false"
|
||||
else: return "false"
|
||||
of irekExists: return "false"
|
||||
else: return ""
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Table scan and storage
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc execScan(ctx: ExecutionContext, table: string): seq[Row] =
|
||||
## Full table scan via LSM-Tree memtable scan.
|
||||
## Rows are stored as: "{table}.{key}" -> value
|
||||
## For simple KV: key is the PK value, value is the serialized row
|
||||
result = @[]
|
||||
let prefix = table & "."
|
||||
for entry in ctx.db.scanMemTable():
|
||||
if entry.deleted:
|
||||
continue
|
||||
if not entry.key.startsWith(prefix):
|
||||
continue
|
||||
if entry.deleted: continue
|
||||
if not entry.key.startsWith(prefix): continue
|
||||
let rest = entry.key[prefix.len..^1]
|
||||
var row: Table[string, string]
|
||||
row["$key"] = rest
|
||||
row["$value"] = cast[string](entry.value)
|
||||
let valStr = cast[string](entry.value)
|
||||
row["$value"] = valStr
|
||||
# Also parse individual columns
|
||||
for k, v in parseRowData(valStr):
|
||||
row[k] = v
|
||||
# Extract PK value from key
|
||||
let eqPos = rest.find('=')
|
||||
if eqPos >= 0:
|
||||
row[rest[0..<eqPos]] = rest[eqPos+1..^1]
|
||||
result.add(row)
|
||||
|
||||
proc execPointRead(ctx: ExecutionContext, table: string, key: string): seq[Row] =
|
||||
## Point read from LSM-Tree
|
||||
let fullKey = table & "." & key
|
||||
let (found, val) = ctx.db.get(fullKey)
|
||||
if found:
|
||||
var row: Table[string, string]
|
||||
row["$key"] = key
|
||||
row["$value"] = cast[string](val)
|
||||
let valStr = cast[string](val)
|
||||
row["$value"] = valStr
|
||||
for k, v in parseRowData(valStr):
|
||||
row[k] = v
|
||||
let eqPos = key.find('=')
|
||||
if eqPos >= 0:
|
||||
row[key[0..<eqPos]] = key[eqPos+1..^1]
|
||||
return @[row]
|
||||
return @[]
|
||||
|
||||
proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]]): int =
|
||||
proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]]): int =
|
||||
var count = 0
|
||||
for rowVals in values:
|
||||
var key = ""
|
||||
@@ -155,132 +277,22 @@ proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], value
|
||||
let valStr = valParts.join(",")
|
||||
let fullKey = table & "." & key
|
||||
|
||||
# Use MVCC transaction if active, otherwise write directly
|
||||
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||
discard ctx.txnManager.write(ctx.pendingTxn, fullKey, cast[seq[byte]](valStr))
|
||||
else:
|
||||
ctx.db.put(fullKey, cast[seq[byte]](valStr))
|
||||
|
||||
# Populate B-Tree indexes (always direct, not transactional for now)
|
||||
for colName in ctx.btrees.keys.toSeq():
|
||||
if colName.startsWith(table & "."):
|
||||
let colOnly = colName[table.len + 1..^1]
|
||||
let colVal = getValue(rowVals, fields, colOnly)
|
||||
if colVal.len > 0 and not isNull(colVal):
|
||||
let entry = IndexEntry(lsmKey: fullKey, rowValue: valStr)
|
||||
ctx.btrees[colName].insert(colVal, entry)
|
||||
ctx.btrees[colName].insert(colVal, IndexEntry(lsmKey: fullKey, rowValue: valStr))
|
||||
|
||||
inc count
|
||||
return count
|
||||
|
||||
proc execUpdateRow(ctx: ExecutionContext, table: string, key: string, sets: seq[(string, string)]): int =
|
||||
let fullKey = table & "." & key
|
||||
let (found, existing) = ctx.db.get(fullKey)
|
||||
if not found:
|
||||
return 0
|
||||
var valStr = cast[string](existing)
|
||||
for (f, v) in sets:
|
||||
let prefix = f & "="
|
||||
var newParts: seq[string] = @[]
|
||||
for part in valStr.split(","):
|
||||
if part.startsWith(prefix):
|
||||
newParts.add(prefix & v)
|
||||
else:
|
||||
newParts.add(part)
|
||||
# If field not in existing, append
|
||||
var hasField = false
|
||||
for part in valStr.split(","):
|
||||
if part.startsWith(prefix):
|
||||
hasField = true
|
||||
break
|
||||
if not hasField:
|
||||
newParts.add(prefix & v)
|
||||
valStr = newParts.join(",")
|
||||
ctx.db.put(fullKey, cast[seq[byte]](valStr))
|
||||
return 1
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Constraint Validation
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
||||
fields: seq[string], values: seq[seq[string]]): (bool, string) =
|
||||
## Validate INSERT/UPDATE constraints.
|
||||
## Returns (success, errorMessage).
|
||||
|
||||
let tbl = ctx.getTableDef(tableName)
|
||||
|
||||
for rowIdx, rowVals in values:
|
||||
for col in tbl.columns:
|
||||
let val = getValue(rowVals, fields, col.name)
|
||||
|
||||
# NOT NULL check
|
||||
if col.isNotNull and isNull(val):
|
||||
return (false, "NOT NULL constraint violated for column '" & col.name & "'")
|
||||
|
||||
# DEFAULT value
|
||||
if isNull(val) and col.defaultVal.len > 0:
|
||||
# We'll handle this in the insert loop
|
||||
discard
|
||||
|
||||
# PRIMARY KEY uniqueness check (against existing data)
|
||||
if 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
|
||||
let (exists, _) = ctx.db.get(pkKey)
|
||||
if exists:
|
||||
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "' for table '" & tableName & "'")
|
||||
|
||||
# UNIQUE constraint check
|
||||
for col in tbl.columns:
|
||||
if col.isUnique:
|
||||
let uVal = getValue(rowVals, fields, col.name)
|
||||
if not isNull(uVal):
|
||||
# Check uniqueness by scanning existing rows
|
||||
let prefix = tableName & "."
|
||||
for entry in ctx.db.scanMemTable():
|
||||
if entry.deleted: continue
|
||||
if entry.key.startsWith(prefix):
|
||||
let existingVal = cast[string](entry.value)
|
||||
let fieldPrefix = col.name & "="
|
||||
for part in existingVal.split(","):
|
||||
if part.startsWith(fieldPrefix) and part[fieldPrefix.len..^1] == uVal:
|
||||
return (false, "UNIQUE constraint violated: duplicate value '" & uVal & "' for column '" & col.name & "'")
|
||||
|
||||
return (true, "")
|
||||
|
||||
proc applyDefaultValues(tbl: TableDef, fields: var seq[string], values: var seq[seq[string]]) =
|
||||
for col in tbl.columns:
|
||||
if col.defaultVal.len == 0: continue
|
||||
var hasField = false
|
||||
for f in fields:
|
||||
if f.toLower() == col.name.toLower():
|
||||
hasField = true
|
||||
break
|
||||
if not hasField:
|
||||
fields.add(col.name)
|
||||
for rowIdx in 0..<values.len:
|
||||
if rowIdx < values.len:
|
||||
values[rowIdx].add(col.defaultVal)
|
||||
else:
|
||||
for rowIdx in 0..<values.len:
|
||||
if rowIdx < values.len:
|
||||
let fidx = fields.len - 1 # approximate
|
||||
var found = false
|
||||
for i, f in fields:
|
||||
if f.toLower() == col.name.toLower() and i < values[rowIdx].len:
|
||||
if isNull(values[rowIdx][i]):
|
||||
values[rowIdx][i] = col.defaultVal
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
# Field may have been added, ensure we have a value
|
||||
discard
|
||||
|
||||
proc execDelete(ctx: ExecutionContext, table: string, key: string): int =
|
||||
proc execDelete*(ctx: ExecutionContext, table: string, key: string): int =
|
||||
let fullKey = table & "." & key
|
||||
let (found, _) = ctx.db.get(fullKey)
|
||||
if found:
|
||||
@@ -291,13 +303,90 @@ proc execDelete(ctx: ExecutionContext, table: string, key: string): int =
|
||||
return 1
|
||||
return 0
|
||||
|
||||
proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Table[string, string]): int =
|
||||
let fullKey = table & "." & key
|
||||
let (found, existing) = ctx.db.get(fullKey)
|
||||
if not found: return 0
|
||||
var parsed = parseRowData(cast[string](existing))
|
||||
for col, val in sets:
|
||||
parsed[col] = val
|
||||
var parts: seq[string] = @[]
|
||||
for col, val in parsed:
|
||||
parts.add(col & "=" & val)
|
||||
let newVal = parts.join(",")
|
||||
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||
discard ctx.txnManager.write(ctx.pendingTxn, fullKey, cast[seq[byte]](newVal))
|
||||
else:
|
||||
ctx.db.put(fullKey, cast[seq[byte]](newVal))
|
||||
return 1
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Constraint Validation
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
||||
fields: seq[string], values: seq[seq[string]]): (bool, string) =
|
||||
let tbl = ctx.getTableDef(tableName)
|
||||
|
||||
for rowIdx, rowVals in values:
|
||||
for col in tbl.columns:
|
||||
let val = getValue(rowVals, fields, col.name)
|
||||
if col.isNotNull and isNull(val):
|
||||
return (false, "NOT NULL constraint violated for column '" & col.name & "'")
|
||||
|
||||
if 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
|
||||
let (exists, _) = ctx.db.get(pkKey)
|
||||
if exists:
|
||||
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "' for table '" & tableName & "'")
|
||||
|
||||
for col in tbl.columns:
|
||||
if col.isUnique:
|
||||
let uVal = getValue(rowVals, fields, col.name)
|
||||
if not isNull(uVal):
|
||||
let idxName = tableName & "." & col.name
|
||||
if idxName in ctx.btrees:
|
||||
if ctx.btrees[idxName].contains(uVal):
|
||||
return (false, "UNIQUE constraint violated: duplicate value '" & uVal & "' for column '" & col.name & "'")
|
||||
|
||||
# CHECK constraints (via table-level cstType="check")
|
||||
for col in tbl.columns:
|
||||
let fkIdx = tableName & "." & col.name
|
||||
# FK check: verify referenced row exists
|
||||
# (skipped for now — needs full table metadata)
|
||||
|
||||
return (true, "")
|
||||
|
||||
proc applyDefaultValues*(tbl: TableDef, fields: var seq[string], values: var seq[seq[string]]) =
|
||||
for col in tbl.columns:
|
||||
if col.defaultVal.len == 0: continue
|
||||
var hasField = false
|
||||
for f in fields:
|
||||
if f.toLower() == col.name.toLower():
|
||||
hasField = true
|
||||
break
|
||||
if not hasField:
|
||||
fields.add(col.name)
|
||||
for rowIdx in 0..<values.len:
|
||||
values[rowIdx].add(col.defaultVal)
|
||||
else:
|
||||
for rowIdx in 0..<values.len:
|
||||
for i, f in fields:
|
||||
if f.toLower() == col.name.toLower() and i < values[rowIdx].len:
|
||||
if isNull(values[rowIdx][i]):
|
||||
values[rowIdx][i] = col.defaultVal
|
||||
break
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# AST → IR Lowering
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc lowerExpr(node: Node): IRExpr =
|
||||
if node == nil:
|
||||
return nil
|
||||
proc lowerExpr*(node: Node): IRExpr =
|
||||
if node == nil: return nil
|
||||
case node.kind
|
||||
of nkIntLit:
|
||||
result = IRExpr(kind: irekLiteral)
|
||||
@@ -355,8 +444,7 @@ proc lowerExpr(node: Node): IRExpr =
|
||||
of "max": result.aggOp = irMax
|
||||
else: result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull))
|
||||
result.aggArgs = @[]
|
||||
for arg in node.funcArgs:
|
||||
result.aggArgs.add(lowerExpr(arg))
|
||||
for arg in node.funcArgs: result.aggArgs.add(lowerExpr(arg))
|
||||
of nkIsExpr:
|
||||
result = IRExpr(kind: irekUnary)
|
||||
result.unOp = if node.isNegated: irIsNotNull else: irIsNull
|
||||
@@ -381,45 +469,38 @@ proc lowerExpr(node: Node): IRExpr =
|
||||
else:
|
||||
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull))
|
||||
|
||||
proc lowerSelect(node: Node): IRPlan =
|
||||
proc lowerSelect*(node: Node): IRPlan =
|
||||
result = IRPlan(kind: irpkScan)
|
||||
if node.selFrom != nil and node.selFrom.fromTable.len > 0:
|
||||
result.scanTable = node.selFrom.fromTable
|
||||
result.scanAlias = node.selFrom.fromAlias
|
||||
|
||||
# WHERE → Filter
|
||||
if node.selWhere != nil and node.selWhere.whereExpr != nil:
|
||||
let filterPlan = IRPlan(kind: irpkFilter)
|
||||
filterPlan.filterSource = result
|
||||
filterPlan.filterCond = lowerExpr(node.selWhere.whereExpr)
|
||||
result = filterPlan
|
||||
|
||||
# GROUP BY
|
||||
if node.selGroupBy.len > 0:
|
||||
let groupPlan = IRPlan(kind: irpkGroupBy)
|
||||
groupPlan.groupSource = result
|
||||
groupPlan.groupKeys = @[]
|
||||
for g in node.selGroupBy:
|
||||
groupPlan.groupKeys.add(lowerExpr(g))
|
||||
for g in node.selGroupBy: groupPlan.groupKeys.add(lowerExpr(g))
|
||||
groupPlan.groupAggs = @[]
|
||||
if node.selHaving != nil:
|
||||
groupPlan.groupHaving = lowerExpr(node.selHaving.havingExpr)
|
||||
result = groupPlan
|
||||
|
||||
# SELECT → Project
|
||||
let projectPlan = IRPlan(kind: irpkProject)
|
||||
projectPlan.projectSource = result
|
||||
projectPlan.projectExprs = @[]
|
||||
projectPlan.projectAliases = @[]
|
||||
for e in node.selResult:
|
||||
projectPlan.projectExprs.add(lowerExpr(e))
|
||||
if e.kind == nkIdent:
|
||||
projectPlan.projectAliases.add(e.identName)
|
||||
else:
|
||||
projectPlan.projectAliases.add("")
|
||||
if e.kind == nkIdent: projectPlan.projectAliases.add(e.identName)
|
||||
else: projectPlan.projectAliases.add("")
|
||||
result = projectPlan
|
||||
|
||||
# ORDER BY → Sort
|
||||
if node.selOrderBy.len > 0:
|
||||
let sortPlan = IRPlan(kind: irpkSort)
|
||||
sortPlan.sortSource = result
|
||||
@@ -430,7 +511,6 @@ proc lowerSelect(node: Node): IRPlan =
|
||||
sortPlan.sortDirs.add(o.orderByDir == sdAsc)
|
||||
result = sortPlan
|
||||
|
||||
# LIMIT/OFFSET
|
||||
if node.selLimit != nil or node.selOffset != nil:
|
||||
let limitPlan = IRPlan(kind: irpkLimit)
|
||||
limitPlan.limitSource = result
|
||||
@@ -440,48 +520,60 @@ proc lowerSelect(node: Node): IRPlan =
|
||||
node.selOffset.offsetExpr.intVal else: 0
|
||||
result = limitPlan
|
||||
|
||||
proc lowerInsert(node: Node): IRPlan =
|
||||
result = IRPlan(kind: irpkInsert)
|
||||
result.insertTable = node.insTarget
|
||||
result.insertFields = @[]
|
||||
for f in node.insFields:
|
||||
if f.kind == nkIdent:
|
||||
result.insertFields.add(f.identName)
|
||||
else:
|
||||
result.insertFields.add("")
|
||||
result.insertValues = @[]
|
||||
for rowNode in node.insValues:
|
||||
var rowVals: seq[IRExpr] = @[]
|
||||
if rowNode.kind == nkArrayLit:
|
||||
for v in rowNode.arrayElems:
|
||||
rowVals.add(lowerExpr(v))
|
||||
else:
|
||||
rowVals.add(lowerExpr(rowNode))
|
||||
result.insertValues.add(rowVals)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# IR Plan Execution
|
||||
# IR Plan Execution (with actual filter/sort/projection)
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc executePlan(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
||||
if plan == nil:
|
||||
return @[]
|
||||
proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
||||
if plan == nil: return @[]
|
||||
|
||||
case plan.kind
|
||||
of irpkScan:
|
||||
result = execScan(ctx, plan.scanTable)
|
||||
return execScan(ctx, plan.scanTable)
|
||||
|
||||
of irpkFilter:
|
||||
let sourceRows = executePlan(ctx, plan.filterSource)
|
||||
result = sourceRows # TODO: actual filter evaluation
|
||||
if plan.filterCond == nil: return sourceRows
|
||||
result = @[]
|
||||
for row in sourceRows:
|
||||
let evalResult = evalExpr(plan.filterCond, row)
|
||||
if evalResult == "true":
|
||||
result.add(row)
|
||||
|
||||
of irpkProject:
|
||||
let sourceRows = executePlan(ctx, plan.projectSource)
|
||||
result = sourceRows
|
||||
if plan.projectAliases.len == 0: return sourceRows
|
||||
result = @[]
|
||||
for row in sourceRows:
|
||||
var newRow: Table[string, string]
|
||||
for i, alias in plan.projectAliases:
|
||||
if i < plan.projectExprs.len:
|
||||
let val = evalExpr(plan.projectExprs[i], row)
|
||||
if alias.len > 0: newRow[alias] = val
|
||||
else: newRow["col" & $i] = val
|
||||
if newRow.len > 0:
|
||||
result.add(newRow)
|
||||
else:
|
||||
result.add(row)
|
||||
|
||||
of irpkSort:
|
||||
let sourceRows = executePlan(ctx, plan.sortSource)
|
||||
result = sourceRows
|
||||
var sourceRows = executePlan(ctx, plan.sortSource)
|
||||
if plan.sortExprs.len == 0: return sourceRows
|
||||
let sortExpr = plan.sortExprs[0]
|
||||
let ascending = if plan.sortDirs.len > 0: plan.sortDirs[0] else: true
|
||||
proc sortCmp(a, b: Row): int =
|
||||
let va = evalExpr(sortExpr, a)
|
||||
let vb = evalExpr(sortExpr, b)
|
||||
try:
|
||||
let fa = parseFloat(va)
|
||||
let fb = parseFloat(vb)
|
||||
if fa < fb: return -1
|
||||
if fa > fb: return 1
|
||||
return 0
|
||||
except:
|
||||
return cmp(va, vb)
|
||||
sourceRows.sort(sortCmp, if ascending: Ascending else: Descending)
|
||||
return sourceRows
|
||||
|
||||
of irpkLimit:
|
||||
let sourceRows = executePlan(ctx, plan.limitSource)
|
||||
@@ -490,33 +582,31 @@ proc executePlan(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
||||
var endIdx = start + int(plan.limitCount)
|
||||
if endIdx > sourceRows.len or plan.limitCount == 0:
|
||||
endIdx = sourceRows.len
|
||||
result = sourceRows[start..<endIdx]
|
||||
return sourceRows[start..<endIdx]
|
||||
|
||||
of irpkGroupBy:
|
||||
result = executePlan(ctx, plan.groupSource)
|
||||
return executePlan(ctx, plan.groupSource)
|
||||
|
||||
of irpkJoin:
|
||||
result = executePlan(ctx, plan.joinLeft)
|
||||
let leftRows = executePlan(ctx, plan.joinLeft)
|
||||
let rightRows = executePlan(ctx, plan.joinRight)
|
||||
return leftRows # simplified: return left side
|
||||
|
||||
else:
|
||||
result = @[]
|
||||
return @[]
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# High-level execute function
|
||||
# High-level execute
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
## Execute a parsed AST statement against the execution context.
|
||||
## Returns (success, errorMessage, affectedRows)
|
||||
proc executeQuery*(ctx: ExecutionContext, astNode: Node): ExecResult =
|
||||
if astNode == nil or astNode.stmts.len == 0:
|
||||
return (true, "", 0)
|
||||
return okResult()
|
||||
|
||||
let stmt = astNode.stmts[0]
|
||||
case stmt.kind
|
||||
of nkSelect:
|
||||
# Check if WHERE clause can use B-Tree index
|
||||
var useIndex = false
|
||||
var idxKey = ""
|
||||
# Try B-Tree index point read first
|
||||
if stmt.selFrom != nil and stmt.selFrom.fromTable.len > 0:
|
||||
if stmt.selWhere != nil and stmt.selWhere.whereExpr != nil:
|
||||
let w = stmt.selWhere.whereExpr
|
||||
@@ -527,23 +617,29 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
if idxName in ctx.btrees:
|
||||
let entries = ctx.btrees[idxName].get(w.binRight.strVal)
|
||||
if entries.len > 0:
|
||||
useIndex = true
|
||||
idxKey = w.binRight.strVal
|
||||
# Fetch actual row data from LSM
|
||||
let rows = execPointRead(ctx, stmt.selFrom.fromTable, colName & "=" & w.binRight.strVal)
|
||||
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
|
||||
var cols: seq[string] = @[]
|
||||
for c in tbl.columns: cols.add(c.name)
|
||||
if cols.len == 0: cols = @["key", "value"]
|
||||
return okResult(rows, cols)
|
||||
|
||||
if useIndex:
|
||||
return (true, "", 1) # Point read via B-Tree (result count)
|
||||
else:
|
||||
let plan = lowerSelect(stmt)
|
||||
let rows = executePlan(ctx, plan)
|
||||
return (true, "", rows.len)
|
||||
# Full pipeline execution
|
||||
let plan = lowerSelect(stmt)
|
||||
let rows = executePlan(ctx, plan)
|
||||
let tbl = ctx.getTableDef(if stmt.selFrom != nil: stmt.selFrom.fromTable else: "")
|
||||
var cols: seq[string] = @[]
|
||||
for c in tbl.columns: cols.add(c.name)
|
||||
if cols.len == 0 and rows.len > 0:
|
||||
for k, _ in rows[0]: cols.add(k)
|
||||
return okResult(rows, cols)
|
||||
|
||||
of nkInsert:
|
||||
var fields: seq[string] = @[]
|
||||
for f in stmt.insFields:
|
||||
if f.kind == nkIdent:
|
||||
fields.add(f.identName)
|
||||
else:
|
||||
fields.add("")
|
||||
if f.kind == nkIdent: fields.add(f.identName)
|
||||
else: fields.add("")
|
||||
|
||||
var values: seq[seq[string]] = @[]
|
||||
for rowNode in stmt.insValues:
|
||||
@@ -562,40 +658,58 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
else: row.add("")
|
||||
values.add(row)
|
||||
|
||||
# If no fields specified, use all columns from table definition
|
||||
if fields.len == 0:
|
||||
let tbl = ctx.getTableDef(stmt.insTarget)
|
||||
if tbl.columns.len > 0:
|
||||
for col in tbl.columns:
|
||||
fields.add(col.name)
|
||||
for col in tbl.columns: fields.add(col.name)
|
||||
|
||||
# Apply DEFAULT values
|
||||
let tbl = ctx.getTableDef(stmt.insTarget)
|
||||
var mutableFields = fields
|
||||
var mutableValues = values
|
||||
applyDefaultValues(tbl, mutableFields, mutableValues)
|
||||
|
||||
# Validate constraints
|
||||
let (valid, errMsg) = validateConstraints(ctx, stmt.insTarget, mutableFields, mutableValues)
|
||||
if not valid:
|
||||
return (false, errMsg, 0)
|
||||
if not valid: return errResult(errMsg)
|
||||
|
||||
let count = execInsert(ctx, stmt.insTarget, mutableFields, mutableValues)
|
||||
return (true, "", count)
|
||||
return okResult(affected=count)
|
||||
|
||||
of nkUpdate:
|
||||
return (true, "", 0)
|
||||
if stmt.updSet.len == 0: return okResult()
|
||||
# Simple UPDATE: scan table, filter by WHERE, apply SET
|
||||
var sets = initTable[string, string]()
|
||||
for s in stmt.updSet:
|
||||
if s.kind == nkBinOp and s.binOp == bkAssign:
|
||||
if s.binLeft.kind == nkIdent:
|
||||
let val = if s.binRight.kind == nkStringLit: s.binRight.strVal
|
||||
elif s.binRight.kind == nkIntLit: $s.binRight.intVal
|
||||
elif s.binRight.kind == nkFloatLit: $s.binRight.floatVal
|
||||
else: ""
|
||||
sets[s.binLeft.identName] = val
|
||||
|
||||
# Scan and apply
|
||||
let rows = execScan(ctx, stmt.updTarget)
|
||||
var count = 0
|
||||
for row in rows:
|
||||
# Check WHERE
|
||||
if stmt.updWhere != nil and stmt.updWhere.whereExpr != nil:
|
||||
let whereExpr = lowerExpr(stmt.updWhere.whereExpr)
|
||||
if evalExpr(whereExpr, row) != "true": continue
|
||||
# Get key from row
|
||||
if "$key" in row:
|
||||
count += execUpdateRow(ctx, stmt.updTarget, row["$key"], sets)
|
||||
return okResult(affected=count)
|
||||
|
||||
of nkDelete:
|
||||
var key = ""
|
||||
if stmt.delWhere != nil and stmt.delWhere.whereExpr != nil:
|
||||
# Extract simple WHERE key = 'value'
|
||||
let w = stmt.delWhere.whereExpr
|
||||
if w.kind == nkBinOp and w.binOp == bkEq:
|
||||
if w.binLeft.kind == nkIdent and w.binRight.kind == nkStringLit:
|
||||
key = w.binLeft.identName & "=" & w.binRight.strVal
|
||||
let count = execDelete(ctx, stmt.delTarget, key)
|
||||
return (true, "", count)
|
||||
# Delete all rows matching WHERE
|
||||
let rows = execScan(ctx, stmt.delTarget)
|
||||
var count = 0
|
||||
for row in rows:
|
||||
if stmt.delWhere != nil and stmt.delWhere.whereExpr != nil:
|
||||
let whereExpr = lowerExpr(stmt.delWhere.whereExpr)
|
||||
if evalExpr(whereExpr, row) != "true": continue
|
||||
if "$key" in row:
|
||||
count += execDelete(ctx, stmt.delTarget, row["$key"])
|
||||
return okResult(affected=count)
|
||||
|
||||
of nkCreateTable:
|
||||
var tbl = TableDef(name: stmt.crtName, columns: @[], pkColumns: @[])
|
||||
@@ -608,25 +722,22 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
of "pkey":
|
||||
colDef.isPk = true
|
||||
tbl.pkColumns.add(col.cdName)
|
||||
let idxName = stmt.crtName & "." & col.cdName
|
||||
var bt = newBTreeIndex[string, IndexEntry]()
|
||||
ctx.btrees[idxName] = bt
|
||||
ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]()
|
||||
of "notnull": colDef.isNotNull = true
|
||||
of "unique":
|
||||
colDef.isUnique = true
|
||||
let idxName2 = stmt.crtName & "." & col.cdName
|
||||
var bt2 = newBTreeIndex[string, IndexEntry]()
|
||||
ctx.btrees[idxName2] = bt2
|
||||
ctx.btrees[stmt.crtName & "." & col.cdName] = newBTreeIndex[string, IndexEntry]()
|
||||
of "default":
|
||||
if cst.cstDefault != nil and cst.cstDefault.kind == nkStringLit:
|
||||
colDef.defaultVal = cst.cstDefault.strVal
|
||||
if cst.cstDefault != nil:
|
||||
if cst.cstDefault.kind == nkStringLit: colDef.defaultVal = cst.cstDefault.strVal
|
||||
elif cst.cstDefault.kind == nkIntLit: colDef.defaultVal = $cst.cstDefault.intVal
|
||||
elif cst.cstDefault.kind == nkBoolLit: colDef.defaultVal = $cst.cstDefault.boolVal
|
||||
elif cst.cstDefault.kind == nkFloatLit: colDef.defaultVal = $cst.cstDefault.floatVal
|
||||
else: discard
|
||||
tbl.columns.add(colDef)
|
||||
ctx.tables[stmt.crtName] = tbl
|
||||
|
||||
# Persist schema as migration
|
||||
let schemaKey = "_schema:migrations:" & $ctx.tables.len
|
||||
let schemaDDL = "CREATE TABLE " & stmt.crtName & " ("
|
||||
# Persist schema
|
||||
var colDefs: seq[string] = @[]
|
||||
for col in tbl.columns:
|
||||
var parts = @[col.name, col.colType]
|
||||
@@ -635,59 +746,50 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
if col.isUnique: parts.add("UNIQUE")
|
||||
if col.defaultVal.len > 0: parts.add("DEFAULT '" & col.defaultVal & "'")
|
||||
colDefs.add(parts.join(" "))
|
||||
ctx.db.put(schemaKey, cast[seq[byte]](colDefs.join(", ") & ")"))
|
||||
let schemaKey = "_schema:migrations:" & $ctx.tables.len
|
||||
ctx.db.put(schemaKey, cast[seq[byte]]("CREATE TABLE " & stmt.crtName & " (" & colDefs.join(", ") & ")"))
|
||||
|
||||
return (true, "", 0)
|
||||
return okResult()
|
||||
|
||||
of nkDropTable:
|
||||
ctx.tables.del(stmt.drtName)
|
||||
# Remove associated B-Tree indexes
|
||||
var toDelete: seq[string] = @[]
|
||||
for idxName, bt in ctx.btrees:
|
||||
if idxName.startsWith(stmt.drtName & "."):
|
||||
toDelete.add(idxName)
|
||||
for idxName in toDelete:
|
||||
ctx.btrees.del(idxName)
|
||||
return (true, "", 0)
|
||||
for idxName in ctx.btrees.keys.toSeq():
|
||||
if idxName.startsWith(stmt.drtName & "."): toDelete.add(idxName)
|
||||
for idxName in toDelete: ctx.btrees.del(idxName)
|
||||
return okResult()
|
||||
|
||||
of nkBeginTxn:
|
||||
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||
# Auto-commit previous pending transaction
|
||||
discard ctx.txnManager.commit(ctx.pendingTxn)
|
||||
ctx.pendingTxn = ctx.txnManager.beginTxn(ilReadCommitted)
|
||||
return (true, "", 0)
|
||||
return okResult(msg="Transaction started")
|
||||
|
||||
of nkCommitTxn:
|
||||
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||
# Flush write set to LSM-Tree
|
||||
for key, version in ctx.pendingTxn.writeSet:
|
||||
if version.value == @[]:
|
||||
ctx.db.delete(key)
|
||||
else:
|
||||
ctx.db.put(key, version.value)
|
||||
if version.value == @[]: ctx.db.delete(key)
|
||||
else: ctx.db.put(key, version.value)
|
||||
discard ctx.txnManager.commit(ctx.pendingTxn)
|
||||
ctx.pendingTxn = nil
|
||||
return (true, "", 0)
|
||||
else:
|
||||
return (false, "No active transaction to commit", 0)
|
||||
return okResult(msg="Transaction committed")
|
||||
return errResult("No active transaction to commit")
|
||||
|
||||
of nkRollbackTxn:
|
||||
if ctx.pendingTxn != nil:
|
||||
discard ctx.txnManager.abortTxn(ctx.pendingTxn)
|
||||
ctx.pendingTxn = nil
|
||||
return (true, "", 0)
|
||||
else:
|
||||
return (false, "No active transaction to rollback", 0)
|
||||
return okResult(msg="Transaction rolled back")
|
||||
return errResult("No active transaction to rollback")
|
||||
|
||||
of nkCreateType:
|
||||
return (true, "", 0)
|
||||
return okResult()
|
||||
|
||||
of nkExplainStmt:
|
||||
if stmt.expStmt != nil and stmt.expStmt.kind == nkSelect:
|
||||
var planStr = "EXPLAIN "
|
||||
if stmt.expStmt.selFrom != nil:
|
||||
planStr &= "SELECT on " & stmt.expStmt.selFrom.fromTable
|
||||
# Check if index can be used
|
||||
var indexUsed = false
|
||||
if stmt.expStmt.selFrom != nil and stmt.expStmt.selFrom.fromTable.len > 0:
|
||||
if stmt.expStmt.selWhere != nil and stmt.expStmt.selWhere.whereExpr != nil:
|
||||
@@ -698,10 +800,12 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
||||
if idxName in ctx.btrees:
|
||||
planStr &= " (using B-Tree index on " & w.binLeft.identName & ")"
|
||||
indexUsed = true
|
||||
if not indexUsed:
|
||||
planStr &= " (full table scan)"
|
||||
return (true, "", 0)
|
||||
return (true, "", 0)
|
||||
if not indexUsed: planStr &= " (full table scan)"
|
||||
return okResult(msg=planStr)
|
||||
return okResult(msg="EXPLAIN")
|
||||
|
||||
of nkAlterTable:
|
||||
return okResult(msg="ALTER TABLE not yet fully implemented")
|
||||
|
||||
else:
|
||||
return (false, "Unsupported statement type: " & $stmt.kind, 0)
|
||||
return errResult("Unsupported statement type: " & $stmt.kind)
|
||||
|
||||
Reference in New Issue
Block a user