Files
Baradb/src/barabadb/query/parser.nim
T
dimgigov 967c0855a5 Bug fixes: composite PK, nl_to_sql sandbox, FK check, SQL injection, storage correctness
Critical fixes:
- Composite PK: execInsert + validateConstraints use all PK columns
- nl_to_sql: non-SELECT SQL no longer executed directly during validation
- FK check: removed O(N) scanMemTable fallback, uses db.get() with SSTables
- exprToSql: nkIdent wrapped in quotes to prevent SQL injection
- restoreSchema: try/except around tokenize/parse for crash resilience
- recovery.nim: lastTxnId tracking + putUnsafe/deleteUnsafe without WAL
- SCRAM: verifyClientProof length check + DefaultIterationCount restored

Storage fixes:
- lsm.nim: SSTable sort order fixed (ascending), close() flushes all memtables
- compaction.nim: tombstones preserved during compaction
- wal.nim: header written for empty existing files, readEntries checks magic
- btree.nim: B+ tree leaf split keeps boundary key
- bloom.nim: deserialize raises on short data
- mmap.nim: bounds checks for adviseWillNeed/DontNeed + posix.close()

Protocol fixes:
- zerocopy.nim: readString bounds check
- wire.nim: deserializeValue 32-bit underflow check
- auth.nim: JWT JSON escaping for claims
- server.nim: readUint32BE bounds check + specific exception handling
- raft.nim: readData checks + specific exception handling

Tests:
- Added Composite Primary Key test suite (4 tests)

Build: 0 warnings, 0 errors
2026-05-18 11:33:11 +03:00

1738 lines
60 KiB
Nim

