@suss/sql 0.32.0 → 0.33.0

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.
package/README.md CHANGED
@@ -12,7 +12,21 @@ A reader, not a pack. A pattern pack that meets a query written as SQL rather th
12
12
  import { readSqlAccess } from "@suss/sql";
13
13
 
14
14
  readSqlAccess("SELECT id, email FROM users WHERE tenant_id = $1");
15
- // [{ table: "users", kind: "read", fields: ["id", "email"], selector: ["tenant_id"] }]
15
+ // [{ table: "users", qualifier: [], kind: "read", fields: ["id", "email"], selector: ["tenant_id"] }]
16
+ ```
17
+
18
+ `qualifier` is the namespaces written in front of the table, outermost first. Postgres and MySQL hand the schema back separately and it never reaches the table, so those are always empty. BigQuery addresses a table as `` `project.dataset.table` `` and the parser hands back the whole quoted string, so this splits it: the table is the one a provider declares, and the dataset is the namespace to record the access under.
19
+
20
+ ```ts
21
+ readSqlAccess("SELECT id FROM `analytics.core.dim_account`", { dialect: "bigquery" });
22
+ // [{ table: "dim_account", qualifier: ["analytics", "core"], ... }]
23
+ ```
24
+
25
+ A pack that reads a table off a call's argument rather than out of a statement, the way `client.get_table("analytics.core.dim_account")` states one, splits it through `splitQualifiedTable` so both spellings land on the same table and the same namespace. It gives back nothing for a table that came through as a parameter.
26
+
27
+ ```ts
28
+ splitQualifiedTable("analytics.core.dim_account");
29
+ // { table: "dim_account", qualifier: ["analytics", "core"] }
16
30
  ```
17
31
 
18
32
  The statement is parsed rather than pattern-matched, so a join contributes every table it reads:
@@ -22,8 +36,76 @@ readSqlAccess("SELECT u.email, o.total FROM users u JOIN orders o ON o.user_id =
22
36
  // [{ table: "users", fields: ["email"], ... }, { table: "orders", fields: ["total"], ... }]
23
37
  ```
24
38
 
39
+ A query written in place of a table is read for its own tables. The alias belongs to that query's columns rather than to a base table, so a column read through the alias is dropped rather than attributed to one:
40
+
41
+ ```ts
42
+ readSqlAccess("SELECT dc.account_id FROM (SELECT account_id FROM dim_contact) dc JOIN dim_account a ON a.id = dc.account_id");
43
+ // [{ table: "dim_account", fields: [], ... }, { table: "dim_contact", fields: ["account_id"], ... }]
44
+ ```
45
+
46
+ A write reads too. An insert can take its rows from a query, and an update or a delete can reach another table to pick the rows it changes:
47
+
48
+ ```ts
49
+ readSqlAccess("INSERT INTO runs (id) SELECT id FROM jobs");
50
+ // [{ table: "runs", kind: "write", fields: ["id"], ... }, { table: "jobs", kind: "read", fields: ["id"], ... }]
51
+ ```
52
+
53
+ Every branch of a set operation is read, whether the statement writes it on its own, inside a `WITH` clause, or in place of a table. `SELECT id FROM orders UNION SELECT id FROM refunds` gives back a read on `orders` and one on `refunds`, each with the field `id`.
54
+
55
+ Wherever a field turns up, it goes on the table the statement qualified it to, and a `WHERE` puts it in the selector rather than the fields. So the table an update reads from lists its join key the way a table a select joins to would:
56
+
57
+ ```ts
58
+ readSqlAccess("UPDATE runs SET state = $1 FROM jobs WHERE runs.job_id = jobs.id");
59
+ // [{ table: "runs", kind: "write", fields: ["state"], selector: ["job_id"] },
60
+ // { table: "jobs", kind: "read", fields: [], selector: ["id"] }]
61
+ ```
62
+
63
+ What a `RETURNING` hands back, and what an `ON CONFLICT` matches on and then sets, are all columns of the one table the statement writes, so they go on that write rather than on a second access: a consumer pairs one access per table against the schema, and a field the statement both writes and hands back is listed once. `INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id` writes `users` with the fields `email`, `id` and `name`.
64
+
65
+ Not every grammar takes every spelling of these. `UPDATE ... FROM` is Postgres alone, and BigQuery parses the statement but drops the clause, so there it gives back the write by itself. No grammar here takes `DELETE ... USING`. `INTERSECT` and `EXCEPT` are Postgres and MySQL, `RETURNING` is Postgres and SQLite, and `ON CONFLICT` is Postgres, where the other three grammars take `ON DUPLICATE KEY UPDATE` instead.
66
+
67
+ The grammar behind this turns down two spellings that say nothing about which table is touched, so a statement it refuses is tried again without them: a parameter written with the type it is read as, such as `$1::text` or `$1::int[]`, and a select that locks the rows it picks, such as `FOR UPDATE SKIP LOCKED`. Neither is rewritten inside a literal, a quoted name or a comment.
68
+
69
+ The grammar also takes a `WITH` clause only in front of a select. In front of an insert, an update or a delete the clause is split off and each query read on its own, which is the most that can be read of a statement the grammar will not take whole:
70
+
71
+ ```ts
72
+ readSqlAccess("WITH due AS (SELECT id FROM jobs FOR UPDATE SKIP LOCKED) INSERT INTO job_runs (job_id) SELECT id FROM due");
73
+ // [{ table: "job_runs", kind: "write", fields: ["job_id"], ... }, { table: "jobs", kind: "read", fields: ["id"], ... }]
74
+ ```
75
+
25
76
  A query written as a tagged template becomes readable through `sqlFromParts`, which writes each interpolation as a parameter. What a query interpolates is a value nearly every time, and a parameter is how the statement would supply one anyway.
26
77
 
78
+ Two things override that. A caller who knows a hole is a table passes it in `substitutions`, which is how a Drizzle query that interpolates a schema object reaches the statement as a table name. And a caller who ran the source's own evaluator over each hole passes the results in `settled`, which are used where the statement writes a name.
79
+
80
+ There are two of those places. One is inside a quoted name, which is how BigQuery addresses a table:
81
+
82
+ ```ts
83
+ // `SELECT id FROM \`${TABLE}\`` with TABLE = "analytics.core.dim_account"
84
+ sqlFromParts(["SELECT id FROM `", "`"], [], ["analytics.core.dim_account"]);
85
+ // SELECT id FROM `analytics.core.dim_account`
86
+ ```
87
+
88
+ The other is straight after `FROM`, `JOIN`, `INTO`, `UPDATE` or `TABLE`, in any case. Postgres code leaves the table unquoted nearly every time, so the quote alone would miss the commonest way a project interpolates one:
89
+
90
+ ```ts
91
+ // `SELECT id FROM ${TABLE} WHERE id = $1` with TABLE = "users"
92
+ sqlFromParts(["SELECT id FROM ", " WHERE id = $1"], [], ["users"]);
93
+ // SELECT id FROM users WHERE id = $1
94
+ ```
95
+
96
+ A hole anywhere else is left alone even when the evaluator settled it. `WHERE tier = ${TIER}` with `TIER = "gold"` would parse as a column called `gold` and put it in the selector, which is worse than the parameter the reader would otherwise see. Where the statement writes a name there is no such ambiguity: whatever the hole came to is part of the name.
97
+
98
+ A part of a name nothing settled stays a parameter, and the qualifier is then read from the table outward and stops there. The part beside the table is the one a scope comes from, so a project read as though it were a dataset would place the access somewhere it never went:
99
+
100
+ ```
101
+ analytics.core.dim_account → dim_account, ["analytics", "core"]
102
+ $1.core.dim_account → dim_account, ["core"]
103
+ analytics.$1.dim_account → dim_account, []
104
+ $1 → nothing
105
+ ```
106
+
107
+ A whole table nothing settled produces nothing at all.
108
+
27
109
  It reads Postgres, MySQL, SQLite, and BigQuery. Pass the dialect the way a pack states its store:
28
110
 
29
111
  ```ts
