Files
Baradb/src/barabadb/query/lexer.nim
T
dimgigov d076cfde3b feat(sql): Vector SQL Integration + test isolation fixes
- Add VECTOR(n) column type support in CREATE TABLE
- Add CREATE INDEX ... USING hnsw/ivfpq for vector indexes
- Add cosine_distance(), euclidean_distance(), inner_product(), l1/l2_distance()
  SQL functions in expression evaluator
- Add <-> nearest-neighbor operator
- Fix ORDER BY with non-projected columns (move irpkSort before irpkProject)
- Fix execInsert to escape comma-containing values (vector literals)
- Fix MERGE tests by using unique temp dirs per test suite
- Add 8 Vector SQL Integration tests (all passing)
- Update PLAN_SQL_ADVANCED.md
2026-05-14 14:14:13 +03:00

676 lines
16 KiB
Nim

## BaraQL Lexer — tokenization
import std/tables
import std/strutils
import std/unicode
type
TokenKind* = enum
# Literals
tkIntLit
tkFloatLit
tkStringLit
tkBoolLit
# Identifiers
tkIdent
# Keywords
tkSelect
tkInsert
tkUpdate
tkDelete
tkFrom
tkWhere
tkAnd
tkOr
tkNot
tkIn
tkIs
tkAs
tkOn
tkJoin
tkLeft
tkRight
tkInner
tkOuter
tkFull
tkCross
tkLateral
tkOrder
tkBy
tkAsc
tkDesc
tkGroup
tkHaving
tkLimit
tkOffset
tkSet
tkInto
tkValues
tkCreate
tkDrop
tkAlter
tkTable
tkIndex
tkType
tkLink
tkProperty
tkRequired
tkMulti
tkSingle
tkTrue
tkFalse
tkNull
tkIf
tkThen
tkElse
tkEnd
tkCase
tkWhen
tkWith
tkRecursive
tkDistinct
tkUnion
tkIntersect
tkExcept
tkAll
tkExists
tkBetween
tkLike
tkILike
tkFilter
tkReturning
tkPrimary
tkKey
tkForeign
tkReferences
tkCascade
tkUnique
tkCheck
tkDefault
tkAdd
tkColumn
tkRename
tkBegin
tkCommit
tkRollback
tkExplain
tkView
tkTrigger
tkBefore
tkAfter
tkInstead
tkOf
tkMigration
tkApply
tkStatus
tkUp
tkDown
tkDryRun
tkUser
tkPolicy
tkEnable
tkDisable
tkRecover
tkTimestamp
tkFor
tkUsing
tkGrant
tkRevoke
tkCount
tkSum
tkAvg
tkMin
tkMax
tkArrayAgg
tkStringAgg
tkGrouping
tkSets
tkRollup
tkCube
tkPivot
tkUnpivot
tkVertex
tkEdge
tkLabels
tkGraphTable
tkMatch
tkColumns
tkSrc
tkDst
tkMerge
tkMatched
tkArray
tkVector
tkGraph
tkDocument
tkArrowR # ->
tkArrowRR # ->>
tkFtsMatch # @@
tkSimilar
tkNearest
tkTo
tkBfs
tkDfs
tkPath
# Window functions
tkOver
tkPartition
tkRows
tkRange
tkUnbounded
tkPreceding
tkFollowing
tkCurrent
tkRowNumber
tkRank
tkDenseRank
tkLead
tkLag
tkFirstValue
tkLastValue
tkNtile
# Operators
tkPlus
tkMinus
tkStar
tkSlash
tkPercent
tkPower
tkEq
tkNotEq
tkLt
tkLtEq
tkGt
tkGtEq
tkAssign
tkArrow
tkDoubleColon
tkColon
tkDot
tkComma
tkSemicolon
tkLParen
tkRParen
tkLBrace
tkRbrace
tkLBracket
tkRBracket
tkAmp
tkPipe
tkTilde
tkConcat
tkCoalesce
tkFloorDiv
tkDistanceOp # <->
tkPlaceholder
# Special
tkEof
tkNewline
tkInvalid
Token* = object
kind*: TokenKind
value*: string
line*: int
col*: int
Lexer* = object
input: string
pos: int
line: int
col: int
const keywords*: Table[string, TokenKind] = {
"select": tkSelect,
"insert": tkInsert,
"update": tkUpdate,
"delete": tkDelete,
"from": tkFrom,
"where": tkWhere,
"and": tkAnd,
"or": tkOr,
"not": tkNot,
"in": tkIn,
"is": tkIs,
"as": tkAs,
"on": tkOn,
"join": tkJoin,
"left": tkLeft,
"right": tkRight,
"inner": tkInner,
"outer": tkOuter,
"full": tkFull,
"cross": tkCross,
"lateral": tkLateral,
"order": tkOrder,
"by": tkBy,
"asc": tkAsc,
"desc": tkDesc,
"group": tkGroup,
"having": tkHaving,
"limit": tkLimit,
"offset": tkOffset,
"set": tkSet,
"into": tkInto,
"values": tkValues,
"create": tkCreate,
"drop": tkDrop,
"alter": tkAlter,
"table": tkTable,
"index": tkIndex,
"type": tkType,
"link": tkLink,
"property": tkProperty,
"required": tkRequired,
"multi": tkMulti,
"single": tkSingle,
"true": tkTrue,
"false": tkFalse,
"null": tkNull,
"if": tkIf,
"then": tkThen,
"else": tkElse,
"end": tkEnd,
"case": tkCase,
"when": tkWhen,
"with": tkWith,
"recursive": tkRecursive,
"distinct": tkDistinct,
"union": tkUnion,
"intersect": tkIntersect,
"except": tkExcept,
"all": tkAll,
"exists": tkExists,
"between": tkBetween,
"like": tkLike,
"ilike": tkILike,
"filter": tkFilter,
"returning": tkReturning,
"primary": tkPrimary,
"key": tkKey,
"foreign": tkForeign,
"references": tkReferences,
"cascade": tkCascade,
"unique": tkUnique,
"check": tkCheck,
"default": tkDefault,
"add": tkAdd,
"column": tkColumn,
"rename": tkRename,
"begin": tkBegin,
"commit": tkCommit,
"rollback": tkRollback,
"explain": tkExplain,
"view": tkView,
"trigger": tkTrigger,
"before": tkBefore,
"after": tkAfter,
"instead": tkInstead,
"of": tkOf,
"migration": tkMigration,
"apply": tkApply,
"status": tkStatus,
"up": tkUp,
"down": tkDown,
"dryrun": tkDryRun,
"user": tkUser,
"policy": tkPolicy,
"enable": tkEnable,
"disable": tkDisable,
"for": tkFor,
"using": tkUsing,
"recover": tkRecover,
"timestamp": tkTimestamp,
"grant": tkGrant,
"revoke": tkRevoke,
"count": tkCount,
"sum": tkSum,
"avg": tkAvg,
"min": tkMin,
"max": tkMax,
"array_agg": tkArrayAgg,
"string_agg": tkStringAgg,
"grouping": tkGrouping,
"sets": tkSets,
"rollup": tkRollup,
"cube": tkCube,
"pivot": tkPivot,
"unpivot": tkUnpivot,
"vertex": tkVertex,
"vertices": tkVertex,
"edge": tkEdge,
"edges": tkEdge,
"label": tkLabels,
"labels": tkLabels,
"graph_table": tkGraphTable,
"match": tkMatch,
"columns": tkColumns,
"src": tkSrc,
"dst": tkDst,
"merge": tkMerge,
"matched": tkMatched,
"array": tkArray,
"vector": tkVector,
"graph": tkGraph,
"document": tkDocument,
"similar": tkSimilar,
"nearest": tkNearest,
"to": tkTo,
"bfs": tkBfs,
"dfs": tkDfs,
"path": tkPath,
"over": tkOver,
"partition": tkPartition,
"rows": tkRows,
"range": tkRange,
"unbounded": tkUnbounded,
"preceding": tkPreceding,
"following": tkFollowing,
"current": tkCurrent,
"row_number": tkRowNumber,
"rank": tkRank,
"dense_rank": tkDenseRank,
"lead": tkLead,
"lag": tkLag,
"first_value": tkFirstValue,
"last_value": tkLastValue,
"ntile": tkNtile,
}.toTable
proc newLexer*(input: string): Lexer =
Lexer(input: input, pos: 0, line: 1, col: 1)
proc peek(l: Lexer): char =
if l.pos < l.input.len:
return l.input[l.pos]
return '\0'
proc advance(l: var Lexer): char =
result = l.input[l.pos]
inc l.pos
if result == '\n':
inc l.line
l.col = 1
else:
inc l.col
proc peekRune(l: Lexer): Rune =
if l.pos < l.input.len:
var p = l.pos
var r: Rune
fastRuneAt(l.input, p, r, true)
return r
return Rune(0)
proc advanceRune(l: var Lexer): Rune =
if l.pos < l.input.len:
var r: Rune
fastRuneAt(l.input, l.pos, r, true)
if r == Rune('\n'):
inc l.line
l.col = 1
else:
inc l.col
return r
return Rune(0)
proc isIdentStartRune(r: Rune): bool =
return isAlpha(r) or r == Rune('_')
proc isIdentPartRune(r: Rune): bool =
return isAlpha(r) or (r.int >= ord('0') and r.int <= ord('9')) or r == Rune('_')
proc skipWhitespace(l: var Lexer) =
while l.pos < l.input.len and l.input[l.pos] in {' ', '\t', '\r', '\n'}:
discard l.advance()
proc skipLineComment(l: var Lexer) =
while l.pos < l.input.len and l.input[l.pos] != '\n':
discard l.advance()
proc skipBlockComment(l: var Lexer) =
discard l.advance() # skip *
discard l.advance() # skip *
while l.pos < l.input.len - 1:
if l.input[l.pos] == '*' and l.input[l.pos + 1] == '/':
discard l.advance()
discard l.advance()
return
discard l.advance()
raise newException(ValueError, "Unclosed block comment at line " & $l.line & ", col " & $l.col)
proc readString(l: var Lexer, quote: char): string =
result = ""
while l.pos < l.input.len and l.input[l.pos] != quote:
if l.input[l.pos] == '\\':
discard l.advance()
if l.pos >= l.input.len: break
case l.input[l.pos]
of 'n': result.add('\n')
of 't': result.add('\t')
of 'r': result.add('\r')
of '\\': result.add('\\')
of '\'': result.add('\'')
of '"': result.add('"')
else: result.add(l.input[l.pos])
else:
result.add(l.input[l.pos])
discard l.advance()
if l.pos < l.input.len:
discard l.advance() # skip closing quote
proc readNumber(l: var Lexer, startLine, startCol: int): Token =
var numStr = ""
var isFloat = false
while l.pos < l.input.len and (l.input[l.pos] in Digits or l.input[l.pos] == '.'):
if l.input[l.pos] == '.':
if isFloat:
break # second dot ends the number
isFloat = true
numStr.add(l.input[l.pos])
discard l.advance()
if isFloat:
Token(kind: tkFloatLit, value: numStr, line: startLine, col: startCol)
else:
Token(kind: tkIntLit, value: numStr, line: startLine, col: startCol)
proc readIdent(l: var Lexer, startLine, startCol: int): Token =
var ident = ""
while l.pos < l.input.len:
let r = l.peekRune()
if isIdentPartRune(r):
var run: Rune
fastRuneAt(l.input, l.pos, run, true)
ident.add($run)
inc l.col
else:
break
let lowerIdent = ident.toLower()
if lowerIdent in keywords:
Token(kind: keywords[lowerIdent], value: ident, line: startLine, col: startCol)
else:
Token(kind: tkIdent, value: ident, line: startLine, col: startCol)
proc nextToken*(l: var Lexer): Token =
l.skipWhitespace()
if l.pos >= l.input.len:
return Token(kind: tkEof, line: l.line, col: l.col)
let startLine = l.line
let startCol = l.col
let ch = l.peek()
case ch
of '/':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '/':
l.skipLineComment()
return l.nextToken()
elif l.pos + 1 < l.input.len and l.input[l.pos + 1] == '*':
l.skipBlockComment()
return l.nextToken()
else:
discard l.advance()
return Token(kind: tkSlash, 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: tkConcat, value: "++", line: startLine, col: startCol)
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 '*':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '*':
discard l.advance()
discard l.advance()
return Token(kind: tkPower, value: "**", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkStar, value: "*", line: startLine, col: startCol)
of '%':
discard l.advance()
return Token(kind: tkPercent, 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: tkArrow, value: "=>", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkEq, value: "==", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkEq, 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: tkAssign, value: ":=", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == ':':
discard l.advance()
discard l.advance()
return Token(kind: tkDoubleColon, value: "::", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkColon, 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: tkNotEq, value: "!=", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "!", line: startLine, col: startCol)
of '<':
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '-' and l.input[l.pos + 2] == '>':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkDistanceOp, value: "<->", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkLtEq, value: "<=", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkNotEq, value: "<>", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkLt, 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: tkGtEq, value: ">=", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkGt, 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: 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()
discard l.advance()
return Token(kind: tkInvalid, value: ".<", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkDot, value: ".", line: startLine, col: startCol)
of ',':
discard l.advance()
return Token(kind: tkComma, value: ",", line: startLine, col: startCol)
of ';':
discard l.advance()
return Token(kind: tkSemicolon, value: ";", line: startLine, col: startCol)
of '(':
discard l.advance()
return Token(kind: tkLParen, value: "(", line: startLine, col: startCol)
of ')':
discard l.advance()
return Token(kind: tkRParen, value: ")", line: startLine, col: startCol)
of '{':
discard l.advance()
return Token(kind: tkLBrace, value: "{", line: startLine, col: startCol)
of '}':
discard l.advance()
return Token(kind: tkRbrace, value: "}", line: startLine, col: startCol)
of '[':
discard l.advance()
return Token(kind: tkLBracket, value: "[", line: startLine, col: startCol)
of ']':
discard l.advance()
return Token(kind: tkRBracket, value: "]", line: startLine, col: startCol)
of '&':
discard l.advance()
return Token(kind: tkAmp, value: "&", line: startLine, col: startCol)
of '|':
discard l.advance()
return Token(kind: tkPipe, value: "|", line: startLine, col: startCol)
of '~':
discard l.advance()
return Token(kind: tkTilde, value: "~", line: startLine, col: startCol)
of '\'', '"':
discard l.advance()
let s = l.readString(ch)
return Token(kind: tkStringLit, value: s, line: startLine, col: startCol)
of '#':
l.skipLineComment()
return l.nextToken()
else:
if ch in Digits:
return l.readNumber(startLine, startCol)
elif ch in IdentStartChars or isIdentStartRune(l.peekRune()):
return l.readIdent(startLine, startCol)
else:
discard l.advance()
return Token(kind: tkInvalid, value: $ch, line: startLine, col: startCol)
proc tokenize*(input: string): seq[Token] =
var lexer = newLexer(input)
result = @[]
while true:
let tok = lexer.nextToken()
result.add(tok)
if tok.kind == tkEof:
break