import { SqlSyntaxError } from './errors.js'; /** @typedef {{type: string, value: any, pos: number, line: number, col: number}} Token */ const KEYWORDS = new Set([ 'AND', 'OR', 'NOT', 'IS', 'NULL', 'LIKE', 'IN', 'BETWEEN', 'TRUE', 'FALSE', 'SELECT', 'FROM', 'WHERE', 'AS', 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'GROUP', 'HAVING', 'JOIN', 'INNER', 'LEFT', 'CROSS', 'ON', 'UNNEST', ]); const TWO_CHAR_PUNCT = new Set(['!=', '<>', '<=', '>=', '||']); const ONE_CHAR_PUNCT = new Set(['(', ')', '[', ']', '{', '}', ':', ',', '.', '+', '-', '*', '/', '%', '=', '<', '>']); /** * Tokenizes an expression string into a flat array of tokens, terminated by an `eof` token. * @param {string} source * @returns {Token[]} */ export function tokenize(source) { const tokens = []; const n = source.length; let i = 0; let line = 1; let col = 1; function advance(count = 1) { for (let k = 0; k < count; k++) { if (source[i] === '\n') { line++; col = 1; } else { col++; } i++; } } /** @param {string} message @param {number} atPos @param {number} atLine @param {number} atCol */ function fail(message, atPos, atLine, atCol) { throw new SqlSyntaxError(message, { pos: atPos, line: atLine, col: atCol }); } while (i < n) { const ch = source[i]; if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') { advance(); continue; } if (ch === '-' && source[i + 1] === '-') { while (i < n && source[i] !== '\n') advance(); continue; } if (ch === '/' && source[i + 1] === '*') { const startPos = i, startLine = line, startCol = col; advance(2); let closed = false; while (i < n) { if (source[i] === '*' && source[i + 1] === '/') { advance(2); closed = true; break; } advance(); } if (!closed) fail('Unterminated block comment', startPos, startLine, startCol); continue; } const startPos = i, startLine = line, startCol = col; if (isDigit(ch)) { let s = ''; while (i < n && isDigit(source[i])) { s += source[i]; advance(); } if (source[i] === '.' && isDigit(source[i + 1])) { s += '.'; advance(); while (i < n && isDigit(source[i])) { s += source[i]; advance(); } } if (source[i] === 'e' || source[i] === 'E') { let j = i + 1; if (source[j] === '+' || source[j] === '-') j++; if (isDigit(source[j])) { let k = j; while (isDigit(source[k])) k++; s += source.slice(i, k); while (i < k) advance(); } } tokens.push({ type: 'number', value: Number(s), pos: startPos, line: startLine, col: startCol }); continue; } if (ch === "'") { advance(); let s = ''; let closed = false; while (i < n) { if (source[i] === "'") { if (source[i + 1] === "'") { s += "'"; advance(2); continue; } advance(); closed = true; break; } s += source[i]; advance(); } if (!closed) fail('Unterminated string literal', startPos, startLine, startCol); tokens.push({ type: 'string', value: s, pos: startPos, line: startLine, col: startCol }); continue; } if (ch === '"') { advance(); let s = ''; let closed = false; while (i < n) { if (source[i] === '"') { if (source[i + 1] === '"') { s += '"'; advance(2); continue; } advance(); closed = true; break; } s += source[i]; advance(); } if (!closed) fail('Unterminated quoted identifier', startPos, startLine, startCol); tokens.push({ type: 'ident', value: s, pos: startPos, line: startLine, col: startCol }); continue; } if (isIdentStart(ch)) { let s = ''; while (i < n && isIdentPart(source[i])) { s += source[i]; advance(); } const upper = s.toUpperCase(); if (KEYWORDS.has(upper)) { tokens.push({ type: upper, value: s, pos: startPos, line: startLine, col: startCol }); } else { tokens.push({ type: 'ident', value: s, pos: startPos, line: startLine, col: startCol }); } continue; } const two = source.slice(i, i + 2); if (TWO_CHAR_PUNCT.has(two)) { tokens.push({ type: two, value: two, pos: startPos, line: startLine, col: startCol }); advance(2); continue; } if (ONE_CHAR_PUNCT.has(ch)) { tokens.push({ type: ch, value: ch, pos: startPos, line: startLine, col: startCol }); advance(); continue; } fail(`Unexpected character ${JSON.stringify(ch)}`, startPos, startLine, startCol); } tokens.push({ type: 'eof', value: null, pos: i, line, col }); return tokens; } /** @param {string} ch */ function isDigit(ch) { return ch >= '0' && ch <= '9'; } /** @param {string} ch */ function isIdentStart(ch) { return ch !== undefined && /[A-Za-z_]/.test(ch); } /** @param {string} ch */ function isIdentPart(ch) { return ch !== undefined && /[A-Za-z0-9_]/.test(ch); }