@askdb/core 1.0.0-beta.51 → 1.0.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +6 -6
  2. package/dist/errors.d.ts +13 -8
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/index.d.ts +9 -9
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +8 -9
  8. package/dist/index.js.map +1 -1
  9. package/dist/logging/pino-supported-levels.d.ts +1 -1
  10. package/dist/logging/pino-supported-levels.js +1 -1
  11. package/dist/schema/v2/loader.js +1 -1
  12. package/dist/schema/v2/loader.js.map +1 -1
  13. package/dist/schema/v2/tenant-policy.d.ts +0 -9
  14. package/dist/schema/v2/tenant-policy.d.ts.map +1 -1
  15. package/dist/schema/v2/tenant-policy.js +0 -11
  16. package/dist/schema/v2/tenant-policy.js.map +1 -1
  17. package/dist/sql/bind.d.ts.map +1 -1
  18. package/dist/sql/bind.js +3 -2
  19. package/dist/sql/bind.js.map +1 -1
  20. package/dist/sql/guardrails.d.ts.map +1 -1
  21. package/dist/sql/guardrails.js +2 -2
  22. package/dist/sql/guardrails.js.map +1 -1
  23. package/dist/sql/lexer.d.ts +12 -2
  24. package/dist/sql/lexer.d.ts.map +1 -1
  25. package/dist/sql/lexer.js +17 -4
  26. package/dist/sql/lexer.js.map +1 -1
  27. package/dist/sql/prompt.js +1 -1
  28. package/dist/sql/prompt.js.map +1 -1
  29. package/dist/sql/readings.d.ts +109 -0
  30. package/dist/sql/readings.d.ts.map +1 -0
  31. package/dist/sql/readings.js +287 -0
  32. package/dist/sql/readings.js.map +1 -0
  33. package/dist/sql/schema-question-precheck.d.ts.map +1 -1
  34. package/dist/sql/schema-question-precheck.js +2 -2
  35. package/dist/sql/schema-question-precheck.js.map +1 -1
  36. package/dist/sql/sensitive-guardrail.d.ts +5 -0
  37. package/dist/sql/sensitive-guardrail.d.ts.map +1 -1
  38. package/dist/sql/sensitive-guardrail.js +49 -24
  39. package/dist/sql/sensitive-guardrail.js.map +1 -1
  40. package/dist/sql/tenant-guardrail.d.ts +3 -1
  41. package/dist/sql/tenant-guardrail.d.ts.map +1 -1
  42. package/dist/sql/tenant-guardrail.js +337 -239
  43. package/dist/sql/tenant-guardrail.js.map +1 -1
  44. package/dist/sql/tenant-placeholders.d.ts +2 -2
  45. package/dist/sql/tenant-placeholders.d.ts.map +1 -1
  46. package/dist/sql/tenant-placeholders.js +2 -2
  47. package/dist/sql/tenant-placeholders.js.map +1 -1
  48. package/dist/sql/validate.d.ts +8 -5
  49. package/dist/sql/validate.d.ts.map +1 -1
  50. package/dist/sql/validate.js +33 -21
  51. package/dist/sql/validate.js.map +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,287 @@