## BaraQL Parser — recursive descent parser
import std/strutils
import lexer
import ast
import ../core/types
type
Parser* = object
tokens: seq[Token]
pos: int
proc newParser*(tokens: seq[Token]): Parser =
Parser(tokens: tokens, pos: 0)
proc peek(p: Parser): Token =
if p.pos < p.tokens.len:
return p.tokens[p.pos]
Token(kind: tkEof)
proc advance(p: var Parser): Token =
result = p.tokens[p.pos]
inc p.pos
proc expect(p: var Parser, kind: TokenKind): Token =
let tok = p.advance()
if tok.kind != kind:
raise newException(ValueError,
"Expected " & $kind & " but got " & $tok.kind & " at line " & $tok.line)
return tok
proc match(p: var Parser, kind: TokenKind): bool =
if p.peek().kind == kind:
discard p.advance()
return true
return false
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 parseSetVar(p: var Parser): Node
proc parseCreateGraph(p: var Parser): Node
proc parseDropGraph(p: var Parser): Node
proc parsePrimary(p: var Parser): Node =
let tok = p.peek()
case tok.kind
of tkIntLit:
discard p.advance()
Node(kind: nkIntLit, intVal: parseInt(tok.value), line: tok.line, col: tok.col)
of tkFloatLit:
discard p.advance()
Node(kind: nkFloatLit, floatVal: parseFloat(tok.value), line: tok.line, col: tok.col)
of tkStringLit:
discard p.advance()
Node(kind: nkStringLit, strVal: tok.value, line: tok.line, col: tok.col)
of tkBoolLit, tkTrue, tkFalse:
discard p.advance()
Node(kind: nkBoolLit, boolVal: tok.value == "true" or tok.kind == tkTrue, line: tok.line, col: tok.col)
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
# Check for function call: ident(...)
if p.peek().kind == tkLParen:
discard p.advance() # consume (
var args: seq[Node] = @[]
if p.peek().kind != tkRParen:
args.add(p.parseExpr())
while p.match(tkComma):
args.add(p.parseExpr())
discard p.expect(tkRParen)
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 = @[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: funcName, line: tok.line, col: tok.col)
return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
of tkLParen:
discard p.advance()
# Check for subquery
if p.peek().kind == tkSelect:
let sub = p.parseSelect()
discard p.expect(tkRParen)
return Node(kind: nkSubquery, subQuery: sub, line: tok.line, col: tok.col)
let expr = p.parseExpr()
discard p.expect(tkRParen)
expr
of tkLBracket:
discard p.advance()
var elems: seq[Node] = @[]
if p.peek().kind != tkRBracket:
elems.add(p.parseExpr())
while p.match(tkComma):
elems.add(p.parseExpr())
discard p.expect(tkRBracket)
Node(kind: nkArrayLit, arrayElems: elems, line: tok.line, col: tok.col)
of tkSelect:
p.parseSelect()
of tkExists:
discard p.advance()
discard p.expect(tkLParen)
let sub = p.parseSelect()
discard p.expect(tkRParen)
Node(kind: nkExists, existsExpr: sub, line: tok.line, col: tok.col)
of tkStar:
discard p.advance()
Node(kind: nkStar, line: tok.line, col: tok.col)
of tkPlaceholder:
discard p.advance()
Node(kind: nkPlaceholder, line: tok.line, col: tok.col)
of tkNot:
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNot, unOperand: operand, line: tok.line, col: tok.col)
of tkMinus:
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
of tkCount, tkSum, tkAvg, tkMin, tkMax, tkArrayAgg, tkStringAgg:
let funcName = tok.value
discard p.advance()
discard p.expect(tkLParen)
var args: seq[Node] = @[]
# Handle DISTINCT inside aggregate
var hasDistinct = false
if p.peek().kind == tkDistinct:
discard p.advance()
hasDistinct = true
if p.peek().kind != tkRParen:
args.add(p.parseExpr())
while p.match(tkComma):
args.add(p.parseExpr())
discard p.expect(tkRParen)
var node = Node(kind: nkFuncCall, funcName: funcName.toLower(), funcArgs: args,
line: tok.line, col: tok.col)
# Handle FILTER (WHERE ...)
if p.match(tkFilter):
discard p.expect(tkLParen)
discard p.expect(tkWhere)
node.funcFilter = p.parseExpr()
discard p.expect(tkRParen)
return node
of tkCase:
discard p.advance()
var caseExpr: Node = nil
# CASE expr WHEN ... THEN ... ELSE ... END
if p.peek().kind != tkWhen:
caseExpr = p.parseExpr()
var whens: seq[(Node, Node)] = @[]
while p.match(tkWhen):
let cond = p.parseExpr()
discard p.expect(tkThen)
let val = p.parseExpr()
whens.add((cond, val))
var elseExpr: Node = nil
if p.match(tkElse):
elseExpr = p.parseExpr()
discard p.expect(tkEnd)
Node(kind: nkCase, caseExpr: caseExpr, caseWhens: whens, caseElse: elseExpr,
line: tok.line, col: tok.col)
else:
raise newException(ValueError,
"Unexpected token " & $tok.kind & " at 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(tkRow)
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}:
let isText = p.peek().kind == tkArrowRR
discard p.advance()
let key = p.expect(tkStringLit).value
result = Node(kind: nkJsonPath, jpLeft: result, jpKey: key, jpAsText: isText,
line: p.peek().line, col: p.peek().col)
proc parseMulDiv(p: var Parser): Node =
result = p.parsePostfix()
while p.peek().kind in {tkStar, tkSlash, tkPercent, tkFloorDiv}:
let op = case p.peek().kind
of tkStar: bkMul
of tkSlash: bkDiv
of tkPercent: bkMod
of tkFloorDiv: bkFloorDiv
else: bkMul
let tok = p.advance()
let right = p.parsePostfix()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseAddSub(p: var Parser): Node =
result = p.parseMulDiv()
while p.peek().kind in {tkPlus, tkMinus, tkConcat}:
let op = case p.peek().kind
of tkPlus: bkAdd
of tkMinus: bkSub
of tkConcat: bkConcat
else: bkAdd
let tok = p.advance()
let right = p.parseMulDiv()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseComparison(p: var Parser): Node =
result = p.parseAddSub()
# Handle BETWEEN ... AND ...
if p.peek().kind == tkBetween:
let tok = p.advance()
let low = p.parseAddSub()
discard p.expect(tkAnd)
let high = p.parseAddSub()
return Node(kind: nkBetweenExpr, betweenExpr: result,
betweenLow: low, betweenHigh: high, line: tok.line, col: tok.col)
# Handle IN (subquery | list)
if p.peek().kind == tkIn:
let tok = p.advance()
let right = p.parseAddSub()
return Node(kind: nkInExpr, inLeft: result, inRight: right,
line: tok.line, col: tok.col)
# Handle LIKE / ILIKE
if p.peek().kind in {tkLike, tkILike}:
let isILike = p.peek().kind == tkILike
let tok = p.advance()
let pattern = p.parseAddSub()
return Node(kind: nkLikeExpr, likeExpr: result, likePattern: pattern,
likeCaseInsensitive: isILike, line: tok.line, col: tok.col)
# Handle IS NULL / IS NOT NULL
if p.peek().kind == tkIs:
let tok = p.advance()
var negated = false
if p.peek().kind == tkNot:
discard p.advance()
negated = true
if p.peek().kind != tkNull:
raise newException(ValueError,
"Expected NULL after IS " & (if negated: "NOT " else: "") & "at line " & $tok.line)
discard p.advance() # consume NULL token
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, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}:
let op = case p.peek().kind
of tkEq: bkEq
of tkNotEq: bkNotEq
of tkLt: bkLt
of tkLtEq: bkLtEq
of tkGt: bkGt
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()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseNot(p: var Parser): Node =
if p.peek().kind == tkNot:
let tok = p.advance()
let operand = p.parseComparison()
return Node(kind: nkUnaryOp, unOp: ukNot, unOperand: operand,
line: tok.line, col: tok.col)
return p.parseComparison()
proc parseAnd(p: var Parser): Node =
result = p.parseNot()
while p.peek().kind == tkAnd:
let tok = p.advance()
let right = p.parseNot()
result = Node(kind: nkBinOp, binOp: bkAnd, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseOr(p: var Parser): Node =
result = p.parseAnd()
while p.peek().kind == tkOr:
let tok = p.advance()
let right = p.parseAnd()
result = Node(kind: nkBinOp, binOp: bkOr, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseExpr(p: var Parser): Node =
return p.parseOr()
proc parseJoinType(p: var Parser): JoinKind =
if p.match(tkInner):
return jkInner
elif p.match(tkLeft):
if p.match(tkOuter): discard
return jkLeft
elif p.match(tkRight):
if p.match(tkOuter): discard
return jkRight
elif p.match(tkFull):
if p.match(tkOuter): discard
return jkFull
elif p.match(tkCross):
return jkCross
return jkInner
proc parseWith(p: var Parser): Node =
# WITH [RECURSIVE] name AS (select), name2 AS (select2) SELECT ...
let tok = p.expect(tkWith)
result = Node(kind: nkWith, line: tok.line, col: tok.col)
result.withBindings = @[]
var isRecursive = false
if p.peek().kind == tkRecursive:
discard p.advance()
isRecursive = true
# Parse first CTE
let cteName = p.expect(tkIdent).value
discard p.expect(tkAs)
discard p.expect(tkLParen)
let cteQuery = p.parseSelect()
discard p.expect(tkRParen)
result.withBindings.add((cteName, cteQuery, isRecursive))
# Parse additional CTEs
while p.match(tkComma):
let name = p.expect(tkIdent).value
discard p.expect(tkAs)
discard p.expect(tkLParen)
let query = p.parseSelect()
discard p.expect(tkRParen)
result.withBindings.add((name, query, isRecursive))
proc parseSelect(p: var Parser): Node =
# Handle WITH (CTE)
var withClause: Node = nil
if p.peek().kind == tkWith:
withClause = p.parseWith()
let tok = p.expect(tkSelect)
result = Node(kind: nkSelect, line: tok.line, col: tok.col)
if withClause != nil:
result.selWith = withClause.withBindings
if p.peek().kind == tkDistinct:
discard p.advance()
result.selDistinct = true
# Parse SELECT list
result.selResult = @[]
var expr = p.parseExpr()
if p.match(tkAs):
expr.exprAlias = p.expect(tkIdent).value
result.selResult.add(expr)
while p.match(tkComma):
expr = p.parseExpr()
if p.match(tkAs):
expr.exprAlias = p.expect(tkIdent).value
result.selResult.add(expr)
# Parse FROM
result.selJoins = @[]
if p.match(tkFrom):
# Handle subquery: (SELECT ...) AS alias
if p.peek().kind == tkLParen:
discard p.advance() # consume (
let subquery = p.parseSelect()
discard p.expect(tkRParen)
var alias = ""
if p.match(tkAs):
alias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
alias = p.advance().value
result.selFrom = Node(kind: nkFrom, fromTable: "(subquery)",
fromAlias: alias, fromSubquery: subquery, line: tok.line, col: tok.col)
elif p.peek().kind == tkGraphTable:
# GRAPH_TABLE(name MATCH (pattern) COLUMNS (cols))
discard p.advance()
discard p.expect(tkLParen)
let graphName = p.expect(tkIdent).value
var hasMatch = p.match(tkMatch)
var patternNodes: seq[string]
var patternEdges: seq[string]
if hasMatch:
# First node
discard p.expect(tkLParen)
if p.peek().kind == tkIdent:
patternNodes.add(p.advance().value)
discard p.expect(tkRParen)
# Edge and next node(s)
while p.peek().kind == tkMinus or p.peek().kind == tkArrowR:
if p.match(tkArrowR):
discard
elif p.match(tkMinus):
if p.peek().kind == tkLBracket:
discard p.advance()
if p.peek().kind == tkIdent:
patternEdges.add(p.advance().value)
discard p.expect(tkRBracket)
discard p.expect(tkArrowR)
else:
discard
if p.peek().kind == tkLParen:
discard p.advance()
if p.peek().kind == tkIdent:
patternNodes.add(p.advance().value)
discard p.expect(tkRParen)
# COLUMNS (col1, col2, ...)
var algo = "bfs"
var maxDepth = -1
var startId = ""
var endId = ""
let startVar = if patternNodes.len > 0: patternNodes[0] else: ""
let endVar = if patternNodes.len > 1: patternNodes[1] else: ""
if p.peek().kind == tkIdent and p.peek().value.toLower() == "algorithm":
discard p.advance()
algo = p.advance().value.toLower()
if p.peek().kind == tkIdent and p.peek().value.toLower() == "start":
discard p.advance()
if p.peek().kind == tkIntLit:
startId = p.advance().value
elif p.peek().kind == tkIdent:
startId = p.advance().value
if p.peek().kind == tkIdent and p.peek().value.toLower() == "end" or p.peek().kind == tkEnd:
discard p.advance()
if p.peek().kind == tkIntLit:
endId = p.advance().value
elif p.peek().kind == tkIdent:
endId = p.advance().value
if p.peek().kind == tkIdent and p.peek().value.toLower() == "maxdepth":
discard p.advance()
if p.peek().kind == tkIntLit:
maxDepth = parseInt(p.advance().value)
var returnCols: seq[string]
if p.match(tkColumns):
discard p.expect(tkLParen)
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkEnd, tkMatch, tkColumns, tkSrc, tkDst, tkBfs, tkDfs, tkMerge}:
var colName = p.advance().value
while p.peek().kind == tkDot:
discard p.advance()
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkBfs, tkDfs, tkMatch, tkColumns, tkEnd, tkSrc, tkDst, tkMerge}:
colName &= "." & p.advance().value
else:
colName &= "." & p.expect(tkIdent).value
returnCols.add(colName)
while p.match(tkComma):
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkEnd, tkMatch, tkColumns, tkSrc, tkDst, tkBfs, tkDfs, tkMerge}:
colName = p.advance().value
while p.peek().kind == tkDot:
discard p.advance()
if p.peek().kind in {tkIdent, tkLabels, tkEdge, tkGraph, tkRank, tkBfs, tkDfs, tkMatch, tkColumns, tkEnd, tkSrc, tkDst, tkMerge}:
colName &= "." & p.advance().value
else:
colName &= "." & p.expect(tkIdent).value
returnCols.add(colName)
if p.match(tkAs):
discard p.advance()
discard p.expect(tkRParen)
discard p.expect(tkRParen)
var startNode: Node = nil
if startId.len > 0:
startNode = Node(kind: nkIntLit, intVal: parseInt(startId))
elif patternNodes.len > 0:
startNode = Node(kind: nkIdent, identName: patternNodes[0])
var endNode: Node = nil
if endId.len > 0:
endNode = Node(kind: nkIntLit, intVal: parseInt(endId))
elif patternNodes.len > 1:
endNode = Node(kind: nkIdent, identName: patternNodes[1])
result.selFrom = Node(kind: nkGraphTraversal, gtGraphName: graphName,
gtStart: startNode, gtEdge: if patternEdges.len > 0: patternEdges[0] else: "",
gtDirection: "out", gtEnd: endNode, gtMaxDepth: maxDepth,
gtReturnCols: returnCols, gtAlgo: algo, line: tok.line, col: tok.col)
else:
let tableTok = p.expect(tkIdent)
var alias = ""
if p.match(tkAs):
alias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
alias = p.advance().value
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value,
fromAlias: alias, line: tableTok.line, col: tableTok.col)
# Comma join: FROM t1, t2 → implicit CROSS JOIN
while p.peek().kind == tkComma:
discard p.advance()
let nextTableTok = p.expect(tkIdent)
var nextAlias = ""
if p.match(tkAs):
nextAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
nextAlias = p.advance().value
let joinNode = Node(kind: nkJoin, joinKind: jkCross,
joinTarget: Node(kind: nkFrom, fromTable: nextTableTok.value,
fromAlias: nextAlias, line: nextTableTok.line, col: nextTableTok.col),
joinOn: nil, joinAlias: nextAlias, joinLateral: false,
line: nextTableTok.line, col: nextTableTok.col)
result.selJoins.add(joinNode)
# Parse PIVOT / UNPIVOT after FROM source
if p.peek().kind == tkPivot:
discard p.advance()
discard p.expect(tkLParen)
let aggFunc = p.parseExpr() # e.g. SUM(salary)
discard p.expect(tkFor)
let forCol = p.expect(tkIdent).value
discard p.expect(tkIn)
discard p.expect(tkLParen)
var inValues: seq[string] = @[]
inValues.add(p.expect(tkStringLit).value)
while p.match(tkComma):
inValues.add(p.expect(tkStringLit).value)
discard p.expect(tkRParen)
discard p.expect(tkRParen)
result.selFrom = Node(kind: nkPivot, pivotSource: result.selFrom,
pivotAgg: aggFunc, pivotForCol: forCol,
pivotInValues: inValues, line: tok.line, col: tok.col)
elif p.peek().kind == tkUnpivot:
discard p.advance()
discard p.expect(tkLParen)
let valCol = p.expect(tkIdent).value
discard p.expect(tkFor)
let forCol = p.expect(tkIdent).value
discard p.expect(tkIn)
discard p.expect(tkLParen)
var inCols: seq[string] = @[]
inCols.add(p.expect(tkIdent).value)
while p.match(tkComma):
inCols.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
discard p.expect(tkRParen)
result.selFrom = Node(kind: nkUnpivot, unpivotSource: result.selFrom,
unpivotValueCol: valCol, unpivotForCol: forCol,
unpivotInCols: inCols, line: tok.line, col: tok.col)
# Parse JOINs
while p.peek().kind == tkJoin or
p.peek().kind == tkLateral or
(p.peek().kind in {tkInner, tkLeft, tkRight, tkFull, tkCross} and
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind in {tkJoin, tkLateral}):
var isLateral = false
var jk = jkInner
# Check for LATERAL before join type
if p.match(tkLateral):
isLateral = true
# Could be standalone LATERAL or LATERAL JOIN
if p.peek().kind == tkJoin:
discard p.advance()
else:
jk = p.parseJoinType()
discard p.expect(tkJoin)
# Check for LATERAL after JOIN keyword
if p.match(tkLateral):
isLateral = true
var joinTarget: Node
var joinAlias = ""
if isLateral:
# LATERAL (subquery) AS alias
discard p.expect(tkLParen)
let subquery = p.parseSelect()
discard p.expect(tkRParen)
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
joinTarget = Node(kind: nkSubquery, subQuery: subquery,
line: tok.line, col: tok.col)
else:
let joinTable = p.expect(tkIdent)
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
joinTarget = Node(kind: nkFrom, fromTable: joinTable.value,
fromAlias: joinAlias, line: joinTable.line, col: joinTable.col)
var joinCond: Node = nil
if p.match(tkOn):
joinCond = p.parseExpr()
result.selJoins.add(Node(kind: nkJoin, joinKind: jk, joinTarget: joinTarget,
joinOn: joinCond, joinAlias: joinAlias, joinLateral: isLateral,
line: tok.line, col: tok.col))
# Parse WHERE
if p.match(tkWhere):
result.selWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
# Parse GROUP BY
if p.match(tkGroup):
discard p.expect(tkBy)
result.selGroupBy = @[]
result.selGroupingSetsKind = gskNone
result.selGroupingSets = @[]
# Check for GROUPING SETS, ROLLUP, CUBE
if p.peek().kind == tkGrouping:
discard p.advance()
discard p.expect(tkSets)
result.selGroupingSetsKind = gskGroupingSets
discard p.expect(tkLParen)
# Parse each set: (col1, col2) or ()
while true:
discard p.expect(tkLParen)
var setExprs: seq[Node] = @[]
if p.peek().kind != tkRParen:
setExprs.add(p.parseExpr())
while p.match(tkComma):
setExprs.add(p.parseExpr())
discard p.expect(tkRParen)
result.selGroupingSets.add(setExprs)
if not p.match(tkComma): break
discard p.expect(tkRParen)
elif p.peek().kind == tkRollup:
discard p.advance()
result.selGroupingSetsKind = gskRollup
discard p.expect(tkLParen)
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
discard p.expect(tkRParen)
elif p.peek().kind == tkCube:
discard p.advance()
result.selGroupingSetsKind = gskCube
discard p.expect(tkLParen)
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
discard p.expect(tkRParen)
else:
# Regular GROUP BY
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
# Parse HAVING
if p.match(tkHaving):
result.selHaving = Node(kind: nkHaving, havingExpr: p.parseExpr())
# Parse ORDER BY
if p.match(tkOrder):
discard p.expect(tkBy)
result.selOrderBy = @[]
var firstExpr = p.parseExpr()
var firstDir = sdAsc
if p.match(tkDesc):
firstDir = sdDesc
elif p.match(tkAsc):
firstDir = sdAsc
result.selOrderBy.add(Node(kind: nkOrderBy, orderByExpr: firstExpr,
orderByDir: firstDir))
while p.match(tkComma):
let expr = p.parseExpr()
var dir = sdAsc
if p.match(tkDesc):
dir = sdDesc
elif p.match(tkAsc):
dir = sdAsc
result.selOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr,
orderByDir: dir))
# Parse LIMIT
if p.match(tkLimit):
result.selLimit = Node(kind: nkLimit, limitExpr: p.parseExpr())
# Parse OFFSET
if p.match(tkOffset):
result.selOffset = Node(kind: nkOffset, offsetExpr: p.parseExpr())
# Parse set operations (UNION / INTERSECT / EXCEPT)
# Left-associative; same precedence level for all
while p.peek().kind in {tkUnion, tkIntersect, tkExcept}:
var sopKind: SetOpKind
let tok = p.advance()
case tok.kind:
of tkUnion: sopKind = sdkUnion
of tkIntersect: sopKind = sdkIntersect
of tkExcept: sopKind = sdkExcept
else: break
var isAll = false
if p.match(tkAll):
isAll = true
let right = p.parseSelect()
let left = result
result = Node(kind: nkSetOp, line: tok.line, col: tok.col)
result.setOpKind = sopKind
result.setOpAll = isAll
result.setOpLeft = left
result.setOpRight = right
proc parseInsert(p: var Parser): Node =
let tok = p.expect(tkInsert)
discard p.match(tkInto) # optional INTO
let target = p.expect(tkIdent).value
result = Node(kind: nkInsert, insTarget: target, line: tok.line, col: tok.col)
result.insFields = @[]
result.insValues = @[]
result.insReturning = @[]
# Parse column list: (col1, col2, ...)
if p.peek().kind == tkLParen:
discard p.advance()
result.insFields.add(p.parseExpr())
while p.match(tkComma):
result.insFields.add(p.parseExpr())
discard p.expect(tkRParen)
# Parse VALUES
if p.match(tkValues):
while true:
var row: seq[Node] = @[]
if p.peek().kind == tkLParen:
discard p.advance()
row.add(p.parseExpr())
while p.match(tkComma):
row.add(p.parseExpr())
discard p.expect(tkRParen)
else:
row.add(p.parseExpr())
result.insValues.add(Node(kind: nkArrayLit, arrayElems: row))
if p.peek().kind != tkComma:
break
discard p.advance()
# Parse ON CONFLICT
if p.peek().kind == tkOn:
discard p.advance()
if p.peek().kind == tkIdent and p.peek().value.toLower() == "conflict":
discard p.advance()
result.insConflict = p.parseExpr()
# Parse RETURNING
if p.match(tkReturning):
result.insReturning.add(p.parseExpr())
while p.match(tkComma):
result.insReturning.add(p.parseExpr())
proc parseUpdate(p: var Parser): Node =
let tok = p.expect(tkUpdate)
let target = p.expect(tkIdent).value
result = Node(kind: nkUpdate, updTarget: target, line: tok.line, col: tok.col)
if p.match(tkSet):
result.updSet = @[]
let field = p.expect(tkIdent).value
discard p.match(tkEq) # = or :=
let val = p.parseExpr()
result.updSet.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: field),
binRight: val))
while p.match(tkComma):
let f = p.expect(tkIdent).value
discard p.match(tkEq)
let v = p.parseExpr()
result.updSet.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: f),
binRight: v))
if p.match(tkWhere):
result.updWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
if p.match(tkReturning):
result.updReturning = @[]
result.updReturning.add(p.parseExpr())
while p.match(tkComma):
result.updReturning.add(p.parseExpr())
proc parseDelete(p: var Parser): Node =
let tok = p.expect(tkDelete)
discard p.match(tkFrom) # optional FROM keyword
let target = p.expect(tkIdent).value
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
if p.match(tkWhere):
result.delWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
if p.match(tkReturning):
result.delReturning = @[]
result.delReturning.add(p.parseExpr())
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)
let name = p.expect(tkIdent).value
result = Node(kind: nkCreateType, ctName: name, line: tok.line, col: tok.col)
result.ctBases = @[]
if p.match(tkIdent):
discard
if p.match(tkLBrace):
result.ctProperties = @[]
result.ctLinks = @[]
while p.peek().kind != tkRbrace and p.peek().kind != tkEof:
discard p.match(tkComma)
var isRequired = false
var isMulti = false
if p.peek().kind == tkRequired:
discard p.advance()
isRequired = true
if p.peek().kind == tkMulti:
discard p.advance()
isMulti = true
let fieldTok = p.expect(tkIdent)
if p.peek().kind == tkArrow:
discard p.advance()
let target = p.expect(tkIdent).value
result.ctLinks.add(Node(kind: nkLinkDef,
ldName: fieldTok.value, ldTarget: target,
ldRequired: isRequired,
ldCardinality: if isMulti: Many else: One))
else:
var typeName = ""
if p.match(tkColon):
typeName = p.expect(tkIdent).value
result.ctProperties.add(Node(kind: nkPropertyDef,
pdName: fieldTok.value, pdType: typeName,
pdRequired: isRequired))
discard p.match(tkSemicolon)
discard p.expect(tkRbrace)
proc parseCreateTable(p: var Parser): Node =
let tok = p.expect(tkCreate)
result = Node(kind: nkCreateTable, line: tok.line, col: tok.col)
result.crtColumns = @[]
result.crtConstraints = @[]
# Optional IF NOT EXISTS
if p.peek().kind == tkIf:
discard p.advance()
discard p.expect(tkNot)
discard p.expect(tkExists)
result.crtIfNotExists = true
discard p.expect(tkTable)
if p.peek().kind == tkIf:
discard p.advance() # if
discard p.expect(tkNot)
discard p.expect(tkExists)
result.crtIfNotExists = true
result.crtName = p.expect(tkIdent).value
discard p.expect(tkLParen)
while p.peek().kind != tkRParen and p.peek().kind != tkEof:
discard p.match(tkComma)
# Check if this is a table-level constraint
if p.peek().kind in {tkPrimary, tkForeign, tkUnique, tkCheck}:
let cst = Node(kind: nkConstraintDef)
cst.cstColumns = @[]; cst.cstRefColumns = @[]
if p.match(tkPrimary):
discard p.expect(tkKey)
cst.cstType = "pkey"
if p.peek().kind == tkLParen:
discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value)
while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
elif p.match(tkForeign):
discard p.expect(tkKey)
cst.cstType = "fkey"
if p.peek().kind == tkLParen:
discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value)
while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
discard p.expect(tkReferences)
cst.cstRefTable = p.expect(tkIdent).value
if p.peek().kind == tkLParen:
discard p.advance()
cst.cstRefColumns.add(p.expect(tkIdent).value)
while p.match(tkComma):
cst.cstRefColumns.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
if p.peek().kind == tkOn:
discard p.advance()
if p.peek().kind == tkDelete:
discard p.advance()
if p.peek().kind == tkCascade:
discard p.advance()
cst.cstOnDelete = "CASCADE"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnDelete = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnDelete = "RESTRICT"
elif p.peek().kind == tkUpdate:
discard p.advance()
if p.peek().kind == tkCascade:
discard p.advance()
cst.cstOnUpdate = "CASCADE"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnUpdate = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnUpdate = "RESTRICT"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnDelete = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnDelete = "RESTRICT"
elif p.peek().kind == tkUpdate:
discard p.advance()
if p.peek().kind == tkCascade:
discard p.advance()
cst.cstOnUpdate = "CASCADE"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnUpdate = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnUpdate = "RESTRICT"
elif p.match(tkUnique):
cst.cstType = "unique"
if p.peek().kind == tkLParen:
discard p.advance()
cst.cstColumns.add(p.expect(tkIdent).value)
while p.match(tkComma):
cst.cstColumns.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
elif p.match(tkCheck):
cst.cstType = "check"
discard p.expect(tkLParen)
cst.cstCheck = p.parseExpr()
discard p.expect(tkRParen)
result.crtConstraints.add(cst)
continue
# Parse column definition
let colName = p.expect(tkIdent).value
var colType = ""
if p.peek().kind == tkIdent:
colType = p.advance().value.toUpper()
if p.peek().kind == tkLParen:
discard p.advance()
let size = p.expect(tkIntLit).value
colType &= "(" & size & ")"
discard p.expect(tkRParen)
elif p.peek().kind == tkVector:
discard p.advance()
colType = "VECTOR"
if p.peek().kind == tkLParen:
discard p.advance()
let size = p.expect(tkIntLit).value
colType &= "(" & size & ")"
discard p.expect(tkRParen)
let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType)
colDef.cdConstraints = @[]
# SERIAL / BIGSERIAL imply AUTO_INCREMENT
if colType in @["SERIAL", "BIGSERIAL"]:
colDef.cdAutoIncrement = true
if colType == "SERIAL":
colDef.cdType = "INTEGER"
else:
colDef.cdType = "BIGINT"
# Parse column constraints
while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences, tkAutoIncrement, tkOn}:
let cst = Node(kind: nkConstraintDef)
cst.cstColumns = @[colName]; cst.cstRefColumns = @[]
if p.match(tkPrimary):
discard p.expect(tkKey)
cst.cstType = "pkey"
elif p.match(tkAutoIncrement):
colDef.cdAutoIncrement = true
continue
elif p.match(tkNot):
discard p.expect(tkNull)
cst.cstType = "notnull"
elif p.match(tkNull):
cst.cstType = "null"
elif p.match(tkUnique):
cst.cstType = "unique"
elif p.match(tkCheck):
cst.cstType = "check"
discard p.expect(tkLParen)
cst.cstCheck = p.parseExpr()
discard p.expect(tkRParen)
elif p.match(tkDefault):
cst.cstType = "default"
cst.cstDefault = p.parseExpr()
elif p.match(tkReferences):
cst.cstType = "fkey"
cst.cstRefTable = p.expect(tkIdent).value
if p.peek().kind == tkLParen:
discard p.advance()
cst.cstRefColumns.add(p.expect(tkIdent).value)
while p.match(tkComma):
cst.cstRefColumns.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
if p.peek().kind == tkOn:
discard p.advance()
if p.peek().kind == tkDelete:
discard p.advance()
if p.peek().kind == tkCascade:
discard p.advance()
cst.cstOnDelete = "CASCADE"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnDelete = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnDelete = "RESTRICT"
elif p.peek().kind == tkUpdate:
discard p.advance()
if p.peek().kind == tkCascade:
discard p.advance()
cst.cstOnUpdate = "CASCADE"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnUpdate = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnUpdate = "RESTRICT"
colDef.cdConstraints.add(cst)
result.crtColumns.add(colDef)
discard p.expect(tkRParen)
proc parseDropTable(p: var Parser): Node =
let tok = p.expect(tkDrop)
result = Node(kind: nkDropTable, line: tok.line, col: tok.col)
if p.peek().kind == tkTable:
discard p.advance()
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
discard p.advance()
discard p.expect(tkExists)
result.drtIfExists = true
result.drtName = p.expect(tkIdent).value
proc parseAlterTable(p: var Parser): Node =
let tok = p.expect(tkAlter)
discard p.expect(tkTable)
let tableName = p.expect(tkIdent).value
# Check for ENABLE/DISABLE ROW LEVEL SECURITY
if p.peek().kind == tkEnable:
discard p.advance()
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(tkRow) # ROW
discard p.expect(tkIdent) # LEVEL
discard p.expect(tkIdent) # SECURITY
return Node(kind: nkDisableRLS, drlsTable: tableName, line: tok.line, col: tok.col)
result = Node(kind: nkAlterTable, line: tok.line, col: tok.col)
result.altName = tableName
result.altOps = @[]
if p.match(tkAdd):
discard p.match(tkColumn)
let colName = p.expect(tkIdent).value
var colType = ""
if p.peek().kind == tkIdent:
colType = p.advance().value.toUpper()
result.altOps.add(Node(kind: nkColumnDef, cdName: colName, cdType: colType))
proc parseCreateIndex(p: var Parser): Node =
let tok = p.expect(tkCreate)
var isUnique = false
if p.peek().kind == tkUnique:
discard p.advance()
isUnique = true
discard p.expect(tkIndex)
var idxName = ""
if p.peek().kind == tkIdent and p.peek().value.toLower() != "on":
idxName = p.advance().value
discard p.match(tkOn)
let tableName = p.expect(tkIdent).value
discard p.match(tkLParen)
var colNames: seq[string] = @[]
colNames.add(p.expect(tkIdent).value)
while p.match(tkComma):
colNames.add(p.expect(tkIdent).value)
discard p.match(tkRParen)
var idxKind = ikBTree
if p.match(tkUsing):
let idxMethod = p.expect(tkIdent).value.toLower()
if idxMethod == "fts" or idxMethod == "fulltext":
idxKind = ikFullText
elif idxMethod == "hnsw":
idxKind = ikHNSW
elif idxMethod == "ivfpq":
idxKind = ikIVFPQ
result = Node(kind: nkCreateIndex, ciName: idxName, ciTarget: tableName,
ciColumns: colNames, ciKind: idxKind, line: tok.line, col: tok.col)
proc parseBeginTxn(p: var Parser): Node =
let tok = p.expect(tkBegin)
result = Node(kind: nkBeginTxn, line: tok.line, col: tok.col)
discard p.match(tkIdent) # optional TRANSACTION / WORK
result.btxnMode = ""
proc parseCommitTxn(p: var Parser): Node =
let tok = p.expect(tkCommit)
result = Node(kind: nkCommitTxn, line: tok.line, col: tok.col)
discard p.match(tkIdent) # optional TRANSACTION / WORK
proc parseRollbackTxn(p: var Parser): Node =
let tok = p.expect(tkRollback)
result = Node(kind: nkRollbackTxn, line: tok.line, col: tok.col)
discard p.match(tkIdent) # optional TRANSACTION / WORK
proc parseExplain(p: var Parser): Node =
let tok = p.expect(tkExplain)
result = Node(kind: nkExplainStmt, line: tok.line, col: tok.col)
if p.peek().kind == tkIdent and p.peek().value.toLower() == "analyze":
discard p.advance()
result.expAnalyze = true
result.expStmt = p.parseStatement()
proc parseCreateView(p: var Parser): Node =
let tok = p.expect(tkCreate)
var orReplace = false
if p.peek().kind == tkIdent and p.peek().value.toLower() == "or":
discard p.advance()
discard p.expect(tkIdent) # REPLACE
orReplace = true
discard p.expect(tkView)
let name = p.expect(tkIdent).value
discard p.expect(tkAs)
let query = p.parseSelect()
result = Node(kind: nkCreateView, cvName: name, cvQuery: query,
cvOrReplace: orReplace, line: tok.line, col: tok.col)
proc parseDropView(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkView)
var ifExists = false
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkDropView, dvName: name, dvIfExists: ifExists,
line: tok.line, col: tok.col)
proc parseCreateTrigger(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkTrigger)
let name = p.expect(tkIdent).value
# Parse timing: BEFORE | AFTER | INSTEAD OF
var timing = ""
let timingTok = p.peek()
if timingTok.kind == tkBefore:
discard p.advance()
timing = "before"
elif timingTok.kind == tkAfter:
discard p.advance()
timing = "after"
elif timingTok.kind == tkInstead:
discard p.advance()
discard p.expect(tkOf)
timing = "instead of"
else:
raise newException(ValueError, "Expected BEFORE, AFTER, or INSTEAD OF in TRIGGER definition")
# Parse event: INSERT | UPDATE | DELETE
var event = ""
let eventTok = p.peek()
if eventTok.kind == tkInsert:
discard p.advance()
event = "INSERT"
elif eventTok.kind == tkUpdate:
discard p.advance()
event = "UPDATE"
elif eventTok.kind == tkDelete:
discard p.advance()
event = "DELETE"
else:
raise newException(ValueError, "Expected INSERT, UPDATE, or DELETE in TRIGGER definition")
discard p.expect(tkOn)
let tableName = p.expect(tkIdent).value
discard p.expect(tkAs)
# Parse action as raw string until end of statement
var actionStr = ""
while p.pos < p.tokens.len and p.tokens[p.pos].kind != tkSemicolon:
actionStr.add(p.tokens[p.pos].value)
actionStr.add(" ")
discard p.advance()
let actionNode = Node(kind: nkStringLit, strVal: actionStr.strip())
result = Node(kind: nkCreateTrigger, trigName: name, trigTable: tableName,
trigTiming: timing, trigEvent: event, trigAction: actionNode,
line: tok.line, col: tok.col)
proc parseDropTrigger(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkTrigger)
var ifExists = false
if p.peek().kind == tkIf:
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkDropTrigger, trigDropName: name, trigDropIfExists: ifExists,
line: tok.line, col: tok.col)
proc parseDropIndex(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkIndex)
var ifExists = false
if p.peek().kind == tkIf:
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkDropIndex, diName: name, line: tok.line, col: tok.col)
proc parseCreateMigration(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkMigration)
let name = p.expect(tkIdent).value
var upBody = ""
var downBody = ""
if p.peek().kind == tkAs:
# Legacy syntax: CREATE MIGRATION name AS 'body'
discard p.advance()
upBody = p.expect(tkStringLit).value
elif p.peek().kind == tkLBrace:
# New syntax: CREATE MIGRATION name { UP: ...; DOWN: ...; }
discard p.advance() # {
while p.peek().kind != tkRBrace and p.peek().kind != tkEof:
var section = ""
let sectionTok = p.peek()
if sectionTok.kind == tkUp:
discard p.advance()
section = "up"
elif sectionTok.kind == tkDown:
discard p.advance()
section = "down"
elif sectionTok.kind == tkIdent:
section = p.expect(tkIdent).value.toLower()
else:
raise newException(ValueError, "Expected UP or DOWN in migration body, got: " & $sectionTok.kind)
discard p.expect(tkColon)
var bodyStr = ""
while p.peek().kind != tkRBrace and p.peek().kind != tkEof:
# Check if next token starts a new section
let nextTok = p.peek()
if (nextTok.kind == tkUp or nextTok.kind == tkDown or
(nextTok.kind == tkIdent and (nextTok.value.toLower() == "up" or nextTok.value.toLower() == "down"))) and
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkColon:
break
bodyStr.add(p.tokens[p.pos].value)
bodyStr.add(" ")
discard p.advance()
if section == "up":
upBody = bodyStr.strip()
elif section == "down":
downBody = bodyStr.strip()
else:
raise newException(ValueError, "Expected UP or DOWN in migration body, got: " & section)
discard p.expect(tkRBrace)
result = Node(kind: nkCreateMigration, cmName: name, cmBody: upBody,
cmDownBody: downBody, cmChecksum: "", cmAuthor: "", cmTimestamp: 0,
line: tok.line, col: tok.col)
proc parseApplyMigration(p: var Parser): Node =
let tok = p.expect(tkApply)
discard p.expect(tkMigration)
let name = p.expect(tkIdent).value
result = Node(kind: nkApplyMigration, amName: name, line: tok.line, col: tok.col)
proc parseMigrationStatus(p: var Parser): Node =
let tok = p.expect(tkMigration)
discard p.expect(tkStatus)
result = Node(kind: nkMigrationStatus, line: tok.line, col: tok.col)
proc parseMigrationUp(p: var Parser): Node =
let tok = p.expect(tkMigration)
discard p.expect(tkUp)
var count = 0
if p.peek().kind == tkIntLit:
count = parseInt(p.expect(tkIntLit).value)
result = Node(kind: nkMigrationUp, muCount: count, line: tok.line, col: tok.col)
proc parseMigrationDown(p: var Parser): Node =
let tok = p.expect(tkMigration)
discard p.expect(tkDown)
var count = 1
if p.peek().kind == tkIntLit:
count = parseInt(p.expect(tkIntLit).value)
result = Node(kind: nkMigrationDown, mdCount: count, line: tok.line, col: tok.col)
proc parseMigrationDryRun(p: var Parser): Node =
let tok = p.expect(tkMigration)
discard p.expect(tkDryRun)
let name = p.expect(tkIdent).value
result = Node(kind: nkMigrationDryRun, mdrName: name, line: tok.line, col: tok.col)
proc parseCreateUser(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkUser)
let name = p.expect(tkIdent).value
var password = ""
var isSuper = false
if p.peek().kind == tkWith:
discard p.advance()
if p.peek().kind == tkIdent and p.peek().value.toLower() == "password":
discard p.advance()
password = p.expect(tkStringLit).value
while p.peek().kind == tkIdent:
let opt = p.peek().value.toLower()
if opt == "superuser":
discard p.advance()
isSuper = true
elif opt == "nosuperuser":
discard p.advance()
isSuper = false
else:
break
result = Node(kind: nkCreateUser, cuName: name, cuPassword: password,
cuSuperuser: isSuper, line: tok.line, col: tok.col)
proc parseDropUser(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkUser)
var ifExists = false
if p.peek().kind == tkIf:
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkDropUser, duName: name, duIfExists: ifExists,
line: tok.line, col: tok.col)
proc parseCreatePolicy(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkPolicy)
let name = p.expect(tkIdent).value
discard p.expect(tkOn)
let tableName = p.expect(tkIdent).value
var cmd = "ALL"
var usingNode: Node = nil
var withCheckNode: Node = nil
if p.peek().kind == tkFor:
discard p.advance()
let cmdTok = p.peek()
if cmdTok.kind == tkIdent or cmdTok.kind == tkSelect or cmdTok.kind == tkInsert or
cmdTok.kind == tkUpdate or cmdTok.kind == tkDelete:
discard p.advance()
cmd = cmdTok.value.toUpper()
else:
raise newException(ValueError, "Expected ALL, SELECT, INSERT, UPDATE, or DELETE in POLICY definition")
if p.peek().kind == tkUsing:
discard p.advance()
usingNode = p.parseExpr()
if p.peek().kind == tkWith:
discard p.advance()
discard p.expect(tkCheck)
withCheckNode = p.parseExpr()
result = Node(kind: nkCreatePolicy, cpName: name, cpTable: tableName,
cpCommand: cmd, cpUsing: usingNode, cpWithCheck: withCheckNode,
line: tok.line, col: tok.col)
proc parseDropPolicy(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkPolicy)
var ifExists = false
if p.peek().kind == tkIf:
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
discard p.expect(tkOn)
let tableName = p.expect(tkIdent).value
result = Node(kind: nkDropPolicy, dpName: name, dpTable: tableName,
dpIfExists: ifExists, line: tok.line, col: tok.col)
proc parseGrant(p: var Parser): Node =
let tok = p.expect(tkGrant)
var priv = ""
let privTok = p.peek()
if privTok.kind == tkIdent or privTok.kind == tkSelect or privTok.kind == tkInsert or
privTok.kind == tkUpdate or privTok.kind == tkDelete:
discard p.advance()
priv = privTok.value.toUpper()
else:
raise newException(ValueError, "Expected privilege in GRANT")
discard p.expect(tkOn)
let tableName = p.expect(tkIdent).value
discard p.expect(tkTo)
let grantee = p.expect(tkIdent).value
result = Node(kind: nkGrant, grPrivilege: priv, grTable: tableName,
grGrantee: grantee, line: tok.line, col: tok.col)
proc parseRevoke(p: var Parser): Node =
let tok = p.expect(tkRevoke)
var priv = ""
let privTok = p.peek()
if privTok.kind == tkIdent or privTok.kind == tkSelect or privTok.kind == tkInsert or
privTok.kind == tkUpdate or privTok.kind == tkDelete:
discard p.advance()
priv = privTok.value.toUpper()
else:
raise newException(ValueError, "Expected privilege in REVOKE")
discard p.expect(tkOn)
let tableName = p.expect(tkIdent).value
discard p.expect(tkFrom)
let grantee = p.expect(tkIdent).value
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 parseCreateGraph(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkGraph)
var ifNotExists = false
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
discard p.advance()
discard p.expect(tkNot)
discard p.expect(tkExists)
ifNotExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkCreateGraph, cgName: name, cgIfNotExists: ifNotExists,
line: tok.line, col: tok.col)
proc parseDropGraph(p: var Parser): Node =
let tok = p.expect(tkDrop)
discard p.expect(tkGraph)
var ifExists = false
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
discard p.advance()
discard p.expect(tkExists)
ifExists = true
let name = p.expect(tkIdent).value
result = Node(kind: nkDropGraph, dgName: name, dgIfExists: ifExists,
line: tok.line, col: tok.col)
proc parseStatement*(p: var Parser): Node =
case p.peek().kind
of tkWith, tkSelect: p.parseSelect()
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]
if next.kind == tkTable or
(next.kind == tkIdent and next.value.toLower() == "if"):
p.parseCreateTable()
elif next.kind == tkIndex or next.kind == tkUnique:
p.parseCreateIndex()
elif next.kind == tkView or
(next.kind == tkIdent and next.value.toLower() == "or"):
p.parseCreateView()
elif next.kind == tkTrigger:
p.parseCreateTrigger()
elif next.kind == tkMigration:
p.parseCreateMigration()
elif next.kind == tkUser:
p.parseCreateUser()
elif next.kind == tkPolicy:
p.parseCreatePolicy()
elif next.kind == tkGraph:
p.parseCreateGraph()
else:
p.parseCreateType()
else:
p.parseCreateType()
of tkDrop:
if p.pos + 1 < p.tokens.len:
let next = p.tokens[p.pos + 1]
if next.kind == tkTable:
p.parseDropTable()
elif next.kind == tkView:
p.parseDropView()
elif next.kind == tkTrigger:
p.parseDropTrigger()
elif next.kind == tkIndex:
p.parseDropIndex()
elif next.kind == tkUser:
p.parseDropUser()
elif next.kind == tkPolicy:
p.parseDropPolicy()
elif next.kind == tkGraph:
p.parseDropGraph()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkAlter:
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkTable:
p.parseAlterTable()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkGrant:
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()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkMigration:
if p.pos + 1 < p.tokens.len:
let next = p.tokens[p.pos + 1]
if next.kind == tkStatus:
p.parseMigrationStatus()
elif next.kind == tkUp:
p.parseMigrationUp()
elif next.kind == tkDown:
p.parseMigrationDown()
elif next.kind == tkDryRun:
p.parseMigrationDryRun()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkBegin: p.parseBeginTxn()
of tkCommit: p.parseCommitTxn()
of tkRollback: p.parseRollbackTxn()
of tkExplain: p.parseExplain()
of tkRecover:
let tok = p.advance()
discard p.expect(tkTo)
discard p.expect(tkTimestamp)
let tsLit = p.expect(tkStringLit)
Node(kind: nkRecoverToTimestamp, recoverTimestamp: tsLit.value,
line: tok.line, col: tok.col)
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
proc parse*(tokens: seq[Token]): Node =
var parser = newParser(tokens)
result = Node(kind: nkStatementList)
while parser.peek().kind != tkEof:
result.stmts.add(parser.parseStatement())
discard parser.match(tkSemicolon)
proc parse*(input: string): Node =
let tokens = tokenize(input)
parse(tokens)