feat(sql): Window Functions + MERGE statement + REST bridge plan
- Add Window Functions: ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE with PARTITION BY, ORDER BY, ROWS/RANGE frame specifications - Add MERGE/UPSERT statement with WHEN MATCHED UPDATE and WHEN NOT MATCHED INSERT - Add SQL/PGQ Property Graph long-term plan in PLAN_SQL_ADVANCED.md - Add 7 new tests for Window Functions and MERGE - Update baraql.md documentation
This commit is contained in:
@@ -8,6 +8,7 @@ type
|
||||
nkInsert
|
||||
nkUpdate
|
||||
nkDelete
|
||||
nkMerge
|
||||
nkCreateType
|
||||
nkDropType
|
||||
nkAlterType
|
||||
@@ -102,6 +103,12 @@ type
|
||||
nkColumnDef
|
||||
nkConstraintDef
|
||||
|
||||
# Window functions
|
||||
nkWindowExpr
|
||||
nkOverClause
|
||||
nkFrameSpec
|
||||
nkFrameBoundary
|
||||
|
||||
# Top-level
|
||||
nkStatementList
|
||||
|
||||
@@ -189,6 +196,15 @@ type
|
||||
delAlias*: string
|
||||
delWhere*: Node
|
||||
delReturning*: seq[Node]
|
||||
of nkMerge:
|
||||
mergeTarget*: string
|
||||
mergeTargetAlias*: string
|
||||
mergeSource*: Node
|
||||
mergeSourceAlias*: string
|
||||
mergeOn*: Node
|
||||
mergeMatchedUpdate*: seq[Node] # SET assignments, empty if no UPDATE
|
||||
mergeNotMatchedInsert*: seq[Node] # column list for INSERT
|
||||
mergeNotMatchedValues*: seq[Node] # value expressions for INSERT
|
||||
of nkCreateType:
|
||||
ctName*: string
|
||||
ctBases*: seq[string]
|
||||
@@ -456,6 +472,24 @@ type
|
||||
idName*: string
|
||||
idExpr*: Node
|
||||
idKind*: IndexKind
|
||||
of nkWindowExpr:
|
||||
winFunc*: string
|
||||
winArgs*: seq[Node]
|
||||
winOver*: Node
|
||||
of nkOverClause:
|
||||
overPartition*: seq[Node]
|
||||
overOrderBy*: seq[Node]
|
||||
overFrame*: Node
|
||||
of nkFrameSpec:
|
||||
frameMode*: string # "ROWS" or "RANGE"
|
||||
frameStartType*: string # "UNBOUNDED PRECEDING", "CURRENT ROW", "EXPR PRECEDING", "EXPR FOLLOWING"
|
||||
frameStartExpr*: Node
|
||||
frameEndType*: string
|
||||
frameEndExpr*: Node
|
||||
frameExclude*: string # "NO OTHERS", "CURRENT ROW", "GROUP", "TIES"
|
||||
of nkFrameBoundary:
|
||||
fbType*: string # "UNBOUNDED PRECEDING", "CURRENT ROW", "EXPR PRECEDING", "EXPR FOLLOWING", "UNBOUNDED FOLLOWING"
|
||||
fbExpr*: Node
|
||||
of nkStatementList:
|
||||
stmts*: seq[Node]
|
||||
|
||||
@@ -470,5 +504,11 @@ proc newNode*(kind: NodeKind, line: int = 0, col: int = 0): Node =
|
||||
of nkAlterTable: result.altOps = @[]
|
||||
of nkColumnDef: result.cdConstraints = @[]
|
||||
of nkConstraintDef: result.cstColumns = @[]; result.cstRefColumns = @[]
|
||||
of nkWindowExpr: result.winArgs = @[]
|
||||
of nkOverClause: result.overPartition = @[]; result.overOrderBy = @[]
|
||||
of nkMerge:
|
||||
result.mergeMatchedUpdate = @[]
|
||||
result.mergeNotMatchedInsert = @[]
|
||||
result.mergeNotMatchedValues = @[]
|
||||
of nkStatementList: result.stmts = @[]
|
||||
else: discard
|
||||
|
||||
@@ -194,6 +194,16 @@ proc codegenPlan*(plan: IRPlan): StorageOp =
|
||||
|
||||
of irpkExplain:
|
||||
return codegenPlan(plan.explainPlan)
|
||||
of irpkWindow:
|
||||
let sourceOp = codegenPlan(plan.winSource)
|
||||
let op = newStorageOp(sokProject)
|
||||
for wf in plan.winFuncs:
|
||||
op.columns.add(wf.wfName)
|
||||
if sourceOp != nil:
|
||||
op.children.add(sourceOp)
|
||||
return op
|
||||
of irpkMerge:
|
||||
return newStorageOp(sokScan)
|
||||
|
||||
proc estimateCost*(op: StorageOp): float64 =
|
||||
if op == nil:
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -1183,6 +1183,28 @@ proc lowerExpr*(node: Node): IRExpr =
|
||||
result = IRExpr(kind: irekExists)
|
||||
of nkStar:
|
||||
result = IRExpr(kind: irekStar)
|
||||
of nkWindowExpr:
|
||||
result = IRExpr(kind: irekWindowFunc)
|
||||
result.wfName = node.winFunc
|
||||
result.wfArgs = @[]
|
||||
for arg in node.winArgs: result.wfArgs.add(lowerExpr(arg))
|
||||
result.wfPartition = @[]
|
||||
if node.winOver != nil:
|
||||
for part in node.winOver.overPartition:
|
||||
result.wfPartition.add(lowerExpr(part))
|
||||
result.wfOrderBy = @[]
|
||||
result.wfOrderDirs = @[]
|
||||
for ob in node.winOver.overOrderBy:
|
||||
result.wfOrderBy.add(lowerExpr(ob.orderByExpr))
|
||||
result.wfOrderDirs.add(ob.orderByDir == sdDesc)
|
||||
if node.winOver.overFrame != nil:
|
||||
result.wfFrameMode = node.winOver.overFrame.frameMode
|
||||
result.wfFrameStart = node.winOver.overFrame.frameStartType
|
||||
result.wfFrameEnd = node.winOver.overFrame.frameEndType
|
||||
else:
|
||||
result.wfFrameMode = "ROWS"
|
||||
result.wfFrameStart = "UNBOUNDED PRECEDING"
|
||||
result.wfFrameEnd = "CURRENT ROW"
|
||||
else:
|
||||
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull))
|
||||
|
||||
@@ -1265,6 +1287,133 @@ proc lowerSelect*(node: Node): IRPlan =
|
||||
node.selOffset.offsetExpr.intVal else: 0
|
||||
result = limitPlan
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Window Function Computation
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc partitionKey(row: Row, partExprs: seq[IRExpr]): string =
|
||||
## Compute a string partition key for a row
|
||||
result = ""
|
||||
for expr in partExprs:
|
||||
result &= evalExpr(expr, row) & "|"
|
||||
|
||||
proc compareRowsByOrder(a, b: Row, orderExprs: seq[IRExpr], orderDirs: seq[bool]): int =
|
||||
## Compare two rows by their ORDER BY expressions
|
||||
for i, expr in orderExprs:
|
||||
let va = evalExpr(expr, a)
|
||||
let vb = evalExpr(expr, b)
|
||||
var cmpRes = 0
|
||||
try:
|
||||
let fa = parseFloat(va)
|
||||
let fb = parseFloat(vb)
|
||||
if fa < fb: cmpRes = -1
|
||||
elif fa > fb: cmpRes = 1
|
||||
except:
|
||||
cmpRes = cmp(va, vb)
|
||||
if cmpRes != 0:
|
||||
return if orderDirs.len > i and orderDirs[i]: -cmpRes else: cmpRes
|
||||
return 0
|
||||
|
||||
proc computeWindowValues*(rows: seq[Row], expr: IRExpr): 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()
|
||||
|
||||
# Partition rows
|
||||
var groups = initTable[string, seq[int]]()
|
||||
for i, row in rows:
|
||||
let pk = partitionKey(row, expr.wfPartition)
|
||||
if pk notin groups:
|
||||
groups[pk] = @[]
|
||||
groups[pk].add(i)
|
||||
|
||||
# For each partition, sort by ORDER BY
|
||||
for pk, idxs in groups:
|
||||
var sortedIdxs = idxs
|
||||
sortedIdxs.sort(proc(a, b: int): int =
|
||||
compareRowsByOrder(rows[a], rows[b], expr.wfOrderBy, expr.wfOrderDirs)
|
||||
)
|
||||
|
||||
case wfName
|
||||
of "row_number":
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
result[rowIdx] = $(pos + 1)
|
||||
of "rank":
|
||||
var currentRank = 1
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
if pos > 0:
|
||||
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs)
|
||||
if cmpRes != 0:
|
||||
currentRank = pos + 1
|
||||
result[rowIdx] = $currentRank
|
||||
of "dense_rank":
|
||||
var currentRank = 1
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
if pos > 0:
|
||||
let cmpRes = compareRowsByOrder(rows[sortedIdxs[pos - 1]], rows[rowIdx], expr.wfOrderBy, expr.wfOrderDirs)
|
||||
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
|
||||
if n < 1: n = 1
|
||||
let groupSize = sortedIdxs.len div n
|
||||
let remainder = sortedIdxs.len mod n
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
var bucket = 1
|
||||
var threshold = groupSize
|
||||
if 0 < remainder: threshold += 1
|
||||
var cumulative = threshold
|
||||
while pos >= cumulative and bucket < n:
|
||||
bucket += 1
|
||||
threshold = groupSize
|
||||
if (bucket - 1) < remainder: threshold += 1
|
||||
cumulative += threshold
|
||||
result[rowIdx] = $bucket
|
||||
of "lead":
|
||||
var offset = 1
|
||||
var defaultVal = ""
|
||||
if expr.wfArgs.len > 1:
|
||||
try: offset = parseInt(evalExpr(expr.wfArgs[1], rows[sortedIdxs[0]])) except: offset = 1
|
||||
if expr.wfArgs.len > 2:
|
||||
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]])
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
let targetPos = pos + offset
|
||||
if targetPos < sortedIdxs.len:
|
||||
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]])
|
||||
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
|
||||
if expr.wfArgs.len > 2:
|
||||
defaultVal = evalExpr(expr.wfArgs[2], rows[sortedIdxs[0]])
|
||||
for pos, rowIdx in sortedIdxs:
|
||||
let targetPos = pos - offset
|
||||
if targetPos >= 0:
|
||||
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[targetPos]])
|
||||
else:
|
||||
result[rowIdx] = defaultVal
|
||||
of "first_value":
|
||||
let firstVal = evalExpr(expr.wfArgs[0], rows[sortedIdxs[0]])
|
||||
for rowIdx in sortedIdxs:
|
||||
result[rowIdx] = firstVal
|
||||
of "last_value":
|
||||
let lastVal = evalExpr(expr.wfArgs[0], rows[sortedIdxs[^1]])
|
||||
for rowIdx in sortedIdxs:
|
||||
result[rowIdx] = lastVal
|
||||
else:
|
||||
# Unknown window function — fill with empty
|
||||
for rowIdx in sortedIdxs:
|
||||
result[rowIdx] = ""
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# IR Plan Execution (with actual filter/sort/projection)
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -1296,6 +1445,41 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
||||
hasAggs = true
|
||||
break
|
||||
|
||||
# Check if projection contains window functions
|
||||
var hasWindowFuncs = false
|
||||
for expr in plan.projectExprs:
|
||||
if expr != nil and expr.kind == irekWindowFunc:
|
||||
hasWindowFuncs = true
|
||||
break
|
||||
|
||||
if hasWindowFuncs:
|
||||
# Pre-compute window function values for all source rows
|
||||
var winValues = newSeq[seq[string]](plan.projectExprs.len)
|
||||
for i, expr in plan.projectExprs:
|
||||
if expr != nil and expr.kind == irekWindowFunc:
|
||||
winValues[i] = computeWindowValues(sourceRows, expr)
|
||||
result = @[]
|
||||
for rowIdx, row in sourceRows:
|
||||
var newRow: Table[string, string]
|
||||
for i, alias in plan.projectAliases:
|
||||
if i < plan.projectExprs.len:
|
||||
let expr = plan.projectExprs[i]
|
||||
if expr.kind == irekWindowFunc:
|
||||
newRow[alias] = winValues[i][rowIdx]
|
||||
elif expr.kind == irekStar:
|
||||
for k, v in row:
|
||||
if not k.startsWith("$") and not k.contains("."):
|
||||
newRow[k] = v
|
||||
else:
|
||||
let val = evalExpr(expr, row)
|
||||
if alias.len > 0: newRow[alias] = val
|
||||
else: newRow["col" & $i] = val
|
||||
if newRow.len > 0:
|
||||
result.add(newRow)
|
||||
else:
|
||||
result.add(row)
|
||||
return result
|
||||
|
||||
if hasAggs:
|
||||
# Produce exactly one row with aggregate values
|
||||
var newRow: Table[string, string]
|
||||
@@ -1663,6 +1847,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
||||
of nkInsert: "INSERT"
|
||||
of nkUpdate: "UPDATE"
|
||||
of nkDelete: "DELETE"
|
||||
of nkMerge: "MERGE"
|
||||
else: $stmt.kind
|
||||
let span = defaultTracer.beginSpan(spanName)
|
||||
defer: defaultTracer.endSpan(span)
|
||||
@@ -2146,6 +2331,76 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
||||
ctx.onChange(ChangeEvent(table: stmt.delTarget, kind: ckDelete, key: old, data: ""))
|
||||
return okResult(affected=count, kvPairs=kvPairs)
|
||||
|
||||
of nkMerge:
|
||||
# Execute source: subquery or table scan
|
||||
var sourceRows: seq[Row] = @[]
|
||||
if stmt.mergeSource != nil:
|
||||
if stmt.mergeSource.kind == nkSelect:
|
||||
let srcRes = executeQuery(ctx, Node(kind: nkStatementList, stmts: @[stmt.mergeSource]))
|
||||
sourceRows = srcRes.rows
|
||||
elif stmt.mergeSource.kind == nkIdent:
|
||||
sourceRows = execScan(ctx, stmt.mergeSource.identName)
|
||||
|
||||
let targetRows = execScan(ctx, stmt.mergeTarget)
|
||||
var count = 0
|
||||
var kvPairs: seq[(string, seq[byte])]
|
||||
|
||||
for srcRow in sourceRows:
|
||||
var matched = false
|
||||
var combinedRow = srcRow
|
||||
for k, v in srcRow:
|
||||
combinedRow[stmt.mergeSourceAlias & "." & k] = v
|
||||
for tgtRow in targetRows:
|
||||
# Evaluate ON condition with both source and target rows visible
|
||||
var rowWithTarget = combinedRow
|
||||
for k, v in tgtRow:
|
||||
rowWithTarget[stmt.mergeTargetAlias & "." & k] = v
|
||||
let onExpr = lowerExpr(stmt.mergeOn)
|
||||
if evalExpr(onExpr, rowWithTarget) == "true":
|
||||
matched = true
|
||||
if stmt.mergeMatchedUpdate.len > 0 and "$key" in tgtRow:
|
||||
var updateSets = initTable[string, string]()
|
||||
for s in stmt.mergeMatchedUpdate:
|
||||
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)
|
||||
var newRow = tgtRow
|
||||
for col, val in updateSets:
|
||||
newRow[col] = val
|
||||
fireTriggers(ctx, stmt.mergeTarget, "before", "update", tgtRow)
|
||||
count += execUpdateRow(ctx, stmt.mergeTarget, tgtRow["$key"], updateSets, kvPairs)
|
||||
fireTriggers(ctx, stmt.mergeTarget, "after", "update", newRow)
|
||||
if ctx.onChange != nil:
|
||||
ctx.onChange(ChangeEvent(table: stmt.mergeTarget, kind: ckUpdate, key: tgtRow["$key"], data: ""))
|
||||
break
|
||||
|
||||
if not matched and stmt.mergeNotMatchedInsert.len > 0:
|
||||
var fields: seq[string] = @[]
|
||||
var values: seq[string] = @[]
|
||||
for i, colNode in stmt.mergeNotMatchedInsert:
|
||||
if colNode.kind == nkIdent:
|
||||
fields.add(colNode.identName)
|
||||
if i < stmt.mergeNotMatchedValues.len:
|
||||
let v = stmt.mergeNotMatchedValues[i]
|
||||
let valExpr = lowerExpr(v)
|
||||
values.add(evalExpr(valExpr, combinedRow))
|
||||
else:
|
||||
values.add("")
|
||||
if fields.len > 0:
|
||||
var row = initTable[string, string]()
|
||||
for i, f in fields:
|
||||
if i < values.len: row[f] = values[i]
|
||||
fireTriggers(ctx, stmt.mergeTarget, "before", "insert", row)
|
||||
var insKvPairs: seq[(string, seq[byte])]
|
||||
count += execInsert(ctx, stmt.mergeTarget, fields, @[values], insKvPairs)
|
||||
for kv in insKvPairs: kvPairs.add(kv)
|
||||
fireTriggers(ctx, stmt.mergeTarget, "after", "insert", row)
|
||||
if ctx.onChange != nil:
|
||||
ctx.onChange(ChangeEvent(table: stmt.mergeTarget, kind: ckInsert, key: "", data: ""))
|
||||
|
||||
return okResult(affected=count, kvPairs=kvPairs)
|
||||
|
||||
of nkCreateTable:
|
||||
var tbl = TableDef(name: stmt.crtName, columns: @[], pkColumns: @[],
|
||||
foreignKeys: @[], checks: @[])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
## BaraQL IR — Intermediate Representation for compilation
|
||||
import std/tables
|
||||
import std/strutils
|
||||
import ../core/types
|
||||
|
||||
type
|
||||
@@ -52,6 +53,7 @@ type
|
||||
irekExists
|
||||
irekStar
|
||||
irekJsonPath
|
||||
irekWindowFunc
|
||||
|
||||
IRJoinKind* = enum
|
||||
irjkInner
|
||||
@@ -71,11 +73,13 @@ type
|
||||
irpkInsert
|
||||
irpkUpdate
|
||||
irpkDelete
|
||||
irpkMerge
|
||||
irpkCreateType
|
||||
irpkUnion
|
||||
irpkCTE
|
||||
irpkValues
|
||||
irpkExplain
|
||||
irpkWindow
|
||||
|
||||
IRPlan* = ref object
|
||||
case kind*: IRPlanKind
|
||||
@@ -121,6 +125,14 @@ type
|
||||
deleteTable*: string
|
||||
deleteAlias*: string
|
||||
deleteSource*: IRPlan
|
||||
of irpkMerge:
|
||||
mergeTarget*: string
|
||||
mergeTargetAlias*: string
|
||||
mergeSourcePlan*: IRPlan
|
||||
mergeOnCond*: IRExpr
|
||||
mergeUpdateSets*: seq[(string, IRExpr)]
|
||||
mergeInsertFields*: seq[string]
|
||||
mergeInsertValues*: seq[seq[IRExpr]]
|
||||
of irpkCreateType:
|
||||
createTypeName*: string
|
||||
createTypeDef*: IRType
|
||||
@@ -136,6 +148,15 @@ type
|
||||
valuesRows*: seq[seq[IRExpr]]
|
||||
of irpkExplain:
|
||||
explainPlan*: IRPlan
|
||||
of irpkWindow:
|
||||
winSource*: IRPlan
|
||||
winFuncs*: seq[IRExpr]
|
||||
winPartition*: seq[IRExpr]
|
||||
winOrderBy*: seq[IRExpr]
|
||||
winOrderDirs*: seq[bool]
|
||||
winFrameMode*: string
|
||||
winFrameStart*: string
|
||||
winFrameEnd*: string
|
||||
|
||||
IRExpr* = ref object
|
||||
case kind*: IRExprKind
|
||||
@@ -172,6 +193,15 @@ type
|
||||
jpExpr*: IRExpr
|
||||
jpKey*: string
|
||||
jpAsText*: bool
|
||||
of irekWindowFunc:
|
||||
wfName*: string
|
||||
wfArgs*: seq[IRExpr]
|
||||
wfPartition*: seq[IRExpr]
|
||||
wfOrderBy*: seq[IRExpr]
|
||||
wfOrderDirs*: seq[bool]
|
||||
wfFrameMode*: string
|
||||
wfFrameStart*: string
|
||||
wfFrameEnd*: string
|
||||
|
||||
type
|
||||
TypeChecker* = ref object
|
||||
@@ -255,3 +285,14 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
|
||||
of irekJsonPath:
|
||||
if expr.jpAsText: return IRType(name: "str", kind: itkScalar)
|
||||
return IRType(name: "json", kind: itkScalar)
|
||||
of irekWindowFunc:
|
||||
# Window functions return int64 for ranking, or the type of the argument for value functions
|
||||
case expr.wfName.toLower()
|
||||
of "row_number", "rank", "dense_rank", "ntile":
|
||||
return IRType(name: "int64", kind: itkScalar)
|
||||
of "lead", "lag", "first_value", "last_value":
|
||||
if expr.wfArgs.len > 0:
|
||||
return tc.inferExpr(expr.wfArgs[0], context)
|
||||
return nil
|
||||
else:
|
||||
return IRType(name: "unknown", kind: itkScalar)
|
||||
|
||||
@@ -120,6 +120,8 @@ type
|
||||
tkAvg
|
||||
tkMin
|
||||
tkMax
|
||||
tkMerge
|
||||
tkMatched
|
||||
tkArray
|
||||
tkVector
|
||||
tkGraph
|
||||
@@ -134,6 +136,24 @@ type
|
||||
tkDfs
|
||||
tkPath
|
||||
|
||||
# Window functions
|
||||
tkOver
|
||||
tkPartition
|
||||
tkRows
|
||||
tkRange
|
||||
tkUnbounded
|
||||
tkPreceding
|
||||
tkFollowing
|
||||
tkCurrent
|
||||
tkRowNumber
|
||||
tkRank
|
||||
tkDenseRank
|
||||
tkLead
|
||||
tkLag
|
||||
tkFirstValue
|
||||
tkLastValue
|
||||
tkNtile
|
||||
|
||||
# Operators
|
||||
tkPlus
|
||||
tkMinus
|
||||
@@ -291,6 +311,8 @@ const keywords*: Table[string, TokenKind] = {
|
||||
"avg": tkAvg,
|
||||
"min": tkMin,
|
||||
"max": tkMax,
|
||||
"merge": tkMerge,
|
||||
"matched": tkMatched,
|
||||
"array": tkArray,
|
||||
"vector": tkVector,
|
||||
"graph": tkGraph,
|
||||
@@ -301,6 +323,22 @@ const keywords*: Table[string, TokenKind] = {
|
||||
"bfs": tkBfs,
|
||||
"dfs": tkDfs,
|
||||
"path": tkPath,
|
||||
"over": tkOver,
|
||||
"partition": tkPartition,
|
||||
"rows": tkRows,
|
||||
"range": tkRange,
|
||||
"unbounded": tkUnbounded,
|
||||
"preceding": tkPreceding,
|
||||
"following": tkFollowing,
|
||||
"current": tkCurrent,
|
||||
"row_number": tkRowNumber,
|
||||
"rank": tkRank,
|
||||
"dense_rank": tkDenseRank,
|
||||
"lead": tkLead,
|
||||
"lag": tkLag,
|
||||
"first_value": tkFirstValue,
|
||||
"last_value": tkLastValue,
|
||||
"ntile": tkNtile,
|
||||
}.toTable
|
||||
|
||||
proc newLexer*(input: string): Lexer =
|
||||
|
||||
@@ -38,6 +38,9 @@ proc parseExpr(p: var Parser): Node
|
||||
proc parseSelect(p: var Parser): Node
|
||||
proc parseStatement*(p: var Parser): Node
|
||||
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 parsePrimary(p: var Parser): Node =
|
||||
let tok = p.peek()
|
||||
@@ -57,8 +60,9 @@ proc parsePrimary(p: var Parser): Node =
|
||||
of tkNull:
|
||||
discard p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
of tkIdent:
|
||||
of tkIdent, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile:
|
||||
discard p.advance()
|
||||
let funcName = tok.value
|
||||
# Check for function call: ident(...)
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance() # consume (
|
||||
@@ -68,15 +72,21 @@ proc parsePrimary(p: var Parser): Node =
|
||||
while p.match(tkComma):
|
||||
args.add(p.parseExpr())
|
||||
discard p.expect(tkRParen)
|
||||
return Node(kind: nkFuncCall, funcName: tok.value, funcArgs: args,
|
||||
line: tok.line, col: tok.col)
|
||||
var funcNode = Node(kind: nkFuncCall, funcName: funcName, funcArgs: args,
|
||||
line: tok.line, col: tok.col)
|
||||
# Check for window function: func(...) OVER (...)
|
||||
if p.peek().kind == tkOver:
|
||||
let overClause = p.parseOverClause()
|
||||
return Node(kind: nkWindowExpr, winFunc: funcName, winArgs: args,
|
||||
winOver: overClause, line: tok.line, col: tok.col)
|
||||
return funcNode
|
||||
# Check for dotted path: ident.ident.ident
|
||||
var parts = @[tok.value]
|
||||
var parts = @[funcName]
|
||||
while p.peek().kind == tkDot:
|
||||
discard p.advance() # consume .
|
||||
parts.add(p.expect(tkIdent).value)
|
||||
if parts.len == 1:
|
||||
return Node(kind: nkIdent, identName: tok.value, line: tok.line, col: tok.col)
|
||||
return Node(kind: nkIdent, identName: funcName, line: tok.line, col: tok.col)
|
||||
return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
|
||||
of tkLParen:
|
||||
discard p.advance()
|
||||
@@ -157,6 +167,80 @@ proc parsePrimary(p: var Parser): Node =
|
||||
discard p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseOverClause(p: var Parser): Node =
|
||||
## Parse OVER ( [PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame] )
|
||||
discard p.expect(tkOver)
|
||||
discard p.expect(tkLParen)
|
||||
result = Node(kind: nkOverClause)
|
||||
# PARTITION BY
|
||||
if p.match(tkPartition):
|
||||
discard p.expect(tkBy)
|
||||
result.overPartition.add(p.parseExpr())
|
||||
while p.match(tkComma):
|
||||
result.overPartition.add(p.parseExpr())
|
||||
# ORDER BY
|
||||
if p.match(tkOrder):
|
||||
discard p.expect(tkBy)
|
||||
var expr = p.parseExpr()
|
||||
var dir = sdAsc
|
||||
if p.match(tkDesc):
|
||||
dir = sdDesc
|
||||
elif p.match(tkAsc):
|
||||
dir = sdAsc
|
||||
result.overOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr, orderByDir: dir))
|
||||
while p.match(tkComma):
|
||||
expr = p.parseExpr()
|
||||
dir = sdAsc
|
||||
if p.match(tkDesc):
|
||||
dir = sdDesc
|
||||
elif p.match(tkAsc):
|
||||
dir = sdAsc
|
||||
result.overOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr, orderByDir: dir))
|
||||
# Frame specification
|
||||
if p.peek().kind in {tkRows, tkRange}:
|
||||
result.overFrame = p.parseFrameSpec()
|
||||
discard p.expect(tkRParen)
|
||||
|
||||
proc parseFrameSpec(p: var Parser): Node =
|
||||
## Parse ROWS|RANGE frame
|
||||
let modeTok = p.advance() # tkRows or tkRange
|
||||
result = Node(kind: nkFrameSpec, frameMode: modeTok.value)
|
||||
if p.match(tkBetween):
|
||||
result.frameStartType = p.parseFrameBoundary()
|
||||
discard p.expect(tkAnd)
|
||||
result.frameEndType = p.parseFrameBoundary()
|
||||
else:
|
||||
result.frameStartType = p.parseFrameBoundary()
|
||||
result.frameEndType = "CURRENT ROW"
|
||||
|
||||
proc parseFrameBoundary(p: var Parser): string =
|
||||
## Returns boundary type string like "UNBOUNDED PRECEDING", "CURRENT ROW", "1 PRECEDING"
|
||||
if p.match(tkUnbounded):
|
||||
if p.match(tkPreceding):
|
||||
return "UNBOUNDED PRECEDING"
|
||||
elif p.match(tkFollowing):
|
||||
return "UNBOUNDED FOLLOWING"
|
||||
else:
|
||||
raise newException(ValueError, "Expected PRECEDING or FOLLOWING after UNBOUNDED")
|
||||
elif p.match(tkCurrent):
|
||||
discard p.expect(tkRows)
|
||||
return "CURRENT ROW"
|
||||
else:
|
||||
# Expect a simple integer literal for offset boundaries
|
||||
let offsetExpr = p.parsePrimary()
|
||||
var offsetStr = ""
|
||||
case offsetExpr.kind:
|
||||
of nkIntLit: offsetStr = $offsetExpr.intVal
|
||||
of nkIdent: offsetStr = offsetExpr.identName
|
||||
else:
|
||||
raise newException(ValueError, "Expected integer literal or identifier in frame boundary, got " & $offsetExpr.kind)
|
||||
if p.match(tkPreceding):
|
||||
return offsetStr & " PRECEDING"
|
||||
elif p.match(tkFollowing):
|
||||
return offsetStr & " FOLLOWING"
|
||||
else:
|
||||
raise newException(ValueError, "Expected PRECEDING or FOLLOWING after frame offset")
|
||||
|
||||
proc parsePostfix(p: var Parser): Node =
|
||||
result = p.parsePrimary()
|
||||
while p.peek().kind in {tkArrowR, tkArrowRR}:
|
||||
@@ -540,6 +624,64 @@ proc parseDelete(p: var Parser): Node =
|
||||
while p.match(tkComma):
|
||||
result.delReturning.add(p.parseExpr())
|
||||
|
||||
proc parseMerge(p: var Parser): Node =
|
||||
let tok = p.expect(tkMerge)
|
||||
discard p.match(tkInto) # optional INTO
|
||||
result = Node(kind: nkMerge, line: tok.line, col: tok.col)
|
||||
result.mergeTarget = p.expect(tkIdent).value
|
||||
if p.match(tkAs):
|
||||
result.mergeTargetAlias = p.expect(tkIdent).value
|
||||
elif p.peek().kind == tkIdent:
|
||||
result.mergeTargetAlias = p.advance().value
|
||||
discard p.expect(tkUsing)
|
||||
# Source can be a table name or a subquery
|
||||
if p.peek().kind == tkLParen:
|
||||
discard p.advance() # consume (
|
||||
result.mergeSource = p.parseSelect()
|
||||
discard p.expect(tkRParen)
|
||||
else:
|
||||
let srcTable = p.expect(tkIdent).value
|
||||
result.mergeSource = Node(kind: nkIdent, identName: srcTable, line: tok.line, col: tok.col)
|
||||
if p.match(tkAs):
|
||||
result.mergeSourceAlias = p.expect(tkIdent).value
|
||||
elif p.peek().kind == tkIdent:
|
||||
result.mergeSourceAlias = p.advance().value
|
||||
discard p.expect(tkOn)
|
||||
result.mergeOn = p.parseExpr()
|
||||
# WHEN MATCHED THEN UPDATE SET ...
|
||||
if p.match(tkWhen):
|
||||
if p.match(tkNot):
|
||||
discard p.expect(tkMatched)
|
||||
discard p.expect(tkThen)
|
||||
discard p.expect(tkInsert)
|
||||
discard p.expect(tkLParen)
|
||||
result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expect(tkIdent).value))
|
||||
while p.match(tkComma):
|
||||
result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expect(tkIdent).value))
|
||||
discard p.expect(tkRParen)
|
||||
discard p.expect(tkValues)
|
||||
discard p.expect(tkLParen)
|
||||
result.mergeNotMatchedValues.add(p.parseExpr())
|
||||
while p.match(tkComma):
|
||||
result.mergeNotMatchedValues.add(p.parseExpr())
|
||||
discard p.expect(tkRParen)
|
||||
else:
|
||||
discard p.expect(tkMatched)
|
||||
discard p.expect(tkThen)
|
||||
discard p.expect(tkUpdate)
|
||||
discard p.expect(tkSet)
|
||||
let col = p.expect(tkIdent).value
|
||||
discard p.expect(tkEq)
|
||||
result.mergeMatchedUpdate.add(Node(kind: nkBinOp, binOp: bkAssign,
|
||||
binLeft: Node(kind: nkIdent, identName: col),
|
||||
binRight: p.parseExpr()))
|
||||
while p.match(tkComma):
|
||||
let col2 = p.expect(tkIdent).value
|
||||
discard p.expect(tkEq)
|
||||
result.mergeMatchedUpdate.add(Node(kind: nkBinOp, binOp: bkAssign,
|
||||
binLeft: Node(kind: nkIdent, identName: col2),
|
||||
binRight: p.parseExpr()))
|
||||
|
||||
proc parseCreateType(p: var Parser): Node =
|
||||
let tok = p.expect(tkCreate)
|
||||
discard p.expect(tkType)
|
||||
@@ -1105,6 +1247,7 @@ proc parseStatement*(p: var Parser): Node =
|
||||
of tkInsert: p.parseInsert()
|
||||
of tkUpdate: p.parseUpdate()
|
||||
of tkDelete: p.parseDelete()
|
||||
of tkMerge: p.parseMerge()
|
||||
of tkCreate:
|
||||
if p.pos + 1 < p.tokens.len:
|
||||
let next = p.tokens[p.pos + 1]
|
||||
|
||||
Reference in New Issue
Block a user