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:
2026-05-14 11:02:09 +03:00
parent 18f3c16b2a
commit e2a526df6f
15 changed files with 942 additions and 8 deletions
+40
View File
@@ -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
+10
View File
@@ -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:
BIN
View File
Binary file not shown.
+255
View File
@@ -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: @[])
+41
View File
@@ -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)
+38
View File
@@ -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 =
+148 -5
View File
@@ -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]