alopex-sql 0.6.0

SQL parser components for the Alopex DB dialect
## AST definitions for Alopex SQL parser.
##
## The parser still exposes the compact SqlNode tree used by the trial tests,
## while the node/field names are aligned with the target Rust AST contract
## from the nim-sql-parser-migration design.

type
  Location* = object
    line*: int
    column*: int

  Span* = object
    start*: Location
    `end`*: Location

  SqlNodeKind* = enum
    nkSelect
    nkInsert
    nkUpdate
    nkDelete
    nkCreateTable
    nkDropTable
    nkCreateIndex
    nkDropIndex
    nkStatementList
    nkIdentifier
    nkStringLit
    nkIntLit
    nkFloatLit
    nkBoolLit
    nkNull
    nkStar
    nkColumnRef
    nkBinaryOp
    nkUnaryOp
    nkFunctionCall
    nkAlias
    nkFromClause
    nkFromTable
    nkFromJoin
    nkFromDerived
    nkWhereClause
    nkOrderByClause
    nkOrderByExpr
    nkGroupByClause
    nkHavingClause
    nkLimitClause
    nkJoin
    nkUsingClause
    nkColumnDef
    nkTypeName
    nkDataTypeVector
    nkConstraint
    nkExprList
    nkWithOptions
    nkIndexOption
    nkVectorLiteral
    nkScalarSubquery
    nkInSubquery
    nkExists
    nkQuantified

  BinaryOpKind* = enum
    opEq, opNeq, opLt, opLe, opGt, opGe
    opAdd, opSub, opMul, opDiv, opMod
    opStringConcat
    opAnd, opOr
    opLike, opNotLike, opIn, opNotIn, opBetween, opNotBetween, opIs

  UnaryOpKind* = enum
    opNot, opNeg, opIsNull, opIsNotNull

  JoinKind* = enum
    jkInner, jkLeft, jkRight, jkFull, jkCross

  QuantifierKind* = enum
    qkAny, qkAll

  SqlNode* = ref object
    span*: Span
    ## Common optional flags used by Rust-compatible variants:
    ## FunctionCall.distinct/star, EXISTS/IN negated, NATURAL join,
    ## ORDER BY direction/null placement.
    funcDistinct*: bool
    funcStar*: bool
    negated*: bool
    natural*: bool
    orderAsc*: int          ## -1 = omitted, 0 = DESC, 1 = ASC
    nullsFirst*: int        ## -1 = omitted, 0 = LAST, 1 = FIRST
    quantifier*: QuantifierKind
    case kind*: SqlNodeKind
    of nkIdentifier, nkStringLit:
      strVal*: string
    of nkIntLit:
      intVal*: int64
    of nkFloatLit:
      floatVal*: float64
    of nkBoolLit:
      boolVal*: bool
    of nkBinaryOp:
      binOp*: BinaryOpKind
      binLeft*, binRight*: SqlNode
    of nkUnaryOp:
      unOp*: UnaryOpKind
      unOperand*: SqlNode
    of nkJoin, nkFromJoin:
      joinKind*: JoinKind
      joinLeft*, joinRight*, joinCond*: SqlNode
      joinUsing*: seq[string]
    of nkAlias:
      aliasExpr*: SqlNode
      aliasName*: string
    of nkColumnDef:
      colName*: string
      colType*: SqlNode
      colConstraints*: seq[SqlNode]
    else:
      children*: seq[SqlNode]

proc emptyLocation*(): Location =
  Location(line: 0, column: 0)

proc emptySpan*(): Span =
  Span(start: emptyLocation(), `end`: emptyLocation())

proc isEmpty*(span: Span): bool =
  span.start.line == 0 and span.start.column == 0

proc newSpan*(line, startCol, endCol: int): Span =
  Span(start: Location(line: line, column: startCol),
       `end`: Location(line: line, column: endCol))

proc newNode*(kind: SqlNodeKind; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: kind, span: span, orderAsc: -1, nullsFirst: -1)

proc newIdent*(name: string; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkIdentifier, strVal: name, span: span, orderAsc: -1, nullsFirst: -1)

proc newStringLit*(val: string; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkStringLit, strVal: val, span: span, orderAsc: -1, nullsFirst: -1)

