turbine-orm 0.65.0 → 0.66.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.
Files changed (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -19,12 +19,20 @@
19
19
  * Row removal hides in more than a leading `DELETE`, so the scan also covers:
20
20
  * the optional-`COLUMN` shorthand (`ALTER TABLE t DROP email`), data-modifying
21
21
  * CTEs (`WITH d AS (DELETE ...) SELECT ...`), `MERGE ... THEN DELETE`, dynamic
22
- * SQL inside a `DO`/function body, and an `UPDATE` whose only WHERE sits inside
23
- * a subquery (which restricts nothing).
22
+ * SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
23
+ * subquery (which restricts nothing), a `DROP ... CASCADE` of a non-table
24
+ * object (which takes dependent COLUMNS with it), a detached partition, and an
25
+ * `EXPLAIN ANALYZE` of any of the above (which really executes it).
26
+ *
27
+ * The lexing, "where does a statement end" and "which characters are code",
28
+ * is NOT done here: it lives in `sql-statements.ts` and is shared with the
29
+ * migration runner. Two lexers is how this guard came to disagree with the
30
+ * executor about what a file contained (see that module's header).
24
31
  */
25
32
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.DESTRUCTIVE_KIND_LABEL = void 0;
33
+ exports.DYNAMIC_TARGET = exports.DESTRUCTIVE_KIND_LABEL = void 0;
27
34
  exports.scanDestructiveSql = scanDestructiveSql;
35
+ const sql_statements_js_1 = require("./sql-statements.js");
28
36
  /** Human explanation per kind, used in CLI output. */
29
37
  exports.DESTRUCTIVE_KIND_LABEL = {
30
38
  'drop-table': 'drops a table and ALL its rows',
@@ -33,106 +41,36 @@ exports.DESTRUCTIVE_KIND_LABEL = {
33
41
  'drop-owned': 'drops every object owned by a role, and their rows',
34
42
  'drop-matview': 'drops a materialized view and its stored rows',
35
43
  'drop-column': 'drops a column and its data in every row',
44
+ 'drop-cascade': 'drops an object AND every dependent object, columns and their data included',
45
+ 'detach-partition': 'detaches a partition, every row in it leaves the table',
36
46
  truncate: 'deletes every row',
37
47
  delete: 'deletes rows',
38
48
  'update-without-where': 'rewrites every row (no WHERE clause)',
39
49
  'alter-column-type': 'rewrites a column type (cast may truncate or fail)',
40
50
  'merge-delete': 'deletes matched rows (MERGE ... THEN DELETE)',
51
+ rename: 'renames a table or column, every query and view referencing the old name breaks',
52
+ 'dynamic-destructive': 'runs destructive SQL assembled at run time, what it destroys cannot be known without running it',
41
53
  };
54
+ /** Unquote a "quoted" identifier for display. */
55
+ const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
56
+ const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
42
57
  /**
43
- * A dollar-quote tag. Postgres allows digits after the first character
44
- * (`$do1$`), so a tag regex that stops at letters reads the body as code and
45
- * misses everything inside it. Same shape as the splitter in `migrate.ts`.
58
+ * Render one `IDENT` capture triple (name, `.qualifier`, qualifier) as text,
59
+ * where `base` is the 1-based index of the triple's first group.
46
60
  */
47
- const DOLLAR_TAG = /^\$([A-Za-z_][A-Za-z_0-9]*)?\$/;
48
- /** Strip -- line comments, C-style block comments, and quoted literals. */
49
- function stripCommentsAndStrings(sql) {
50
- const blocks = [];
51
- let out = '';
52
- let i = 0;
53
- while (i < sql.length) {
54
- const two = sql.slice(i, i + 2);
55
- if (two === '--') {
56
- const nl = sql.indexOf('\n', i);
57
- i = nl === -1 ? sql.length : nl; // keep the newline
58
- }
59
- else if (two === '/*') {
60
- const end = sql.indexOf('*/', i + 2);
61
- i = end === -1 ? sql.length : end + 2;
62
- out += ' ';
63
- }
64
- else if (sql[i] === "'") {
65
- // Single-quoted literal. `''` always escapes a quote; inside an E-string
66
- // (`E'...'`) a backslash escapes the next character too, so `E'a\'b'` is
67
- // ONE literal. Without the E-string case the scan ends the literal at the
68
- // backslash-quote and treats the rest of the file as code, which
69
- // (worse) then hides every following statement from the guard.
70
- const escapes = isEscapeStringPrefix(sql, i);
71
- let j = i + 1;
72
- while (j < sql.length) {
73
- if (escapes && sql[j] === '\\')
74
- j += 2;
75
- else if (sql[j] === "'" && sql[j + 1] === "'")
76
- j += 2;
77
- else if (sql[j] === "'")
78
- break;
79
- else
80
- j++;
81
- }
82
- i = j + 1;
83
- out += "''";
84
- }
85
- else if (sql[i] === '"') {
86
- // Quoted identifier. Kept VERBATIM (rules match on identifiers), but it
87
- // has to be consumed as one token: an apostrophe inside a quoted name
88
- // (`"customer's_orders"`) would otherwise open a string literal and hide
89
- // every statement after it from the scan.
90
- let j = i + 1;
91
- while (j < sql.length) {
92
- if (sql[j] === '"' && sql[j + 1] === '"')
93
- j += 2;
94
- else if (sql[j] === '"')
95
- break;
96
- else
97
- j++;
98
- }
99
- out += sql.slice(i, Math.min(j + 1, sql.length));
100
- i = j + 1;
101
- }
102
- else if (sql[i] === '$' && DOLLAR_TAG.test(sql.slice(i))) {
103
- // dollar-quoted literal ($$...$$ / $tag$...$tag$)
104
- const tag = sql.slice(i).match(DOLLAR_TAG)?.[0] ?? '$$';
105
- const end = sql.indexOf(tag, i + tag.length);
106
- blocks.push({ at: out.length, body: sql.slice(i + tag.length, end === -1 ? sql.length : end) });
107
- i = end === -1 ? sql.length : end + tag.length;
108
- out += "''";
109
- }
110
- else {
111
- out += sql[i];
112
- i++;
113
- }
114
- }
115
- return { text: out, blocks };
116
- }
61
+ const qualified = (m, base) => m[base + 2] ? `${ident(m[base])}.${ident(m[base + 2])}` : ident(m[base]);
117
62
  /**
118
- * True when the quote at `quoteAt` opens an E-string (`E'...'`), where a
119
- * backslash escapes the next character. The preceding `E` must not itself be
120
- * part of an identifier, so `some_table'` never turns the following literal
121
- * into an E-string. Ordinary literals are left alone on purpose: with the
122
- * modern `standard_conforming_strings = on` default, `'a\'` IS a complete
123
- * string. Mirrors the same-named helper in `migrate.ts`; kept local so this
124
- * module stays a pure leaf with no CLI imports of its own.
63
+ * Filler between an `ALTER TABLE <name>` head and the sub-action keyword that
64
+ * follows it, for the multi-action forms.
65
+ *
66
+ * NOT `[\s\S]*?`: that lets the lazy walk stop INSIDE a quoted identifier,
67
+ * because a quoted name is kept verbatim in the stripped text. `ALTER TABLE t
68
+ * ADD COLUMN "drop me" int` was flagged `drop-column` on `t.me`, a pure false
69
+ * positive on a statement that adds data and removes none. Consuming either a
70
+ * WHOLE quoted identifier or a single non-quote character means the scan can
71
+ * never begin a keyword match part-way through a quoted name.
125
72
  */
126
- function isEscapeStringPrefix(sql, quoteAt) {
127
- const prev = sql[quoteAt - 1];
128
- if (prev !== 'E' && prev !== 'e')
129
- return false;
130
- const before = sql[quoteAt - 2];
131
- return before === undefined || !/[A-Za-z0-9_$"]/.test(before);
132
- }
133
- /** Unquote a "quoted" identifier for display. */
134
- const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
135
- const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
73
+ const OUTSIDE_QUOTES = '(?:"[^"]*"|[^"])*?';
136
74
  /** Ordered rules, first match per statement wins. */
137
75
  const RULES = [
138
76
  {
@@ -161,6 +99,17 @@ const RULES = [
161
99
  regex: new RegExp(String.raw `^DROP\s+OWNED\s+BY\s+${IDENT}`, 'i'),
162
100
  target: (m) => ident(m[1]),
163
101
  },
102
+ {
103
+ // `DROP <object> ... CASCADE` on anything OTHER than a table. Without
104
+ // CASCADE these are safe: Postgres refuses the drop while a dependency
105
+ // exists. With it, the dependents go too, and a dependent of a TYPE or a
106
+ // DOMAIN is typically a COLUMN, so `DROP TYPE order_status CASCADE`
107
+ // silently removes `orders.status` and every value in it. The presence of
108
+ // CASCADE is therefore the whole rule, not an extra detail.
109
+ kind: 'drop-cascade',
110
+ regex: new RegExp(String.raw `^DROP\s+(TYPE|DOMAIN|EXTENSION|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|AGGREGATE)\s+(IF\s+EXISTS\s+)?${IDENT}${OUTSIDE_QUOTES}\bCASCADE\b`, 'i'),
111
+ target: (m) => `${m[1].toUpperCase()} ${qualified(m, 3)}`,
112
+ },
164
113
  {
165
114
  kind: 'truncate',
166
115
  regex: new RegExp(String.raw `^TRUNCATE\s+(TABLE\s+)?(ONLY\s+)?${IDENT}`, 'i'),
@@ -171,12 +120,19 @@ const RULES = [
171
120
  // column and its data exactly like the spelled-out form. The lookahead
172
121
  // excludes the other `DROP <thing>` sub-actions, none of which lose rows.
173
122
  kind: 'drop-column',
174
- regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bDROP\s+(?!CONSTRAINT\b|DEFAULT\b|NOT\b|IDENTITY\b|EXPRESSION\b)(COLUMN\s+)?(IF\s+EXISTS\s+)?${IDENT}`, 'i'),
123
+ regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}${OUTSIDE_QUOTES}\bDROP\s+(?!CONSTRAINT\b|DEFAULT\b|NOT\b|IDENTITY\b|EXPRESSION\b)(COLUMN\s+)?(IF\s+EXISTS\s+)?${IDENT}`, 'i'),
175
124
  target: (m) => `${ident(m[3])}.${ident(m[8])}`,
176
125
  },
126
+ {
127
+ // A detached partition keeps its rows, but they leave the parent table:
128
+ // every query against the parent stops seeing them the moment this runs.
129
+ kind: 'detach-partition',
130
+ regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}\s+DETACH\s+PARTITION\s+${IDENT}`, 'i'),
131
+ target: (m) => `${qualified(m, 3)}.${qualified(m, 6)}`,
132
+ },
177
133
  {
178
134
  kind: 'alter-column-type',
179
- regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bALTER\s+(COLUMN\s+)?${IDENT}\s+(SET\s+DATA\s+)?TYPE\b`, 'i'),
135
+ regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}${OUTSIDE_QUOTES}\bALTER\s+(COLUMN\s+)?${IDENT}\s+(SET\s+DATA\s+)?TYPE\b`, 'i'),
180
136
  target: (m) => `${ident(m[3])}.${ident(m[7])}`,
181
137
  },
182
138
  {
@@ -187,7 +143,7 @@ const RULES = [
187
143
  {
188
144
  // MERGE's DELETE action removes rows from the target table.
189
145
  kind: 'merge-delete',
190
- regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}\b[\s\S]*?\bTHEN\s+DELETE\b`, 'i'),
146
+ regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}\b${OUTSIDE_QUOTES}\bTHEN\s+DELETE\b`, 'i'),
191
147
  target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
192
148
  },
193
149
  {
@@ -198,6 +154,25 @@ const RULES = [
198
154
  target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
199
155
  also: (stmt) => !hasTopLevelWhere(stmt),
200
156
  },
157
+ // Renames come LAST: they destroy no data, so any statement that is BOTH a
158
+ // rename and a data-loss operation should report the data loss instead.
159
+ {
160
+ // `ALTER TABLE t RENAME TO u`. Nothing is lost, but every query, view,
161
+ // function, and application reference to `t` breaks the moment it lands,
162
+ // and it is the second half of the sanctioned backfill swap, so an operator
163
+ // confirming a migration deserves to see it in the inventory.
164
+ kind: 'rename',
165
+ regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}\s+RENAME\s+TO\s+${IDENT}`, 'i'),
166
+ target: (m) => qualified(m, 3),
167
+ },
168
+ {
169
+ // `ALTER TABLE t RENAME [COLUMN] old TO new`. The lookahead keeps the
170
+ // table-rename form (handled above) and `RENAME CONSTRAINT` (which breaks
171
+ // nothing a query can name) out of this rule.
172
+ kind: 'rename',
173
+ regex: new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}\s+RENAME\s+(COLUMN\s+)?(?!TO\b|CONSTRAINT\b)${IDENT}\s+TO\s+${IDENT}`, 'i'),
174
+ target: (m) => `${qualified(m, 3)}.${qualified(m, 7)}`,
175
+ },
201
176
  ];
202
177
  /** True when the statement has a `WHERE` outside every parenthesized group. */
203
178
  function hasTopLevelWhere(stmt) {
@@ -284,25 +259,79 @@ function closingParenIndex(stmt, openAt) {
284
259
  }
285
260
  return stmt.length;
286
261
  }
262
+ /**
263
+ * An `EXPLAIN` prefix, either the parenthesized option list or the bare keyword
264
+ * form. Used to answer one question: does this EXPLAIN actually RUN the
265
+ * statement it wraps?
266
+ */
267
+ const EXPLAIN_PREFIX = /^EXPLAIN\s*(?:\(([^)]*)\)|((?:(?:ANALYZE|ANALYSE|VERBOSE)\s+)*))/i;
268
+ /**
269
+ * `EXPLAIN ANALYZE <DML>` EXECUTES the statement, it does not merely plan it.
270
+ * `EXPLAIN ANALYZE DELETE FROM t` empties `t`, and the guard used to see a
271
+ * statement whose head was `EXPLAIN` and match no rule at all. Returned is the
272
+ * inner statement when the EXPLAIN executes, otherwise the statement unchanged
273
+ * (a plain `EXPLAIN DELETE ...` only plans, and must NOT be flagged).
274
+ *
275
+ * `ANALYZE false` / `ANALYZE off` is treated as executing even though it does
276
+ * not: this module's asymmetry is that a false positive costs a confirmation
277
+ * prompt while a false negative costs data, and spelling out boolean option
278
+ * values here would trade the cheap error for the expensive one.
279
+ */
280
+ function stripExecutingExplain(stmt) {
281
+ const m = EXPLAIN_PREFIX.exec(stmt);
282
+ if (!m)
283
+ return stmt;
284
+ const options = m[1] ?? m[2] ?? '';
285
+ if (!/\bANALY[SZ]E\b/i.test(options))
286
+ return stmt;
287
+ return stmt.slice(m[0].length).trimStart();
288
+ }
287
289
  /** Statements whose dollar-quoted body is executable SQL rather than data. */
288
290
  const PROCEDURAL_STATEMENT = /^(DO\b|CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|PROCEDURE)\b)/i;
289
291
  /**
290
- * Candidate fragments inside a procedural body (a `DO $$ ... $$` block or a
291
- * function source). The body's own string literals are NOT stripped here: the
292
- * whole point is dynamic SQL, whose payload lives in a literal
293
- * (`EXECUTE 'DROP TABLE users'`). Rules are anchored, so every keyword-leading
294
- * position in the body is offered as its own candidate. This deliberately
295
- * over-reports (a body that merely mentions "drop table" in a message string is
296
- * flagged) in keeping with the module's false-positives-only asymmetry.
292
+ * Candidate fragments inside a procedural body (a `DO $$ ... $$` block, a
293
+ * function source, or a PG14+ `BEGIN ATOMIC` body). The body's own string
294
+ * literals are NOT stripped: the whole point is dynamic SQL, whose payload
295
+ * lives in a literal (`EXECUTE 'DROP TABLE users'`). Rules are anchored, so
296
+ * every keyword-leading position in the body is offered as its own candidate.
297
+ * This deliberately over-reports (a body that merely mentions "drop table" in a
298
+ * message string is flagged) in keeping with the module's
299
+ * false-positives-only asymmetry.
300
+ *
301
+ * Comments come out via the SHARED tokenizer, never a regex. The pair that used
302
+ * to do it here, `/\/\*[\s\S]*?\*\//g` and `/--[^\n]*\/g`, was the exact
303
+ * hand-written lexer the tokenizer was written to delete, still in place one
304
+ * level down and on the path that exists specifically to catch dynamic SQL.
305
+ * Neither pattern nests and neither respects string literals, so ONE earlier
306
+ * literal containing `--` or an unclosed `/*` blanked every destructive
307
+ * statement after it. Both of these reported an empty inventory and dropped the
308
+ * table on PostgreSQL 16.14:
309
+ *
310
+ * DO $$ DECLARE s text := 'x --'; BEGIN EXECUTE 'DROP TABLE users'; END $$;
311
+ * DO $$ DECLARE s text := 'a /*'; BEGIN EXECUTE 'DROP TABLE users';
312
+ * RAISE NOTICE '% b *\/', s; END $$;
313
+ *
314
+ * The `--` shape is the worse of the two because it is reachable by ACCIDENT:
315
+ * any single-line body whose earlier literal holds a `--` (a date range, a
316
+ * separator, a placeholder) hides everything that follows it.
317
+ *
318
+ * Tokenizing also bounds each candidate at its own statement instead of at the
319
+ * end of the body, so a later `WHERE` can no longer talk the `update-without-
320
+ * where` rule out of an earlier unrestricted UPDATE.
297
321
  */
