Skip to content

Commit 7397e60

Browse files
authored
refactor(vnext): share bounded SQL lexer (#187)
## Summary - extract the streaming bounded SQL lexer from the relation-site state machine into one package-private module - preserve the exact PostgreSQL, DuckDB, BigQuery, and Dremio lexical profiles, UTF-16 offsets, embedded-region barriers, quote/comment behavior, one-token pushback, and 16,384-lexeme ceiling - keep query-site keyword, comment-cursor, region, and resource semantics local to the consumer - translate generic lexer resource evidence through an exhaustive package-owned map - add direct boundary tests without exposing tokens or lexer APIs from the package This is a zero-semantics prerequisite for the separate bounded CTE layout/visibility recognizer. The recognizers will initially use separate streaming traversals over the same lexical implementation; any traversal fusion remains benchmark-driven. ## Performance An initial broader helper extraction caused a reproducible Vite SSR namespace-call regression on the hot path. The boundary was narrowed before commit. Stable means at the exact head are back at the PR #186 baseline: - exact 10 KiB statement: about 0.56–0.59 ms - 1,000 classified aliases: about 0.36–0.38 ms - 1,000 authenticated `USING` columns: about 0.21 ms The lexer remains streaming and does not allocate a token tape. ## Verification - 1,543 tests passed plus 1 expected failure - changed coverage: 97.05% statements, 95.79% branches, 100% functions, 97.04% lines - bounded lexer coverage: 98.92% statements/lines, 98.78% branches, 100% functions - repository coverage: 95.34% statements, 92.05% branches, 96.15% functions, 95.33% lines - source, test, loose-optional, and demo typechecks pass - repository oxlint, test-integrity, and diff checks pass - browser, package, worker-placement, demo, and benchmark gates pass - 20,000 deterministic differential lexer comparisons passed across dialects, masked regions, subranges, UTF-16, comments, quotes, punctuation, and pushback - independent SQL/API and concurrency/performance reviewers approved exact commit `ed079df0f68aadaf8957a7ca5e6c497e91b74860` Part of #169. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Refactored vNext to share a streaming bounded SQL lexer and the embedded-region lookup, and switched `query-site` to use them. Behavior is unchanged across PostgreSQL, DuckDB, BigQuery, and Dremio; this unblocks the bounded CTE recognizer in #169. - **Refactors** - Moved the lexer into package-private `src/vnext/bounded-sql-lexer.ts`. - Centralized `findSqlEmbeddedRegionAtOrAfter` in `src/vnext/source.ts` and reused it in the lexer and `query-site` (with tests). - Preserves lexical profiles, UTF-16 offsets, embedded-region barriers, quote/comment rules, one-token pushback, and the 16,384-lexeme cap. - Kept consumer-specific semantics in `query-site`; mapped lexer resource signals to local `query-site` resources. - Removed duplicated lexer and region-lookup code from `src/vnext/query-site.ts` and wired it to the shared modules. <sup>Written for commit 76190f9. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/marimo-team/codemirror-sql/pull/187?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent 8347817 commit 7397e60

5 files changed

Lines changed: 504 additions & 284 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
});

src/vnext/__tests__/source.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
createIdentitySqlSource,
44
createMaskedSqlSource,
5+
findSqlEmbeddedRegionAtOrAfter,
56
mapAnalysisRangeToOriginal,
67
mapOriginalRangeToAnalysis,
78
MAX_SQL_EMBEDDED_REGIONS,
@@ -188,6 +189,20 @@ describe("SQL source snapshots", () => {
188189
expect(source.analysisText).toBe(" b ");
189190
});
190191

192+
it("finds the first embedded region ending after a position", () => {
193+
const source = createMaskedSqlSource("abcdef", [
194+
{ from: 1, language: "python", to: 2 },
195+
{ from: 3, language: "jinja", to: 5 },
196+
]);
197+
198+
expect(findSqlEmbeddedRegionAtOrAfter(source, 0)).toBe(0);
199+
expect(findSqlEmbeddedRegionAtOrAfter(source, 1)).toBe(0);
200+
expect(findSqlEmbeddedRegionAtOrAfter(source, 2)).toBe(1);
201+
expect(findSqlEmbeddedRegionAtOrAfter(source, 4)).toBe(1);
202+
expect(findSqlEmbeddedRegionAtOrAfter(source, 5)).toBe(2);
203+
expect(findSqlEmbeddedRegionAtOrAfter(source, 6)).toBe(2);
204+
});
205+
191206
it.each([
192207
null,
193208
{},

0 commit comments

Comments
 (0)