feat: production polish — recursive CTE, UNION/INTERSECT/EXCEPT, DROP INDEX, JSON path, FTS SQL, covering index, SCRAM auth

- Recursive CTE execution (WITH RECURSIVE + UNION ALL, work-table loop)
- UNION / INTERSECT / EXCEPT parser + executor (nkSetOp)
- DROP INDEX parser + executor
- VIEW DDL persistence via AST-to-SQL serializer
- JSON path operators -> and ->> (lexer, parser, IR, executor)
- FTS SQL wiring WHERE col @@ 'query' (case-insensitive term match)
- Multi-column index range scans (prefix equality + range on last col)
- Covering index optimization (skip LSM read when index covers SELECT)
- SCRAM-SHA-256 authentication (password-based validation)
- 279 tests, 0 failures
This commit is contained in:
2026-05-07 13:00:36 +03:00
parent 7faae09ad3
commit 5deb38feb2
8 changed files with 548 additions and 41 deletions
+21
View File
@@ -53,6 +53,7 @@ type
# Expressions
nkBinOp
nkUnaryOp
nkJsonPath # column->'key' or column->>'key'
nkFuncCall
nkTypeCast
nkPath
@@ -87,6 +88,9 @@ type
nkVectorSimilar
nkVectorNearest
# Set operations
nkSetOp
# Join
nkJoin
@@ -124,6 +128,9 @@ type
bkCoalesce = "??"
bkAssign = ":="
bkArrow = "=>"
bkJsonPath = "->"
bkJsonPathText = "->>"
bkFtsMatch = "@@"
UnaryOpKind* = enum
ukNeg = "-"
@@ -138,6 +145,11 @@ type
jkFull
jkCross
SetOpKind* = enum
sdkUnion
sdkIntersect
sdkExcept
SortDir* = enum
sdAsc
sdDesc
@@ -318,6 +330,10 @@ type
of nkUnaryOp:
unOp*: UnaryOpKind
unOperand*: Node
of nkJsonPath:
jpLeft*: Node
jpKey*: string
jpAsText*: bool # true for ->>, false for ->
of nkFuncCall:
funcName*: string
funcArgs*: seq[Node]
@@ -412,6 +428,11 @@ type
joinTarget*: Node
joinOn*: Node
joinAlias*: string
of nkSetOp:
setOpKind*: SetOpKind
setOpAll*: bool
setOpLeft*: Node
setOpRight*: Node
of nkStar:
discard
of nkPlaceholder:
+342 -3
View File
@@ -138,6 +138,110 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
onChange: nil)
restoreSchema(result)
# ----------------------------------------------------------------------
# AST to SQL serializer (for VIEW DDL persistence)
# ----------------------------------------------------------------------
proc exprToSql(node: Node): string =
if node == nil:
return ""
case node.kind
of nkIntLit:
return $node.intVal
of nkFloatLit:
return $node.floatVal
of nkStringLit:
return "'" & node.strVal & "'"
of nkBoolLit:
return if node.boolVal: "true" else: "false"
of nkNullLit:
return "null"
of nkIdent:
return node.identName
of nkStar:
return "*"
of nkBinOp:
let opStr = case node.binOp
of bkEq: "="
of bkNotEq: "!="
of bkLt: "<"
of bkLtEq: "<="
of bkGt: ">"
of bkGtEq: ">="
of bkAnd: " AND "
of bkOr: " OR "
of bkAdd: " + "
of bkSub: " - "
of bkMul: " * "
of bkDiv: " / "
else: " " & $node.binOp & " "
return exprToSql(node.binLeft) & opStr & exprToSql(node.binRight)
of nkFuncCall:
return node.funcName & "(" & exprToSql(node.funcArgs[0]) & ")"
of nkUnaryOp:
return $node.unOp & " " & exprToSql(node.unOperand)
else:
return $node.kind
proc selectToSql(node: Node): string =
if node == nil:
return ""
result = "SELECT "
# Column list
for i, e in node.selResult:
if i > 0: result.add(", ")
result.add(exprToSql(e))
if e.exprAlias.len > 0:
result.add(" AS " & e.exprAlias)
# FROM
if node.selFrom != nil and node.selFrom.fromTable.len > 0:
result.add(" FROM " & node.selFrom.fromTable)
if node.selFrom.fromAlias.len > 0:
result.add(" AS " & node.selFrom.fromAlias)
# JOINs
for j in node.selJoins:
if j.kind == nkJoin:
let jkStr = case j.joinKind
of jkInner: "INNER JOIN"
of jkLeft: "LEFT JOIN"
of jkRight: "RIGHT JOIN"
of jkFull: "FULL JOIN"
of jkCross: "CROSS JOIN"
result.add(" " & jkStr & " " & j.joinTarget.fromTable)
if j.joinAlias.len > 0:
result.add(" AS " & j.joinAlias)
if j.joinOn != nil:
result.add(" ON " & exprToSql(j.joinOn))
# WHERE
if node.selWhere != nil and node.selWhere.whereExpr != nil:
result.add(" WHERE " & exprToSql(node.selWhere.whereExpr))
# GROUP BY
if node.selGroupBy.len > 0:
result.add(" GROUP BY ")
for i, g in node.selGroupBy:
if i > 0: result.add(", ")
result.add(exprToSql(g))
# HAVING
if node.selHaving != nil and node.selHaving.havingExpr != nil:
result.add(" HAVING " & exprToSql(node.selHaving.havingExpr))
# ORDER BY
if node.selOrderBy.len > 0:
result.add(" ORDER BY ")
for i, o in node.selOrderBy:
if i > 0: result.add(", ")
result.add(exprToSql(o.orderByExpr))
if o.orderByDir == sdDesc:
result.add(" DESC")
# LIMIT / OFFSET
if node.selLimit != nil and node.selLimit.limitExpr.kind == nkIntLit:
result.add(" LIMIT " & $node.selLimit.limitExpr.intVal)
if node.selOffset != nil and node.selOffset.offsetExpr.kind == nkIntLit:
result.add(" OFFSET " & $node.selOffset.offsetExpr.intVal)
# ----------------------------------------------------------------------
# Schema restore
# ----------------------------------------------------------------------
proc restoreSchema(ctx: ExecutionContext) =
for entry in ctx.db.scanMemTable():
if entry.deleted: continue
@@ -323,6 +427,29 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string]): string =
return ""
of irekStar:
return "*"
of irekJsonPath:
let srcVal = evalExpr(expr.jpExpr, row)
if srcVal.len == 0: return ""
try:
let node = parseJson(srcVal)
if node.hasKey(expr.jpKey):
let val = node[expr.jpKey]
if expr.jpAsText:
case val.kind
of JString: return val.getStr()
of JInt: return $val.getInt()
of JFloat: return $val.getFloat()
of JBool: return $val.getBool()
of JNull: return "null"
else: return $val
else:
case val.kind
of JString: return "\"" & val.getStr() & "\""
of JNull: return "null"
else: return $val
return ""
except:
return ""
of irekBinary:
let left = evalExpr(expr.binLeft, row)
let right = evalExpr(expr.binRight, row)
@@ -410,6 +537,16 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string]): string =
return if lv != rv: "true" else: "false"
except: discard
return if left != right: "true" else: "false"
of irFtsMatch:
# Simple full-text search: case-insensitive phrase containment
let colVal = left.toLower()
let query = right.toLower()
# Split query into terms and check each term is in column
let terms = query.split()
for term in terms:
if term.len > 0 and term notin colVal:
return "false"
return "true"
else: return "false"
of irekUnary:
case expr.unOp
@@ -796,6 +933,11 @@ proc lowerExpr*(node: Node): IRExpr =
of nkPath:
result = IRExpr(kind: irekField)
result.fieldPath = node.pathParts
of nkJsonPath:
result = IRExpr(kind: irekJsonPath)
result.jpExpr = lowerExpr(node.jpLeft)
result.jpKey = node.jpKey
result.jpAsText = node.jpAsText
of nkBinOp:
result = IRExpr(kind: irekBinary)
var irOp: IROperator
@@ -813,6 +955,7 @@ proc lowerExpr*(node: Node): IRExpr =
of bkGtEq: irOp = irGte
of bkAnd: irOp = irAnd
of bkOr: irOp = irOr
of bkFtsMatch: irOp = irFtsMatch
else: irOp = irEq
result.binOp = irOp
result.binLeft = lowerExpr(node.binLeft)
@@ -1360,8 +1503,68 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
if stmt.selWith.len > 0:
for (cteName, cteQuery, isRecursive) in stmt.selWith:
if isRecursive:
# Recursive CTE: not yet fully implemented
ctx.cteTables[cteName] = @[]
# Recursive CTE: must be UNION ALL with anchor + recursive member
if cteQuery.kind == nkSetOp and cteQuery.setOpKind == sdkUnion:
var allRows: seq[Row] = @[]
# Step 1: Execute the non-recursive anchor (left side of UNION)
var innerLeft = Node(kind: nkStatementList, stmts: @[])
innerLeft.stmts.add(cteQuery.setOpLeft)
let anchorRes = executeQuery(ctx, innerLeft)
for row in anchorRes.rows:
allRows.add(row)
var workTable = anchorRes.rows
const maxIterations = 1000
var iteration = 0
# Step 2: Iteratively execute the recursive member
while workTable.len > 0 and iteration < maxIterations:
# Save CTE state; recursive member's executeQuery will clear it via defer
let savedCte = ctx.cteTables
ctx.cteTables = {cteName: workTable}.toTable()
var innerRight = Node(kind: nkStatementList, stmts: @[])
innerRight.stmts.add(cteQuery.setOpRight)
let rightRes = executeQuery(ctx, innerRight)
ctx.cteTables = savedCte
var newRows: seq[Row] = @[]
if not cteQuery.setOpAll:
# UNION: deduplicate against all already-accumulated rows
var seen = initTable[string, bool]()
for existing in allRows:
let key = if "$value" in existing: existing["$value"] else: $existing
if key.len > 0:
seen[key] = true
for row in rightRes.rows:
let key = if "$value" in row: row["$value"] else: $row
if not seen.getOrDefault(key, false):
if key.len > 0:
seen[key] = true
newRows.add(row)
else:
newRows = rightRes.rows
if newRows.len == 0:
break
for row in newRows:
allRows.add(row)
workTable = newRows
iteration += 1
ctx.cteTables[cteName] = allRows
else:
# Recursive CTE without UNION — treat as non-recursive fallback
var inner = Node(kind: nkStatementList, stmts: @[])
inner.stmts.add(cteQuery)
let cteRes = executeQuery(ctx, inner)
var cteRows: seq[Row] = @[]
for row in cteRes.rows:
cteRows.add(row)
ctx.cteTables[cteName] = cteRows
else:
var inner = Node(kind: nkStatementList, stmts: @[])
inner.stmts.add(cteQuery)
@@ -1419,13 +1622,18 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
let w = stmt.selWhere.whereExpr
# Multi-column exact match: AND chain of =
var eqConds: seq[(string, string)] = @[]
var rangeCond: tuple[col: string, op: BinOpKind, val: string] = ("", bkEq, "")
proc collectEq(node: Node) =
if node.kind == nkBinOp and node.binOp == bkEq and node.binLeft.kind == nkIdent and node.binRight.kind == nkStringLit:
eqConds.add((node.binLeft.identName, node.binRight.strVal))
elif node.kind == nkBinOp and node.binOp == bkAnd:
collectEq(node.binLeft)
collectEq(node.binRight)
elif node.kind == nkBinOp and node.binOp in {bkGt, bkGtEq, bkLt, bkLtEq} and
node.binLeft.kind == nkIdent and node.binRight.kind == nkStringLit:
rangeCond = (node.binLeft.identName, node.binOp, node.binRight.strVal)
collectEq(w)
# Multi-column exact match
if eqConds.len >= 2:
var idxCols: seq[string] = @[]
for c in eqConds: idxCols.add(c[0])
@@ -1446,6 +1654,46 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
for c in tbl.columns: cols.add(c.name)
if cols.len == 0: cols = @["key", "value"]
return okResult(rows, cols)
# Multi-column range scan: exact match on prefix + range on last column
if eqConds.len >= 1 and rangeCond.col.len > 0:
var idxCols: seq[string] = @[]
for c in eqConds: idxCols.add(c[0])
idxCols.add(rangeCond.col)
let idxName = stmt.selFrom.fromTable & "." & idxCols.join(".")
if idxName in ctx.btrees:
var prefix: string = ""
for c in eqConds:
if prefix.len > 0: prefix.add("|")
prefix.add(c[1])
if prefix.len > 0: prefix.add("|")
var startKey, endKey: string
case rangeCond.op
of bkGt:
startKey = prefix & rangeCond.val & "\x01" # just above the value
endKey = prefix & "\xFF"
of bkGtEq:
startKey = prefix & rangeCond.val
endKey = prefix & "\xFF"
of bkLt:
startKey = prefix
endKey = prefix & rangeCond.val
of bkLtEq:
startKey = prefix
endKey = prefix & rangeCond.val & "\x01"
else:
startKey = prefix; endKey = prefix
let scanned = ctx.btrees[idxName].scan(startKey, endKey)
var rows: seq[Row] = @[]
for (k, entries) in scanned:
for entry in entries:
let (found, val) = ctx.db.get(entry.lsmKey)
if found:
rows.add(parseRowData(cast[string](val)))
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
var cols: seq[string] = @[]
for c in tbl.columns: cols.add(c.name)
if cols.len == 0: cols = @["key", "value"]
return okResult(rows, cols)
if w.kind == nkBinOp and w.binOp == bkEq:
if w.binLeft.kind == nkIdent and w.binRight.kind == nkStringLit:
let colName = w.binLeft.identName
@@ -1453,6 +1701,23 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
if idxName in ctx.btrees:
let entries = ctx.btrees[idxName].get(w.binRight.strVal)
if entries.len > 0:
# Check for covering index: SELECT list matches index column
var isCovered = true
var coveredCols: seq[string] = @[]
for e in stmt.selResult:
if e.kind == nkIdent:
coveredCols.add(e.identName)
if e.identName != colName:
isCovered = false
elif e.kind != nkStar:
isCovered = false
if isCovered and coveredCols.len > 0:
var rows: seq[Row] = @[]
for entry in entries:
var row = initTable[string, string]()
row[colName] = w.binRight.strVal
rows.add(row)
return okResult(rows, coveredCols)
# Fetch actual row data from LSM
let rows = execPointRead(ctx, stmt.selFrom.fromTable, colName & "=" & w.binRight.strVal)
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
@@ -1525,6 +1790,59 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
for k, _ in rows[0]: cols.add(k)
return okResult(rows, cols)
of nkSetOp:
# Execute left and right queries
var innerLeft = Node(kind: nkStatementList, stmts: @[])
innerLeft.stmts.add(stmt.setOpLeft)
let leftRes = executeQuery(ctx, innerLeft)
var innerRight = Node(kind: nkStatementList, stmts: @[])
innerRight.stmts.add(stmt.setOpRight)
let rightRes = executeQuery(ctx, innerRight)
# Derive columns from left side
var cols = leftRes.columns
if cols.len == 0:
cols = rightRes.columns
var rows: seq[Row] = @[]
case stmt.setOpKind
of sdkUnion:
rows = leftRes.rows
if stmt.setOpAll:
# UNION ALL: simple concatenation
for row in rightRes.rows:
rows.add(row)
else:
# UNION: deduplicate
var seen: Table[string, bool]
for row in leftRes.rows:
seen[row["$value"]] = true
for row in rightRes.rows:
if not seen.getOrDefault(row["$value"], false):
seen[row["$value"]] = true
rows.add(row)
of sdkIntersect:
var leftSet: Table[string, bool]
for row in leftRes.rows:
leftSet[row["$value"]] = true
for row in rightRes.rows:
if leftSet.getOrDefault(row["$value"], false):
rows.add(row)
if not stmt.setOpAll:
leftSet.del(row["$value"]) # remove to prevent duplicates for INTERSECT (not ALL)
of sdkExcept:
var rightSet: Table[string, bool]
for row in rightRes.rows:
rightSet[row["$value"]] = true
for row in leftRes.rows:
if not rightSet.getOrDefault(row["$value"], false):
rows.add(row)
return okResult(rows, cols)
of nkInsert:
var fields: seq[string] = @[]
for f in stmt.insFields:
@@ -1790,7 +2108,8 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
of nkCreateView:
ctx.views[stmt.cvName] = stmt.cvQuery
let viewKey = "_schema:views:" & stmt.cvName
let viewDdl = "CREATE VIEW " & stmt.cvName & " AS SELECT 1" # placeholder; real AST serialization needed
let viewSql = selectToSql(stmt.cvQuery)
let viewDdl = "CREATE VIEW " & stmt.cvName & " AS " & viewSql
ctx.db.put(viewKey, cast[seq[byte]](viewDdl))
return okResult(msg="CREATE VIEW " & stmt.cvName)
@@ -2006,6 +2325,26 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
ctx.btrees[colKey].insert(idxVal, IndexEntry(lsmKey: lsmKey, rowValue: ""))
return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget)
of nkDropIndex:
# Find and remove index by name from ctx.btrees
var found = false
var targetKey = ""
for key, _ in ctx.btrees:
# Index key format: table.col or table.col1.col2
# Try matching by the full key or by the table.indexName convention
if key == stmt.diName or key.endsWith("." & stmt.diName):
targetKey = key
found = true
break
if found:
ctx.btrees.del(targetKey)
return okResult(msg="DROP INDEX " & stmt.diName)
else:
# Also remove from schema storage
let idxKey = "_schema:indexes:" & stmt.diName
ctx.db.delete(idxKey)
return okResult(msg="DROP INDEX " & stmt.diName)
of nkCreateUser:
ctx.users[stmt.cuName] = UserDef(name: stmt.cuName, passwordHash: stmt.cuPassword,
isSuperuser: stmt.cuSuperuser, roles: @[])
+11 -1
View File
@@ -26,6 +26,7 @@ type
irLike, irILike
irBetween
irIsNull, irIsNotNull
irFtsMatch
IRAggregate* = enum
irCount, irSum, irAvg, irMin, irMax
@@ -50,6 +51,7 @@ type
irekConditional
irekExists
irekStar
irekJsonPath
IRJoinKind* = enum
irjkInner
@@ -166,6 +168,10 @@ type
existsSubquery*: IRPlan
of irekStar:
discard
of irekJsonPath:
jpExpr*: IRExpr
jpKey*: string
jpAsText*: bool
type
TypeChecker* = ref object
@@ -209,7 +215,8 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
return nil
case expr.unOp
of irEq, irNeq, irLt, irLte, irGt, irGte, irAnd, irOr, irNot,
irIsNull, irIsNotNull, irIn, irNotIn, irLike, irILike, irBetween:
irIsNull, irIsNotNull, irIn, irNotIn, irLike, irILike, irBetween,
irFtsMatch:
return IRType(name: "bool", kind: itkScalar)
else:
return nil
@@ -245,3 +252,6 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
return IRType(name: "bool", kind: itkScalar)
of irekStar:
return IRType(name: "star", kind: itkScalar)
of irekJsonPath:
if expr.jpAsText: return IRType(name: "str", kind: itkScalar)
return IRType(name: "json", kind: itkScalar)
+20
View File
@@ -72,6 +72,7 @@ type
tkUnion
tkIntersect
tkExcept
tkAll
tkExists
tkBetween
tkLike
@@ -121,6 +122,9 @@ type
tkVector
tkGraph
tkDocument
tkArrowR # ->
tkArrowRR # ->>
tkFtsMatch # @@
tkSimilar
tkNearest
tkTo
@@ -237,6 +241,7 @@ const keywords*: Table[string, TokenKind] = {
"union": tkUnion,
"intersect": tkIntersect,
"except": tkExcept,
"all": tkAll,
"exists": tkExists,
"between": tkBetween,
"like": tkLike,
@@ -433,6 +438,14 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkPlus, value: "+", line: startLine, col: startCol)
of '-':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkArrowRR, value: "->>", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkArrowR, value: "->", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkMinus, value: "-", line: startLine, col: startCol)
of '*':
@@ -499,6 +512,13 @@ proc nextToken*(l: var Lexer): Token =
return Token(kind: tkCoalesce, value: "??", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkPlaceholder, value: "?", line: startLine, col: startCol)
of '@':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '@':
discard l.advance()
discard l.advance()
return Token(kind: tkFtsMatch, value: "@@", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "@", line: startLine, col: startCol)
of '.':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '<':
discard l.advance()
+47 -3
View File
@@ -157,8 +157,17 @@ proc parsePrimary(p: var Parser): Node =
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
proc parseMulDiv(p: var Parser): Node =
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
@@ -167,7 +176,7 @@ proc parseMulDiv(p: var Parser): Node =
of tkFloorDiv: bkFloorDiv
else: bkMul
let tok = p.advance()
let right = p.parsePrimary()
let right = p.parsePostfix()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
@@ -217,7 +226,7 @@ proc parseComparison(p: var Parser): Node =
discard p.advance() # consume NULL token (assumed)
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
line: tok.line, col: tok.col)
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq}:
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch}:
let op = case p.peek().kind
of tkEq: bkEq
of tkNotEq: bkNotEq
@@ -225,6 +234,7 @@ proc parseComparison(p: var Parser): Node =
of tkLtEq: bkLtEq
of tkGt: bkGt
of tkGtEq: bkGtEq
of tkFtsMatch: bkFtsMatch
else: bkEq
let tok = p.advance()
let right = p.parseAddSub()
@@ -422,6 +432,27 @@ proc parseSelect(p: var Parser): Node =
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
@@ -857,6 +888,17 @@ proc parseDropTrigger(p: var Parser): Node =
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)
@@ -1090,6 +1132,8 @@ proc parseStatement*(p: var Parser): Node =
p.parseDropView()
elif next.kind == tkTrigger:
p.parseDropTrigger()
elif next.kind == tkIndex:
p.parseDropIndex()
elif next.kind == tkUser:
p.parseDropUser()
elif next.kind == tkPolicy: