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.
- package/README.md +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- 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,
|
|
23
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
291
|
-
* function source). The body's own string
|
|
292
|
-
* whole point is dynamic SQL, whose payload
|
|
293
|
-
* (`EXECUTE 'DROP TABLE users'`). Rules are anchored, so
|
|
294
|
-
* position in the body is offered as its own candidate.
|
|
295
|
-
* over-reports (a body that merely mentions "drop table" in a
|
|
296
|
-
* flagged) in keeping with the module's
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
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(
|
|
329
|
-
...cteSubstatements(
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
}
|
package/dist/cjs/cli/index.d.ts
CHANGED
|
@@ -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
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
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;
|