## 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]
  return 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

# Token kinds that can also serve as identifiers in table/column name positions
const identLikeKinds = {tkIdent, tkLabels, tkCount, tkSum, tkAvg, tkMin, tkMax,
    tkArrayAgg, tkStringAgg, tkJsonFmt, tkArray, tkVector, tkGraph, tkDocument,
    tkHeader, tkFormat, tkDelimiter, tkBatch, tkCsv, tkNdjson,
    tkStatus, tkMigration, tkApply, tkUp, tkDown, tkDryRun,
    tkUser, tkPolicy, tkEnable, tkDisable, tkRecover,
    tkBefore, tkAfter, tkInstead, tkOf}

proc expectIdent(p: var Parser): Token =
  ## Expect a token that can serve as an identifier (table name, column name, alias, etc.).
  ## Accepts tkIdent plus keyword tokens that are commonly used as names.
  let tok = p.peek()
  if tok.kind in identLikeKinds:
    return p.advance()
  raise newException(ValueError,
    "Expected identifier but got " & $tok.kind & " at line " & $tok.line)

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, tkLabels, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile,
      tkHeader, tkFormat, tkDelimiter, tkBatch, tkCsv, tkNdjson,
      tkStatus, tkMigration, tkApply, tkUp, tkDown, tkDryRun,
      tkUser, tkPolicy, tkEnable, tkDisable, tkRecover,
      tkBefore, tkAfter, tkInstead, tkOf:
    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.expectIdent().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:
    # If not followed by '(', treat as a regular identifier (column reference)
    if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind != tkLParen:
      discard p.advance()
      let identName = tok.value
      var parts = @[identName]
      while p.peek().kind == tkDot:
        discard p.advance()
        parts.add(p.expectIdent().value)
      if parts.len == 1:
        return Node(kind: nkIdent, identName: identName, line: tok.line, col: tok.col)
      return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
    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,
                    funcDistinct: hasDistinct,
                    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 parsePower(p: var Parser): Node =
  result = p.parsePostfix()
  if p.peek().kind == tkPower:
    let tok = p.advance()
    let right = p.parsePower()  # right-associative
    result = Node(kind: nkBinOp, binOp: bkPow, binLeft: result, binRight: right,
                  line: tok.line, col: tok.col)

