| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- package lexer
- import (
- "strings"
- "unicode"
- )
- // Lexer tokenizes SQL input.
- type Lexer struct {
- input string
- pos int // current position in input
- readPos int // reading position (after current char)
- ch byte // current char under examination
- line int // current line number (1-based)
- column int // current column number (1-based)
- }
- // New creates a new Lexer for the given input.
- func New(input string) *Lexer {
- l := &Lexer{
- input: input,
- line: 1,
- column: 0,
- }
- l.readChar()
- return l
- }
- // readChar advances the lexer by one character.
- func (l *Lexer) readChar() {
- if l.readPos >= len(l.input) {
- l.ch = 0
- } else {
- l.ch = l.input[l.readPos]
- }
- l.pos = l.readPos
- l.readPos++
- l.column++
- if l.ch == '\n' {
- l.line++
- l.column = 0
- }
- }
- // peekChar returns the next character without advancing.
- func (l *Lexer) peekChar() byte {
- if l.readPos >= len(l.input) {
- return 0
- }
- return l.input[l.readPos]
- }
- // NextToken returns the next token from the input.
- func (l *Lexer) NextToken() Token {
- l.skipWhitespace()
- tok := Token{
- Line: l.line,
- Column: l.column,
- }
- switch l.ch {
- case 0:
- tok.Type = TokenEOF
- tok.Literal = ""
- case '+':
- tok.Type = TokenPlus
- tok.Literal = "+"
- l.readChar()
- case '*':
- tok.Type = TokenStar
- tok.Literal = "*"
- l.readChar()
- case '/':
- tok.Type = TokenSlash
- tok.Literal = "/"
- l.readChar()
- case '%':
- tok.Type = TokenPercent
- tok.Literal = "%"
- l.readChar()
- case '(':
- tok.Type = TokenLParen
- tok.Literal = "("
- l.readChar()
- case ')':
- tok.Type = TokenRParen
- tok.Literal = ")"
- l.readChar()
- case ',':
- tok.Type = TokenComma
- tok.Literal = ","
- l.readChar()
- case ';':
- tok.Type = TokenSemicolon
- tok.Literal = ";"
- l.readChar()
- case '.':
- tok.Type = TokenDot
- tok.Literal = "."
- l.readChar()
- case '=':
- tok.Type = TokenEq
- tok.Literal = "="
- l.readChar()
- case '<':
- if l.peekChar() == '=' {
- l.readChar()
- tok.Type = TokenLte
- tok.Literal = "<="
- } else if l.peekChar() == '>' {
- l.readChar()
- tok.Type = TokenNeq
- tok.Literal = "<>"
- } else {
- tok.Type = TokenLt
- tok.Literal = "<"
- }
- l.readChar()
- case '>':
- if l.peekChar() == '=' {
- l.readChar()
- tok.Type = TokenGte
- tok.Literal = ">="
- } else {
- tok.Type = TokenGt
- tok.Literal = ">"
- }
- l.readChar()
- case '!':
- if l.peekChar() == '=' {
- l.readChar()
- tok.Type = TokenNeq
- tok.Literal = "!="
- l.readChar()
- } else {
- tok.Type = TokenError
- tok.Literal = "unexpected character: !"
- l.readChar()
- }
- case '|':
- if l.peekChar() == '|' {
- l.readChar()
- tok.Type = TokenConcat
- tok.Literal = "||"
- l.readChar()
- } else {
- tok.Type = TokenError
- tok.Literal = "unexpected character: |"
- l.readChar()
- }
- case '-':
- if l.peekChar() == '-' {
- // Line comment
- tok = l.readLineComment()
- } else {
- tok.Type = TokenMinus
- tok.Literal = "-"
- l.readChar()
- }
- case '\'':
- tok = l.readString()
- case '"':
- tok = l.readQuotedIdentifier('"')
- case '`':
- tok = l.readQuotedIdentifier('`')
- case '[':
- tok = l.readBracketIdentifier()
- default:
- if isLetter(l.ch) || l.ch == '_' {
- tok = l.readIdentifier()
- } else if isDigit(l.ch) {
- tok = l.readNumber()
- } else {
- tok.Type = TokenError
- tok.Literal = "unexpected character: " + string(l.ch)
- l.readChar()
- }
- }
- return tok
- }
- // skipWhitespace skips spaces, tabs, and newlines.
- func (l *Lexer) skipWhitespace() {
- for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
- l.readChar()
- }
- // Also skip block comments
- if l.ch == '/' && l.peekChar() == '*' {
- l.skipBlockComment()
- l.skipWhitespace()
- }
- }
- // skipBlockComment skips /* ... */ comments.
- func (l *Lexer) skipBlockComment() {
- l.readChar() // skip /
- l.readChar() // skip *
- for {
- if l.ch == 0 {
- return // EOF in comment
- }
- if l.ch == '*' && l.peekChar() == '/' {
- l.readChar() // skip *
- l.readChar() // skip /
- return
- }
- l.readChar()
- }
- }
- // readLineComment reads a -- line comment.
- func (l *Lexer) readLineComment() Token {
- tok := Token{
- Type: TokenComment,
- Line: l.line,
- Column: l.column,
- }
- startPos := l.pos
- for l.ch != '\n' && l.ch != 0 {
- l.readChar()
- }
- tok.Literal = l.input[startPos:l.pos]
- return tok
- }
- // readString reads a 'string literal'.
- func (l *Lexer) readString() Token {
- tok := Token{
- Type: TokenString,
- Line: l.line,
- Column: l.column,
- }
- l.readChar() // skip opening quote
- var sb strings.Builder
- for {
- if l.ch == 0 {
- tok.Type = TokenError
- tok.Literal = "unterminated string"
- return tok
- }
- if l.ch == '\'' {
- if l.peekChar() == '\'' {
- // Escaped quote
- sb.WriteByte('\'')
- l.readChar()
- l.readChar()
- } else {
- // End of string
- l.readChar()
- break
- }
- } else {
- sb.WriteByte(l.ch)
- l.readChar()
- }
- }
- tok.Literal = sb.String()
- return tok
- }
- // readQuotedIdentifier reads a "quoted identifier" or `backtick identifier`.
- func (l *Lexer) readQuotedIdentifier(quote byte) Token {
- tok := Token{
- Type: TokenIdent,
- Line: l.line,
- Column: l.column,
- }
- l.readChar() // skip opening quote
- startPos := l.pos
- for l.ch != quote && l.ch != 0 {
- l.readChar()
- }
- if l.ch == 0 {
- tok.Type = TokenError
- tok.Literal = "unterminated identifier"
- return tok
- }
- tok.Literal = l.input[startPos:l.pos]
- l.readChar() // skip closing quote
- return tok
- }
- // readBracketIdentifier reads a [bracket identifier] (SQL Server style).
- func (l *Lexer) readBracketIdentifier() Token {
- tok := Token{
- Type: TokenIdent,
- Line: l.line,
- Column: l.column,
- }
- l.readChar() // skip [
- startPos := l.pos
- for l.ch != ']' && l.ch != 0 {
- l.readChar()
- }
- if l.ch == 0 {
- tok.Type = TokenError
- tok.Literal = "unterminated identifier"
- return tok
- }
- tok.Literal = l.input[startPos:l.pos]
- l.readChar() // skip ]
- return tok
- }
- // readIdentifier reads an identifier or keyword.
- func (l *Lexer) readIdentifier() Token {
- tok := Token{
- Line: l.line,
- Column: l.column,
- }
- startPos := l.pos
- for isLetter(l.ch) || isDigit(l.ch) || l.ch == '_' {
- l.readChar()
- }
- literal := l.input[startPos:l.pos]
- tok.Literal = literal
- tok.Type = LookupKeyword(strings.ToUpper(literal))
- return tok
- }
- // readNumber reads an integer or float.
- func (l *Lexer) readNumber() Token {
- tok := Token{
- Type: TokenNumber,
- Line: l.line,
- Column: l.column,
- }
- startPos := l.pos
- // Read integer part
- for isDigit(l.ch) {
- l.readChar()
- }
- // Check for decimal point
- if l.ch == '.' && isDigit(l.peekChar()) {
- l.readChar() // skip .
- for isDigit(l.ch) {
- l.readChar()
- }
- }
- // Check for exponent
- if l.ch == 'e' || l.ch == 'E' {
- l.readChar()
- if l.ch == '+' || l.ch == '-' {
- l.readChar()
- }
- for isDigit(l.ch) {
- l.readChar()
- }
- }
- tok.Literal = l.input[startPos:l.pos]
- return tok
- }
- // Tokenize returns all tokens from the input.
- func (l *Lexer) Tokenize() []Token {
- var tokens []Token
- for {
- tok := l.NextToken()
- tokens = append(tokens, tok)
- if tok.Type == TokenEOF || tok.Type == TokenError {
- break
- }
- }
- return tokens
- }
- func isLetter(ch byte) bool {
- return unicode.IsLetter(rune(ch))
- }
- func isDigit(ch byte) bool {
- return ch >= '0' && ch <= '9'
- }
|