@@ -33,7 +115,7 @@ readSqlAccess("SELECT `id` FROM `users`", { dialect: "mysql" });
33
115
  ## What it will not tell you
34
116
 
35
117
  - **A dialect it does not read gives back nothing.** So does a statement it cannot parse. Neither produces a guess built out of whatever the text happens to spell.
36
- - **An unqualified field in a join is left out.** `SELECT id FROM users u JOIN orders o ON ...` says nothing about which table `id` is on, so it is dropped rather than attributed to both.
118
+ - **An unqualified field in a join is left out.** `SELECT id FROM users u JOIN orders o ON ...` says nothing about which table `id` is on, so it is dropped rather than attributed to both. A query written in place of a table counts as one of the sides, since it could have supplied the field too.
37
119
  - **An interpolated table or clause makes a statement unreadable.** ``sql`SELECT * FROM ${table}` `` cannot be settled without running it, so it produces nothing.
38
120
 
39
121
  ## Where it fits in suss
package/dist/index.cjs CHANGED
@@ -21998,6 +21998,7 @@ var require_sqlite = __commonJS({
21998
21998
  var index_exports = {};
21999
21999
  __export(index_exports, {
22000
22000
  readSqlAccess: () => readSqlAccess,
22001
+ splitQualifiedTable: () => splitQualifiedTable,
22001
22002
  sqlFromParts: () => sqlFromParts
22002
22003
  });
22003
22004
  module.exports = __toCommonJS(index_exports);
@@ -22012,68 +22013,351 @@ var DIALECTS = {
22012
22013
  bigquery: { module: import_bigquery.default, database: "bigquery" }
22013
22014
  };
22014
22015
  function readSqlAccess(sql, options = {}) {
22015
- const dialect = DIALECTS[options.dialect ?? "postgresql"];
22016
- if (dialect === void 0) {
22016
+ const grammar = DIALECTS[options.dialect ?? "postgresql"];
22017
+ if (grammar === void 0) {
22017
22018
  return [];
22018
22019
  }
22020
+ return accessesInSql(sql, grammar).map(qualified).filter((access) => access !== null);
22021
+ }
22022
+ function accessesInSql(sql, grammar) {
22023
+ const written = parsedAccesses(sql, grammar);
22024
+ if (written !== null) {
22025
+ return written;
22026
+ }
22027
+ const readable = rewrittenForGrammar(sql);
22028
+ const rewritten = readable === sql ? null : parsedAccesses(readable, grammar);
22029
+ return rewritten ?? accessesAroundWith(readable, grammar);
22030
+ }
22031
+ function parsedAccesses(sql, grammar, defined = /* @__PURE__ */ new Set()) {
22019
22032
  let parsed;
22020
22033
  try {
22021
- parsed = new dialect.module.Parser().astify(sql, {
22022
- database: dialect.database
22034
+ parsed = new grammar.module.Parser().astify(sql, {
22035
+ database: grammar.database
22023
22036
  });
22024
22037
  } catch {
22025
- return [];
22038
+ return null;
22026
22039
  }
22027
22040
  const statements = Array.isArray(parsed) ? parsed : [parsed];
22028
- return statements.flatMap((statement) => accessesIn(statement, /* @__PURE__ */ new Set()));
22041
+ return statements.flatMap((statement) => accessesIn(statement, defined));
22029
22042
  }
22030
- function sqlFromParts(parts, substitutions = []) {
22031
- return parts.map((part, index) => {
22032
- if (index === 0) {
22033
- return part;
22043
+ var QUALIFIER_SEPARATOR = ".";
22044
+ function splitQualifiedTable(name) {
22045
+ const parts = name.split(QUALIFIER_SEPARATOR);
22046
+ const table = parts[parts.length - 1] ?? name;
22047
+ if (!isSettled(table)) {
22048
+ return null;
22049
+ }
22050
+ return { table, qualifier: namespacesAround(parts.slice(0, -1)) };
22051
+ }
22052
+ function namespacesAround(namespaces) {
22053
+ const found = [];
22054
+ for (let index = namespaces.length - 1; index >= 0; index -= 1) {
22055
+ const part = namespaces[index];
22056
+ if (part === void 0 || !isSettled(part)) {
22057
+ break;
22034
22058
  }
22035
- const settled = substitutions[index - 1];
22036
- return `${settled ?? `$${index}`}${part}`;
22059
+ found.unshift(part);
22060
+ }
22061
+ return found;
22062
+ }
22063
+ function qualified(access) {
22064
+ const split = splitQualifiedTable(access.table);
22065
+ return split === null ? null : { ...access, ...split };
22066
+ }
22067
+ function isSettled(part) {
22068
+ return part !== "" && !/^\$\d+$/.test(part);
22069
+ }
22070
+ function sqlFromParts(parts, substitutions = [], settled = []) {
22071
+ let closing = null;
22072
+ return parts.map((part, index) => {
22073
+ const names = namePosition(closing, parts[index - 1] ?? "");
22074
+ const inHole = index === 0 ? null : substitutions[index - 1] ?? (names ? settled[index - 1] ?? null : null);
22075
+ closing = quoteAfter(closing, part);
22076
+ return index === 0 ? part : `${inHole ?? `$${index}`}${part}`;
22037
22077
  }).join("");
22038
22078
  }
22079
+ var TABLE_KEYWORD = /\b(?:from|join|into|update|table)\s+$/i;
22080
+ function namePosition(closing, before) {
22081
+ return closing !== null || TABLE_KEYWORD.test(before);
22082
+ }
22083
+ var NAME_QUOTES = { '"': '"', "`": "`", "[": "]" };
22084
+ function quoteAfter(open, part) {
22085
+ let closing = open;
22086
+ for (const character of part) {
22087
+ if (closing === null) {
22088
+ closing = NAME_QUOTES[character] ?? null;
22089
+ continue;
22090
+ }
22091
+ if (character === closing) {
22092
+ closing = null;
22093
+ }
22094
+ }
22095
+ return closing;
22096
+ }
22097
+ var TEXT_RUNS = [
22098
+ { open: "--", close: "\n", comment: true },
22099
+ { open: "/*", close: "*/", comment: true },
22100
+ { open: "'", close: "'", comment: false },
22101
+ { open: '"', close: '"', comment: false },
22102
+ { open: "`", close: "`", comment: false }
22103
+ ];
22104
+ var DOLLAR_TAG = /\$(?:[A-Za-z_]\w*)?\$/y;
22105
+ function textRunAt(sql, index) {
22106
+ DOLLAR_TAG.lastIndex = index;
22107
+ const tag = DOLLAR_TAG.exec(sql)?.[0];
22108
+ if (tag !== void 0) {
22109
+ return { end: closingAt(sql, index + tag.length, tag), comment: false };
22110
+ }
22111
+ for (const run of TEXT_RUNS) {
22112
+ if (sql.startsWith(run.open, index)) {
22113
+ const from = index + run.open.length;
22114
+ return { end: closingAt(sql, from, run.close), comment: run.comment };
22115
+ }
22116
+ }
22117
+ return null;
22118
+ }
22119
+ function closingAt(sql, from, close) {
22120
+ const at = sql.indexOf(close, from);
22121
+ return at === -1 ? sql.length : at + close.length;
22122
+ }
22123
+ function codeMask(sql) {
22124
+ const mask = new Array(sql.length).fill(true);
22125
+ let index = 0;
22126
+ while (index < sql.length) {
22127
+ const run = textRunAt(sql, index);
22128
+ if (run === null) {
22129
+ index += 1;
22130
+ continue;
22131
+ }
22132
+ for (let at = index; at < run.end; at += 1) {
22133
+ mask[at] = false;
22134
+ }
22135
+ index = run.end;
22136
+ }
22137
+ return mask;
22138
+ }
22139
+ function replaceInCode(sql, pattern, rewrite) {
22140
+ const mask = codeMask(sql);
22141
+ let rewritten = "";
22142
+ let from = 0;
22143
+ for (const match of sql.matchAll(pattern)) {
22144
+ const at = match.index;
22145
+ if (at === void 0 || mask[at] === false) {
22146
+ continue;
22147
+ }
22148
+ rewritten += sql.slice(from, at) + rewrite(match);
22149
+ from = at + match[0].length;
22150
+ }
22151
+ return rewritten + sql.slice(from);
22152
+ }
22153
+ var TYPE_WORD = String.raw`(?:"[^"]*"|\w+)`;
22154
+ var TYPE_SIZE = String.raw`(?:\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\))?`;
22155
+ var TYPE_TAIL = String.raw`(?:\s+(?:precision|varying|with(?:out)?\s+time\s+zone))?`;
22156
+ var CAST_TYPE = `${TYPE_WORD}(?:\\s*\\.\\s*${TYPE_WORD})?${TYPE_SIZE}${TYPE_TAIL}${TYPE_SIZE}(?:\\s*\\[\\s*\\])*`;
22157
+ var PARAMETER_CAST = new RegExp(
22158
+ String.raw`(\$\d+)(?:\s*::\s*${CAST_TYPE})+`,
22159
+ "gi"
22160
+ );
22161
+ var LOCKING_CLAUSE = new RegExp(
22162
+ String.raw`\bfor\s+(?:no\s+key\s+update|key\s+share|update|share)` + String.raw`(?:\s+of\s+${TYPE_WORD}(?:\s*,\s*${TYPE_WORD})*)?` + String.raw`(?:\s+(?:nowait|skip\s+locked))?`,
22163
+ "gi"
22164
+ );
22165
+ function rewrittenForGrammar(sql) {
22166
+ const uncast = replaceInCode(sql, PARAMETER_CAST, castAwayFrom);
22167
+ return replaceInCode(uncast, LOCKING_CLAUSE, nothing);
22168
+ }
22169
+ function castAwayFrom(match) {
22170
+ return match[1] ?? "";
22171
+ }
22172
+ function nothing() {
22173
+ return "";
22174
+ }
22175
+ function accessesAroundWith(sql, grammar) {
22176
+ const split = splitWith(sql);
22177
+ if (split === null) {
22178
+ return [];
22179
+ }
22180
+ const names = /* @__PURE__ */ new Set();
22181
+ const inside = [];
22182
+ for (const table of split.tables) {
22183
+ inside.push(...parsedAccesses(table.body, grammar, names) ?? []);
22184
+ names.add(table.name);
22185
+ }
22186
+ const own = parsedAccesses(split.main, grammar, names) ?? [];
22187
+ return [...own, ...inside];
22188
+ }
22189
+ function splitWith(sql) {
22190
+ const mask = codeMask(sql);
22191
+ let index = nextToken(sql, 0);
22192
+ if (!wordAt(sql, index, "with")) {
22193
+ return null;
22194
+ }
22195
+ index = pastWord(sql, index, "with");
22196
+ if (wordAt(sql, index, "recursive")) {
22197
+ index = pastWord(sql, index, "recursive");
22198
+ }
22199
+ const tables = [];
22200
+ for (; ; ) {
22201
+ const named = nameAt(sql, index);
22202
+ if (named === null) {
22203
+ return null;
22204
+ }
22205
+ index = afterColumnList(sql, mask, nextToken(sql, named.end));
22206
+ if (index === -1 || !wordAt(sql, index, "as")) {
22207
+ return null;
22208
+ }
22209
+ index = pastMaterialized(sql, pastWord(sql, index, "as"));
22210
+ const end = afterGroup(sql, mask, index);
22211
+ if (end === null) {
22212
+ return null;
22213
+ }
22214
+ tables.push({ name: named.name, body: sql.slice(index + 1, end - 1) });
22215
+ index = nextToken(sql, end);
22216
+ if (sql[index] !== ",") {
22217
+ return { tables, main: sql.slice(index) };
22218
+ }
22219
+ index = nextToken(sql, index + 1);
22220
+ }
22221
+ }
22222
+ function afterColumnList(sql, mask, index) {
22223
+ if (sql[index] !== "(") {
22224
+ return index;
22225
+ }
22226
+ const end = afterGroup(sql, mask, index);
22227
+ return end === null ? -1 : nextToken(sql, end);
22228
+ }
22229
+ function pastMaterialized(sql, index) {
22230
+ const past = wordAt(sql, index, "not") ? pastWord(sql, index, "not") : index;
22231
+ return wordAt(sql, past, "materialized") ? pastWord(sql, past, "materialized") : past;
22232
+ }
22233
+ function nextToken(sql, from) {
22234
+ let index = from;
22235
+ while (index < sql.length) {
22236
+ if (/\s/.test(sql[index] ?? "")) {
22237
+ index += 1;
22238
+ continue;
22239
+ }
22240
+ const run = textRunAt(sql, index);
22241
+ if (run === null || !run.comment) {
22242
+ return index;
22243
+ }
22244
+ index = run.end;
22245
+ }
22246
+ return index;
22247
+ }
22248
+ function wordAt(sql, index, word) {
22249
+ const written = sql.slice(index, index + word.length).toLowerCase();
22250
+ return written === word && !/\w/.test(sql[index + word.length] ?? "");
22251
+ }
22252
+ function pastWord(sql, index, word) {
22253
+ return nextToken(sql, index + word.length);
22254
+ }
22255
+ function nameAt(sql, index) {
22256
+ if (sql[index] === '"') {
22257
+ const close = sql.indexOf('"', index + 1);
22258
+ return close === -1 ? null : { name: sql.slice(index + 1, close), end: close + 1 };
22259
+ }
22260
+ const word = /^\w+/.exec(sql.slice(index))?.[0];
22261
+ return word === void 0 ? null : { name: word, end: index + word.length };
22262
+ }
22263
+ function afterGroup(sql, mask, index) {
22264
+ if (sql[index] !== "(" || mask[index] === false) {
22265
+ return null;
22266
+ }
22267
+ let depth = 0;
22268
+ for (let at = index; at < sql.length; at += 1) {
22269
+ if (mask[at] === false) {
22270
+ continue;
22271
+ }
22272
+ if (sql[at] === "(") {
22273
+ depth += 1;
22274
+ continue;
22275
+ }
22276
+ if (sql[at] !== ")") {
22277
+ continue;
22278
+ }
22279
+ depth -= 1;
22280
+ if (depth === 0) {
22281
+ return at + 1;
22282
+ }
22283
+ }
22284
+ return null;
22285
+ }
22039
22286
  function accessesIn(statement, defined) {
22040
22287
  const node = asNode(statement);
22041
22288
  if (node === null) {
22042
22289
  return [];
22043
22290
  }
22291
+ const clause = commonTables(node, defined);
22292
+ return [...ownAccesses(node, clause.names), ...clause.inside];
22293
+ }
22294
+ function ownAccesses(node, defined) {
22044
22295
  if (node.type === "select") {
22045
- return selectAccesses(node, defined);
22296
+ return [
22297
+ ...selectAccesses(node, defined),
22298
+ ...accessesIn(node._next, defined)
22299
+ ];
22046
22300
  }
22047
22301
  if (node.type === "insert") {
22048
- return oneAccess(firstTable(node.table), {
22049
- kind: "write",
22050
- fields: namesOf(node.columns),
22051
- selector: []
22052
- });
22302
+ return insertAccesses(node, defined);
22053
22303
  }
22054
22304
  if (node.type === "update") {
22055
- return oneAccess(firstTable(node.table), {
22056
- kind: "write",
22057
- fields: refsIn(node.set).map((ref) => ref.field),
22058
- selector: selectorFields(node.where)
22059
- });
22305
+ return updateAccesses(node, defined);
22060
22306
  }
22061
22307
  if (node.type === "delete") {
22062
- return oneAccess(firstTable(node.from) ?? firstTable(node.table), {
22063
- kind: "write",
22064
- fields: [],
22065
- selector: selectorFields(node.where)
22066
- });
22308
+ return deleteAccesses(node, defined);
22067
22309
  }
22068
22310
  return [];
22069
22311
  }
22070
- function oneAccess(table, rest) {
22071
- return table === null ? [] : [{ table, ...rest }];
22312
+ function insertAccesses(node, defined) {
22313
+ const conflict = conflictClauses(node);
22314
+ const stated = {
22315
+ fields: [
22316
+ ...unqualified(namesOf(node.columns)),
22317
+ ...refsIn(node.returning),
22318
+ ...conflict.fields
22319
+ ],
22320
+ selector: conflict.selector
22321
+ };
22322
+ const sources = sourcesIn(node.table, defined);
22323
+ return [
22324
+ ...accessesAcross(sources, firstTable(node.table), stated, defined),
22325
+ ...accessesIn(node.values, defined)
22326
+ ];
22327
+ }
22328
+ function updateAccesses(node, defined) {
22329
+ const sources = sourcesAcross([node.table, node.from], defined);
22330
+ const stated = {
22331
+ fields: [...setTargets(node.set), ...refsIn(node.returning)],
22332
+ selector: refsIn(node.where)
22333
+ };
22334
+ return accessesAcross(sources, firstTable(node.table), stated, defined);
22335
+ }
22336
+ function deleteAccesses(node, defined) {
22337
+ const written = deletedTable(node);
22338
+ const stated = {
22339
+ fields: refsIn(node.returning),
22340
+ selector: refsIn(node.where)
22341
+ };
22342
+ const sources = withTable(sourcesIn(node.from, defined), written);
22343
+ return accessesAcross(sources, written, stated, defined);
22072
22344
  }
22073
- function selectAccesses(statement, outer) {
22345
+ function conflictClauses(node) {
22346
+ const conflict = asNode(node.conflict);
22347
+ const action = asNode(asNode(conflict?.action)?.expr);
22348
+ return {
22349
+ fields: [
22350
+ ...refsIn(conflict?.target),
22351
+ ...setTargets(action?.set),
22352
+ ...setTargets(asNode(node.on_duplicate_update)?.set)
22353
+ ],
22354
+ selector: refsIn(action?.where)
22355
+ };
22356
+ }
22357
+ function commonTables(node, outer) {
22074
22358
  const names = new Set(outer);
22075
22359
  const inside = [];
22076
- for (const entry of Array.isArray(statement.with) ? statement.with : []) {
22360
+ for (const entry of Array.isArray(node.with) ? node.with : []) {
22077
22361
  const cte = asNode(entry);
22078
22362
  if (cte === null) {
22079
22363
  continue;
@@ -22082,52 +22366,69 @@ function selectAccesses(statement, outer) {
22082
22366
  if (name === null) {
22083
22367
  continue;
22084
22368
  }
22085
- inside.push(...accessesIn(cte.stmt, names));
22369
+ inside.push(...accessesIn(statementOf(cte.stmt), names));
22086
22370
  names.add(name);
22087
22371
  }
22088
- const tables = tablesIn(statement.from);
22089
- if (tables.size === 0) {
22090
- return inside;
22372
+ return { names, inside };
22373
+ }
22374
+ function deletedTable(node) {
22375
+ const first = asNode(Array.isArray(node.table) ? node.table[0] : null);
22376
+ const aliased = first === null ? null : stringOf(first.as);
22377
+ if (first !== null && stringOf(first.table) === "FROM" && aliased !== null) {
22378
+ return aliased;
22091
22379
  }
22092
- const named = [...new Set(tables.values())];
22093
- const only = named.length === 1 ? named[0] ?? null : null;
22094
- const fields = /* @__PURE__ */ new Map();
22095
- const selectors = /* @__PURE__ */ new Map();
22096
- const record = (into, alias, field) => {
22097
- const table = alias === void 0 ? only : tables.get(alias) ?? null;
22098
- if (table === null) {
22099
- return;
22100
- }
22101
- const found = into.get(table) ?? /* @__PURE__ */ new Set();
22102
- found.add(field);
22103
- into.set(table, found);
22380
+ return firstTable(node.from) ?? firstTable(node.table);
22381
+ }
22382
+ function selectAccesses(statement, names) {
22383
+ const stated = {
22384
+ fields: refsIn(statement.columns),
22385
+ selector: refsIn(statement.where)
22104
22386
  };
22105
- for (const ref of refsIn(statement.columns)) {
22106
- record(fields, ref.table, ref.field);
22107
- }
22108
- for (const ref of refsIn(statement.where)) {
22109
- record(selectors, ref.table, ref.field);
22110
- }
22111
- const own = named.filter((table) => !names.has(table)).map((table) => ({
22387
+ return accessesAcross(sourcesIn(statement.from, names), null, stated, names);
22388
+ }
22389
+ function accessesAcross(sources, written, stated, defined) {
22390
+ const named = [...new Set(sources.tables.values())];
22391
+ const only = named.length === 1 && !sources.derived ? named[0] ?? null : null;
22392
+ const fields = byTable(stated.fields, sources.tables, written ?? only);
22393
+ const selector = byTable(stated.selector, sources.tables, only);
22394
+ const own = named.filter((table) => !defined.has(table)).map((table) => ({
22112
22395
  table,
22113
- kind: "read",
22114
- fields: [...fields.get(table) ?? []],
22115
- selector: [...selectors.get(table) ?? []]
22396
+ qualifier: [],
22397
+ kind: table === written ? "write" : "read",
22398
+ fields: fields.get(table) ?? [],
22399
+ selector: selector.get(table) ?? []
22116
22400
  }));
22117
- return [...own, ...inside];
22401
+ return [...own, ...sources.inside];
22118
22402
  }
22119
- function tablesIn(from) {
22120
- const tables = /* @__PURE__ */ new Map();
22121
- if (!Array.isArray(from)) {
22122
- return tables;
22403
+ function byTable(refs, tables, only) {
22404
+ const found = /* @__PURE__ */ new Map();
22405
+ for (const ref of refs) {
22406
+ const table = ref.table === void 0 ? only : tables.get(ref.table) ?? null;
22407
+ if (table === null) {
22408
+ continue;
22409
+ }
22410
+ const already = found.get(table) ?? [];
22411
+ if (!already.includes(ref.field)) {
22412
+ already.push(ref.field);
22413
+ }
22414
+ found.set(table, already);
22123
22415
  }
22124
- for (const entry of from) {
22416
+ return found;
22417
+ }
22418
+ function sourcesIn(from, defined) {
22419
+ const tables = /* @__PURE__ */ new Map();
22420
+ const inside = [];
22421
+ let derived = false;
22422
+ for (const entry of Array.isArray(from) ? from : []) {
22125
22423
  const node = asNode(entry);
22126
22424
  if (node === null) {
22127
22425
  continue;
22128
22426
  }
22129
22427
  const table = stringOf(node.table);
22130
22428
  if (table === null) {
22429
+ const query = statementOf(node.expr);
22430
+ derived = derived || query !== null;
22431
+ inside.push(...accessesIn(query, defined));
22131
22432
  continue;
22132
22433
  }
22133
22434
  tables.set(table, table);
@@ -22136,10 +22437,37 @@ function tablesIn(from) {
22136
22437
  tables.set(alias, table);
22137
22438
  }
22138
22439
  }
22139
- return tables;
22440
+ return { tables, inside, derived };
22441
+ }
22442
+ function statementOf(value) {
22443
+ const node = asNode(value);
22444
+ if (node === null) {
22445
+ return null;
22446
+ }
22447
+ return node.ast ?? node;
22448
+ }
22449
+ function sourcesAcross(clauses, defined) {
22450
+ const tables = /* @__PURE__ */ new Map();
22451
+ const inside = [];
22452
+ let derived = false;
22453
+ for (const clause of clauses) {
22454
+ const found = sourcesIn(clause, defined);
22455
+ for (const [name, table] of found.tables) {
22456
+ tables.set(name, table);
22457
+ }
22458
+ inside.push(...found.inside);
22459
+ derived = derived || found.derived;
22460
+ }
22461
+ return { tables, inside, derived };
22462
+ }
22463
+ function withTable(sources, table) {
22464
+ if (table === null || sources.tables.has(table)) {
22465
+ return sources;
22466
+ }
22467
+ return { ...sources, tables: new Map([[table, table], ...sources.tables]) };
22140
22468
  }
22141
22469
  function firstTable(value) {
22142
- return [...tablesIn(value).values()][0] ?? null;
22470
+ return [...sourcesIn(value, /* @__PURE__ */ new Set()).tables.values()][0] ?? null;
22143
22471
  }
22144
22472
  function refsIn(value) {
22145
22473
  const found = [];
@@ -22168,11 +22496,19 @@ function refsIn(value) {
22168
22496
  walk(value);
22169
22497
  return found;
22170
22498
  }
22171
- function selectorFields(where) {
22499
+ function unqualified(names) {
22500
+ return names.map((field) => ({ table: void 0, field }));
22501
+ }
22502
+ function setTargets(value) {
22503
+ if (!Array.isArray(value)) {
22504
+ return [];
22505
+ }
22172
22506
  const found = [];
22173
- for (const ref of refsIn(where)) {
22174
- if (!found.includes(ref.field)) {
22175
- found.push(ref.field);
22507
+ for (const entry of value) {
22508
+ const node = asNode(entry);
22509
+ const field = node === null ? null : columnName(node.column);
22510
+ if (node !== null && field !== null) {
22511
+ found.push({ table: stringOf(node.table) ?? void 0, field });
22176
22512
  }
22177
22513
  }
22178
22514
  return found;
@@ -22207,5 +22543,6 @@ function asNode(value) {
22207
22543
  // Annotate the CommonJS export names for ESM import in node:
22208
22544
  0 && (module.exports = {
22209
22545
  readSqlAccess,
22546
+ splitQualifiedTable,
22210
22547
  sqlFromParts
22211
22548
  });
package/dist/index.d.cts CHANGED
@@ -10,11 +10,20 @@
10
10
  *
11
11
  * A statement this cannot parse gives back nothing, which is what a
12
12
  * reader that cannot settle something says everywhere else. The README
13
- * says which dialects it reads and what it leaves out.
13
+ * says which dialects it reads and what it leaves out. A spelling the
14
+ * grammar turns down is rewritten and tried again rather than given up.
14
15
  */
15
16
  /** One table a statement touches, and what it does to it. */
16
17
  interface SqlAccess {
17
18
  table: string;
19
+ /**
20
+ * The namespaces the statement put in front of the table, outermost
21
+ * first. BigQuery writes `project.dataset.table`, so a reader that
22
+ * kept the whole string would record a container no provider spells.
23
+ * A part the caller could not settle is left out rather than carried
24
+ * through as a parameter.
25
+ */
26
+ qualifier: string[];
18
27
  kind: "read" | "write";
19
28
  /** The fields the statement states, or `["*"]` for a whole row. */
20
29
  fields: string[];
@@ -27,6 +36,19 @@ interface SqlReadOptions {
27
36
  }
28
37
  /** Every table a statement touches, or nothing when it cannot be read. */
29
38
  declare function readSqlAccess(sql: string, options?: SqlReadOptions): SqlAccess[];
39
+ /** A table name, split from the namespaces written in front of it. */
40
+ interface QualifiedTable {
41
+ table: string;
42
+ /** The namespaces in front of the table, outermost first. */
43
+ qualifier: string[];
44
+ }
45
+ /**
46
+ * A table split from the namespaces written in front of it, or nothing
47
+ * when the table itself came through as a parameter. A pack that reads
48
+ * a table off an argument splits it through here too, so it agrees with
49
+ * a table read out of a statement. The README says more.
50
+ */
51
+ declare function splitQualifiedTable(name: string): QualifiedTable | null;
30
52
  /**
31
53
  * The SQL a tagged template states, with each interpolation written as
32
54
  * a parameter. What a query interpolates is a value nearly every time,
@@ -39,6 +61,6 @@ declare function readSqlAccess(sql: string, options?: SqlReadOptions): SqlAccess
39
61
  * settle stays a parameter, and a statement that needed one somewhere a
40
62
  * parameter cannot go reads as nothing.
41
63
  */
42
- declare function sqlFromParts(parts: readonly string[], substitutions?: ReadonlyArray<string | null>): string;
64
+ declare function sqlFromParts(parts: readonly string[], substitutions?: ReadonlyArray<string | null>, settled?: ReadonlyArray<string | null>): string;
43
65
 
44
- export { type SqlAccess, type SqlReadOptions, readSqlAccess, sqlFromParts };
66
+ export { type QualifiedTable, type SqlAccess, type SqlReadOptions, readSqlAccess, splitQualifiedTable, sqlFromParts };
package/dist/index.d.ts CHANGED
@@ -10,11 +10,20 @@
10
10
  *
11
11
  * A statement this cannot parse gives back nothing, which is what a
12
12
  * reader that cannot settle something says everywhere else. The README
13
- * says which dialects it reads and what it leaves out.
13
+ * says which dialects it reads and what it leaves out. A spelling the
14
+ * grammar turns down is rewritten and tried again rather than given up.
14
15
  */
15
16
  /** One table a statement touches, and what it does to it. */
16
17
  interface SqlAccess {
17
18
  table: string;
19
+ /**
20
+ * The namespaces the statement put in front of the table, outermost
21
+ * first. BigQuery writes `project.dataset.table`, so a reader that
22
+ * kept the whole string would record a container no provider spells.
23
+ * A part the caller could not settle is left out rather than carried
24
+ * through as a parameter.
25
+ */
26
+ qualifier: string[];
18
27
  kind: "read" | "write";
19
28
  /** The fields the statement states, or `["*"]` for a whole row. */
20
29
  fields: string[];
@@ -27,6 +36,19 @@ interface SqlReadOptions {
27
36
  }
28
37
  /** Every table a statement touches, or nothing when it cannot be read. */
29
38
  declare function readSqlAccess(sql: string, options?: SqlReadOptions): SqlAccess[];
39
+ /** A table name, split from the namespaces written in front of it. */
40
+ interface QualifiedTable {
41
+ table: string;
42
+ /** The namespaces in front of the table, outermost first. */
43
+ qualifier: string[];
44
+ }
45
+ /**
46
+ * A table split from the namespaces written in front of it, or nothing
47
+ * when the table itself came through as a parameter. A pack that reads
48
+ * a table off an argument splits it through here too, so it agrees with
49
+ * a table read out of a statement. The README says more.
50
+ */
51
+ declare function splitQualifiedTable(name: string): QualifiedTable | null;
30
52
  /**
31
53
  * The SQL a tagged template states, with each interpolation written as
32
54
  * a parameter. What a query interpolates is a value nearly every time,
@@ -39,6 +61,6 @@ declare function readSqlAccess(sql: string, options?: SqlReadOptions): SqlAccess
39
61
  * settle stays a parameter, and a statement that needed one somewhere a
40
62
  * parameter cannot go reads as nothing.
41
63
  */
42
- declare function sqlFromParts(parts: readonly string[], substitutions?: ReadonlyArray<string | null>): string;
64
+ declare function sqlFromParts(parts: readonly string[], substitutions?: ReadonlyArray<string | null>, settled?: ReadonlyArray<string | null>): string;
43
65
 
44
- export { type SqlAccess, type SqlReadOptions, readSqlAccess, sqlFromParts };
66
+ export { type QualifiedTable, type SqlAccess, type SqlReadOptions, readSqlAccess, splitQualifiedTable, sqlFromParts };
package/dist/index.js CHANGED
@@ -22000,68 +22000,351 @@ var DIALECTS = {
22000
22000
  bigquery: { module: import_bigquery.default, database: "bigquery" }
22001
22001
  };
22002
22002
  function readSqlAccess(sql, options = {}) {
22003
- const dialect = DIALECTS[options.dialect ?? "postgresql"];
22004
- if (dialect === void 0) {
22003
+ const grammar = DIALECTS[options.dialect ?? "postgresql"];
22004
+ if (grammar === void 0) {
22005
22005
  return [];
22006
22006
  }
22007
+ return accessesInSql(sql, grammar).map(qualified).filter((access) => access !== null);
22008
+ }
22009
+ function accessesInSql(sql, grammar) {
22010
+ const written = parsedAccesses(sql, grammar);
22011
+ if (written !== null) {
22012
+ return written;
22013
+ }
22014
+ const readable = rewrittenForGrammar(sql);
22015
+ const rewritten = readable === sql ? null : parsedAccesses(readable, grammar);
22016
+ return rewritten ?? accessesAroundWith(readable, grammar);
22017
+ }
22018
+ function parsedAccesses(sql, grammar, defined = /* @__PURE__ */ new Set()) {
22007
22019
  let parsed;
22008
22020
  try {
22009
- parsed = new dialect.module.Parser().astify(sql, {
22010
- database: dialect.database
22021
+ parsed = new grammar.module.Parser().astify(sql, {
22022
+ database: grammar.database
22011
22023
  });
22012
22024
  } catch {
22013
- return [];
22025
+ return null;
22014
22026
  }
22015
22027
  const statements = Array.isArray(parsed) ? parsed : [parsed];
22016
- return statements.flatMap((statement) => accessesIn(statement, /* @__PURE__ */ new Set()));
22028
+ return statements.flatMap((statement) => accessesIn(statement, defined));
22017
22029
  }
22018
- function sqlFromParts(parts, substitutions = []) {
22019
- return parts.map((part, index) => {
22020
- if (index === 0) {
22021
- return part;
22030
+ var QUALIFIER_SEPARATOR = ".";
22031
+ function splitQualifiedTable(name) {
22032
+ const parts = name.split(QUALIFIER_SEPARATOR);
22033
+ const table = parts[parts.length - 1] ?? name;
22034
+ if (!isSettled(table)) {
22035
+ return null;
22036
+ }
22037
+ return { table, qualifier: namespacesAround(parts.slice(0, -1)) };
22038
+ }
22039
+ function namespacesAround(namespaces) {
22040
+ const found = [];
22041
+ for (let index = namespaces.length - 1; index >= 0; index -= 1) {
22042
+ const part = namespaces[index];
22043
+ if (part === void 0 || !isSettled(part)) {
22044
+ break;
22022
22045
  }
22023
- const settled = substitutions[index - 1];
22024
- return `${settled ?? `$${index}`}${part}`;
22046
+ found.unshift(part);
22047
+ }
22048
+ return found;
22049
+ }
22050
+ function qualified(access) {
22051
+ const split = splitQualifiedTable(access.table);
22052
+ return split === null ? null : { ...access, ...split };
22053
+ }
22054
+ function isSettled(part) {
22055
+ return part !== "" && !/^\$\d+$/.test(part);
22056
+ }
22057
+ function sqlFromParts(parts, substitutions = [], settled = []) {
22058
+ let closing = null;
22059
+ return parts.map((part, index) => {
22060
+ const names = namePosition(closing, parts[index - 1] ?? "");
22061
+ const inHole = index === 0 ? null : substitutions[index - 1] ?? (names ? settled[index - 1] ?? null : null);
22062
+ closing = quoteAfter(closing, part);
22063
+ return index === 0 ? part : `${inHole ?? `$${index}`}${part}`;
22025
22064
  }).join("");
22026
22065
  }
22066
+ var TABLE_KEYWORD = /\b(?:from|join|into|update|table)\s+$/i;
22067
+ function namePosition(closing, before) {
22068
+ return closing !== null || TABLE_KEYWORD.test(before);
22069
+ }
22070
+ var NAME_QUOTES = { '"': '"', "`": "`", "[": "]" };
22071
+ function quoteAfter(open, part) {
22072
+ let closing = open;
22073
+ for (const character of part) {
22074
+ if (closing === null) {
22075
+ closing = NAME_QUOTES[character] ?? null;
22076
+ continue;
22077
+ }
22078
+ if (character === closing) {
22079
+ closing = null;
22080
+ }
22081
+ }
22082
+ return closing;
22083
+ }
22084
+ var TEXT_RUNS = [
22085
+ { open: "--", close: "\n", comment: true },
22086
+ { open: "/*", close: "*/", comment: true },
22087
+ { open: "'", close: "'", comment: false },
22088
+ { open: '"', close: '"', comment: false },
22089
+ { open: "`", close: "`", comment: false }
22090
+ ];
22091
+ var DOLLAR_TAG = /\$(?:[A-Za-z_]\w*)?\$/y;
22092
+ function textRunAt(sql, index) {
22093
+ DOLLAR_TAG.lastIndex = index;
22094
+ const tag = DOLLAR_TAG.exec(sql)?.[0];
22095
+ if (tag !== void 0) {
22096
+ return { end: closingAt(sql, index + tag.length, tag), comment: false };
22097
+ }
22098
+ for (const run of TEXT_RUNS) {
22099
+ if (sql.startsWith(run.open, index)) {
22100
+ const from = index + run.open.length;
22101
+ return { end: closingAt(sql, from, run.close), comment: run.comment };
22102
+ }
22103
+ }
22104
+ return null;
22105
+ }
22106
+ function closingAt(sql, from, close) {
22107
+ const at = sql.indexOf(close, from);
22108
+ return at === -1 ? sql.length : at + close.length;
22109
+ }
22110
+ function codeMask(sql) {
22111
+ const mask = new Array(sql.length).fill(true);
22112
+ let index = 0;
22113
+ while (index < sql.length) {
22114
+ const run = textRunAt(sql, index);
22115
+ if (run === null) {
22116
+ index += 1;
22117
+ continue;
22118
+ }
22119
+ for (let at = index; at < run.end; at += 1) {
22120
+ mask[at] = false;
22121
+ }
22122
+ index = run.end;
22123
+ }
22124
+ return mask;
22125
+ }
22126
+ function replaceInCode(sql, pattern, rewrite) {
22127
+ const mask = codeMask(sql);
22128
+ let rewritten = "";
22129
+ let from = 0;
22130
+ for (const match of sql.matchAll(pattern)) {
22131
+ const at = match.index;
22132
+ if (at === void 0 || mask[at] === false) {
22133
+ continue;
22134
+ }
22135
+ rewritten += sql.slice(from, at) + rewrite(match);
22136
+ from = at + match[0].length;
22137
+ }
22138
+ return rewritten + sql.slice(from);
22139
+ }
22140
+ var TYPE_WORD = String.raw`(?:"[^"]*"|\w+)`;
22141
+ var TYPE_SIZE = String.raw`(?:\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\))?`;
22142
+ var TYPE_TAIL = String.raw`(?:\s+(?:precision|varying|with(?:out)?\s+time\s+zone))?`;
22143
+ var CAST_TYPE = `${TYPE_WORD}(?:\\s*\\.\\s*${TYPE_WORD})?${TYPE_SIZE}${TYPE_TAIL}${TYPE_SIZE}(?:\\s*\\[\\s*\\])*`;
22144
+ var PARAMETER_CAST = new RegExp(
22145
+ String.raw`(\$\d+)(?:\s*::\s*${CAST_TYPE})+`,
22146
+ "gi"
22147
+ );
22148
+ var LOCKING_CLAUSE = new RegExp(
22149
+ String.raw`\bfor\s+(?:no\s+key\s+update|key\s+share|update|share)` + String.raw`(?:\s+of\s+${TYPE_WORD}(?:\s*,\s*${TYPE_WORD})*)?` + String.raw`(?:\s+(?:nowait|skip\s+locked))?`,
22150
+ "gi"
22151
+ );
22152
+ function rewrittenForGrammar(sql) {
22153
+ const uncast = replaceInCode(sql, PARAMETER_CAST, castAwayFrom);
22154
+ return replaceInCode(uncast, LOCKING_CLAUSE, nothing);
22155
+ }
22156
+ function castAwayFrom(match) {
22157
+ return match[1] ?? "";
22158
+ }
22159
+ function nothing() {
22160
+ return "";
22161
+ }
22162
+ function accessesAroundWith(sql, grammar) {
22163
+ const split = splitWith(sql);
22164
+ if (split === null) {
22165
+ return [];
22166
+ }
22167
+ const names = /* @__PURE__ */ new Set();
22168
+ const inside = [];
22169
+ for (const table of split.tables) {
22170
+ inside.push(...parsedAccesses(table.body, grammar, names) ?? []);
22171
+ names.add(table.name);
22172
+ }
22173
+ const own = parsedAccesses(split.main, grammar, names) ?? [];
22174
+ return [...own, ...inside];
22175
+ }
22176
+ function splitWith(sql) {
22177
+ const mask = codeMask(sql);
22178
+ let index = nextToken(sql, 0);
22179
+ if (!wordAt(sql, index, "with")) {
22180
+ return null;
22181
+ }
22182
+ index = pastWord(sql, index, "with");
22183
+ if (wordAt(sql, index, "recursive")) {
22184
+ index = pastWord(sql, index, "recursive");
22185
+ }
22186
+ const tables = [];
22187
+ for (; ; ) {
22188
+ const named = nameAt(sql, index);
22189
+ if (named === null) {
22190
+ return null;
22191
+ }
22192
+ index = afterColumnList(sql, mask, nextToken(sql, named.end));
22193
+ if (index === -1 || !wordAt(sql, index, "as")) {
22194
+ return null;
22195
+ }
22196
+ index = pastMaterialized(sql, pastWord(sql, index, "as"));
22197
+ const end = afterGroup(sql, mask, index);
22198
+ if (end === null) {
22199
+ return null;
22200
+ }
22201
+ tables.push({ name: named.name, body: sql.slice(index + 1, end - 1) });
22202
+ index = nextToken(sql, end);
22203
+ if (sql[index] !== ",") {
22204
+ return { tables, main: sql.slice(index) };
22205
+ }
22206
+ index = nextToken(sql, index + 1);
22207
+ }
22208
+ }
22209
+ function afterColumnList(sql, mask, index) {
22210
+ if (sql[index] !== "(") {
22211
+ return index;
22212
+ }
22213
+ const end = afterGroup(sql, mask, index);
22214
+ return end === null ? -1 : nextToken(sql, end);
22215
+ }
22216
+ function pastMaterialized(sql, index) {
22217
+ const past = wordAt(sql, index, "not") ? pastWord(sql, index, "not") : index;
22218
+ return wordAt(sql, past, "materialized") ? pastWord(sql, past, "materialized") : past;
22219
+ }
22220
+ function nextToken(sql, from) {
22221
+ let index = from;
22222
+ while (index < sql.length) {
22223
+ if (/\s/.test(sql[index] ?? "")) {
22224
+ index += 1;
22225
+ continue;
22226
+ }
22227
+ const run = textRunAt(sql, index);
22228
+ if (run === null || !run.comment) {
22229
+ return index;
22230
+ }
22231
+ index = run.end;
22232
+ }
22233
+ return index;
22234
+ }
22235
+ function wordAt(sql, index, word) {
22236
+ const written = sql.slice(index, index + word.length).toLowerCase();
22237
+ return written === word && !/\w/.test(sql[index + word.length] ?? "");
22238
+ }
22239
+ function pastWord(sql, index, word) {
22240
+ return nextToken(sql, index + word.length);
22241
+ }
22242
+ function nameAt(sql, index) {
22243
+ if (sql[index] === '"') {
22244
+ const close = sql.indexOf('"', index + 1);
22245
+ return close === -1 ? null : { name: sql.slice(index + 1, close), end: close + 1 };
22246
+ }
22247
+ const word = /^\w+/.exec(sql.slice(index))?.[0];
22248
+ return word === void 0 ? null : { name: word, end: index + word.length };
22249
+ }
22250
+ function afterGroup(sql, mask, index) {
22251
+ if (sql[index] !== "(" || mask[index] === false) {
22252
+ return null;
22253
+ }
22254
+ let depth = 0;
22255
+ for (let at = index; at < sql.length; at += 1) {
22256
+ if (mask[at] === false) {
22257
+ continue;
22258
+ }
22259
+ if (sql[at] === "(") {
22260
+ depth += 1;
22261
+ continue;
22262
+ }
22263
+ if (sql[at] !== ")") {
22264
+ continue;
22265
+ }
22266
+ depth -= 1;
22267
+ if (depth === 0) {
22268
+ return at + 1;
22269
+ }
22270
+ }
22271
+ return null;
22272
+ }
22027
22273
  function accessesIn(statement, defined) {
22028
22274
  const node = asNode(statement);
22029
22275
  if (node === null) {
22030
22276
  return [];
22031
22277
  }
22278
+ const clause = commonTables(node, defined);
22279
+ return [...ownAccesses(node, clause.names), ...clause.inside];
22280
+ }
22281
+ function ownAccesses(node, defined) {
22032
22282
  if (node.type === "select") {
22033
- return selectAccesses(node, defined);
22283
+ return [
22284
+ ...selectAccesses(node, defined),
22285
+ ...accessesIn(node._next, defined)
22286
+ ];
22034
22287
  }
22035
22288
  if (node.type === "insert") {
22036
- return oneAccess(firstTable(node.table), {
22037
- kind: "write",
22038
- fields: namesOf(node.columns),
22039
- selector: []
22040
- });
22289
+ return insertAccesses(node, defined);
22041
22290
  }
22042
22291
  if (node.type === "update") {
22043
- return oneAccess(firstTable(node.table), {
22044
- kind: "write",
22045
- fields: refsIn(node.set).map((ref) => ref.field),
22046
- selector: selectorFields(node.where)
22047
- });
22292
+ return updateAccesses(node, defined);
22048
22293
  }
22049
22294
  if (node.type === "delete") {
22050
- return oneAccess(firstTable(node.from) ?? firstTable(node.table), {
22051
- kind: "write",
22052
- fields: [],
22053
- selector: selectorFields(node.where)
22054
- });
22295
+ return deleteAccesses(node, defined);
22055
22296
  }
22056
22297
  return [];
22057
22298
  }
22058
- function oneAccess(table, rest) {
22059
- return table === null ? [] : [{ table, ...rest }];
22299
+ function insertAccesses(node, defined) {
22300
+ const conflict = conflictClauses(node);
22301
+ const stated = {
22302
+ fields: [
22303
+ ...unqualified(namesOf(node.columns)),
22304
+ ...refsIn(node.returning),
22305
+ ...conflict.fields
22306
+ ],
22307
+ selector: conflict.selector
22308
+ };
22309
+ const sources = sourcesIn(node.table, defined);
22310
+ return [
22311
+ ...accessesAcross(sources, firstTable(node.table), stated, defined),
22312
+ ...accessesIn(node.values, defined)
22313
+ ];
22314
+ }
22315
+ function updateAccesses(node, defined) {
22316
+ const sources = sourcesAcross([node.table, node.from], defined);
22317
+ const stated = {
22318
+ fields: [...setTargets(node.set), ...refsIn(node.returning)],
22319
+ selector: refsIn(node.where)
22320
+ };
22321
+ return accessesAcross(sources, firstTable(node.table), stated, defined);
22322
+ }
22323
+ function deleteAccesses(node, defined) {
22324
+ const written = deletedTable(node);
22325
+ const stated = {
22326
+ fields: refsIn(node.returning),
22327
+ selector: refsIn(node.where)
22328
+ };
22329
+ const sources = withTable(sourcesIn(node.from, defined), written);
22330
+ return accessesAcross(sources, written, stated, defined);
22060
22331
  }
22061
- function selectAccesses(statement, outer) {
22332
+ function conflictClauses(node) {
22333
+ const conflict = asNode(node.conflict);
22334
+ const action = asNode(asNode(conflict?.action)?.expr);
22335
+ return {
22336
+ fields: [
22337
+ ...refsIn(conflict?.target),
22338
+ ...setTargets(action?.set),
22339
+ ...setTargets(asNode(node.on_duplicate_update)?.set)
22340
+ ],
22341
+ selector: refsIn(action?.where)
22342
+ };
22343
+ }
22344
+ function commonTables(node, outer) {
22062
22345
  const names = new Set(outer);
22063
22346
  const inside = [];
22064
- for (const entry of Array.isArray(statement.with) ? statement.with : []) {
22347
+ for (const entry of Array.isArray(node.with) ? node.with : []) {
22065
22348
  const cte = asNode(entry);
22066
22349
  if (cte === null) {
22067
22350
  continue;
@@ -22070,52 +22353,69 @@ function selectAccesses(statement, outer) {
22070
22353
  if (name === null) {
22071
22354
  continue;
22072
22355
  }
22073
- inside.push(...accessesIn(cte.stmt, names));
22356
+ inside.push(...accessesIn(statementOf(cte.stmt), names));
22074
22357
  names.add(name);
22075
22358
  }
22076
- const tables = tablesIn(statement.from);
22077
- if (tables.size === 0) {
22078
- return inside;
22359
+ return { names, inside };
22360
+ }
22361
+ function deletedTable(node) {
22362
+ const first = asNode(Array.isArray(node.table) ? node.table[0] : null);
22363
+ const aliased = first === null ? null : stringOf(first.as);
22364
+ if (first !== null && stringOf(first.table) === "FROM" && aliased !== null) {
22365
+ return aliased;
22079
22366
  }
22080
- const named = [...new Set(tables.values())];
22081
- const only = named.length === 1 ? named[0] ?? null : null;
22082
- const fields = /* @__PURE__ */ new Map();
22083
- const selectors = /* @__PURE__ */ new Map();
22084
- const record = (into, alias, field) => {
22085
- const table = alias === void 0 ? only : tables.get(alias) ?? null;
22086
- if (table === null) {
22087
- return;
22088
- }
22089
- const found = into.get(table) ?? /* @__PURE__ */ new Set();
22090
- found.add(field);
22091
- into.set(table, found);
22367
+ return firstTable(node.from) ?? firstTable(node.table);
22368
+ }
22369
+ function selectAccesses(statement, names) {
22370
+ const stated = {
22371
+ fields: refsIn(statement.columns),
22372
+ selector: refsIn(statement.where)
22092
22373
  };
22093
- for (const ref of refsIn(statement.columns)) {
22094
- record(fields, ref.table, ref.field);
22095
- }
22096
- for (const ref of refsIn(statement.where)) {
22097
- record(selectors, ref.table, ref.field);
22098
- }
22099
- const own = named.filter((table) => !names.has(table)).map((table) => ({
22374
+ return accessesAcross(sourcesIn(statement.from, names), null, stated, names);
22375
+ }
22376
+ function accessesAcross(sources, written, stated, defined) {
22377
+ const named = [...new Set(sources.tables.values())];
22378
+ const only = named.length === 1 && !sources.derived ? named[0] ?? null : null;
22379
+ const fields = byTable(stated.fields, sources.tables, written ?? only);
22380
+ const selector = byTable(stated.selector, sources.tables, only);
22381
+ const own = named.filter((table) => !defined.has(table)).map((table) => ({
22100
22382
  table,
22101
- kind: "read",
22102
- fields: [...fields.get(table) ?? []],
22103
- selector: [...selectors.get(table) ?? []]
22383
+ qualifier: [],
22384
+ kind: table === written ? "write" : "read",
22385
+ fields: fields.get(table) ?? [],
22386
+ selector: selector.get(table) ?? []
22104
22387
  }));
22105
- return [...own, ...inside];
22388
+ return [...own, ...sources.inside];
22106
22389
  }
22107
- function tablesIn(from) {
22108
- const tables = /* @__PURE__ */ new Map();
22109
- if (!Array.isArray(from)) {
22110
- return tables;
22390
+ function byTable(refs, tables, only) {
22391
+ const found = /* @__PURE__ */ new Map();
22392
+ for (const ref of refs) {
22393
+ const table = ref.table === void 0 ? only : tables.get(ref.table) ?? null;
22394
+ if (table === null) {
22395
+ continue;
22396
+ }
22397
+ const already = found.get(table) ?? [];
22398
+ if (!already.includes(ref.field)) {
22399
+ already.push(ref.field);
22400
+ }
22401
+ found.set(table, already);
22111
22402
  }
22112
- for (const entry of from) {
22403
+ return found;
22404
+ }
22405
+ function sourcesIn(from, defined) {
22406
+ const tables = /* @__PURE__ */ new Map();
22407
+ const inside = [];
22408
+ let derived = false;
22409
+ for (const entry of Array.isArray(from) ? from : []) {
22113
22410
  const node = asNode(entry);
22114
22411
  if (node === null) {
22115
22412
  continue;
22116
22413
  }
22117
22414
  const table = stringOf(node.table);
22118
22415
  if (table === null) {
22416
+ const query = statementOf(node.expr);
22417
+ derived = derived || query !== null;
22418
+ inside.push(...accessesIn(query, defined));
22119
22419
  continue;
22120
22420
  }
22121
22421
  tables.set(table, table);
@@ -22124,10 +22424,37 @@ function tablesIn(from) {
22124
22424
  tables.set(alias, table);
22125
22425
  }
22126
22426
  }
22127
- return tables;
22427
+ return { tables, inside, derived };
22428
+ }
22429
+ function statementOf(value) {
22430
+ const node = asNode(value);
22431
+ if (node === null) {
22432
+ return null;
22433
+ }
22434
+ return node.ast ?? node;
22435
+ }
22436
+ function sourcesAcross(clauses, defined) {
22437
+ const tables = /* @__PURE__ */ new Map();
22438
+ const inside = [];
22439
+ let derived = false;
22440
+ for (const clause of clauses) {
22441
+ const found = sourcesIn(clause, defined);
22442
+ for (const [name, table] of found.tables) {
22443
+ tables.set(name, table);
22444
+ }
22445
+ inside.push(...found.inside);
22446
+ derived = derived || found.derived;
22447
+ }
22448
+ return { tables, inside, derived };
22449
+ }
22450
+ function withTable(sources, table) {
22451
+ if (table === null || sources.tables.has(table)) {
22452
+ return sources;
22453
+ }
22454
+ return { ...sources, tables: new Map([[table, table], ...sources.tables]) };
22128
22455
  }
22129
22456
  function firstTable(value) {
22130
- return [...tablesIn(value).values()][0] ?? null;
22457
+ return [...sourcesIn(value, /* @__PURE__ */ new Set()).tables.values()][0] ?? null;
22131
22458
  }
22132
22459
  function refsIn(value) {
22133
22460
  const found = [];
@@ -22156,11 +22483,19 @@ function refsIn(value) {
22156
22483
  walk(value);
22157
22484
  return found;
22158
22485
  }
22159
- function selectorFields(where) {
22486
+ function unqualified(names) {
22487
+ return names.map((field) => ({ table: void 0, field }));
22488
+ }
22489
+ function setTargets(value) {
22490
+ if (!Array.isArray(value)) {
22491
+ return [];
22492
+ }
22160
22493
  const found = [];
22161
- for (const ref of refsIn(where)) {
22162
- if (!found.includes(ref.field)) {
22163
- found.push(ref.field);
22494
+ for (const entry of value) {
22495
+ const node = asNode(entry);
22496
+ const field = node === null ? null : columnName(node.column);
22497
+ if (node !== null && field !== null) {
22498
+ found.push({ table: stringOf(node.table) ?? void 0, field });
22164
22499
  }
22165
22500
  }
22166
22501
  return found;
@@ -22194,5 +22529,6 @@ function asNode(value) {
22194
22529
  }
22195
22530
  export {
22196
22531
  readSqlAccess,
22532
+ splitQualifiedTable,
22197
22533
  sqlFromParts
22198
22534
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/sql",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "description": "Read which tables and fields a SQL statement touches, so suss can check the query against the schema it runs on.",
6
6
  "keywords": [