proc newIntLit*(val: int64; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkIntLit, intVal: val, span: span, orderAsc: -1, nullsFirst: -1)

proc newFloatLit*(val: float64; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkFloatLit, floatVal: val, span: span, orderAsc: -1, nullsFirst: -1)

proc newBoolLit*(val: bool; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkBoolLit, boolVal: val, span: span, orderAsc: -1, nullsFirst: -1)

proc newNull*(span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkNull, span: span, orderAsc: -1, nullsFirst: -1)

proc newStar*(span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkStar, span: span, orderAsc: -1, nullsFirst: -1)

proc newBinaryOp*(op: BinaryOpKind, left, right: SqlNode; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkBinaryOp, binOp: op, binLeft: left, binRight: right,
          span: span, orderAsc: -1, nullsFirst: -1)

proc newUnaryOp*(op: UnaryOpKind, operand: SqlNode; span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkUnaryOp, unOp: op, unOperand: operand,
          span: span, orderAsc: -1, nullsFirst: -1)

proc newJoin*(joinKind: JoinKind, left, right: SqlNode, condition: SqlNode = nil;
              usingCols: seq[string] = @[], natural = false;
              span: Span = emptySpan()): SqlNode =
  SqlNode(kind: nkJoin, joinKind: joinKind, joinLeft: left, joinRight: right,
          joinCond: condition, joinUsing: usingCols, natural: natural,
          span: span, orderAsc: -1, nullsFirst: -1)

proc fillMissingSpans*(node: SqlNode; fallback: Span) =
  if node == nil:
    return
  if node.span.isEmpty:
    node.span = fallback
  case node.kind
  of nkIdentifier, nkStringLit, nkIntLit, nkFloatLit, nkBoolLit, nkNull, nkStar:
    discard
  of nkBinaryOp:
    node.binLeft.fillMissingSpans(node.span)
    node.binRight.fillMissingSpans(node.span)
  of nkUnaryOp:
    node.unOperand.fillMissingSpans(node.span)
  of nkJoin, nkFromJoin:
    node.joinLeft.fillMissingSpans(node.span)
    node.joinRight.fillMissingSpans(node.span)
    node.joinCond.fillMissingSpans(node.span)
  of nkAlias:
    node.aliasExpr.fillMissingSpans(node.span)
  of nkColumnDef:
    node.colType.fillMissingSpans(node.span)
    for child in node.colConstraints:
      child.fillMissingSpans(node.span)
  else:
    for child in node.children:
      child.fillMissingSpans(node.span)

proc `$`*(node: SqlNode): string =
  ## Debug representation of AST node
  if node == nil:
    return "nil"
  case node.kind
  of nkIdentifier:
    result = "Ident(" & node.strVal & ")"
  of nkStringLit:
    result = "Str('" & node.strVal & "')"
  of nkIntLit:
    result = "Int(" & $node.intVal & ")"
  of nkFloatLit:
    result = "Float(" & $node.floatVal & ")"
  of nkBoolLit:
    result = "Bool(" & $node.boolVal & ")"
  of nkNull:
    result = "NULL"
  of nkStar:
    result = "*"
  of nkBinaryOp:
    result = "BinOp(" & $node.binOp & ", " & $node.binLeft & ", " & $node.binRight & ")"
  of nkUnaryOp:
    result = "UnaryOp(" & $node.unOp & ", " & $node.unOperand & ")"
  of nkJoin, nkFromJoin:
    result = "Join(" & $node.joinKind & ", " & $node.joinLeft & ", " & $node.joinRight
    if node.joinCond != nil:
      result &= ", ON " & $node.joinCond
    if node.joinUsing.len > 0:
      result &= ", USING " & $node.joinUsing
    if node.natural:
      result &= ", NATURAL"
    result &= ")"
  of nkAlias:
    result = "Alias(" & $node.aliasExpr & " AS " & node.aliasName & ")"
  of nkColumnDef:
    result = "ColDef(" & node.colName & " " & $node.colType & ")"
  else:
    result = $node.kind & "("
    for i, child in node.children:
      if i > 0:
        result &= ", "
      result &= $child
    result &= ")"