proc parseMulDiv(p: var Parser): Node =
  result = p.parsePower()
  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()
  # Check for NOT prefix on IN/LIKE/BETWEEN
  var negated = false
  if p.peek().kind == tkNot:
    discard p.advance()
    negated = true
  # 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,
                betweenNegated: negated,
                line: tok.line, col: tok.col)
  # Handle IN (subquery | list)
  if p.peek().kind == tkIn:
    let tok = p.advance()
    discard p.expect(tkLParen)
    let right: Node =
      if p.peek().kind == tkSelect:
        let sub = p.parseSelect()
        discard p.expect(tkRParen)
        Node(kind: nkSubquery, subQuery: sub, line: tok.line, col: tok.col)
      else:
        var elems: seq[Node] = @[]
        elems.add(p.parseExpr())
        while p.match(tkComma):
          elems.add(p.parseExpr())
        discard p.expect(tkRParen)
        Node(kind: nkArrayLit, arrayElems: elems, line: tok.line, col: tok.col)
    return Node(kind: nkInExpr, inLeft: result, inRight: right,
                inNegated: negated,
                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,
                likeNegated: negated,
                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)
  # If we consumed NOT but didn't find IN/LIKE/BETWEEN, wrap the full comparison in NOT
  if negated:
    result = Node(kind: nkUnaryOp, unOp: ukNot, unOperand: result, line: 0, col: 0)

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.expectIdent().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.expectIdent().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.expectIdent().value
  result.selResult.add(expr)
  while p.match(tkComma):
    expr = p.parseExpr()
    if p.match(tkAs):
      expr.exprAlias = p.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent()
      var alias = ""
      if p.match(tkAs):
        alias = p.expectIdent().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.expectIdent()
      var nextAlias = ""
      if p.match(tkAs):
        nextAlias = p.expectIdent().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.expectIdent().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.expectIdent().value
      discard p.expect(tkFor)
      let forCol = p.expectIdent().value
      discard p.expect(tkIn)
      discard p.expect(tkLParen)
      var inCols: seq[string] = @[]
      inCols.add(p.expectIdent().value)
      while p.match(tkComma):
        inCols.add(p.expectIdent().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.expectIdent().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.expectIdent()
        if p.match(tkAs):
          joinAlias = p.expectIdent().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.expectIdent().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.expectIdent().value
  result = Node(kind: nkUpdate, updTarget: target, line: tok.line, col: tok.col)
  if p.match(tkSet):
    result.updSet = @[]
    let field = p.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().value
  if p.match(tkAs):
    result.mergeTargetAlias = p.expectIdent().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.expectIdent().value
    result.mergeSource = Node(kind: nkIdent, identName: srcTable, line: tok.line, col: tok.col)
  if p.match(tkAs):
    result.mergeSourceAlias = p.expectIdent().value
  elif p.peek().kind == tkIdent:
    result.mergeSourceAlias = p.advance().value
  discard p.expect(tkOn)
  result.mergeOn = p.parseExpr()
  # Parse MERGE branches: WHEN MATCHED [AND cond] THEN ... / WHEN NOT MATCHED [AND cond] THEN ...
  while p.match(tkWhen):
    var isNotMatched = p.match(tkNot)
    discard p.expect(tkMatched)
    # Optional AND <condition>
    if p.match(tkAnd):
      result.mergeMatchedCondition = p.parseExpr()
    discard p.expect(tkThen)
    if p.peek().kind == tkDelete:
      discard p.advance()
      if not isNotMatched:
        result.mergeMatchedDelete = true
      else:
        # WHEN NOT MATCHED THEN DELETE is non-standard but treat as no-op
        discard
    elif p.peek().kind == tkDo:
      discard p.advance()  # consume DO
      discard p.expect(tkNothing)
      if isNotMatched:
        result.mergeNotMatchedNothing = true
    elif p.peek().kind == tkInsert:
      discard p.advance()
      discard p.expect(tkLParen)
      result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expectIdent().value))
      while p.match(tkComma):
        result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expectIdent().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)
    elif p.peek().kind == tkUpdate:
      discard p.advance()
      discard p.expect(tkSet)
      let col = p.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent()
      if p.peek().kind == tkArrow:
        discard p.advance()
        let target = p.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().value)
          while p.match(tkComma):
            cst.cstColumns.add(p.expectIdent().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.expectIdent().value)
          while p.match(tkComma):
            cst.cstColumns.add(p.expectIdent().value)
          discard p.expect(tkRParen)
        discard p.expect(tkReferences)
        cst.cstRefTable = p.expectIdent().value
        if p.peek().kind == tkLParen:
          discard p.advance()
          cst.cstRefColumns.add(p.expectIdent().value)
          while p.match(tkComma):
            cst.cstRefColumns.add(p.expectIdent().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 == 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.expectIdent().value)
          while p.match(tkComma):
            cst.cstColumns.add(p.expectIdent().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.expectIdent().value
    var colType = ""
    let typeKinds = {tkIdent, tkVector, tkJsonFmt, tkArray, tkDocument, tkGraph}
    if p.peek().kind in typeKinds:
      let typeTok = p.advance()
      if typeTok.kind == tkVector:
        colType = "VECTOR"
      elif typeTok.kind == tkJsonFmt:
        colType = "JSON"
      elif typeTok.kind == tkArray:
        colType = "ARRAY"
      elif typeTok.kind == tkDocument:
        colType = "DOCUMENT"
      elif typeTok.kind == tkGraph:
        colType = "GRAPH"
      else:
        colType = typeTok.value.toUpper()
      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.expectIdent().value
        if p.peek().kind == tkLParen:
          discard p.advance()
          cst.cstRefColumns.add(p.expectIdent().value)
          while p.match(tkComma):
            cst.cstRefColumns.add(p.expectIdent().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.expectIdent().value

proc parseAlterTable(p: var Parser): Node =
  let tok = p.expect(tkAlter)
  discard p.expect(tkTable)
  let tableName = p.expectIdent().value
  # Check for ENABLE/DISABLE ROW LEVEL SECURITY
  if p.peek().kind == tkEnable:
    discard p.advance()
    discard p.expect(tkRow)    # ROW
    discard p.expectIdent()  # LEVEL
    discard p.expectIdent()  # 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.expectIdent()  # LEVEL
    discard p.expectIdent()  # 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.expectIdent().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.expectIdent().value
  discard p.match(tkLParen)
  var colNames: seq[string] = @[]
  colNames.add(p.expectIdent().value)
  while p.match(tkComma):
    colNames.add(p.expectIdent().value)
  discard p.match(tkRParen)
  var idxKind = ikBTree
  if p.match(tkUsing):
    let idxMethod = p.expectIdent().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.expectIdent()  # REPLACE
    orReplace = true
  discard p.expect(tkView)
  let name = p.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().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.expectIdent().value
  result = Node(kind: nkMigrationDryRun, mdrName: name, line: tok.line, col: tok.col)

proc parseImportFrom(p: var Parser): Node =
  let tok = p.expect(tkImport)
  discard p.expect(tkFrom)
  let path = p.expect(tkStringLit).value
  discard p.expect(tkInto)
  let table = p.expectIdent().value
  var format = "csv"
  var delimiter = ','
  var hasHeader = true
  var batchSize = 1000
  while p.peek().kind in {tkFormat, tkDelimiter, tkHeader, tkBatch}:
    let kw = p.advance()
    case kw.kind
    of tkFormat:
      format = p.expectIdent().value.toLower()
    of tkDelimiter:
      let delim = p.expect(tkStringLit).value
      if delim.len > 0: delimiter = delim[0]
    of tkHeader:
      if p.peek().kind == tkTrue:
        discard p.advance()
        hasHeader = true
      elif p.peek().kind == tkFalse:
        discard p.advance()
        hasHeader = false
      else:
        let hdr = p.expectIdent().value.toLower()
        hasHeader = hdr == "true" or hdr == "yes"
    of tkBatch:
      batchSize = parseInt(p.expect(tkIntLit).value)
    else: discard
  result = Node(kind: nkImportFrom, impPath: path, impTable: table,
                impFormat: format, impDelimiter: delimiter,
                impHasHeader: hasHeader, impBatchSize: batchSize,
                line: tok.line, col: tok.col)

proc parseExportTo(p: var Parser): Node =
  let tok = p.expect(tkExport)
  discard p.expect(tkTo)
  let path = p.expect(tkStringLit).value
  discard p.expect(tkFrom)
  let table = p.expectIdent().value
  var format = "csv"
  var delimiter = ','
  var includeHeader = true
  while p.peek().kind in {tkFormat, tkDelimiter, tkHeader}:
    let kw = p.advance()
    case kw.kind
    of tkFormat:
      format = p.expectIdent().value.toLower()
    of tkDelimiter:
      let delim = p.expect(tkStringLit).value
      if delim.len > 0: delimiter = delim[0]
    of tkHeader:
      if p.peek().kind == tkTrue:
        discard p.advance()
        includeHeader = true
      elif p.peek().kind == tkFalse:
        discard p.advance()
        includeHeader = false
      else:
        let hdr = p.expectIdent().value.toLower()
        includeHeader = hdr == "true" or hdr == "yes"
    else: discard
  result = Node(kind: nkExportTo, expPath: path, expTable: table,
                expFormat: format, expDelimiter: delimiter,
                expIncludeHeader: includeHeader,
                line: tok.line, col: tok.col)

proc parseCreateUser(p: var Parser): Node =
  let tok = p.expect(tkCreate)
  discard p.expect(tkUser)
  let name = p.expectIdent().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.expectIdent().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.expectIdent().value
  discard p.expect(tkOn)
  let tableName = p.expectIdent().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.expectIdent().value
  discard p.expect(tkOn)
  let tableName = p.expectIdent().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.expectIdent().value
  discard p.expect(tkTo)
  let grantee = p.expectIdent().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.expectIdent().value
  discard p.expect(tkFrom)
  let grantee = p.expectIdent().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.expectIdent().value
  while p.peek().kind == tkDot:
    discard p.advance()
    varName.add(".")
    varName.add(p.expectIdent().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.expectIdent().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.expectIdent().value
  result = Node(kind: nkDropGraph, dgName: name, dgIfExists: ifExists,
                line: tok.line, col: tok.col)

proc parseCreateDatabase(p: var Parser): Node =
  let tok = p.expect(tkCreate)
  discard p.expect(tkDatabase)
  var ifNotExists = false
  if p.peek().kind == tkIf:
    discard p.advance()
    discard p.expect(tkNot)
    discard p.expect(tkExists)
    ifNotExists = true
  let name = p.expectIdent().value
  result = Node(kind: nkCreateDatabase, cdDbName: name, cdIfNotExists: ifNotExists,
                line: tok.line, col: tok.col)

proc parseDropDatabase(p: var Parser): Node =
  let tok = p.expect(tkDrop)
  discard p.expect(tkDatabase)
  var ifExists = false
  if p.peek().kind == tkIf:
    discard p.advance()
    discard p.expect(tkExists)
    ifExists = true
  let name = p.expectIdent().value
  result = Node(kind: nkDropDatabase, ddDbName: name, ddIfExists: ifExists,
                line: tok.line, col: tok.col)

proc parseUseDatabase(p: var Parser): Node =
  let tok = p.expect(tkUse)
  let name = p.expectIdent().value
  result = Node(kind: nkUseDatabase, udDbName: name,
                line: tok.line, col: tok.col)

proc parseShowDatabases(p: var Parser): Node =
  let tok = p.expect(tkShow)
  discard p.expect(tkDatabases)
  result = Node(kind: nkShowDatabases, line: tok.line, col: tok.col)

proc parseShowTables(p: var Parser): Node =
  let tok = p.expect(tkShow)
  discard p.expect(tkTable)  # consume TABLES or TABLE
  result = Node(kind: nkShowTables, line: tok.line, col: tok.col)
  result.stTableName = ""

proc parseDescribeTable(p: var Parser): Node =
  let tok = p.expect(tkShow)
  discard p.expect(tkColumns)
  discard p.expect(tkFrom)
  result = Node(kind: nkShowTables, line: tok.line, col: tok.col)
  result.stTableName = p.expectIdent().value

proc parseStatement*(p: var Parser): Node =
  case p.peek().kind
  of tkWith, tkSelect: p.parseSelect()
  of tkInsert: p.parseInsert()
  of tkImport: p.parseImportFrom()
  of tkExport: p.parseExportTo()
  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()
      elif next.kind == tkDatabase:
        p.parseCreateDatabase()
      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()
      elif next.kind == tkDatabase:
        p.parseDropDatabase()
      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 tkUse:
    p.parseUseDatabase()
  of tkShow:
    if p.pos + 1 < p.tokens.len:
      let next = p.tokens[p.pos + 1].kind
      if next == tkDatabases:
        p.parseShowDatabases()
      elif next == tkTable:
        p.parseShowTables()
      elif next == tkColumns:
        p.parseDescribeTable()
      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 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)
