|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { |
| 3 | + BoundedSqlLexer, |
| 4 | + MAX_BOUNDED_SQL_LEXEMES, |
| 5 | + type BoundedSqlLexeme, |
| 6 | +} from "../bounded-sql-lexer.js"; |
| 7 | +import { |
| 8 | + BIGQUERY_SQL_LEXICAL_PROFILE, |
| 9 | + POSTGRESQL_SQL_LEXICAL_PROFILE, |
| 10 | + type SqlLexicalProfile, |
| 11 | +} from "../lexical.js"; |
| 12 | +import { |
| 13 | + createIdentitySqlSource, |
| 14 | + createMaskedSqlSource, |
| 15 | + type SqlSourceSnapshot, |
| 16 | +} from "../source.js"; |
| 17 | + |
| 18 | +function lex( |
| 19 | + source: SqlSourceSnapshot, |
| 20 | + profile: SqlLexicalProfile = POSTGRESQL_SQL_LEXICAL_PROFILE, |
| 21 | +): { |
| 22 | + readonly lexemes: readonly BoundedSqlLexeme[]; |
| 23 | + readonly resource: BoundedSqlLexer["resource"]; |
| 24 | +} { |
| 25 | + const lexer = new BoundedSqlLexer( |
| 26 | + source, |
| 27 | + 0, |
| 28 | + source.analysisText.length, |
| 29 | + profile, |
| 30 | + ); |
| 31 | + const lexemes: BoundedSqlLexeme[] = []; |
| 32 | + while (true) { |
| 33 | + const lexeme = lexer.next(); |
| 34 | + if (!lexeme) { |
| 35 | + return { lexemes, resource: lexer.resource }; |
| 36 | + } |
| 37 | + lexemes.push(lexeme); |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +describe("bounded SQL lexer", () => { |
| 42 | + it("streams words, punctuation, strings, comments, and UTF-16 ranges", () => { |
| 43 | + const text = "Se😀lect . 'x' -- note\n/* nested /* x */ */ end"; |
| 44 | + expect(lex(createIdentitySqlSource(text))).toEqual({ |
| 45 | + lexemes: [ |
| 46 | + { closed: true, from: 0, kind: "word", to: 8 }, |
| 47 | + { closed: true, from: 9, kind: "punctuation", to: 10 }, |
| 48 | + { closed: true, from: 11, kind: "string", to: 14 }, |
| 49 | + { closed: true, from: 15, kind: "line-comment", to: 22 }, |
| 50 | + { closed: true, from: 23, kind: "comment", to: 43 }, |
| 51 | + { closed: true, from: 44, kind: "word", to: 47 }, |
| 52 | + ], |
| 53 | + resource: null, |
| 54 | + }); |
| 55 | + }); |
| 56 | + |
| 57 | + it("keeps BigQuery backticks, raw triples, and hash comments atomic", () => { |
| 58 | + const text = "`a.b` R'''raw\\value''' # comment"; |
| 59 | + expect( |
| 60 | + lex( |
| 61 | + createIdentitySqlSource(text), |
| 62 | + BIGQUERY_SQL_LEXICAL_PROFILE, |
| 63 | + ), |
| 64 | + ).toEqual({ |
| 65 | + lexemes: [ |
| 66 | + { |
| 67 | + closed: true, |
| 68 | + from: 0, |
| 69 | + kind: "quoted-identifier", |
| 70 | + to: 5, |
| 71 | + }, |
| 72 | + { closed: true, from: 6, kind: "word", to: 7 }, |
| 73 | + { closed: true, from: 7, kind: "string", to: 22 }, |
| 74 | + { |
| 75 | + closed: true, |
| 76 | + from: 23, |
| 77 | + kind: "line-comment", |
| 78 | + to: 32, |
| 79 | + }, |
| 80 | + ], |
| 81 | + resource: null, |
| 82 | + }); |
| 83 | + }); |
| 84 | + |
| 85 | + it("emits embedded regions as barriers and finds exact boundaries", () => { |
| 86 | + const source = createMaskedSqlSource("a {value} b", [ |
| 87 | + { from: 2, language: "python", to: 9 }, |
| 88 | + ]); |
| 89 | + expect(lex(source).lexemes).toEqual([ |
| 90 | + { closed: true, from: 0, kind: "word", to: 1 }, |
| 91 | + { closed: true, from: 2, kind: "barrier", to: 9 }, |
| 92 | + { closed: true, from: 10, kind: "word", to: 11 }, |
| 93 | + ]); |
| 94 | + }); |
| 95 | + |
| 96 | + it("pushes back one token without spending the budget twice", () => { |
| 97 | + const source = createIdentitySqlSource("one two"); |
| 98 | + const lexer = new BoundedSqlLexer( |
| 99 | + source, |
| 100 | + 0, |
| 101 | + source.analysisText.length, |
| 102 | + POSTGRESQL_SQL_LEXICAL_PROFILE, |
| 103 | + ); |
| 104 | + const first = lexer.next(); |
| 105 | + expect(first).not.toBeNull(); |
| 106 | + if (!first) { |
| 107 | + throw new Error("Expected first lexeme"); |
| 108 | + } |
| 109 | + lexer.pushBack(first); |
| 110 | + expect(lexer.next()).toBe(first); |
| 111 | + expect(lexer.next()).toEqual({ |
| 112 | + closed: true, |
| 113 | + from: 4, |
| 114 | + kind: "word", |
| 115 | + to: 7, |
| 116 | + }); |
| 117 | + expect(lexer.next()).toBeNull(); |
| 118 | + expect(lexer.resource).toBeNull(); |
| 119 | + }); |
| 120 | + |
| 121 | + it("fails closed immediately after the shared lexeme budget", () => { |
| 122 | + const words = Array.from( |
| 123 | + { length: MAX_BOUNDED_SQL_LEXEMES + 1 }, |
| 124 | + () => "x", |
| 125 | + ).join(" "); |
| 126 | + const result = lex(createIdentitySqlSource(words)); |
| 127 | + expect(result.lexemes).toHaveLength(MAX_BOUNDED_SQL_LEXEMES); |
| 128 | + expect(result.resource).toBe("lexical-token"); |
| 129 | + }); |
| 130 | + |
| 131 | + it("reports oversized dollar-quote delimiters without emitting a token", () => { |
| 132 | + const source = createIdentitySqlSource( |
| 133 | + `$${"a".repeat(257)}$unterminated`, |
| 134 | + ); |
| 135 | + expect(lex(source)).toEqual({ |
| 136 | + lexemes: [], |
| 137 | + resource: "dollar-quote-delimiter", |
| 138 | + }); |
| 139 | + }); |
| 140 | +}); |
0 commit comments