Skip to content

Commit 8347817

Browse files
authored
feat(vnext): recognize bounded relation query sites (#186)
## Summary - add a private parser-independent partial `SELECT` relation-site recognizer - authenticate full relation paths and statement-relative UTF-16 replacement ranges - enforce dialect-owned identifier/path decoding and path depth under a global safety ceiling - recognize base `FROM`, qualified prefixes, aliases, joins, same-depth commas, nested queries, and dialect-owned `NATURAL` joins - authenticate the bounded `USING(identifier [, identifier ...])` grammar before crossing a join constraint - deliberately fail closed on `ON` until a parser-backed or separately specified expression recognizer can prove its boundary - preserve frame-local nested-query recognition after proven relation transitions and valid `USING` clause exits ## Safety and performance - active statement: 65,536 UTF-16 units - lexemes: 16,384 - nesting: 128 - path: dialect limit under global 32 - decoded identifier segment: 256 UTF-16 units - exact 10 KiB statement: about 0.56 ms mean - 1,000 classified aliases: about 0.36 ms mean - 1,000 authenticated `USING` columns: about 0.21 ms mean - recognizer state remains bounded per query frame and per active `USING` constraint ## Verification - 1,537 tests passed plus 1 expected failure - changed coverage: 96.84% statements, 95.76% branches, 100% functions, 96.83% lines - repository coverage: 95.31% statements, 92.04% branches, 96.14% functions, 95.30% 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 - three independent adversarial reviewers approved exact commit `01cd8d29bf99a7c07f6a72ca746da328847c6dfb` Part of #169.
1 parent ef47c7f commit 8347817

5 files changed

Lines changed: 3496 additions & 5 deletions

File tree

docs/adr/0005-parser-independent-relation-completion.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,20 @@ embedded region are unavailable or inactive according to the closed result.
9797
CTE visibility is the one narrow scope-semantics exception; general
9898
parser-derived scope semantics remain deferred.
9999

100+
The recognizer may cross a `USING` join constraint only after authenticating
101+
the complete bounded grammar `USING(identifier [, identifier ...])` with
102+
dialect-owned identifier validation. It does not interpret `ON` expressions:
103+
encountering `ON` makes the query site unavailable until a future
104+
parser-backed or separately specified expression recognizer can prove the
105+
boundary.
106+
100107
The conformance corpus includes positive base `FROM`, qualified prefix,
101-
aliased `JOIN`, same-depth comma, and nested supported-query cases. It includes
108+
aliased `JOIN`, authenticated `USING`, same-depth comma, and nested
109+
supported-query cases. It includes
102110
negative `IS DISTINCT FROM`, `substring(... FROM ...)`, `extract(... FROM
103-
...)`, `DELETE FROM`, `COPY ... FROM`, set-operation, `QUALIFY`, `WINDOW`, join
104-
constraint, DML, and expression cases. A keyword match alone never creates a
105-
site.
111+
...)`, `DELETE FROM`, `COPY ... FROM`, set-operation, `QUALIFY`, `WINDOW`,
112+
`ON`, malformed `USING`, DML, and expression cases. A keyword match alone
113+
never creates a site.
106114

107115
The result distinguishes:
108116

@@ -518,7 +526,7 @@ The initial checked limits are:
518526
| Lexical tokens | 16,384 |
519527
| Parenthesis/query depth | 128 |
520528
| CTE declarations | 256 |
521-
| Identifier path segments | 4 |
529+
| Identifier path segments | 32 global ceiling; dialect runtime sets the checked limit |
522530
| Identifier segment | 256 UTF-16 units |
523531
| Catalog scope | 512 UTF-16 units |
524532
| Search paths | 32 |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"test:worker-placement": "node ./scripts/worker-placement.mjs",
2828
"test:integrity": "node ./scripts/check-test-integrity.mjs",
2929
"bench:parser-adapter": "vitest bench --run src/vnext/__tests__/node-sql-parser-adapter.bench.ts",
30+
"bench:query-site": "vitest bench --run src/vnext/__tests__/query-site.bench.ts",
3031
"bench:statement-index": "vitest bench --run src/vnext/__tests__/statement-index.bench.ts",
3132
"test:package": "node ./scripts/clean.mjs && tsc && node ./scripts/package-smoke.mjs",
3233
"demo": "vite build",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { bench, describe } from "vitest";
2+
import {
3+
recognizeSqlRelationQuerySite,
4+
type SqlQuerySiteDialect,
5+
} from "../query-site.js";
6+
import { createIdentitySqlSource } from "../source.js";
7+
import {
8+
buildSqlStatementIndex,
9+
DUCKDB_SQL_LEXICAL_PROFILE,
10+
findSqlStatementSlot,
11+
} from "../statement-index.js";
12+
13+
const dialect: SqlQuerySiteDialect = {
14+
classifyIdentifierToken: (rawIdentifier) => ({
15+
status: "identifier",
16+
value: rawIdentifier,
17+
}),
18+
decodeRelationPath: (rawPath, cursorOffset) => ({
19+
finalSegment: { from: 0, to: rawPath.length },
20+
prefix: {
21+
quoted: false,
22+
value: rawPath.slice(0, cursorOffset),
23+
},
24+
qualifier: [],
25+
quality: "exact",
26+
status: "decoded",
27+
}),
28+
lexicalProfile: DUCKDB_SQL_LEXICAL_PROFILE,
29+
maximumPathDepth: 16,
30+
supportsNaturalJoin: true,
31+
};
32+
const TEN_KIBIBYTES = 10 * 1_024;
33+
const queryPrefix = "SELECT ";
34+
const querySuffix = " FROM schema_prefix";
35+
const projectedListLength =
36+
TEN_KIBIBYTES - queryPrefix.length - querySuffix.length;
37+
const projectedList = `${"x,".repeat(
38+
Math.floor((projectedListLength - 1) / 2),
39+
)}x`;
40+
const tenKilobyteQuery = `${queryPrefix}${projectedList}${" ".repeat(
41+
projectedListLength - projectedList.length,
42+
)}${querySuffix}`;
43+
if (tenKilobyteQuery.length !== TEN_KIBIBYTES) {
44+
throw new Error("Query benchmark fixture must be exactly 10 KiB");
45+
}
46+
const source = createIdentitySqlSource(tenKilobyteQuery);
47+
const index = buildSqlStatementIndex(
48+
source.analysisText,
49+
dialect.lexicalProfile,
50+
);
51+
const position = tenKilobyteQuery.length;
52+
const slot = findSqlStatementSlot(index, position, "left");
53+
const aliasHeavyQuery = `SELECT * FROM ${Array.from(
54+
{ length: 1_000 },
55+
(_, aliasIndex) => `table_name alias_${aliasIndex}`,
56+
).join(", ")}, `;
57+
const aliasHeavySource = createIdentitySqlSource(aliasHeavyQuery);
58+
const aliasHeavyIndex = buildSqlStatementIndex(
59+
aliasHeavySource.analysisText,
60+
dialect.lexicalProfile,
61+
);
62+
const aliasHeavyPosition = aliasHeavyQuery.length;
63+
const aliasHeavySlot = findSqlStatementSlot(
64+
aliasHeavyIndex,
65+
aliasHeavyPosition,
66+
"left",
67+
);
68+
const usingHeavyQuery = `SELECT * FROM first_table JOIN second_table USING(${Array.from(
69+
{ length: 1_000 },
70+
(_, columnIndex) => `column_${columnIndex}`,
71+
).join(", ")}) JOIN target`;
72+
const usingHeavySource = createIdentitySqlSource(usingHeavyQuery);
73+
const usingHeavyIndex = buildSqlStatementIndex(
74+
usingHeavySource.analysisText,
75+
dialect.lexicalProfile,
76+
);
77+
const usingHeavyPosition = usingHeavyQuery.length;
78+
const usingHeavySlot = findSqlStatementSlot(
79+
usingHeavyIndex,
80+
usingHeavyPosition,
81+
"left",
82+
);
83+
84+
describe("query-site recognizer", () => {
85+
bench("10 KiB active statement", () => {
86+
recognizeSqlRelationQuerySite(source, slot, position, dialect);
87+
});
88+
89+
bench("1,000 classified aliases", () => {
90+
recognizeSqlRelationQuerySite(
91+
aliasHeavySource,
92+
aliasHeavySlot,
93+
aliasHeavyPosition,
94+
dialect,
95+
);
96+
});
97+
98+
bench("1,000 authenticated USING columns", () => {
99+
recognizeSqlRelationQuerySite(
100+
usingHeavySource,
101+
usingHeavySlot,
102+
usingHeavyPosition,
103+
dialect,
104+
);
105+
});
106+
});

0 commit comments

Comments
 (0)