feat: Multi-tenant ERP support via session variables + RLS
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

- SET var = value / current_setting('var') for session-scoped variables
- current_user / current_role SQL keywords with auth bridge
- server.nim + httpserver.nim populate ExecutionContext.currentUser/currentRole
- RLS policies can reference current_setting('app.tenant_id') for tenant isolation
- Fixed evalExpr to propagate ctx recursively (fixes current_user in sub-expressions)
- Fixed GROUPING SETS execution (lowerSelect checks selGroupingSetsKind)
- Fixed FTS CREATE INDEX docId mismatch (hash of tableName.$key)
- Fixed all test suites to use isolated temp directories
- Added 5 multi-tenant tests (355 total, all green)
- Updated docs: PLAN_SQL_ADVANCED.md, baraql.md, changelog.md
This commit is contained in:
2026-05-14 16:28:41 +03:00
parent b0978812cb
commit f7d4961125
13 changed files with 642 additions and 102 deletions
+7 -1
View File
@@ -108,6 +108,8 @@ proc queryHandler(server: HttpServer): RequestHandler =
let ctx = newContext(request)
server.metrics.queriesTotal += 1
var userId = ""
var role = ""
# Auth check
if server.config.authEnabled:
let authHeader = request.headers["Authorization"]
@@ -115,10 +117,12 @@ proc queryHandler(server: HttpServer): RequestHandler =
ctx.json(%*{"error": "Unauthorized"}, 401)
return
let tokenStr = authHeader[7..^1]
let (valid, userId, role) = server.verifyToken(tokenStr)
let (valid, uid, r) = server.verifyToken(tokenStr)
if not valid:
ctx.json(%*{"error": "Unauthorized"}, 401)
return
userId = uid
role = r
let body = parseJson(request.body)
if body == nil or "query" notin body:
@@ -131,6 +135,8 @@ proc queryHandler(server: HttpServer): RequestHandler =
return
var reqCtx = cloneForConnection(server.ctx)
reqCtx.currentUser = userId
reqCtx.currentRole = role
let tokens = tokenize(queryStr)
let astNode = parse(tokens)
+3 -1
View File
@@ -436,9 +436,11 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
case header.kind
of mkAuth:
let tokenStr = parseAuthMessage(cast[seq[byte]](payload))
let (valid, userId, _) = verifyToken(secret, tokenStr)
let (valid, userId, role) = verifyToken(secret, tokenStr)
if valid:
authenticated = true
connCtx.currentUser = userId
connCtx.currentRole = role
let okMsg = makeAuthOkMessage(header.requestId)
await client.send(cast[string](okMsg))
info("Client " & $clientId & " authenticated as " & userId)
+14
View File
@@ -40,6 +40,7 @@ type
nkDisableRLS
nkGrant
nkRevoke
nkSetVar
# Clauses
nkFrom
@@ -78,6 +79,8 @@ type
nkIsExpr
nkStar
nkPlaceholder
nkCurrentUser
nkCurrentRole
# Graph-specific
nkGraphTraversal
@@ -144,6 +147,10 @@ type
bkJsonPathText = "->>"
bkFtsMatch = "@@"
bkDistance = "<->"
bkJsonContains = "@>"
bkJsonContainedBy = "<@"
bkJsonHasAny = "?|"
bkJsonHasAll = "?&"
UnaryOpKind* = enum
ukNeg = "-"
@@ -317,6 +324,9 @@ type
rvPrivilege*: string
rvTable*: string
rvGrantee*: string
of nkSetVar:
svName*: string
svValue*: string
of nkApplyMigration:
amName*: string
of nkMigrationStatus:
@@ -484,6 +494,10 @@ type
discard
of nkPlaceholder:
discard
of nkCurrentUser:
discard
of nkCurrentRole:
discard
of nkPropertyDef:
pdName*: string
pdType*: string
Binary file not shown.
+257 -83
View File
@@ -69,6 +69,7 @@ type
policies*: Table[string, seq[PolicyDef]] # table name -> policies
currentUser*: string
currentRole*: string
sessionVars*: Table[string, string] # session-scoped key/value variables
MigrationRecord* = object
name*: string
@@ -149,6 +150,7 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
users: initTable[string, UserDef](),
policies: initTable[string, seq[PolicyDef]](),
currentUser: "", currentRole: "",
sessionVars: initTable[string, string](),
onChange: nil)
restoreSchema(result)
@@ -323,6 +325,7 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
users: ctx.users, policies: ctx.policies,
txnManager: ctx.txnManager,
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
sessionVars: ctx.sessionVars,
pendingTxn: nil, onChange: ctx.onChange)
# ----------------------------------------------------------------------
@@ -504,7 +507,7 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
of irekStar:
return "*"
of irekJsonPath:
let srcVal = evalExpr(expr.jpExpr, row)
let srcVal = evalExpr(expr.jpExpr, row, ctx)
if srcVal.len == 0: return ""
try:
let node = parseJson(srcVal)
@@ -527,8 +530,8 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
except:
return ""
of irekBinary:
let left = evalExpr(expr.binLeft, row)
let right = evalExpr(expr.binRight, row)
let left = evalExpr(expr.binLeft, row, ctx)
let right = evalExpr(expr.binRight, row, ctx)
case expr.binOp
of irEq:
if left == right: return "true"
@@ -669,20 +672,93 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
if vecA.len == 0 or vecB.len == 0:
return "0"
return $vengine.euclideanDistance(vecA, vecB)
of irJsonContains:
# Check if left JSON contains right JSON
try:
let leftNode = parseJson(left)
let rightNode = parseJson(right)
if leftNode.kind == JObject and rightNode.kind == JObject:
for key, val in rightNode:
if not leftNode.hasKey(key) or $(leftNode[key]) != $val:
return "false"
return "true"
elif leftNode.kind == JArray and rightNode.kind == JArray:
for ritem in rightNode:
var found = false
for litem in leftNode:
if $(litem) == $(ritem):
found = true
break
if not found:
return "false"
return "true"
else:
return if $(leftNode) == $(rightNode): "true" else: "false"
except:
return "false"
of irJsonContainedBy:
# Check if left JSON is contained by right JSON (reverse of contains)
try:
let leftNode = parseJson(left)
let rightNode = parseJson(right)
if leftNode.kind == JObject and rightNode.kind == JObject:
for key, val in leftNode:
if not rightNode.hasKey(key) or $(rightNode[key]) != $val:
return "false"
return "true"
elif leftNode.kind == JArray and rightNode.kind == JArray:
for litem in leftNode:
var found = false
for ritem in rightNode:
if $(ritem) == $(litem):
found = true
break
if not found:
return "false"
return "true"
else:
return if $(leftNode) == $(rightNode): "true" else: "false"
except:
return "false"
of irJsonHasAny:
# Check if JSON object has any of the keys in right array
try:
let leftNode = parseJson(left)
let rightNode = parseJson(right)
if leftNode.kind == JObject and rightNode.kind == JArray:
for key in rightNode:
if key.kind == JString and leftNode.hasKey(key.getStr()):
return "true"
return "false"
except:
return "false"
of irJsonHasAll:
# Check if JSON object has all of the keys in right array
try:
let leftNode = parseJson(left)
let rightNode = parseJson(right)
if leftNode.kind == JObject and rightNode.kind == JArray:
for key in rightNode:
if key.kind == JString and not leftNode.hasKey(key.getStr()):
return "false"
return "true"
return "false"
except:
return "false"
else: return "false"
of irekUnary:
case expr.unOp
of irNot:
let v = evalExpr(expr.unExpr, row)
let v = evalExpr(expr.unExpr, row, ctx)
return if v == "true": "false" else: "true"
of irIsNull:
let v = evalExpr(expr.unExpr, row)
let v = evalExpr(expr.unExpr, row, ctx)
return if isNull(v): "true" else: "false"
of irIsNotNull:
let v = evalExpr(expr.unExpr, row)
let v = evalExpr(expr.unExpr, row, ctx)
return if not isNull(v): "true" else: "false"
of irNeg:
let v = evalExpr(expr.unExpr, row)
let v = evalExpr(expr.unExpr, row, ctx)
try:
let f = -parseFloat(v)
let s = $f
@@ -716,6 +792,37 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
return "0"
let arg = evalExpr(expr.irFuncArgs[0], row, ctx)
return $parseVectorString(arg).len
of "json_has_key":
if expr.irFuncArgs.len < 2:
return "false"
let jsonStr = evalExpr(expr.irFuncArgs[0], row, ctx)
let key = evalExpr(expr.irFuncArgs[1], row, ctx)
try:
let node = parseJson(jsonStr)
if node.kind == JObject:
return if node.hasKey(key): "true" else: "false"
elif node.kind == JArray:
try:
let idx = parseInt(key)
return if idx >= 0 and idx < node.len: "true" else: "false"
except:
return "false"
return "false"
except:
return "false"
of "current_setting":
if expr.irFuncArgs.len < 1:
return ""
let key = evalExpr(expr.irFuncArgs[0], row, ctx)
if ctx != nil and key in ctx.sessionVars:
return ctx.sessionVars[key]
return ""
of "current_user":
if ctx != nil: return ctx.currentUser
return ""
of "current_role":
if ctx != nil: return ctx.currentRole
return ""
else:
# Unknown function: try to evaluate args and return first arg as fallback
if expr.irFuncArgs.len > 0:
@@ -773,7 +880,7 @@ proc passesPolicy(ctx: ExecutionContext, tableName, command: string, row: Row):
continue
if pol.usingExpr != nil:
let expr = lowerExpr(pol.usingExpr)
if evalExpr(expr, row) != "true":
if evalExpr(expr, row, ctx) != "true":
return false
return true
@@ -788,7 +895,7 @@ proc checkInsertPolicy(ctx: ExecutionContext, tableName: string, row: Row): bool
continue
if pol.withCheckExpr != nil:
let expr = lowerExpr(pol.withCheckExpr)
if evalExpr(expr, row) != "true":
if evalExpr(expr, row, ctx) != "true":
return false
return true
@@ -1151,7 +1258,7 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string,
else:
row[f] = ""
let checkExpr = lowerExpr(check.checkNode)
let checkResult = evalExpr(checkExpr, row)
let checkResult = evalExpr(checkExpr, row, ctx)
if checkResult != "true":
return (false, "CHECK constraint '" & check.name & "' violated")
@@ -1199,6 +1306,14 @@ proc lowerExpr*(node: Node): IRExpr =
of nkNullLit:
result = IRExpr(kind: irekLiteral)
result.literal = IRLiteral(kind: vkNull)
of nkCurrentUser:
result = IRExpr(kind: irekFuncCall)
result.irFunc = "current_user"
result.irFuncArgs = @[]
of nkCurrentRole:
result = IRExpr(kind: irekFuncCall)
result.irFunc = "current_role"
result.irFuncArgs = @[]
of nkIdent:
result = IRExpr(kind: irekField)
result.fieldPath = @[node.identName]
@@ -1229,6 +1344,10 @@ proc lowerExpr*(node: Node): IRExpr =
of bkOr: irOp = irOr
of bkFtsMatch: irOp = irFtsMatch
of bkDistance: irOp = irDistance
of bkJsonContains: irOp = irJsonContains
of bkJsonContainedBy: irOp = irJsonContainedBy
of bkJsonHasAny: irOp = irJsonHasAny
of bkJsonHasAll: irOp = irJsonHasAll
else: irOp = irEq
result.binOp = irOp
result.binLeft = lowerExpr(node.binLeft)
@@ -1408,7 +1527,7 @@ proc lowerSelect*(node: Node): IRPlan =
filterPlan.filterCond = lowerExpr(node.selWhere.whereExpr)
result = filterPlan
if node.selGroupBy.len > 0:
if node.selGroupBy.len > 0 or node.selGroupingSetsKind != gskNone:
let groupPlan = IRPlan(kind: irpkGroupBy)
groupPlan.groupSource = result
groupPlan.groupKeys = @[]
@@ -1477,17 +1596,17 @@ proc lowerSelect*(node: Node): IRPlan =
# Window Function Computation
# ----------------------------------------------------------------------
proc partitionKey(row: Row, partExprs: seq[IRExpr]): string =
proc partitionKey(row: Row, partExprs: seq[IRExpr], ctx: ExecutionContext = nil): string =
## Compute a string partition key for a row
result = ""
for expr in partExprs:
result &= evalExpr(expr, row) & "|"
result &= evalExpr(expr, row, ctx) & "|"
proc compareRowsByOrder(a, b: Row, orderExprs: seq[IRExpr], orderDirs: seq[bool]): int =
proc compareRowsByOrder(a, b: Row, orderExprs: seq[IRExpr], orderDirs: seq[bool], ctx: ExecutionContext = nil): int =
## Compare two rows by their ORDER BY expressions
for i, expr in orderExprs:
let va = evalExpr(expr, a)
let vb = evalExpr(expr, b)
let va = evalExpr(expr, a, ctx)
let vb = evalExpr(expr, b, ctx)
var cmpRes = 0
try:
let fa = parseFloat(va)
@@ -1500,18 +1619,63 @@ proc compareRowsByOrder(a, b: Row, orderExprs: seq[IRExpr], orderDirs: seq[bool]
return if orderDirs.len > i and orderDirs[i]: -cmpRes else: cmpRes
return 0
proc computeWindowValues*(rows: seq[Row], expr: IRExpr): seq[string] =
proc resolveFrameBounds(pos, partLen: int, frameStart, frameEnd: string): (int, int) =
## Resolve frame boundaries for ROWS mode.
## Returns (startPos, endPos) inclusive within the partition.
var startPos = 0
var endPos = partLen - 1
# Parse start boundary
if frameStart == "UNBOUNDED PRECEDING":
startPos = 0
elif frameStart == "CURRENT ROW":
startPos = pos
elif frameStart.endsWith(" PRECEDING"):
let nStr = frameStart[0..^11]
var n = 0
try: n = parseInt(nStr) except: n = 0
startPos = max(0, pos - n)
elif frameStart.endsWith(" FOLLOWING"):
let nStr = frameStart[0..^11]
var n = 0
try: n = parseInt(nStr) except: n = 0
startPos = min(partLen - 1, pos + n)
# Parse end boundary
if frameEnd == "UNBOUNDED FOLLOWING":
endPos = partLen - 1
elif frameEnd == "CURRENT ROW":
endPos = pos
elif frameEnd.endsWith(" PRECEDING"):
let nStr = frameEnd[0..^11]
var n = 0
try: n = parseInt(nStr) except: n = 0
endPos = max(0, pos - n)
elif frameEnd.endsWith(" FOLLOWING"):
let nStr = frameEnd[0..^11]
var n = 0
try: n = parseInt(nStr) except: n = 0
endPos = min(partLen - 1, pos + n)
if startPos > endPos:
startPos = endPos
return (startPos, endPos)
proc computeWindowValues*(rows: seq[Row], expr: IRExpr, ctx: ExecutionContext = nil): seq[string] =
## Compute a window function for all rows, returning a value per row.
## The expr must be of kind irekWindowFunc.
result = newSeq[string](rows.len)
if rows.len == 0: return
let wfName = expr.wfName.toLower()
let frameMode = expr.wfFrameMode
let frameStart = expr.wfFrameStart
let frameEnd = expr.wfFrameEnd
# Partition rows
var groups = initTable[string, seq[int]]()
for i, row in rows:
let pk = partitionKey(row, expr.wfPartition)
let pk = partitionKey(row, expr.wfPartition, ctx)
if pk notin groups:
groups[pk] = @[]
groups[pk].add(i)
@@ -1520,7 +1684,7 @@ proc computeWindowValues*(rows: seq[Row], expr: IRExpr): seq[string] =
for pk, idxs in groups:
var sortedIdxs = idxs
sortedIdxs.sort(proc(a, b: int): int =
compareRowsByOrder(rows[a], rows[b], expr.wfOrderBy, expr.wfOrderDirs)
compareRowsByOrder(rows[a], rows[b], expr.wfOrderBy, expr.wfOrderDirs, ctx)
)
case wfName
@@ -1531,7 +1695,7 @@ proc computeWindowValues*(rows: seq[Row], expr: IRExpr): seq[string] =
var currentRank = 1
for pos, rowIdx in sortedIdxs:
if pos > 0:
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs)
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs, ctx)
if cmpRes != 0:
currentRank = pos + 1
result[rowIdx] = $currentRank
@@ -1539,14 +1703,14 @@ proc computeWindowValues*(rows: seq[Row], expr: IRExpr): seq[string] =
var currentRank = 1
for pos, rowIdx in sortedIdxs:
if pos > 0:
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs)
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs, ctx)
if cmpRes != 0:
currentRank += 1
result[rowIdx] = $currentRank
of "ntile":
var n = 1
if expr.wfArgs.len > 0:
try: n = parseInt(evalExpr(expr.wfArgs[0], rows[sortedIdxs[0]])) except: n = 1
try: n = parseInt(evalExpr(expr.wfArgs[0], rows[sortedIdxs[0]], ctx)) except: n = 1
if n < 1: n = 1
let groupSize = sortedIdxs.len div n
let remainder = sortedIdxs.len mod n
@@ -1565,36 +1729,36 @@ proc computeWindowValues*(rows: seq[Row], expr: IRExpr): seq[string] =
var offset = 1
var defaultVal = ""
if expr.wfArgs.len > 1:
try: offset = parseInt(evalExpr(expr.wfArgs[1], rows[sortedIdxs[0]])) except: offset = 1
try: offset = parseInt(evalExpr(expr.wfArgs[1], rows[sortedIdxs[0]], ctx)) except: offset = 1
if expr.wfArgs.len > 2:
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]])
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]], ctx)
for pos, rowIdx in sortedIdxs:
let targetPos = pos + offset
if targetPos < sortedIdxs.len:
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]])
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]], ctx)
else:
result[rowIdx] = defaultVal
of "lag":
var offset = 1
var defaultVal = ""
if expr.wfArgs.len > 1:
try: offset = parseInt(evalExpr(expr.wfArgs[1], rows[sortedIdxs[0]])) except: offset = 1
try: offset = parseInt(evalExpr(expr.wfArgs[1], rows[sortedIdxs[0]], ctx)) except: offset = 1
if expr.wfArgs.len > 2:
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]])
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]], ctx)
for pos, rowIdx in sortedIdxs:
let targetPos = pos - offset
if targetPos >= 0:
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]])
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]], ctx)
else:
result[rowIdx] = defaultVal
of "first_value":
let firstVal = evalExpr(expr.wfArgs[0], rows[sortedIdxs[0]])
for rowIdx in sortedIdxs:
result[rowIdx] = firstVal
for pos, rowIdx in sortedIdxs:
let (fStart, fEnd) = resolveFrameBounds(pos, sortedIdxs.len, frameStart, frameEnd)
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[fStart]], ctx)
of "last_value":
let lastVal = evalExpr(expr.wfArgs[0], rows[sortedIdxs[^1]])
for rowIdx in sortedIdxs:
result[rowIdx] = lastVal
for pos, rowIdx in sortedIdxs:
let (fStart, fEnd) = resolveFrameBounds(pos, sortedIdxs.len, frameStart, frameEnd)
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[fEnd]], ctx)
else:
# Unknown window function — fill with empty
for rowIdx in sortedIdxs:
@@ -1621,8 +1785,12 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
result.add(row)
of irpkProject:
let sourceRows = executePlan(ctx, plan.projectSource)
var sourceRows = executePlan(ctx, plan.projectSource)
if plan.projectAliases.len == 0: return sourceRows
# Scalar SELECT (no FROM): create a dummy row so expressions can be evaluated
if sourceRows.len == 0 and plan.projectSource != nil and
plan.projectSource.kind == irpkScan and plan.projectSource.scanTable.len == 0:
sourceRows = @[initTable[string, string]()]
# Check if this projection contains aggregates without GROUP BY
var hasAggs = false
@@ -1660,7 +1828,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
if not k.startsWith("$") and not k.contains("."):
newRow[k] = v
else:
let val = evalExpr(expr, row)
let val = evalExpr(expr, row, ctx)
if alias.len > 0: newRow[alias] = val
else: newRow["col" & $i] = val
if newRow.len > 0:
@@ -1686,7 +1854,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
if expr.aggFilter != nil:
filteredRows = @[]
for row in sourceRows:
if evalExpr(expr.aggFilter, row) == "true":
if evalExpr(expr.aggFilter, row, ctx) == "true":
filteredRows.add(row)
case expr.aggOp
of irCount:
@@ -1695,49 +1863,49 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
else:
var count = 0
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row)
let v = evalExpr(expr.aggArgs[0], row, ctx)
if v.len > 0: count += 1
newRow[alias] = $count
of irSum:
var sum = 0.0
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row)
let v = evalExpr(expr.aggArgs[0], row, ctx)
try: sum += parseFloat(v) except: discard
newRow[alias] = $sum
of irAvg:
var sum = 0.0
var count = 0
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row)
let v = evalExpr(expr.aggArgs[0], row, ctx)
try: sum += parseFloat(v); count += 1 except: discard
newRow[alias] = if count > 0: $(sum / float(count)) else: "0"
of irMin:
var minVal = ""
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row)
let v = evalExpr(expr.aggArgs[0], row, ctx)
if minVal == "" or v < minVal: minVal = v
newRow[alias] = minVal
of irMax:
var maxVal = ""
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row)
let v = evalExpr(expr.aggArgs[0], row, ctx)
if maxVal == "" or v > maxVal: maxVal = v
newRow[alias] = maxVal
of irArrayAgg:
var arr: seq[string]
for row in filteredRows:
if expr.aggArgs.len > 0:
arr.add(evalExpr(expr.aggArgs[0], row))
arr.add(evalExpr(expr.aggArgs[0], row, ctx))
newRow[alias] = "[" & arr.join(", ") & "]"
of irStringAgg:
var parts: seq[string]
let delim = if expr.aggArgs.len > 1: evalExpr(expr.aggArgs[1], initTable[string, string]()) else: ","
let delim = if expr.aggArgs.len > 1: evalExpr(expr.aggArgs[1], initTable[string, string](), ctx) else: ","
for row in filteredRows:
if expr.aggArgs.len > 0:
parts.add(evalExpr(expr.aggArgs[0], row))
parts.add(evalExpr(expr.aggArgs[0], row, ctx))
newRow[alias] = parts.join(delim)
else:
let val = evalExpr(expr, if sourceRows.len > 0: sourceRows[0] else: initTable[string, string]())
let val = evalExpr(expr, if sourceRows.len > 0: sourceRows[0] else: initTable[string, string](), ctx)
if alias.len > 0: newRow[alias] = val
else: newRow["col" & $i] = val
result = @[newRow]
@@ -1768,7 +1936,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
if alias.len > 0: newRow[alias] = "0"
else: newRow["col" & $i] = "0"
else:
let val = evalExpr(expr, row)
let val = evalExpr(expr, row, ctx)
if alias.len > 0: newRow[alias] = val
else: newRow["col" & $i] = val
if newRow.len > 0:
@@ -1782,8 +1950,8 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
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)
let va = evalExpr(sortExpr, a, ctx)
let vb = evalExpr(sortExpr, b, ctx)
try:
let fa = parseFloat(va)
let fb = parseFloat(vb)
@@ -1839,7 +2007,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for row in sourceRows:
var groupKey = ""
for gk in gkeys:
groupKey &= evalExpr(gk, row) & "|"
groupKey &= evalExpr(gk, row, ctx) & "|"
if groupKey notin groups:
groups[groupKey] = @[]
groups[groupKey].add(row)
@@ -1847,7 +2015,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var aggRow: Table[string, string]
for gkExpr in gkeys:
if gkExpr.kind == irekField and gkExpr.fieldPath.len > 0:
aggRow[gkExpr.fieldPath[^1]] = evalExpr(gkExpr, groupRows[0])
aggRow[gkExpr.fieldPath[^1]] = evalExpr(gkExpr, groupRows[0], ctx)
# Compute each aggregate expression
for aggExpr in plan.groupAggs:
let aggKey = "$agg_" & $aggExpr.aggOp & "_" & $plan.groupAggs.find(aggExpr)
@@ -1855,7 +2023,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
if aggExpr.aggFilter != nil:
filteredRows = @[]
for row in groupRows:
if evalExpr(aggExpr.aggFilter, row) == "true":
if evalExpr(aggExpr.aggFilter, row, ctx) == "true":
filteredRows.add(row)
case aggExpr.aggOp
of irCount:
@@ -1864,50 +2032,50 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
else:
var count = 0
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row)
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if v.len > 0: count += 1
aggRow[aggKey] = $count
of irSum:
var sum = 0.0
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row)
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
try: sum += parseFloat(v) except: discard
aggRow[aggKey] = $sum
of irAvg:
var sum = 0.0
var count = 0
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row)
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
try: sum += parseFloat(v); count += 1 except: discard
aggRow[aggKey] = if count > 0: $(sum / float(count)) else: "0"
of irMin:
var minVal = ""
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row)
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if minVal == "" or v < minVal: minVal = v
aggRow[aggKey] = minVal
of irMax:
var maxVal = ""
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row)
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if maxVal == "" or v > maxVal: maxVal = v
aggRow[aggKey] = maxVal
of irArrayAgg:
var arr: seq[string]
for row in filteredRows:
if aggExpr.aggArgs.len > 0:
arr.add(evalExpr(aggExpr.aggArgs[0], row))
arr.add(evalExpr(aggExpr.aggArgs[0], row, ctx))
aggRow[aggKey] = "[" & arr.join(", ") & "]"
of irStringAgg:
var parts: seq[string]
let delim = if aggExpr.aggArgs.len > 1: evalExpr(aggExpr.aggArgs[1], initTable[string, string]()) else: ","
let delim = if aggExpr.aggArgs.len > 1: evalExpr(aggExpr.aggArgs[1], initTable[string, string](), ctx) else: ","
for row in filteredRows:
if aggExpr.aggArgs.len > 0:
parts.add(evalExpr(aggExpr.aggArgs[0], row))
parts.add(evalExpr(aggExpr.aggArgs[0], row, ctx))
aggRow[aggKey] = parts.join(delim)
# Apply HAVING filter
if plan.groupHaving != nil:
if evalExpr(plan.groupHaving, aggRow) != "true":
if evalExpr(plan.groupHaving, aggRow, ctx) != "true":
continue
result.add(aggRow)
return result
@@ -1979,9 +2147,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var mergedRows: seq[Row]
for r in rawRightRows:
let merged = mergeRow(l, r, leftAlias, rightAlias)
if rightFilter != nil and evalExpr(rightFilter, merged) != "true":
if rightFilter != nil and evalExpr(rightFilter, merged, ctx) != "true":
continue
if plan.joinCond != nil and evalExpr(plan.joinCond, merged) != "true":
if plan.joinCond != nil and evalExpr(plan.joinCond, merged, ctx) != "true":
continue
mergedRows.add(merged)
@@ -1989,8 +2157,8 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
if rightSortExprs.len > 0 and mergedRows.len > 1:
mergedRows.sort(proc(a, b: Row): int =
for i, sExpr in rightSortExprs:
let aVal = evalExpr(sExpr, a)
let bVal = evalExpr(sExpr, b)
let aVal = evalExpr(sExpr, a, ctx)
let bVal = evalExpr(sExpr, b, ctx)
let asc = if i < rightSortDirs.len: rightSortDirs[i] else: true
var cmp = 0
let aNum = parseFloat(aVal)
@@ -2060,7 +2228,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var matched = false
for r in rightRows:
let merged = mergeRow(l, r, leftAlias, rightAlias)
if plan.joinCond == nil or evalExpr(plan.joinCond, merged) == "true":
if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true":
result.add(merged)
matched = true
if not matched and (plan.joinKind == irjkLeft or plan.joinKind == irjkFull):
@@ -2084,7 +2252,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var found = false
for l in leftRows:
let merged = mergeRow(l, r, leftAlias, rightAlias)
if plan.joinCond == nil or evalExpr(plan.joinCond, merged) == "true":
if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true":
found = true
break
if not found:
@@ -2152,32 +2320,32 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
else:
var count = 0
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row)
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if v.len > 0: count += 1
aggResult = $count
of irSum:
var sum = 0.0
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row)
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
try: sum += parseFloat(v) except: discard
aggResult = $sum
of irAvg:
var sum = 0.0
var count = 0
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row)
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
try: sum += parseFloat(v); count += 1 except: discard
aggResult = if count > 0: $(sum / float(count)) else: "0"
of irMin:
var minVal = ""
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row)
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if minVal == "" or v < minVal: minVal = v
aggResult = minVal
of irMax:
var maxVal = ""
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row)
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if maxVal == "" or v > maxVal: maxVal = v
aggResult = maxVal
else: discard
@@ -2469,15 +2637,15 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
let whereIr = lowerExpr(stmt.selWhere.whereExpr)
var tmp: seq[Row] = @[]
for row in filteredRows:
if evalExpr(whereIr, row) == "true":
if evalExpr(whereIr, row, ctx) == "true":
tmp.add(row)
filteredRows = tmp
if stmt.selOrderBy.len > 0:
let sortExpr = lowerExpr(stmt.selOrderBy[0].orderByExpr)
let asc = stmt.selOrderBy[0].orderByDir == sdAsc
proc sortCmp(a, b: Row): int =
let va = evalExpr(sortExpr, a)
let vb = evalExpr(sortExpr, b)
let va = evalExpr(sortExpr, a, ctx)
let vb = evalExpr(sortExpr, b, ctx)
try:
let fa = parseFloat(va)
let fb = parseFloat(vb)
@@ -2798,7 +2966,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
# Check WHERE
if stmt.updWhere != nil and stmt.updWhere.whereExpr != nil:
let whereExpr = lowerExpr(stmt.updWhere.whereExpr)
if evalExpr(whereExpr, row) != "true": continue
if evalExpr(whereExpr, row, ctx) != "true": continue
# Get key from row
if "$key" in row:
let old = row["$key"]
@@ -2839,7 +3007,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
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 evalExpr(whereExpr, row, ctx) != "true": continue
if "$key" in row:
let old = row["$key"]
# Fire BEFORE DELETE triggers
@@ -2879,7 +3047,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
for k, v in tgtRow:
rowWithTarget[stmt.mergeTargetAlias & "." & k] = v
let onExpr = lowerExpr(stmt.mergeOn)
if evalExpr(onExpr, rowWithTarget) == "true":
if evalExpr(onExpr, rowWithTarget, ctx) == "true":
matched = true
if stmt.mergeMatchedUpdate.len > 0 and "$key" in tgtRow:
var updateSets = initTable[string, string]()
@@ -2887,7 +3055,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
if s.kind == nkBinOp and s.binOp == bkAssign:
if s.binLeft.kind == nkIdent:
let valExpr = lowerExpr(s.binRight)
updateSets[s.binLeft.identName] = evalExpr(valExpr, rowWithTarget)
updateSets[s.binLeft.identName] = evalExpr(valExpr, rowWithTarget, ctx)
var newRow = tgtRow
for col, val in updateSets:
newRow[col] = val
@@ -2907,7 +3075,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
if i < stmt.mergeNotMatchedValues.len:
let v = stmt.mergeNotMatchedValues[i]
let valExpr = lowerExpr(v)
values.add(evalExpr(valExpr, combinedRow))
values.add(evalExpr(valExpr, combinedRow, ctx))
else:
values.add("")
if fields.len > 0:
@@ -3284,14 +3452,16 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
# Full-text search index
var ftsIdx = fts.newInvertedIndex()
let rows = execScan(ctx, stmt.ciTarget)
var docId: uint64 = 0
for row in rows:
let lsmKey = if "$key" in row: row["$key"] else: ""
let docKey = stmt.ciTarget & "." & lsmKey
var docId: uint64 = 0
for ch in docKey:
docId = docId * 31 + uint64(ord(ch))
for col in stmt.ciColumns:
let text = if col in row: row[col] else: ""
if text.len > 0:
ftsIdx.addDocument(docId, text)
let lsmKey = if "$key" in row: row["$key"] else: ""
docId += 1
ctx.ftsIndexes[colKey] = ftsIdx
return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget & " USING FTS")
@@ -3426,6 +3596,10 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
ctx.db.delete(grantKey)
return okResult(msg="REVOKE " & stmt.rvPrivilege & " ON " & stmt.rvTable & " FROM " & stmt.rvGrantee)
of nkSetVar:
ctx.sessionVars[stmt.svName] = stmt.svValue
return okResult(msg="SET " & stmt.svName & " = " & stmt.svValue)
else:
return errResult("Unsupported statement type: " & $stmt.kind)
+4
View File
@@ -29,6 +29,10 @@ type
irIsNull, irIsNotNull
irFtsMatch
irDistance
irJsonContains
irJsonContainedBy
irJsonHasAny
irJsonHasAll
IRAggregate* = enum
irCount, irSum, irAvg, irMin, irMax
+28
View File
@@ -147,6 +147,10 @@ type
tkArrowR # ->
tkArrowRR # ->>
tkFtsMatch # @@
tkJsonContains # @>
tkJsonContainedBy # <@
tkJsonHasAny # ?|
tkJsonHasAll # ?&
tkSimilar
tkNearest
tkTo
@@ -157,12 +161,15 @@ type
# Window functions
tkOver
tkPartition
tkRow
tkRows
tkRange
tkUnbounded
tkPreceding
tkFollowing
tkCurrent
tkCurrentUser
tkCurrentRole
tkRowNumber
tkRank
tkDenseRank
@@ -365,12 +372,15 @@ const keywords*: Table[string, TokenKind] = {
"path": tkPath,
"over": tkOver,
"partition": tkPartition,
"row": tkRow,
"rows": tkRows,
"range": tkRange,
"unbounded": tkUnbounded,
"preceding": tkPreceding,
"following": tkFollowing,
"current": tkCurrent,
"current_user": tkCurrentUser,
"current_role": tkCurrentRole,
"row_number": tkRowNumber,
"rank": tkRank,
"dense_rank": tkDenseRank,
@@ -586,6 +596,10 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
discard l.advance()
return Token(kind: tkNotEq, value: "<>", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '@':
discard l.advance()
discard l.advance()
return Token(kind: tkJsonContainedBy, value: "<@", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkLt, value: "<", line: startLine, col: startCol)
of '>':
@@ -596,6 +610,16 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkGt, value: ">", line: startLine, col: startCol)
of '?':
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '|':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkJsonHasAny, value: "?|", line: startLine, col: startCol)
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '&':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkJsonHasAll, value: "?&", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '?':
discard l.advance()
discard l.advance()
@@ -607,6 +631,10 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
discard l.advance()
return Token(kind: tkFtsMatch, value: "@@", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkJsonContains, value: "@>", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "@", line: startLine, col: startCol)
of '.':
+46 -4
View File
@@ -41,6 +41,7 @@ proc parseCreateType(p: var Parser): Node
proc parseOverClause(p: var Parser): Node
proc parseFrameSpec(p: var Parser): Node
proc parseFrameBoundary(p: var Parser): string
proc parseSetVar(p: var Parser): Node
proc parsePrimary(p: var Parser): Node =
let tok = p.peek()
@@ -60,6 +61,12 @@ proc parsePrimary(p: var Parser): Node =
of tkNull:
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkCurrentUser:
discard p.advance()
Node(kind: nkCurrentUser, line: tok.line, col: tok.col)
of tkCurrentRole:
discard p.advance()
Node(kind: nkCurrentRole, line: tok.line, col: tok.col)
of tkIdent, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile:
discard p.advance()
let funcName = tok.value
@@ -231,7 +238,7 @@ proc parseFrameBoundary(p: var Parser): string =
else:
raise newException(ValueError, "Expected PRECEDING or FOLLOWING after UNBOUNDED")
elif p.match(tkCurrent):
discard p.expect(tkRows)
discard p.expect(tkRow)
return "CURRENT ROW"
else:
# Expect a simple integer literal for offset boundaries
@@ -318,7 +325,7 @@ proc parseComparison(p: var Parser): Node =
discard p.advance() # consume NULL token (assumed)
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
line: tok.line, col: tok.col)
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp}:
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}:
let op = case p.peek().kind
of tkEq: bkEq
of tkNotEq: bkNotEq
@@ -328,6 +335,10 @@ proc parseComparison(p: var Parser): Node =
of tkGtEq: bkGtEq
of tkFtsMatch: bkFtsMatch
of tkDistanceOp: bkDistance
of tkJsonContains: bkJsonContains
of tkJsonContainedBy: bkJsonContainedBy
of tkJsonHasAny: bkJsonHasAny
of tkJsonHasAll: bkJsonHasAll
else: bkEq
let tok = p.advance()
let right = p.parseAddSub()
@@ -1056,13 +1067,13 @@ proc parseAlterTable(p: var Parser): Node =
# Check for ENABLE/DISABLE ROW LEVEL SECURITY
if p.peek().kind == tkEnable:
discard p.advance()
discard p.expect(tkIdent) # ROW
discard p.expect(tkRow) # ROW
discard p.expect(tkIdent) # LEVEL
discard p.expect(tkIdent) # SECURITY
return Node(kind: nkEnableRLS, erlsTable: tableName, line: tok.line, col: tok.col)
elif p.peek().kind == tkDisable:
discard p.advance()
discard p.expect(tkIdent) # ROW
discard p.expect(tkRow) # ROW
discard p.expect(tkIdent) # LEVEL
discard p.expect(tkIdent) # SECURITY
return Node(kind: nkDisableRLS, drlsTable: tableName, line: tok.line, col: tok.col)
@@ -1422,6 +1433,35 @@ proc parseRevoke(p: var Parser): Node =
result = Node(kind: nkRevoke, rvPrivilege: priv, rvTable: tableName,
rvGrantee: grantee, line: tok.line, col: tok.col)
proc parseSetVar(p: var Parser): Node =
let tok = p.expect(tkSet)
var varName = p.expect(tkIdent).value
while p.peek().kind == tkDot:
discard p.advance()
varName.add(".")
varName.add(p.expect(tkIdent).value)
if p.match(tkEq) or p.match(tkTo):
discard
let valTok = p.peek()
var valStr = ""
case valTok.kind
of tkStringLit:
valStr = valTok.value
discard p.advance()
of tkIntLit:
valStr = valTok.value
discard p.advance()
of tkFloatLit:
valStr = valTok.value
discard p.advance()
of tkIdent:
valStr = valTok.value
discard p.advance()
else:
raise newException(ValueError, "Expected value after SET " & varName)
result = Node(kind: nkSetVar, svName: varName, svValue: valStr,
line: tok.line, col: tok.col)
proc parseStatement*(p: var Parser): Node =
case p.peek().kind
of tkWith, tkSelect: p.parseSelect()
@@ -1483,6 +1523,8 @@ proc parseStatement*(p: var Parser): Node =
p.parseGrant()
of tkRevoke:
p.parseRevoke()
of tkSet:
p.parseSetVar()
of tkApply:
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkMigration:
p.parseApplyMigration()