1
+ /**
2
+ * How each SQL check reads a statement, per dialect: the lexer readings it must pass under. This
3
+ * module is the one place that decides them (#815, ADR 0010's stage (c), "one shared analysis"),
4
+ * and {@link readSql} lexes each reading once per statement, so a reading changes in one place.
5
+ * The checks stay heuristics (ADR 0012).
6
+ *
7
+ * The sets differ by check. #815 moved them here unchanged; merging them would tighten some checks
8
+ * and loosen others, so that is its own decision.
9
+ *
10
+ * | Check | A built-in engine | Any other id, or no dialect |
11
+ * |---|---|---|
12
+ * | read-only ({@link readOnlyReadings}) | the engine's profile | every engine's profile |
13
+ * | sensitive ({@link sensitiveReadings}) | the engine's (Postgres family: plus CockroachDB's comment end) | the generic profile, plus each engine's that lexes cleanly |
14
+ * | tenant: statements and calls ({@link tenantStatementReadings}) | the engine's (Postgres family: both comment ends) | every engine's, Postgres and MySQL with both string settings |
15
+ * | tenant: predicates ({@link tenantCodeReadings}) | the engine's view | the standard, Postgres, MySQL and SQL Server views, the first two with both comment ends; the Postgres and SQL Server views with block comments nested and not |
16
+ * | placeholders ({@link placeholderReading}) | the engine's | the generic profile |
17
+ * | names outside printable ASCII, tenant and sensitive ({@link unmatchedNonAsciiName}) | `sqlserver`: the engine's; any other engine: none | as for tenant statements |
18
+ * | `'…'` read as a name or an alias, tenant and sensitive ({@link stringsSqliteReadsAsNames}) | `sqlite`: its readings; any other engine: none | the generic and standard readings, and SQLite's |
19
+ * | Postgres-only forms (`postgresReadingProfiles` in `lexer.ts`) | the engine's, when it is on the Postgres lexer | Postgres's, with backslash escapes too when set |
20
+ *
21
+ * Every MySQL reading is taken twice by the tenant check, with `/*! … *\/` bodies as code and as
22
+ * comments, since a server older than the comment's version skips the body. Its Postgres and SQL
23
+ * Server views, which nest block comments as those engines do, are taken twice too, with the
24
+ * comments nested and not, so a predicate counts only where the two agree on what is code. A `postgres` dialect
25
+ * may point at CockroachDB, which is wire-compatible but ends a `--` comment only at `\n`, so the
26
+ * Postgres family is read with both comment ends where that decides what is code.
27
+ */
28
+ import { isBuiltInDialectId } from "./dialect-spec.js";
29
+ import { ENGINE_LEXER_PROFILES, GENERIC_LEXER, lexerProfileFor, lexSql } from "./lexer.js";
30
+ /**
31
+ * Whether `dialect` names a built-in engine family, which the checks read its own way. Any other
32
+ * id is read every engine's way, and checked against every built-in spec's denylist.
33
+ */
34
+ export function isKnownEngine(dialect) {
35
+ return lexerProfileFor(dialect) !== undefined;
36
+ }
37
+ /** A Postgres-family profile, ended at `\n` only the way CockroachDB ends a `--` comment. */
38
+ function asCockroachDb(profile) {
39
+ return { ...profile, lineCommentEndsAtLfOnly: true };
40
+ }
41
+ /**
42
+ * The read-only check (`validateSelectSql`): the engine's profile, or every engine's. It refuses
43
+ * every comment, so where a comment ends doesn't change its verdict.
44
+ */
45
+ export function readOnlyReadings(dialect) {
46
+ const profile = lexerProfileFor(dialect);
47
+ return profile ? [profile] : ENGINE_LEXER_PROFILES;
48
+ }
49
+ /**
50
+ * The sensitive check. With a built-in engine, its profile (for the Postgres family, the references
51
+ * CockroachDB's comment end shows are added too). Otherwise the generic profile decides scope issues
52
+ * (`primary`), and the references found under each engine's profile that lexes cleanly are added to
53
+ * it (`union`), so a column one engine's quoting rules hide is still reported.
54
+ */
55
+ export function sensitiveReadings(dialect) {
56
+ const profile = dialect ? lexerProfileFor(dialect) : undefined;
57
+ if (!profile)
58
+ return { primary: GENERIC_LEXER, union: ENGINE_LEXER_PROFILES };
59
+ return { primary: profile, union: profile.id === "postgres" ? [asCockroachDb(profile)] : [] };
60
+ }
61
+ /**
62
+ * Every way a server may lex `sql` into statements, for the tenant check's statement and call
63
+ * checks: the dialect's profile when it names a built-in engine, otherwise every engine's, and for
64
+ * Postgres and MySQL both string settings (a backslash ends a literal under one and escapes the
65
+ * quote under the other). A MySQL profile is read with its `/*!NNNNN … *\/` bodies as code and as
66
+ * comments, since a server older than NNNNN skips them, and a Postgres-family one with both
67
+ * comment ends.
68
+ */
69
+ export function tenantStatementReadings(dialect) {
70
+ const known = dialect?.id !== undefined ? lexerProfileFor(dialect) : undefined;
71
+ const profiles = known
72
+ ? known.id === "postgres" ? [known, asCockroachDb(known)] : [known]
73
+ : ENGINE_LEXER_PROFILES.flatMap((p) => p.id === "postgres" || p.id === "mysql" ? [p, { ...p, backslashEscapes: !p.backslashEscapes }] : [p]);
74
+ return bothExecutableCommentReadings(profiles);
75
+ }
76
+ /** Printable ASCII, the only characters a name can hold for the checks to match it by spelling everywhere. */
77
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
78
+ /**
79
+ * The first name in `read`, quoted or not, that holds a character outside printable ASCII and isn't
80
+ * one of `known` exactly, as written; undefined when there is none. SQL Server resolves a name
81
+ * through the database collation, which ignores some characters (U+200D, U+FEFF, any emoji) and
82
+ * reads others as ASCII letters (fullwidth letters, small capitals), so such a name can reach a
83
+ * table or column that the tenant and sensitive checks, which match names by spelling, never see.
84
+ * Read under `sqlserver`, and under any id that is no built-in engine or no dialect, each reading
85
+ * from {@link tenantStatementReadings}; the other built-in engines compare such a name as written.
86
+ */
87
+ export function unmatchedNonAsciiName(read, dialect, known) {
88
+ if (dialect !== undefined && isBuiltInDialectId(dialect.id) && dialect.id !== "sqlserver")
89
+ return undefined;
90
+ for (const profile of tenantStatementReadings(dialect)) {
91
+ for (const token of read.tokens(profile)) {
92
+ const name = token.kind === "word" ? token.text : token.kind === "quoted_identifier" ? token.value : undefined;
93
+ if (name !== undefined && !PRINTABLE_ASCII.test(name) && !known.has(name))
94
+ return token.text;
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /** Whether `profile` may be SQLite's reading: SQLite's own, or the generic and standard ones a dialect that may be SQLite gets. */
100
+ export function mayBeSqlite(profile) {
101
+ return profile.id === "sqlite" || profile.id === "generic";
102
+ }
103
+ /** Words that start a clause other than `FROM`, so a `,` after one is in no `FROM` list. */
104
+ const NOT_A_FROM_LIST = new Set(["select", "where", "group", "having", "order", "limit", "offset", "window", "values", "returning", "union", "intersect", "except", "set", "with"]);
105
+ /**
106
+ * The indices in `tokens` of the `'…'` strings that SQLite reads as names. SQLite takes a
107
+ * single-quoted literal where only a name may stand as that name, so `FROM 'orders'` reads the
108
+ * table `orders`. A string counts right after `FROM`, `JOIN` or `IN` (`IN 'orders'` reads a table),
109
+ * after a `,` in a `FROM` list, or beside a `.` (`main.'orders'`, `'main'.orders`). With
110
+ * `withAliases`, it also counts right after a table or CTE reference, or after `AS` following one,
111
+ * where it is that read's alias (`FROM orders 'o'`). The tenant and sensitive checks read such a
112
+ * string as a quoted name under a reading that {@link mayBeSqlite may be SQLite's}; only the tenant
113
+ * check reads aliases, since the sensitive check already fails closed on a qualifier it can't bind.
114
+ */
115
+ export function stringsSqliteReadsAsNames(tokens, withAliases) {
116
+ const code = [...tokens.keys()].filter((i) => tokens[i].kind !== "comment");
117
+ const at = (j) => (j >= 0 && j < code.length ? tokens[code[j]] : undefined);
118
+ const is = (t, kind, text) => t?.kind === kind && t.lower === text;
119
+ const inFromList = (comma) => {
120
+ let depth = 0;
121
+ for (let j = comma - 1; j >= 0; j--) {
122
+ const t = at(j);
123
+ if (is(t, "punct", ")"))
124
+ depth++;
125
+ else if (is(t, "punct", "(")) {
126
+ if (depth === 0)
127
+ return false;
128
+ depth--;
129
+ }
130
+ else if (depth === 0 && is(t, "punct", ";"))
131
+ return false;
132
+ else if (depth === 0 && t.kind === "word") {
133
+ if (t.lower === "from")
134
+ return true;
135
+ if (NOT_A_FROM_LIST.has(t.lower))
136
+ return false;
137
+ }
138
+ }
139
+ return false;
140
+ };
141
+ const names = new Set();
142
+ /** Positions in `code` of the names that stand in a table's place: `FROM orders`, `JOIN main.orders`. */
143
+ const sources = new Set();
144
+ for (let j = 0; j < code.length; j++) {
145
+ const t = at(j);
146
+ const prev = at(j - 1);
147
+ const plainString = t.kind === "string" && t.text.startsWith("'");
148
+ const startsSource = is(prev, "word", "from") || is(prev, "word", "join") ||
149
+ (is(prev, "punct", ",") && inFromList(j - 1)) || (is(prev, "punct", ".") && sources.has(j - 2));
150
+ if (startsSource && (t.kind === "word" || t.kind === "quoted_identifier" || plainString))
151
+ sources.add(j);
152
+ if (!plainString)
153
+ continue;
154
+ const alias = withAliases && (sources.has(j - 1) || (is(prev, "word", "as") && sources.has(j - 2)));
155
+ const named = alias || is(prev, "punct", ".") || is(at(j + 1), "punct", ".") ||
156
+ is(prev, "word", "from") || is(prev, "word", "join") || is(prev, "word", "in") ||
157
+ (is(prev, "punct", ",") && inFromList(j - 1));
158
+ if (named)
159
+ names.add(code[j]);
160
+ }
161
+ return names;
162
+ }
163
+ /** A view that nests block comments, also read with them ending at the first `*\/`. */
164
+ function bothBlockCommentReadings(profiles) {
165
+ return profiles.flatMap((p) => (p.nestedBlockComments ? [p, { ...p, nestedBlockComments: false }] : [p]));
166
+ }
167
+ /** A profile that reads `/*!NNNNN … *\/` bodies as code, also read with them as comments. */
168
+ function bothExecutableCommentReadings(profiles) {
169
+ return profiles.flatMap((p) => (p.executableComments ? [p, { ...p, executableComments: false }] : [p]));
170
+ }
171
+ /**
172
+ * The tenant check's standard view: how its own scanner split code from strings and comments, kept
173
+ * as a lexer profile (#815). `'…'` strings with `''` escapes; `"…"`, `` `…` `` and `[…]` quoted
174
+ * names under every engine, where the first `]` ends a bracketed name; `--` and `/* *\/` comments,
175
+ * never nested, where a line comment ends only at `\n`.
176
+ */
177
+ const STANDARD_VIEW = {
178
+ id: "generic",
179
+ backslashEscapes: false,
180
+ doubleQuote: "identifier",
181
+ backtickIdentifiers: true,
182
+ bracketIdentifiers: "simple",
183
+ dollarQuotes: false,
184
+ escapeStrings: false,
185
+ hashComments: false,
186
+ dashCommentNeedsSpace: false,
187
+ nestedBlockComments: false,
188
+ executableComments: false,
189
+ atVariables: false,
190
+ hashIdentifiers: false,
191
+ tsqlLiterals: false,
192
+ lineCommentEndsAtLfOnly: true,
193
+ strictStringPrefixes: true,
194
+ };
195
+ /**
196
+ * The standard view, with Postgres `E'…'` escape strings, `$tag$…$tag$` strings, and block comments
197
+ * nested the way Postgres and CockroachDB nest them.
198
+ */
199
+ const POSTGRES_VIEW = {
200
+ ...STANDARD_VIEW,
201
+ id: "postgres",
202
+ escapeStrings: true,
203
+ dollarQuotes: true,
204
+ nestedBlockComments: true,
205
+ };
206
+ /**
207
+ * The standard view, read the MySQL way: `"…"` is a string, backslash escapes, `#` comments, `--`
208
+ * only before whitespace, and `/*! … *\/` bodies as code.
209
+ */
210
+ const MYSQL_VIEW = {
211
+ ...STANDARD_VIEW,
212
+ id: "mysql",
213
+ doubleQuote: "string",
214
+ backslashEscapes: true,
215
+ hashComments: true,
216
+ dashCommentNeedsSpace: true,
217
+ executableComments: true,
218
+ };
219
+ /** A view whose line comments end at `\r` too, as on Postgres and SQL Server. */
220
+ const crEnds = (view) => ({ ...view, lineCommentEndsAtLfOnly: false });
221
+ /**
222
+ * The standard view, read the SQL Server way (#623): a line comment ends at `\r` too, a name or
223
+ * number ends where SQL Server ends it (`tsqlLiterals`: after `0x` or `1E`, and at whitespace such
224
+ * as U+00A0, U+200B and U+0085), so a keyword written right after one is a word of its own, `]]`
225
+ * inside a bracketed name is an escaped `]`, and block comments nest.
226
+ */
227
+ const SQLSERVER_VIEW = {
228
+ ...crEnds(STANDARD_VIEW),
229
+ id: "sqlserver",
230
+ tsqlLiterals: true,
231
+ bracketIdentifiers: "doubled",
232
+ nestedBlockComments: true,
233
+ };
234
+ /**
235
+ * The tenant check's predicate readings. A built-in engine gets its own view: the MySQL one twice,
236
+ * with executable comments as code and as comments; the Postgres one with both comment ends; the
237
+ * SQL Server one. The Postgres and SQL Server views nest block comments, and each is read again with
238
+ * them not nested. Backslash escapes follow the spec as the lexer reads it: an unset value on MySQL
239
+ * or MariaDB is the server default, on. Any other id, or none, gets the standard and Postgres views
240
+ * with both comment ends, the MySQL view and the SQL Server view: a table counts as referenced when
241
+ * any of them sees it, a predicate only when every one does.
242
+ */
243
+ export function tenantCodeReadings(dialect) {
244
+ const views = (profiles) => bothExecutableCommentReadings(bothBlockCommentReadings(profiles)).map((profile) => ({ profile, pipesAreOr: profile.id === "mysql" }));
245
+ if (dialect === undefined || !isBuiltInDialectId(dialect.id)) {
246
+ return views([STANDARD_VIEW, crEnds(STANDARD_VIEW), POSTGRES_VIEW, crEnds(POSTGRES_VIEW), MYSQL_VIEW, SQLSERVER_VIEW]);
247
+ }
248
+ const mysqlFamily = dialect.id === "mysql" || dialect.id === "mariadb";
249
+ const backslashEscapes = mysqlFamily ? dialect.backslashEscapes !== false : dialect.backslashEscapes === true;
250
+ const engine = mysqlFamily
251
+ ? [MYSQL_VIEW]
252
+ : dialect.id === "postgres" || dialect.id === "cockroachdb"
253
+ ? [POSTGRES_VIEW, crEnds(POSTGRES_VIEW)]
254
+ : dialect.id === "sqlserver"
255
+ ? [SQLSERVER_VIEW]
256
+ : [STANDARD_VIEW];
257
+ return views(engine.map((view) => ({ ...view, backslashEscapes })));
258
+ }
259
+ /** Placeholder scans (`tokenizeSqlSpans`): the engine's profile, or the generic one. */
260
+ export function placeholderReading(dialect) {
261
+ return (dialect && lexerProfileFor(dialect)) ?? GENERIC_LEXER;
262
+ }
263
+ /** Read `sql`: {@link SqlRead.tokens} lexes it under a profile the first time it is asked for one. */
264
+ export function readSql(sql) {
265
+ const lexed = new Map();
266
+ return {
267
+ sql,
268
+ tokens(profile) {
269
+ const key = profileKey(profile);
270
+ let tokens = lexed.get(key);
271
+ if (tokens === undefined) {
272
+ tokens = lexSql(sql, profile);
273
+ lexed.set(key, tokens);
274
+ }
275
+ return tokens;
276
+ },
277
+ };
278
+ }
279
+ /** Two profiles with the same rules lex the same tokens. */
280
+ function profileKey(p) {
281
+ return [
282
+ p.id, p.backslashEscapes, p.doubleQuote, p.backtickIdentifiers, p.bracketIdentifiers, p.dollarQuotes,
283
+ p.escapeStrings, p.hashComments, p.dashCommentNeedsSpace, p.nestedBlockComments, p.executableComments,
284
+ p.atVariables, p.hashIdentifiers, p.tsqlLiterals, p.lineCommentEndsAtLfOnly === true, p.strictStringPrefixes === true,
285
+ ].join("|");
286
+ }
287
+ //# sourceMappingURL=readings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readings.js","sourceRoot":"","sources":["../../src/sql/readings.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema-question-precheck.d.ts","sourceRoot":"","sources":["../../src/sql/schema-question-precheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAS9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CASvF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAwB7F"}
1
+ {"version":3,"file":"schema-question-precheck.d.ts","sourceRoot":"","sources":["../../src/sql/schema-question-precheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAS9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOvF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAwB7F"}
@@ -5,10 +5,10 @@ const JOIN_RELATION_HINT = /\b(join|joined|joins|inner\s+join|left\s+join|right\
5
5
  const FROM_JOIN_TABLE = /\b(?:from|join)\s+([a-zA-Z_][\w]*)\b/gi;
6
6
  export function assertNlToSqlInputs(schema, question) {
7
7
  if (schema.tables.length === 0) {
8
- throw new AskDbError("Schema has no tables. Add at least one table to the AskDB schema JSON (see fixtures/schemas/ in this repo).");
8
+ throw new AskDbError("Schema has no tables. Pass a `schema` with at least one table.");
9
9
  }
10
10
  if (!question.trim()) {
11
- throw new AskDbError("Question is empty. Pass a non-blank natural-language question with -q / --question.");
11
+ throw new AskDbError("Question is empty. Pass a non-blank natural-language `question`.");
12
12
  }
13
13
  }
14
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema-question-precheck.js","sourceRoot":"","sources":["../../src/sql/schema-question-precheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEzD,MAAM,kBAAkB,GACtB,oMAAoM,CAAC;AAEvM,oGAAoG;AACpG,MAAM,eAAe,GAAG,wCAAwC,CAAC;AAEjE,MAAM,UAAU,mBAAmB,CAAC,MAA2B,EAAE,QAAgB;IAC/E,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,UAAU,CAClB,6GAA6G,CAC9G,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACrB,MAAM,IAAI,UAAU,CAAC,qFAAqF,CAAC,CAAC;IAC9G,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,MAA2B;IACjF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,wEAAwE;IACxE,MAAM,OAAO,GAAG,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5F,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAErE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9D,KAAK,CAAC,IAAI,CACR,kIAAkI,CACnI,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAClB,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,gCAAgC,GAAG,+BAA+B,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"schema-question-precheck.js","sourceRoot":"","sources":["../../src/sql/schema-question-precheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEzD,MAAM,kBAAkB,GACtB,oMAAoM,CAAC;AAEvM,oGAAoG;AACpG,MAAM,eAAe,GAAG,wCAAwC,CAAC;AAEjE,MAAM,UAAU,mBAAmB,CAAC,MAA2B,EAAE,QAAgB;IAC/E,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,UAAU,CAAC,gEAAgE,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACrB,MAAM,IAAI,UAAU,CAAC,kEAAkE,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,MAA2B;IACjF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,wEAAwE;IACxE,MAAM,OAAO,GAAG,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5F,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAErE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9D,KAAK,CAAC,IAAI,CACR,kIAAkI,CACnI,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAClB,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,gCAAgC,GAAG,+BAA+B,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -64,4 +64,9 @@ export declare function scanSensitiveReferences(sql: string, schema: AnyNormaliz
64
64
  */
65
65
  export declare function schemaHasSensitiveIdentifiers(schema: AnyNormalizedSchema): boolean;
66
66
  export { formatSensitiveReference };
67
+ /**
68
+ * Every table, namespace and column name in `schema`, exactly as it spells them. Internal to
69
+ * `@askdb/core`: the tenant check counts a name outside printable ASCII only when it is one of them.
70
+ */
71
+ export declare function schemaNames(schema: AnyNormalizedSchema): ReadonlySet<string>;
67
72
  //# sourceMappingURL=sensitive-guardrail.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sensitive-guardrail.d.ts","sourceRoot":"","sources":["../../src/sql/sensitive-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAU,wBAAwB,EAAoC,MAAM,uBAAuB,CAAC;AAW3G,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,kCAAkC,GAAG;IAC/C;;;;OAIG;IACH,IAAI,CAAC,EAAE,sBAAsB,CAAC;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,kBAAkB,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,8FAA8F;IAC9F,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,oBAAoB,CAAC;CACxC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,mBAAmB,EAC3B,OAAO,CAAC,EAAE,kCAAkC,GAC3C,wBAAwB,CAM1B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,kBAAkB,CAAC,GAAG,SAAS,GAChE,wBAAwB,CAG1B;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAElF;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
1
+ {"version":3,"file":"sensitive-guardrail.d.ts","sourceRoot":"","sources":["../../src/sql/sensitive-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAU,wBAAwB,EAAoC,MAAM,uBAAuB,CAAC;AAI3G,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,kCAAkC,GAAG;IAC/C;;;;OAIG;IACH,IAAI,CAAC,EAAE,sBAAsB,CAAC;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,kBAAkB,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,8FAA8F;IAC9F,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,oBAAoB,CAAC;CACxC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,mBAAmB,EAC3B,OAAO,CAAC,EAAE,kCAAkC,GAC3C,wBAAwB,CAM1B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,kBAAkB,CAAC,GAAG,SAAS,GAChE,wBAAwB,CAG1B;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAElF;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAoFpC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,mBAAmB,GAAG,WAAW,CAAC,MAAM,CAAC,CAE5E"}
@@ -1,5 +1,6 @@
1
1
  import { decide, formatSensitiveReference, sensitiveFindings, throwIfDenied } from "./guardrail-decide.js";
2
- import { ENGINE_LEXER_PROFILES, GENERIC_LEXER, findPostgresOnlyForms, hasUnterminatedToken, lexSql, lexerProfileFor, } from "./lexer.js";
2
+ import { findPostgresOnlyForms, hasUnterminatedToken } from "./lexer.js";
3
+ import { isKnownEngine, mayBeSqlite, readSql, sensitiveReadings, stringsSqliteReadsAsNames, unmatchedNonAsciiName } from "./readings.js";
3
4
  /**
4
5
  * Check whether a SQL statement references tables/columns marked `sensitive`
5
6
  * in the schema artifact.
@@ -88,17 +89,38 @@ function indexSchema(schema) {
88
89
  else
89
90
  byName.set(entry.lower, [entry]);
90
91
  });
91
- const index = { tables, byName, hasSensitive };
92
+ const names = new Set();
93
+ for (const t of tables) {
94
+ names.add(t.name);
95
+ if (t.schema !== undefined)
96
+ names.add(t.schema);
97
+ for (const c of t.columns)
98
+ names.add(c.name);
99
+ }
100
+ const index = { tables, byName, hasSensitive, names };
92
101
  indexCache.set(schema, index);
93
102
  return index;
94
103
  }
104
+ /**
105
+ * Every table, namespace and column name in `schema`, exactly as it spells them. Internal to
106
+ * `@askdb/core`: the tenant check counts a name outside printable ASCII only when it is one of them.
107
+ */
108
+ export function schemaNames(schema) {
109
+ return indexSchema(schema).names;
110
+ }
95
111
  /**
96
112
  * Map lexer tokens onto the scanner's simpler vocabulary. Comments are dropped (MySQL
97
- * `/*! … *\/` bodies are already lexed as code).
113
+ * `/*! … *\/` bodies are already lexed as code). A `'…'` at an index in `names` is a quoted name,
114
+ * as SQLite reads `FROM 'users'`.
98
115
  */
99
- function toToks(tokens) {
116
+ function toToks(tokens, names) {
100
117
  const out = [];
101
- for (const t of tokens) {
118
+ for (const [index, t] of tokens.entries()) {
119
+ if (names.has(index)) {
120
+ const inner = t.text.slice(1, t.unterminated ? undefined : -1).replaceAll("''", "'");
121
+ out.push({ kind: "word", value: inner, lower: inner.toLowerCase(), quoted: true });
122
+ continue;
123
+ }
102
124
  switch (t.kind) {
103
125
  case "comment":
104
126
  break;
@@ -219,28 +241,25 @@ function selectModifiersFor(dialect) {
219
241
  }
220
242
  }
221
243
  /**
222
- * Lex and scan. With a known dialect the statement is lexed the way that engine reads it
223
- * (a Postgres-family one also the way CockroachDB ends a line comment, with the references
224
- * found there added). Without one (or for an unrecognized id) the generic reading decides scope
225
- * issues, and references are unioned across every engine's reading that lexes cleanly —
244
+ * Lex and scan, under the readings `sensitiveReadings` gives. With a known dialect the statement is
245
+ * lexed the way that engine reads it (a Postgres-family one also the way CockroachDB ends a line
246
+ * comment, with the references found there added). Without one (or for an unrecognized id) the
247
+ * generic reading decides scope issues, and references are unioned across every engine's reading
248
+ * that lexes cleanly —
226
249
  * so a column hidden from one reading by an engine-specific quote or comment rule (for
227
250
  * example MySQL `'\''` or Postgres `E'\''`) is still reported. Readings with an
228
251
  * unterminated token are skipped: the engine would reject that SQL outright.
229
252
  */
230
253
  function scanSql(sql, index, dialect) {
231
- const profile = dialect ? lexerProfileFor(dialect) : undefined;
232
- // A `postgres` dialect may point at CockroachDB, where only `\n` ends a line comment: the
233
- // references that reading finds are added too.
234
- const modifiers = profile ? selectModifiersFor(dialect) : SELECT_MODIFIERS.any;
235
- const primary = scanTokens(lexSql(sql, profile ?? GENERIC_LEXER), index, modifiers);
236
- const others = !profile
237
- ? ENGINE_LEXER_PROFILES
238
- : profile.id === "postgres" ? [{ ...profile, lineCommentEndsAtLfOnly: true }] : [];
239
- for (const p of others) {
240
- const tokens = lexSql(sql, p);
254
+ const read = readSql(sql);
255
+ const readings = sensitiveReadings(dialect);
256
+ const modifiers = dialect && isKnownEngine(dialect) ? selectModifiersFor(dialect) : SELECT_MODIFIERS.any;
257
+ const primary = scanTokens(read.tokens(readings.primary), index, modifiers, mayBeSqlite(readings.primary));
258
+ for (const p of readings.union) {
259
+ const tokens = read.tokens(p);
241
260
  if (hasUnterminatedToken(tokens))
242
261
  continue;
243
- for (const [key, ref] of scanTokens(tokens, index, modifiers).found) {
262
+ for (const [key, ref] of scanTokens(tokens, index, modifiers, mayBeSqlite(p)).found) {
244
263
  const existing = primary.found.get(key);
245
264
  if (!existing)
246
265
  primary.found.set(key, ref);
@@ -248,7 +267,12 @@ function scanSql(sql, index, dialect) {
248
267
  existing.matchKind = "qualified";
249
268
  }
250
269
  }
251
- return finalize(withPostgresOnlyIssues(primary, sql, dialect));
270
+ const outcome = withPostgresOnlyIssues(primary, sql, dialect);
271
+ // SQL Server's collation may read a name outside printable ASCII as a sensitive column's, which
272
+ // the scan above matches by spelling: unless the schema spells it exactly, fail closed.
273
+ if (unmatchedNonAsciiName(read, dialect, index.names) !== undefined)
274
+ outcome.issues.add("UNICODE_ESCAPED_IDENTIFIER");
275
+ return finalize(outcome);
252
276
  }
253
277
  /**
254
278
  * `outcome` with a scope issue for each form only the Postgres reading can hold: a `U&"…"` name,
@@ -278,8 +302,9 @@ function finalize(outcome) {
278
302
  ...(unresolvedScope ? { unresolvedScope } : {}),
279
303
  };
280
304
  }
281
- function scanTokens(lexed, index, modifiers) {
282
- const tokens = toToks(lexed);
305
+ function scanTokens(lexed, index, modifiers, sqlite) {
306
+ // A reading that may be SQLite's reads a '…' in a table's place as the table's name.
307
+ const tokens = toToks(lexed, sqlite ? stringsSqliteReadsAsNames(lexed, false) : new Set());
283
308
  const isWord = (t) => t?.kind === "word";
284
309
  const isPunct = (t, v) => t?.kind === "punct" && t.value === v;
285
310
  const isKeywordTok = (t, v) => t?.kind === "word" && !t.quoted && t.lower === v;
@@ -864,7 +889,7 @@ const SCOPE_ISSUE_TEXT = {
864
889
  UNKNOWN_QUALIFIER: "a qualified reference used a qualifier that is not a known table, alias, or CTE",
865
890
  OPAQUE_TABLE_SOURCE: "a table source is not a relation name (table function or table-valued expression)",
866
891
  UNTERMINATED_TOKEN: "a string, quoted identifier, or comment never closes, so the rest of the statement could not be scanned",
867
- UNICODE_ESCAPED_IDENTIFIER: "a Unicode-escaped name (U&\"…\") can't be matched against sensitive columns, since Postgres decodes it",
892
+ UNICODE_ESCAPED_IDENTIFIER: "a name can't be matched against sensitive columns as written: a Unicode-escaped name (U&\"…\"), which Postgres decodes, or a name outside printable ASCII that the schema doesn't spell, which SQL Server's collation may read as another",
868
893
  STRING_CONTINUATION: "an E'…' string continues on a later line, which Postgres reads with backslash escapes, so where the string ends is unclear",
869
894
  ESCAPED_BYTE_STRING: "a b'…' byte string holds a backslash or continues on a later line, which CockroachDB reads with backslash escapes, so where the string ends is unclear",
870
895
  };