298
322
  function proceduralCandidates(body) {
299
- const withoutComments = body.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/--[^\n]*/g, ' ');
300
323
  const out = [];
301
- const re = /\b(?:DROP|TRUNCATE|DELETE|ALTER|UPDATE|MERGE)\s/gi;
302
- let m = re.exec(withoutComments);
303
- while (m !== null) {
304
- out.push(withoutComments.slice(m.index));
305
- m = re.exec(withoutComments);
324
+ for (const statement of (0, sql_statements_js_1.tokenizeSql)(body)) {
325
+ // `code` is the statement with comments gone and everything else verbatim.
326
+ // Literals have to survive: they are where dynamic SQL keeps its payload,
327
+ // and `stripped` would have emptied exactly them.
328
+ const text = statement.code;
329
+ const re = /\b(?:DROP|TRUNCATE|DELETE|ALTER|UPDATE|MERGE)\s/gi;
330
+ let m = re.exec(text);
331
+ while (m !== null) {
332
+ out.push(text.slice(m.index));
333
+ m = re.exec(text);
334
+ }
306
335
  }
307
336
  return out;
308
337
  }
@@ -312,43 +341,122 @@ function proceduralCandidates(body) {
312
341
  */
313
342
  function scanDestructiveSql(sql) {
314
343
  const found = [];
315
- const cleaned = stripCommentsAndStrings(sql);
316
- let offset = 0;
317
- for (const rawStmt of cleaned.text.split(';')) {
318
- const start = offset;
319
- const end = offset + rawStmt.length;
320
- offset = end + 1; // the ';' consumed by split
321
- const stmt = rawStmt.trim();
344
+ for (const statement of (0, sql_statements_js_1.tokenizeSql)(sql)) {
345
+ if (statement.commentOnly)
346
+ continue;
347
+ const stmt = statement.stripped;
322
348
  if (!stmt)
323
349
  continue;
350
+ // `EXPLAIN ANALYZE` is a wrapper that RUNS its argument, so the rules are
351
+ // matched against what it wraps. `display` keeps the EXPLAIN, so the
352
+ // inventory shows the operator the statement they actually wrote.
353
+ const body = stripExecutingExplain(stmt);
324
354
  // Top level, then data-modifying CTEs, then any procedural body this
325
355
  // statement blanked. First match per statement wins, as before.
326
356
  const display = stmt.replace(/\s+/g, ' ');
327
357
  const candidates = [
328
- stripLeadingCtes(stmt),
329
- ...cteSubstatements(stmt),
358
+ stripLeadingCtes(body),
359
+ ...cteSubstatements(body),
330
360
  ].map((text) => ({
331
361
  text,
332
362
  display,
333
363
  }));
334
364
  // Only a DO block / routine body is procedural SQL. A dollar-quoted literal
335
365
  // used as DATA (`INSERT ... VALUES ($$DELETE FROM x$$)`) stays a literal.
366
+ // The tokenizer hands back each statement's OWN blocks, so which body
367
+ // belongs to which statement is no longer an offset calculation that can
368
+ // disagree with the statement split.
336
369
  const procedural = PROCEDURAL_STATEMENT.test(stmt);
337
- for (const block of procedural ? cleaned.blocks : []) {
338
- if (block.at < start || block.at >= end)
339
- continue;
340
- for (const text of proceduralCandidates(block.body)) {
370
+ const proceduralTexts = [];
371
+ for (const block of procedural ? statement.blocks : []) {
372
+ for (const text of proceduralCandidates(block)) {
341
373
  // The body was blanked in `display`, so name the fragment that matched.
342
374
  candidates.push({ text, display: `${display} [in block: ${text.replace(/\s+/g, ' ').slice(0, 60)}]` });
375
+ proceduralTexts.push(text);
343
376
  }
344
377
  }
378
+ let matched = false;
345
379
  for (const candidate of candidates) {
346
380
  const hit = matchRules(candidate.text);
347
381
  if (!hit)
348
382
  continue;
349
383
  found.push({ statement: candidate.display, kind: hit.kind, target: hit.target });
384
+ matched = true;
385
+ break;
386
+ }
387
+ if (matched)
388
+ continue;
389
+ // Nothing matched a rule. Inside a PROCEDURAL body that is not the end of
390
+ // the question, because the rules all need a parseable object name and
391
+ // dynamic SQL does not have one until it runs. All three of these execute a
392
+ // real `DROP TABLE` on PostgreSQL 16 and reported NOTHING:
393
+ //
394
+ // DO $$ BEGIN EXECUTE format('DROP TABLE %I', 'users'); END $$;
395
+ // DO $$ BEGIN EXECUTE 'DROP ' || 'TABLE users'; END $$;
396
+ // DO $$ BEGIN EXECUTE 'DROP TABLE ' || quote_ident('users'); END $$;
397
+ //
398
+ // A clean inventory for a file that drops a table is the exact failure this
399
+ // module exists to prevent: the operator confirms what they were shown, and
400
+ // the unlisted statement runs under that confirmation. So report it with an
401
+ // explicit unknown target and let them decide.
402
+ //
403
+ // Gated on evidence of RUNTIME ASSEMBLY rather than on the verb alone, and
404
+ // that gate is complete rather than heuristic: a destructive statement whose
405
+ // object name is written out literally already matches a rule above, so the
406
+ // only way to be dynamic is to concatenate or format. Without the gate,
407
+ // `RAISE NOTICE 'DROP the mic'` would prompt, and a guard that fires on
408
+ // prose teaches operators to confirm without reading, which costs more than
409
+ // it saves.
410
+ for (const text of proceduralTexts) {
411
+ const kind = dynamicDestructiveKind(text);
412
+ if (!kind)
413
+ continue;
414
+ found.push({
415
+ statement: `${display} [in block: ${text.replace(/\s+/g, ' ').slice(0, 60)}]`,
416
+ kind,
417
+ target: exports.DYNAMIC_TARGET,
418
+ });
350
419
  break;
351
420
  }
352
421
  }
353
422
  return found;
354
423
  }
424
+ /** Shown in place of an object name that does not exist until the block runs. */
425
+ exports.DYNAMIC_TARGET = '<name assembled at run time>';
426
+ /** `||`, `format(...)`, or a `quote_*` helper: the ways a body builds SQL. */
427
+ const DYNAMIC_ASSEMBLY = /\|\||\bformat\s*\(|\bquote_(?:ident|literal|nullable)\s*\(|%[IsL]/;
428
+ /**
429
+ * The kind a runtime-assembled procedural fragment should be reported as, or
430
+ * `null` when it is not dynamic (so a rule already had its chance) or its verb
431
+ * is not one that destroys data on its own.
432
+ *
433
+ * `ALTER` and `UPDATE` are deliberately absent even though
434
+ * {@link proceduralCandidates} collects them: their destructive forms are
435
+ * narrow (`ALTER COLUMN ... TYPE`, an `UPDATE` with no `WHERE`) and neither is
436
+ * decidable from a fragment whose tail is a runtime expression, so including
437
+ * them would flag every dynamic `UPDATE ... WHERE` in the file.
438
+ */
439
+ function dynamicDestructiveKind(text) {
440
+ if (!DYNAMIC_ASSEMBLY.test(text))
441
+ return null;
442
+ if (/^DROP\s+TABLE\b/i.test(text))
443
+ return 'drop-table';
444
+ if (/^DROP\s+SCHEMA\b/i.test(text))
445
+ return 'drop-schema';
446
+ if (/^DROP\s+DATABASE\b/i.test(text))
447
+ return 'drop-database';
448
+ if (/^DROP\s+MATERIALIZED\s+VIEW\b/i.test(text))
449
+ return 'drop-matview';
450
+ // A bare `DROP` whose object keyword is itself part of the runtime expression
451
+ // (`'DROP ' || 'TABLE users'`). Deliberately NOT reported as `drop-cascade`:
452
+ // that label claims dependent objects go too, which would be a factual claim
453
+ // about the operator's migration that we cannot support. Being alarming is
454
+ // fine here; being wrong is not.
455
+ if (/^DROP\b/i.test(text))
456
+ return 'dynamic-destructive';
457
+ if (/^TRUNCATE\b/i.test(text))
458
+ return 'truncate';
459
+ if (/^DELETE\b/i.test(text))
460
+ return 'delete';
461
+ return null;
462
+ }
@@ -393,10 +393,16 @@ export declare function initEnvNotice(input: {
393
393
  * database (unless `--no-db`), and emit (a) a Markdown resolution report and
394
394
  * (b) a typed `prisma-map.ts` name map next to the generated client.
395
395
  *
396
- * NOTE: within THIS command `--schema` names the Prisma schema FILE (not the
397
- * Postgres namespace, which the rest of the CLI's `--schema` means). The
398
- * Postgres namespace is `public` here; multi-schema (`@@schema`) is unsupported
399
- * in v1 and listed as a parser note in the report.
396
+ * NOTE: within THIS command `--schema` names the Prisma schema FILE, or the
397
+ * DIRECTORY of a multi-file schema (Prisma >= 5.15), not the Postgres namespace
398
+ * that the rest of the CLI's `--schema` means. The Postgres namespace is
399
+ * `public` here.
400
+ *
401
+ * Multi-schema (`@@schema`) is unsupported: the parser emits a warning naming
402
+ * the attribute, which lands in the report's "Parser notes" section, and two
403
+ * models that resolve to the same bare table name are both marked UNRESOLVED
404
+ * rather than silently collapsed (see flagDuplicateTables in prisma-resolve.ts).
405
+ * This JSDoc used to claim the note existed before either half was written.
400
406
  */
401
407
  /** Outcome of {@link resolveMigrateFromPrismaUrl}. */
402
408
  export interface MigrateFromPrismaUrl {
@@ -420,6 +426,17 @@ export interface MigrateFromPrismaUrl {
420
426
  * pass on every run.
421
427
  */
422
428
  export declare function resolveMigrateFromPrismaUrl(configUrl: string | undefined, ast: Pick<PrismaSchemaAst, 'datasources'>, env: Record<string, string | undefined>): MigrateFromPrismaUrl;
429
+ /**
430
+ * Read a Prisma schema from a FILE or from a multi-file schema DIRECTORY
431
+ * (Prisma >= 5.15, GA). A directory used to reach `readFileSync` unchanged and
432
+ * die with a raw `EISDIR`, which is a stack trace rather than an answer.
433
+ *
434
+ * Prisma treats every `.prisma` file under the directory as one logical schema,
435
+ * so they are concatenated in sorted order (stable across machines) and parsed
436
+ * as a single source. Line numbers in any parse error then refer to that
437
+ * concatenation, so each file is introduced by a comment naming it.
438
+ */
439
+ export declare function readPrismaSchemaSource(path: string): string;
423
440
  export declare function buildMigrateDeployOptions(args: CliArgs): {
424
441
  allowDrift: boolean;
425
442
  allowDestructive: true;