turbine-orm 0.81.0-next.d9b06dd → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/cjs/cli/config.d.ts
CHANGED
|
@@ -24,12 +24,22 @@ export interface TurbineCliConfig {
|
|
|
24
24
|
* to hand-edit every call site. A typo here is an error, not a silent no-op.
|
|
25
25
|
*/
|
|
26
26
|
relationNames?: Record<string, Record<string, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* TypeScript type overrides for generated row and input types, as
|
|
29
|
+
* `{ '<table>.<column>': '<type>' }` with database names. The main use is a
|
|
30
|
+
* `json` / `jsonb` column, which otherwise generates as `unknown`:
|
|
31
|
+
* `{ 'users.settings': '{ theme: string; beta?: boolean }' }`. The type text
|
|
32
|
+
* is emitted verbatim. A key naming no table or column is an error, not a
|
|
33
|
+
* silent no-op. A code-first column's `tsType` feeds the same map.
|
|
34
|
+
*/
|
|
35
|
+
columnTypes?: Record<string, string>;
|
|
27
36
|
/**
|
|
28
37
|
* Extension for the generated `index.ts` sibling imports (F3):
|
|
29
|
-
* `'js'` (`./types.js`), `'
|
|
30
|
-
*
|
|
38
|
+
* `'js'` (`./types.js`), `'ts'` (`./types.ts`, for Node's native type
|
|
39
|
+
* stripping), `'none'` (`./types`), or `'auto'` (default: tsconfig-detected,
|
|
40
|
+
* falling back to `'js'`; never picks `'ts'`).
|
|
31
41
|
*/
|
|
32
|
-
importExtension?: 'js' | 'none' | 'auto';
|
|
42
|
+
importExtension?: 'js' | 'ts' | 'none' | 'auto';
|
|
33
43
|
/**
|
|
34
44
|
* Keep raw database column names as generated field names (snake_case)
|
|
35
45
|
* instead of camelCase (F4). Opt-in; default `false`.
|
|
@@ -54,7 +64,11 @@ export interface TurbineCliConfig {
|
|
|
54
64
|
* are set.
|
|
55
65
|
*/
|
|
56
66
|
seed?: string;
|
|
57
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The `defineSchema()` file (default `./turbine/schema.ts`): what `push` applies,
|
|
69
|
+
* and where `generate` reads the `pii` / `updatedAt` / `maxLength` tags the
|
|
70
|
+
* database cannot store.
|
|
71
|
+
*/
|
|
58
72
|
schemaFile?: string;
|
|
59
73
|
/**
|
|
60
74
|
* Database adapter for PostgreSQL-compatible databases that need
|
|
@@ -159,14 +173,22 @@ export interface ResolvedConfig {
|
|
|
159
173
|
migrationsDir: string;
|
|
160
174
|
seedFile?: string;
|
|
161
175
|
schemaFile: string;
|
|
176
|
+
/**
|
|
177
|
+
* True when `--schema-file` or the config `schemaFile` key named the file, as
|
|
178
|
+
* opposed to the `./turbine/schema.ts` default. A named file that is missing is
|
|
179
|
+
* an error; a missing default just means a database-first project.
|
|
180
|
+
*/
|
|
181
|
+
schemaFileConfigured?: boolean;
|
|
162
182
|
/** Resolved generator import-extension mode (F3). */
|
|
163
|
-
importExtension: 'js' | 'none' | 'auto';
|
|
183
|
+
importExtension: 'js' | 'ts' | 'none' | 'auto';
|
|
164
184
|
/** Resolved keep-column-names generator flag (F4). */
|
|
165
185
|
keepColumnNames: boolean;
|
|
166
186
|
/** Resolved opt-out of the unique-FK → hasOne introspection flip (F2). */
|
|
167
187
|
legacyToManyUniques: boolean;
|
|
168
188
|
/** Resolved derived-relation rename map, or undefined when none is declared. */
|
|
169
189
|
relationNames?: Record<string, Record<string, string>>;
|
|
190
|
+
/** Column type overrides for the generator, or undefined when none is declared. */
|
|
191
|
+
columnTypes?: Record<string, string>;
|
|
170
192
|
}
|
|
171
193
|
export interface CliOverrides {
|
|
172
194
|
url?: string;
|
|
@@ -176,10 +198,17 @@ export interface CliOverrides {
|
|
|
176
198
|
schemaFile?: string;
|
|
177
199
|
include?: string[];
|
|
178
200
|
exclude?: string[];
|
|
179
|
-
importExtension?: 'js' | 'none' | 'auto';
|
|
201
|
+
importExtension?: 'js' | 'ts' | 'none' | 'auto';
|
|
180
202
|
keepColumnNames?: boolean;
|
|
181
203
|
legacyToManyUniques?: boolean;
|
|
182
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The first candidate that is a non-blank string. An empty or whitespace-only
|
|
207
|
+
* value counts as unset, because an exported-but-empty variable (`DATABASE_URL=`
|
|
208
|
+
* in a shell profile or a `.env` template) is how "not configured" is usually
|
|
209
|
+
* spelled, and `??` would let it beat a real value further down the chain.
|
|
210
|
+
*/
|
|
211
|
+
export declare function firstNonEmpty(...candidates: Array<string | undefined>): string | undefined;
|
|
183
212
|
/**
|
|
184
213
|
* Merge config file values with CLI overrides and env vars.
|
|
185
214
|
* Priority: CLI flags > env vars > config file > defaults.
|
package/dist/cjs/cli/config.js
CHANGED
|
@@ -46,6 +46,7 @@ exports.unwrapConfigModule = unwrapConfigModule;
|
|
|
46
46
|
exports.loadConfigResult = loadConfigResult;
|
|
47
47
|
exports.loadConfig = loadConfig;
|
|
48
48
|
exports.findConfigFile = findConfigFile;
|
|
49
|
+
exports.firstNonEmpty = firstNonEmpty;
|
|
49
50
|
exports.resolveConfig = resolveConfig;
|
|
50
51
|
exports.resolveSeedFile = resolveSeedFile;
|
|
51
52
|
exports.connectionStringHasPassword = connectionStringHasPassword;
|
|
@@ -195,23 +196,38 @@ function findConfigFile(cwd) {
|
|
|
195
196
|
}
|
|
196
197
|
return null;
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The first candidate that is a non-blank string. An empty or whitespace-only
|
|
201
|
+
* value counts as unset, because an exported-but-empty variable (`DATABASE_URL=`
|
|
202
|
+
* in a shell profile or a `.env` template) is how "not configured" is usually
|
|
203
|
+
* spelled, and `??` would let it beat a real value further down the chain.
|
|
204
|
+
*/
|
|
205
|
+
function firstNonEmpty(...candidates) {
|
|
206
|
+
for (const value of candidates) {
|
|
207
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
198
212
|
/**
|
|
199
213
|
* Merge config file values with CLI overrides and env vars.
|
|
200
214
|
* Priority: CLI flags > env vars > config file > defaults.
|
|
201
215
|
*/
|
|
202
216
|
function resolveConfig(fileConfig, overrides) {
|
|
203
217
|
return {
|
|
204
|
-
url: overrides.url
|
|
218
|
+
url: firstNonEmpty(overrides.url, process.env.DATABASE_URL, fileConfig.url) ?? '',
|
|
205
219
|
out: overrides.out ?? fileConfig.out ?? './generated/turbine',
|
|
206
220
|
schema: overrides.schema ?? fileConfig.schema ?? 'public',
|
|
207
221
|
include: overrides.include ?? fileConfig.include ?? [],
|
|
208
222
|
relationNames: fileConfig.relationNames,
|
|
223
|
+
columnTypes: fileConfig.columnTypes,
|
|
209
224
|
exclude: overrides.exclude ?? fileConfig.exclude ?? [],
|
|
210
225
|
migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
|
|
211
226
|
// `seedFile` is canonical (what the docs and `turbine init` use); `seed` is a
|
|
212
227
|
// back-compat alias kept working for configs scaffolded before 0.50.
|
|
213
228
|
seedFile: fileConfig.seedFile ?? fileConfig.seed,
|
|
214
229
|
schemaFile: overrides.schemaFile ?? fileConfig.schemaFile ?? './turbine/schema.ts',
|
|
230
|
+
schemaFileConfigured: (overrides.schemaFile ?? fileConfig.schemaFile) !== undefined,
|
|
215
231
|
importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
|
|
216
232
|
keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
|
|
217
233
|
legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
|
|
@@ -19,16 +19,19 @@
|
|
|
19
19
|
* the optional-`COLUMN` shorthand (`ALTER TABLE t DROP email`), data-modifying
|
|
20
20
|
* CTEs (`WITH d AS (DELETE ...) SELECT ...`), `MERGE ... THEN DELETE`, dynamic
|
|
21
21
|
* SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
|
|
22
|
-
* subquery (which restricts nothing)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* subquery (which restricts nothing) or is a constant-true predicate (`WHERE
|
|
23
|
+
* true`, `WHERE 1 = 1`), a `DROP ... CASCADE` of a non-table object (which takes
|
|
24
|
+
* dependent COLUMNS with it), a detached partition, an `ON CONFLICT DO UPDATE`
|
|
25
|
+
* with no `WHERE` (it rewrites every conflicting row), a `PREPARE`d destructive
|
|
26
|
+
* statement, `lo_unlink` (it deletes large objects), and an `EXPLAIN ANALYZE` of
|
|
27
|
+
* any of the above (which really executes it).
|
|
25
28
|
*
|
|
26
29
|
* The lexing, "where does a statement end" and "which characters are code",
|
|
27
30
|
* is NOT done here: it lives in `sql-statements.ts` and is shared with the
|
|
28
31
|
* migration runner. Two lexers is how this guard came to disagree with the
|
|
29
32
|
* executor about what a file contained (see that module's header).
|
|
30
33
|
*/
|
|
31
|
-
export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'drop-cascade' | 'detach-partition' | 'truncate' | 'delete' | 'update-without-where' | 'alter-column-type' | 'merge-delete' | 'rename' | 'rewrite-rule' | 'dynamic-destructive' | 'dynamic-unclassified';
|
|
34
|
+
export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'drop-cascade' | 'detach-partition' | 'truncate' | 'delete' | 'update-without-where' | 'overwrite-on-conflict' | 'large-object-unlink' | 'alter-column-type' | 'merge-delete' | 'rename' | 'rewrite-rule' | 'dynamic-destructive' | 'dynamic-unclassified';
|
|
32
35
|
export interface DestructiveStatement {
|
|
33
36
|
/** The offending SQL statement (trimmed, possibly long, display truncated) */
|
|
34
37
|
statement: string;
|
|
@@ -20,9 +20,12 @@
|
|
|
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
22
|
* SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
|
|
23
|
-
* subquery (which restricts nothing)
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* subquery (which restricts nothing) or is a constant-true predicate (`WHERE
|
|
24
|
+
* true`, `WHERE 1 = 1`), a `DROP ... CASCADE` of a non-table object (which takes
|
|
25
|
+
* dependent COLUMNS with it), a detached partition, an `ON CONFLICT DO UPDATE`
|
|
26
|
+
* with no `WHERE` (it rewrites every conflicting row), a `PREPARE`d destructive
|
|
27
|
+
* statement, `lo_unlink` (it deletes large objects), and an `EXPLAIN ANALYZE` of
|
|
28
|
+
* any of the above (which really executes it).
|
|
26
29
|
*
|
|
27
30
|
* The lexing, "where does a statement end" and "which characters are code",
|
|
28
31
|
* is NOT done here: it lives in `sql-statements.ts` and is shared with the
|
|
@@ -45,7 +48,9 @@ exports.DESTRUCTIVE_KIND_LABEL = {
|
|
|
45
48
|
'detach-partition': 'detaches a partition, every row in it leaves the table',
|
|
46
49
|
truncate: 'deletes every row',
|
|
47
50
|
delete: 'deletes rows',
|
|
48
|
-
'update-without-where': 'rewrites every row (no WHERE clause)',
|
|
51
|
+
'update-without-where': 'rewrites every row (no WHERE clause, or one that is always true)',
|
|
52
|
+
'overwrite-on-conflict': 'overwrites every conflicting row (ON CONFLICT DO UPDATE with no restricting WHERE)',
|
|
53
|
+
'large-object-unlink': 'permanently deletes large objects (lo_unlink)',
|
|
49
54
|
'alter-column-type': 'rewrites a column type (cast may truncate or fail)',
|
|
50
55
|
'merge-delete': 'deletes matched rows (MERGE ... THEN DELETE)',
|
|
51
56
|
rename: 'renames a table or column, every query and view referencing the old name breaks',
|
|
@@ -56,6 +61,12 @@ exports.DESTRUCTIVE_KIND_LABEL = {
|
|
|
56
61
|
/** Unquote a "quoted" identifier for display. */
|
|
57
62
|
const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
|
|
58
63
|
const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
|
|
64
|
+
/**
|
|
65
|
+
* Ends an `IDENT` match. Not `\b`: after a closing `"` the next character is
|
|
66
|
+
* not a word character either, so `\b` fails and `UPDATE "users" SET ...` was
|
|
67
|
+
* never matched.
|
|
68
|
+
*/
|
|
69
|
+
const NOT_IDENT_CHAR = String.raw `(?![\w$])`;
|
|
59
70
|
/**
|
|
60
71
|
* Render one `IDENT` capture triple (name, `.qualifier`, qualifier) as text,
|
|
61
72
|
* where `base` is the 1-based index of the triple's first group.
|
|
@@ -106,10 +117,11 @@ const RULES = [
|
|
|
106
117
|
// CASCADE these are safe: Postgres refuses the drop while a dependency
|
|
107
118
|
// exists. With it, the dependents go too, and a dependent of a TYPE or a
|
|
108
119
|
// DOMAIN is typically a COLUMN, so `DROP TYPE order_status CASCADE`
|
|
109
|
-
// silently removes `orders.status` and every value in it
|
|
110
|
-
//
|
|
120
|
+
// silently removes `orders.status` and every value in it, and a dependent of
|
|
121
|
+
// a VIEW can be a materialized view, whose stored rows go with it. The
|
|
122
|
+
// presence of CASCADE is therefore the whole rule, not an extra detail.
|
|
111
123
|
kind: 'drop-cascade',
|
|
112
|
-
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'),
|
|
124
|
+
regex: new RegExp(String.raw `^DROP\s+(TYPE|DOMAIN|EXTENSION|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|AGGREGATE|VIEW)\s+(IF\s+EXISTS\s+)?${IDENT}${OUTSIDE_QUOTES}\bCASCADE\b`, 'i'),
|
|
113
125
|
target: (m) => `${m[1].toUpperCase()} ${qualified(m, 3)}`,
|
|
114
126
|
},
|
|
115
127
|
{
|
|
@@ -145,16 +157,43 @@ const RULES = [
|
|
|
145
157
|
{
|
|
146
158
|
// MERGE's DELETE action removes rows from the target table.
|
|
147
159
|
kind: 'merge-delete',
|
|
148
|
-
regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}
|
|
160
|
+
regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}${NOT_IDENT_CHAR}${OUTSIDE_QUOTES}\bTHEN\s+DELETE\b`, 'i'),
|
|
149
161
|
target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
|
|
150
162
|
},
|
|
151
163
|
{
|
|
152
164
|
// A WHERE inside a scalar subquery (`SET x = (SELECT ... WHERE ...)`) does
|
|
153
|
-
// NOT restrict the rows updated, so the guard tests only the TOP level
|
|
165
|
+
// NOT restrict the rows updated, so the guard tests only the TOP level, and
|
|
166
|
+
// a top-level WHERE that is always true restricts nothing either.
|
|
154
167
|
kind: 'update-without-where',
|
|
155
|
-
regex: new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}
|
|
168
|
+
regex: new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}${NOT_IDENT_CHAR}`, 'i'),
|
|
156
169
|
target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
|
|
157
|
-
also: (stmt) => !
|
|
170
|
+
also: (stmt) => !hasRestrictingWhere(stmt),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
// `INSERT ... ON CONFLICT ... DO UPDATE SET ...` rewrites every row the
|
|
174
|
+
// insert conflicts with. The WHERE that narrows it sits AFTER `DO UPDATE`;
|
|
175
|
+
// the one before it is the conflict target's index predicate and restricts
|
|
176
|
+
// nothing. Add `WHERE t.col IS DISTINCT FROM EXCLUDED.col` to say which rows
|
|
177
|
+
// may change.
|
|
178
|
+
kind: 'overwrite-on-conflict',
|
|
179
|
+
regex: new RegExp(String.raw `^INSERT\s+INTO\s+${IDENT}${NOT_IDENT_CHAR}`, 'i'),
|
|
180
|
+
target: (m) => (m[3] ? `${ident(m[1])}.${ident(m[3])}` : ident(m[1])),
|
|
181
|
+
also: (stmt) => {
|
|
182
|
+
const code = stmt.replace(/"(?:[^"]|"")*"/g, '""');
|
|
183
|
+
if (!/\bON\s+CONFLICT\b/i.test(code))
|
|
184
|
+
return false;
|
|
185
|
+
const at = code.search(/\bDO\s+UPDATE\b/i);
|
|
186
|
+
return at !== -1 && !hasRestrictingWhere(code.slice(at));
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
// `SELECT lo_unlink(oid) FROM pg_largeobject_metadata` deletes the large
|
|
191
|
+
// objects whatever they hold, and there is no WHERE to look for. Statements
|
|
192
|
+
// that only NAME the function (GRANT, COMMENT, ALTER, DROP) call nothing.
|
|
193
|
+
kind: 'large-object-unlink',
|
|
194
|
+
regex: /\blo_unlink\s*\(/i,
|
|
195
|
+
target: () => 'large objects',
|
|
196
|
+
also: (stmt) => !/^(?:GRANT|REVOKE|COMMENT|ALTER|DROP)\b/i.test(stmt),
|
|
158
197
|
},
|
|
159
198
|
{
|
|
160
199
|
// `CREATE RULE r AS ON INSERT TO t DO INSTEAD DELETE FROM u`. The rule
|
|
@@ -173,7 +212,7 @@ const RULES = [
|
|
|
173
212
|
// that appears in ordinary migrations. DELETE, TRUNCATE and DROP stay
|
|
174
213
|
// unconditional: a DELETE inside a rule destroys on every matching
|
|
175
214
|
// statement whether or not it is narrowed, and the other two take no WHERE.
|
|
176
|
-
also: (stmt) => !/\bDO\s+(?:ALSO\s+|INSTEAD\s+)?\(?\s*UPDATE\b/i.test(stmt) || !
|
|
215
|
+
also: (stmt) => !/\bDO\s+(?:ALSO\s+|INSTEAD\s+)?\(?\s*UPDATE\b/i.test(stmt) || !hasRestrictingWhere(stmt),
|
|
177
216
|
},
|
|
178
217
|
// Renames come LAST: they destroy no data, so any statement that is BOTH a
|
|
179
218
|
// rename and a data-loss operation should report the data loss instead.
|
|
@@ -195,21 +234,165 @@ const RULES = [
|
|
|
195
234
|
target: (m) => `${qualified(m, 3)}.${qualified(m, 7)}`,
|
|
196
235
|
},
|
|
197
236
|
];
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The predicate of the statement's top-level `WHERE` (outside every
|
|
239
|
+
* parenthesized group), up to a top-level `RETURNING` or the end, or `null` when
|
|
240
|
+
* there is none.
|
|
241
|
+
*/
|
|
242
|
+
function topLevelWherePredicate(stmt) {
|
|
243
|
+
// A quoted identifier is consumed whole, so a column named "where" is not one.
|
|
244
|
+
const re = /"(?:[^"]|"")*"|[()]|\bWHERE\b|\bRETURNING\b/gi;
|
|
201
245
|
let depth = 0;
|
|
246
|
+
let from = -1;
|
|
202
247
|
let m = re.exec(stmt);
|
|
203
248
|
while (m !== null) {
|
|
204
|
-
if (m[0]
|
|
249
|
+
if (m[0].startsWith('"')) {
|
|
250
|
+
// not a keyword
|
|
251
|
+
}
|
|
252
|
+
else if (m[0] === '(')
|
|
205
253
|
depth++;
|
|
206
254
|
else if (m[0] === ')')
|
|
207
255
|
depth = Math.max(0, depth - 1);
|
|
208
|
-
else if (depth === 0)
|
|
209
|
-
|
|
256
|
+
else if (depth === 0) {
|
|
257
|
+
if (from === -1 && /^WHERE$/i.test(m[0]))
|
|
258
|
+
from = m.index + m[0].length;
|
|
259
|
+
else if (from !== -1 && /^RETURNING$/i.test(m[0]))
|
|
260
|
+
return stmt.slice(from, m.index).trim();
|
|
261
|
+
}
|
|
210
262
|
m = re.exec(stmt);
|
|
211
263
|
}
|
|
212
|
-
return
|
|
264
|
+
return from === -1 ? null : stmt.slice(from).trim();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* True when the statement has a top-level `WHERE` that can leave some rows out.
|
|
268
|
+
* A predicate proven always true (`true`, `1 = 1`, `x OR true`) restricts nothing
|
|
269
|
+
* and counts as no WHERE. A predicate this cannot read is assumed to restrict,
|
|
270
|
+
* and that statement is then NOT flagged, so only a proof of constant-true may
|
|
271
|
+
* turn a WHERE into none.
|
|
272
|
+
*/
|
|
273
|
+
function hasRestrictingWhere(stmt) {
|
|
274
|
+
const predicate = topLevelWherePredicate(stmt);
|
|
275
|
+
return predicate !== null && constantValue(predicate) !== true;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Split on a top-level (paren depth 0, outside `CASE ... END`) `AND` or `OR`.
|
|
279
|
+
* The `AND` of a `BETWEEN a AND b` belongs to the BETWEEN and splits nothing.
|
|
280
|
+
*/
|
|
281
|
+
function splitTopLevel(text, keyword) {
|
|
282
|
+
const re = /"(?:[^"]|"")*"|[()]|\b(?:AND|OR|BETWEEN|CASE|END)\b/gi;
|
|
283
|
+
const parts = [];
|
|
284
|
+
let depth = 0;
|
|
285
|
+
let caseDepth = 0;
|
|
286
|
+
let betweenOpen = 0;
|
|
287
|
+
let last = 0;
|
|
288
|
+
let m = re.exec(text);
|
|
289
|
+
while (m !== null) {
|
|
290
|
+
const token = m[0].toUpperCase();
|
|
291
|
+
if (token.startsWith('"')) {
|
|
292
|
+
// a quoted identifier, never a keyword
|
|
293
|
+
}
|
|
294
|
+
else if (token === '(')
|
|
295
|
+
depth++;
|
|
296
|
+
else if (token === ')')
|
|
297
|
+
depth = Math.max(0, depth - 1);
|
|
298
|
+
else if (depth === 0) {
|
|
299
|
+
if (token === 'CASE')
|
|
300
|
+
caseDepth++;
|
|
301
|
+
else if (token === 'END')
|
|
302
|
+
caseDepth = Math.max(0, caseDepth - 1);
|
|
303
|
+
else if (caseDepth === 0) {
|
|
304
|
+
if (token === 'BETWEEN')
|
|
305
|
+
betweenOpen++;
|
|
306
|
+
else if (token === 'AND' && betweenOpen > 0)
|
|
307
|
+
betweenOpen--;
|
|
308
|
+
else if (token === keyword) {
|
|
309
|
+
parts.push(text.slice(last, m.index));
|
|
310
|
+
last = m.index + m[0].length;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
m = re.exec(text);
|
|
315
|
+
}
|
|
316
|
+
parts.push(text.slice(last));
|
|
317
|
+
return parts.map((part) => part.trim());
|
|
318
|
+
}
|
|
319
|
+
/** Strip parentheses that wrap the WHOLE text: `((a = 1))` becomes `a = 1`. */
|
|
320
|
+
function unwrapParens(text) {
|
|
321
|
+
let out = text.trim();
|
|
322
|
+
while (out.startsWith('(') && closingParenIndex(out, 0) === out.length - 1)
|
|
323
|
+
out = out.slice(1, -1).trim();
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
const NUMBER_LITERAL = String.raw `[+-]?\d+(?:\.\d+)?`;
|
|
327
|
+
const COMPARISON = new RegExp(String.raw `^(${NUMBER_LITERAL}|'')\s*(=|<>|!=|<=|>=|<|>)\s*(${NUMBER_LITERAL}|'')$`);
|
|
328
|
+
const SELF_COMPARISON = new RegExp(String.raw `^(${IDENT})\s*(=|<>|!=|<=|>=|<|>)\s*(${IDENT})$`);
|
|
329
|
+
/**
|
|
330
|
+
* The truth value of a predicate when it does not depend on any row: `true`,
|
|
331
|
+
* `false`, or `undefined` when it does (or when this cannot tell). Strings reach
|
|
332
|
+
* here already emptied to `''`, so two string literals compare equal; the only
|
|
333
|
+
* mistake that costs is calling a false predicate true, a prompt on a statement
|
|
334
|
+
* that updates nothing.
|
|
335
|
+
*/
|
|
336
|
+
function constantValue(predicate) {
|
|
337
|
+
const text = unwrapParens(predicate);
|
|
338
|
+
if (text === '')
|
|
339
|
+
return undefined;
|
|
340
|
+
const ors = splitTopLevel(text, 'OR');
|
|
341
|
+
if (ors.length > 1) {
|
|
342
|
+
const values = ors.map(constantValue);
|
|
343
|
+
if (values.includes(true))
|
|
344
|
+
return true;
|
|
345
|
+
return values.every((v) => v === false) ? false : undefined;
|
|
346
|
+
}
|
|
347
|
+
const ands = splitTopLevel(text, 'AND');
|
|
348
|
+
if (ands.length > 1) {
|
|
349
|
+
const values = ands.map(constantValue);
|
|
350
|
+
if (values.includes(false))
|
|
351
|
+
return false;
|
|
352
|
+
return values.every((v) => v === true) ? true : undefined;
|
|
353
|
+
}
|
|
354
|
+
const negated = /^NOT\s+([\s\S]+)$/i.exec(text);
|
|
355
|
+
if (negated) {
|
|
356
|
+
const inner = constantValue(negated[1]);
|
|
357
|
+
return inner === undefined ? undefined : !inner;
|
|
358
|
+
}
|
|
359
|
+
if (/^TRUE$/i.test(text))
|
|
360
|
+
return true;
|
|
361
|
+
if (/^(?:FALSE|NULL)$/i.test(text))
|
|
362
|
+
return false;
|
|
363
|
+
const literals = COMPARISON.exec(text);
|
|
364
|
+
if (literals)
|
|
365
|
+
return compareConstants(literals[1], literals[2], literals[3]);
|
|
366
|
+
// `id = id` is true for every row whose id is not null; calling it true errs
|
|
367
|
+
// toward the prompt, which is the safe side.
|
|
368
|
+
const self = SELF_COMPARISON.exec(text);
|
|
369
|
+
if (self && self[1].toLowerCase() === self[6].toLowerCase())
|
|
370
|
+
return compareConstants('0', self[5], '0');
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
/** Compare two literals, where two `''` (emptied strings) are taken as equal. */
|
|
374
|
+
function compareConstants(left, op, right) {
|
|
375
|
+
const a = left === "''" ? 0 : Number(left);
|
|
376
|
+
const b = right === "''" ? 0 : Number(right);
|
|
377
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
378
|
+
return undefined;
|
|
379
|
+
switch (op) {
|
|
380
|
+
case '=':
|
|
381
|
+
return a === b;
|
|
382
|
+
case '<>':
|
|
383
|
+
case '!=':
|
|
384
|
+
return a !== b;
|
|
385
|
+
case '<':
|
|
386
|
+
return a < b;
|
|
387
|
+
case '<=':
|
|
388
|
+
return a <= b;
|
|
389
|
+
case '>':
|
|
390
|
+
return a > b;
|
|
391
|
+
case '>=':
|
|
392
|
+
return a >= b;
|
|
393
|
+
default:
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
213
396
|
}
|
|
214
397
|
/**
|
|
215
398
|
* A leading CTE list is a prefix, not a statement: `WITH c AS (SELECT 1) DELETE
|
|
@@ -269,6 +452,25 @@ function copySubstatements(stmt) {
|
|
|
269
452
|
// above rather than being judged as one blob here.
|
|
270
453
|
return /^(?:DELETE|UPDATE|INSERT|MERGE|WITH)\b/i.test(inner) ? [inner] : [];
|
|
271
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* The statement a `PREPARE name [(types)] AS <statement>` stores. Nothing runs at
|
|
457
|
+
* PREPARE time, but `EXECUTE name` runs it later on the same connection, so the
|
|
458
|
+
* stored statement is judged as if it had been written out. A prepared `DELETE`
|
|
459
|
+
* or unrestricted `UPDATE` followed by `EXECUTE` was reported as clean.
|
|
460
|
+
*/
|
|
461
|
+
function prepareSubstatements(stmt) {
|
|
462
|
+
const head = /^PREPARE\s+(?:"[^"]+"|[A-Za-z_][\w$]*)\s*/i.exec(stmt);
|
|
463
|
+
if (!head)
|
|
464
|
+
return [];
|
|
465
|
+
let rest = stmt.slice(head[0].length);
|
|
466
|
+
if (rest.startsWith('('))
|
|
467
|
+
rest = rest.slice(closingParenIndex(rest, 0) + 1).trimStart();
|
|
468
|
+
const as = /^AS\s+/i.exec(rest);
|
|
469
|
+
if (!as)
|
|
470
|
+
return [];
|
|
471
|
+
const inner = rest.slice(as[0].length);
|
|
472
|
+
return [stripLeadingCtes(inner), ...cteSubstatements(inner)];
|
|
473
|
+
}
|
|
272
474
|
/**
|
|
273
475
|
* Data-modifying CTE bodies: `WITH d AS (DELETE FROM t ...) SELECT ...` runs a
|
|
274
476
|
* real DELETE even though the statement reads as a SELECT. Each candidate is cut
|
|
@@ -338,7 +540,8 @@ function proceduralCandidates(body) {
|
|
|
338
540
|
// Literals have to survive: they are where dynamic SQL keeps its payload,
|
|
339
541
|
// and `stripped` would have emptied exactly them.
|
|
340
542
|
const text = statement.code;
|
|
341
|
-
|
|
543
|
+
// `DO UPDATE` is the tail of an `ON CONFLICT` clause, judged with its INSERT.
|
|
544
|
+
const re = /\b(?:(?:DROP|TRUNCATE|DELETE|ALTER|MERGE|INSERT)\s|(?<!\bDO\s+)UPDATE\s|lo_unlink\s*\()/gi;
|
|
342
545
|
let m = re.exec(text);
|
|
343
546
|
while (m !== null) {
|
|
344
547
|
out.push({ text: text.slice(m.index), before: text.slice(0, m.index), statementCode: text });
|
|
@@ -370,6 +573,7 @@ function scanDestructiveSql(sql) {
|
|
|
370
573
|
stripLeadingCtes(body),
|
|
371
574
|
...cteSubstatements(body),
|
|
372
575
|
...copySubstatements(body).flatMap((inner) => [inner, ...cteSubstatements(inner)]),
|
|
576
|
+
...prepareSubstatements(body),
|
|
373
577
|
].map((text) => ({
|
|
374
578
|
text,
|
|
375
579
|
display,
|
|
@@ -107,11 +107,13 @@ const CATALOG = {
|
|
|
107
107
|
'TLS: a self-signed or expired certificate, or a hostname the certificate does not cover.',
|
|
108
108
|
'The connection string is malformed, or points at a pooler port the server is not on.',
|
|
109
109
|
'The server closed an idle pooled connection (proxy idle timeout, restart, failover).',
|
|
110
|
+
'A transaction-pooling endpoint (PgBouncer in transaction mode, a managed `-pooler` host, port 6432 or 6543) with named prepared statements: sqlstate 42P05 or 26000.',
|
|
110
111
|
],
|
|
111
112
|
howToFix: [
|
|
112
113
|
'Read `err.sqlstate` and the hint in the message: each driver code carries its own next step.',
|
|
113
114
|
'Check the host, port and database name; for TLS, supply the CA via `ssl: { ca }` rather than disabling verification.',
|
|
114
115
|
'For a serverless driver, pass the external pool on `TurbineConfig.pool` instead of a connection string.',
|
|
116
|
+
'For 42P05 / 26000 behind a pooler, connect to the direct endpoint, set `preparedStatements: false`, or run PgBouncer 1.21+ with `max_prepared_statements` above 0.',
|
|
115
117
|
],
|
|
116
118
|
properties: ['sqlstate'],
|
|
117
119
|
},
|
|
@@ -349,6 +351,40 @@ const CATALOG = {
|
|
|
349
351
|
],
|
|
350
352
|
properties: ['reason'],
|
|
351
353
|
},
|
|
354
|
+
TURBINE_E019: {
|
|
355
|
+
className: 'TransactionRolledBackError',
|
|
356
|
+
retryable: false,
|
|
357
|
+
origin: 'turbine',
|
|
358
|
+
whenThrown: 'A transaction callback returned or committed after work inside it had already failed, so the server rolled the transaction back.',
|
|
359
|
+
likelyCauses: [
|
|
360
|
+
'The callback caught an error from a statement (a unique violation, a timeout) and carried on, then returned. PostgreSQL refuses every later statement in an aborted transaction and answers COMMIT with ROLLBACK.',
|
|
361
|
+
'A query inside `$transaction` hit its per-query `timeout`; the transaction cannot continue after the cancel.',
|
|
362
|
+
'Raw `transaction()`: a statement failed and the code returned without a SAVEPOINT around it.',
|
|
363
|
+
],
|
|
364
|
+
howToFix: [
|
|
365
|
+
'Let the first error propagate so the whole transaction fails with its own class, or wrap the step that may fail in a nested `tx.$transaction` (a SAVEPOINT) and catch around that.',
|
|
366
|
+
'On raw `transaction()`, issue `SAVEPOINT` before the statement that may fail and `ROLLBACK TO SAVEPOINT` when it does.',
|
|
367
|
+
'Deadlock, serialization and lock-not-available failures keep their own classes and stay retryable; E019 is not, because retrying the same callback repeats the same swallowed failure.',
|
|
368
|
+
],
|
|
369
|
+
properties: [],
|
|
370
|
+
},
|
|
371
|
+
TURBINE_E020: {
|
|
372
|
+
className: 'InvariantError',
|
|
373
|
+
retryable: false,
|
|
374
|
+
origin: 'turbine',
|
|
375
|
+
whenThrown: 'Turbine caught a bug in itself before sending a statement it knew was wrong: two internal paths that must agree did not, or a branch no argument can reach was reached.',
|
|
376
|
+
likelyCauses: [
|
|
377
|
+
'`invariant: "sql-cache-lockstep"`: the SQL-template cache built a statement whose parameters, or whose cached text, disagree with what a fresh build of the same query produces.',
|
|
378
|
+
'`invariant: "sql-cache-param-count"`: a cached statement was about to run with a different number of parameters than its placeholders.',
|
|
379
|
+
'Any other `invariant` id names one internal rule; the message says which two things disagreed.',
|
|
380
|
+
],
|
|
381
|
+
howToFix: [
|
|
382
|
+
'Report it at https://github.com/zvndev/turbine-orm/issues with the message and `err.invariant`. The message carries the query shape and no row or parameter value.',
|
|
383
|
+
'Nothing in the call is wrong, so rewriting the arguments is a workaround at best. There is no option that turns these checks off: each one fires only where Turbine would otherwise have sent the wrong SQL or the wrong parameters.',
|
|
384
|
+
'Do not retry: the same call fails the same way.',
|
|
385
|
+
],
|
|
386
|
+
properties: ['invariant'],
|
|
387
|
+
},
|
|
352
388
|
};
|
|
353
389
|
/** Every code in the catalog, in code order. */
|
|
354
390
|
exports.CATALOGUED_ERROR_CODES = Object.keys(CATALOG).sort();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text the CLI prints for a caught error. A zero-import leaf, so every
|
|
3
|
+
* command can use it.
|
|
4
|
+
*
|
|
5
|
+
* Reading `.message` alone is not enough. When `localhost` resolves to both
|
|
6
|
+
* `::1` and `127.0.0.1` and both refuse, Node's `net` rejects with an
|
|
7
|
+
* `AggregateError` whose message is EMPTY: the reasons are on its `errors`.
|
|
8
|
+
* Every command then printed its prefix and nothing else ("Failed to start
|
|
9
|
+
* Studio: "), and the top-level handler, which classifies by message text,
|
|
10
|
+
* could not tell that it was a refused connection.
|
|
11
|
+
*
|
|
12
|
+
* Order: the error's own message; else the messages of the errors it
|
|
13
|
+
* aggregates (each once); else its `cause`; else its `code`; else its name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function errorText(err: unknown, depth?: number): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.errorText = errorText;
|
|
4
|
+
/**
|
|
5
|
+
* The text the CLI prints for a caught error. A zero-import leaf, so every
|
|
6
|
+
* command can use it.
|
|
7
|
+
*
|
|
8
|
+
* Reading `.message` alone is not enough. When `localhost` resolves to both
|
|
9
|
+
* `::1` and `127.0.0.1` and both refuse, Node's `net` rejects with an
|
|
10
|
+
* `AggregateError` whose message is EMPTY: the reasons are on its `errors`.
|
|
11
|
+
* Every command then printed its prefix and nothing else ("Failed to start
|
|
12
|
+
* Studio: "), and the top-level handler, which classifies by message text,
|
|
13
|
+
* could not tell that it was a refused connection.
|
|
14
|
+
*
|
|
15
|
+
* Order: the error's own message; else the messages of the errors it
|
|
16
|
+
* aggregates (each once); else its `cause`; else its `code`; else its name.
|
|
17
|
+
*/
|
|
18
|
+
function errorText(err, depth = 0) {
|
|
19
|
+
if (!(err instanceof Error))
|
|
20
|
+
return String(err);
|
|
21
|
+
if (err.message)
|
|
22
|
+
return err.message;
|
|
23
|
+
if (depth < 4) {
|
|
24
|
+
const inner = err.errors;
|
|
25
|
+
if (Array.isArray(inner)) {
|
|
26
|
+
const parts = [...new Set(inner.map((e) => errorText(e, depth + 1)).filter((t) => t !== ''))];
|
|
27
|
+
if (parts.length > 0)
|
|
28
|
+
return parts.join('; ');
|
|
29
|
+
}
|
|
30
|
+
if (err.cause !== undefined) {
|
|
31
|
+
const cause = errorText(err.cause, depth + 1);
|
|
32
|
+
if (cause !== '')
|
|
33
|
+
return cause;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const code = err.code;
|
|
37
|
+
if (typeof code === 'string' && code !== '')
|
|
38
|
+
return code;
|
|
39
|
+
return err.name || 'Error';
|
|
40
|
+
}
|