turbine-orm 0.81.0 → 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/index.js
CHANGED
|
@@ -60,6 +60,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
60
60
|
};
|
|
61
61
|
})();
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
+
exports.MIGRATE_POOLER_EXPLANATION = exports.DEFAULT_LOCK_WAIT_SECONDS = void 0;
|
|
63
64
|
exports.canonicalCommand = canonicalCommand;
|
|
64
65
|
exports.knownCommands = knownCommands;
|
|
65
66
|
exports.flagsForCommand = flagsForCommand;
|
|
@@ -81,6 +82,8 @@ exports.scaffoldEnvForUrl = scaffoldEnvForUrl;
|
|
|
81
82
|
exports.generatedClientImport = generatedClientImport;
|
|
82
83
|
exports.initEnvNotice = initEnvNotice;
|
|
83
84
|
exports.refuseSchemaFilePath = refuseSchemaFilePath;
|
|
85
|
+
exports.overlayCodeFirstTags = overlayCodeFirstTags;
|
|
86
|
+
exports.generatorColumnTypes = generatorColumnTypes;
|
|
84
87
|
exports.resolveMigrateFromPrismaUrl = resolveMigrateFromPrismaUrl;
|
|
85
88
|
exports.readPrismaSchemaSource = readPrismaSchemaSource;
|
|
86
89
|
exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
|
|
@@ -105,8 +108,10 @@ const prisma_schema_fingerprint_js_1 = require("../prisma-schema-fingerprint.js"
|
|
|
105
108
|
const utils_js_1 = require("../query/utils.js");
|
|
106
109
|
const schema_js_1 = require("../schema.js");
|
|
107
110
|
const schema_sql_js_1 = require("../schema-sql.js");
|
|
111
|
+
const code_first_tags_js_1 = require("./code-first-tags.js");
|
|
108
112
|
const config_js_1 = require("./config.js");
|
|
109
113
|
const destructive_js_1 = require("./destructive.js");
|
|
114
|
+
const error_text_js_1 = require("./error-text.js");
|
|
110
115
|
const loader_js_1 = require("./loader.js");
|
|
111
116
|
const mcp_js_1 = require("./mcp.js");
|
|
112
117
|
const migrate_js_1 = require("./migrate.js");
|
|
@@ -116,6 +121,8 @@ const prisma_resolve_js_1 = require("./prisma-resolve.js");
|
|
|
116
121
|
const prisma_schema_js_1 = require("./prisma-schema.js");
|
|
117
122
|
const studio_js_1 = require("./studio.js");
|
|
118
123
|
const ui_js_1 = require("./ui.js");
|
|
124
|
+
/** How long a bare `migrate deploy --wait` waits for a concurrent deploy to finish. */
|
|
125
|
+
exports.DEFAULT_LOCK_WAIT_SECONDS = 300;
|
|
119
126
|
/**
|
|
120
127
|
* Fail an argument-parse error the way every other CLI failure looks: banner,
|
|
121
128
|
* red error line, then indented hint lines. The flag validators used to call
|
|
@@ -170,6 +177,7 @@ const GLOBAL_FLAGS = [
|
|
|
170
177
|
const COMMAND_FLAGS = {
|
|
171
178
|
init: ['--yes', '--schema-file', '--skip-schema', '--with-schema', '--skip-seed', '--skip-push', '--skip-generate'],
|
|
172
179
|
generate: [
|
|
180
|
+
'--schema-file',
|
|
173
181
|
'--zod',
|
|
174
182
|
'--include-views',
|
|
175
183
|
'--no-timestamp',
|
|
@@ -179,7 +187,7 @@ const COMMAND_FLAGS = {
|
|
|
179
187
|
'--allow-empty',
|
|
180
188
|
],
|
|
181
189
|
'migrate-from-prisma': ['--no-db', '--if-db', '--allow-partial', '--no-timestamp'],
|
|
182
|
-
push: ['--schema-file', '--dry-run', '--allow-destructive'],
|
|
190
|
+
push: ['--schema-file', '--dry-run', '--exit-code', '--allow-destructive'],
|
|
183
191
|
migrate: [
|
|
184
192
|
'--schema-file',
|
|
185
193
|
'--auto',
|
|
@@ -187,8 +195,12 @@ const COMMAND_FLAGS = {
|
|
|
187
195
|
'--recipe',
|
|
188
196
|
'--step',
|
|
189
197
|
'--dry-run',
|
|
198
|
+
'--exit-code',
|
|
199
|
+
'--wait',
|
|
190
200
|
'--allow-drift',
|
|
191
201
|
'--allow-destructive',
|
|
202
|
+
'--allow-pending',
|
|
203
|
+
'--allow-pooler',
|
|
192
204
|
],
|
|
193
205
|
seed: [],
|
|
194
206
|
status: [],
|
|
@@ -366,6 +378,8 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
366
378
|
const unknownFlags = [];
|
|
367
379
|
/** The raw `--step` operand, judged after the loop. Absent means unset. */
|
|
368
380
|
let stepRaw;
|
|
381
|
+
/** The raw `--wait` operand, judged after the loop: undefined absent, null bare `--wait`. */
|
|
382
|
+
let waitRaw;
|
|
369
383
|
for (; i < args.length; i++) {
|
|
370
384
|
const arg = args[i];
|
|
371
385
|
const next = args[i + 1];
|
|
@@ -373,8 +387,15 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
373
387
|
// (`--step -1`, `--host -x`) is consumed by its own case and never reaches
|
|
374
388
|
// this test. A bare `-`-prefixed token that is not a flag reaches it and is
|
|
375
389
|
// rejected: no command takes a negative-number positional.
|
|
376
|
-
|
|
390
|
+
// `--wait=30` is the one flag whose value attaches with `=`, so it is
|
|
391
|
+
// matched by its name and handled here rather than by a `case` label.
|
|
392
|
+
const attachedWait = arg.startsWith('--wait=');
|
|
393
|
+
if (accepted && arg.startsWith('-') && !accepted.has(attachedWait ? '--wait' : arg))
|
|
377
394
|
unknownFlags.push(arg);
|
|
395
|
+
if (attachedWait) {
|
|
396
|
+
waitRaw = arg.slice('--wait='.length);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
378
399
|
switch (arg) {
|
|
379
400
|
case '--url':
|
|
380
401
|
case '-u':
|
|
@@ -418,6 +439,23 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
418
439
|
case '--dry-run':
|
|
419
440
|
result.dryRun = true;
|
|
420
441
|
break;
|
|
442
|
+
case '--exit-code':
|
|
443
|
+
result.exitCode = true;
|
|
444
|
+
break;
|
|
445
|
+
case '--allow-pending':
|
|
446
|
+
result.allowPending = true;
|
|
447
|
+
break;
|
|
448
|
+
case '--wait':
|
|
449
|
+
// `--wait 30` reads the next token only when it is all digits: `deploy`
|
|
450
|
+
// takes no positional, so a bare number after the flag is its value.
|
|
451
|
+
if (next !== undefined && /^\d+$/.test(next)) {
|
|
452
|
+
waitRaw = next;
|
|
453
|
+
i++;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
waitRaw = null;
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
421
459
|
case '--auto':
|
|
422
460
|
result.auto = true;
|
|
423
461
|
break;
|
|
@@ -500,8 +538,8 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
500
538
|
break;
|
|
501
539
|
case '--import-ext':
|
|
502
540
|
case '--import-extension':
|
|
503
|
-
if (next !== 'js' && next !== 'none' && next !== 'auto') {
|
|
504
|
-
failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext
|
|
541
|
+
if (next !== 'js' && next !== 'ts' && next !== 'none' && next !== 'auto') {
|
|
542
|
+
failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, ts, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext ts');
|
|
505
543
|
}
|
|
506
544
|
result.importExtension = next;
|
|
507
545
|
i++;
|
|
@@ -592,6 +630,18 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
592
630
|
}
|
|
593
631
|
result.step = parsed;
|
|
594
632
|
}
|
|
633
|
+
if (waitRaw !== undefined && accepted !== undefined) {
|
|
634
|
+
if (waitRaw === null) {
|
|
635
|
+
result.wait = exports.DEFAULT_LOCK_WAIT_SECONDS;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const parsed = /^\d+$/.test(waitRaw.trim()) ? Number.parseInt(waitRaw, 10) : Number.NaN;
|
|
639
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
640
|
+
failArg(`${(0, ui_js_1.cyan)('--wait')} expects a positive whole number of seconds, got ${(0, ui_js_1.bold)(waitRaw === '' ? '(nothing)' : `"${waitRaw}"`)}.`, `Bare ${'--wait'} waits ${exports.DEFAULT_LOCK_WAIT_SECONDS} seconds. To not wait at all, leave the flag off.`, 'Example: npx turbine migrate deploy --wait=120');
|
|
641
|
+
}
|
|
642
|
+
result.wait = parsed;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
595
645
|
return result;
|
|
596
646
|
}
|
|
597
647
|
// ---------------------------------------------------------------------------
|
|
@@ -790,6 +840,11 @@ function loadDotEnvForCli(deps = {}) {
|
|
|
790
840
|
if (!fileExists(envPath)) {
|
|
791
841
|
return { fileExists: false, loaded: false, unsupported: false, databaseUrlProvenance: shellOrNone };
|
|
792
842
|
}
|
|
843
|
+
// An exported-but-empty `DATABASE_URL=` means "not configured", and the loader
|
|
844
|
+
// never overrides a variable that is already set, so it would otherwise block
|
|
845
|
+
// the value in the `.env` for good.
|
|
846
|
+
if (env.DATABASE_URL === '')
|
|
847
|
+
delete env.DATABASE_URL;
|
|
793
848
|
const loader = deps.loadEnvFile !== undefined
|
|
794
849
|
? deps.loadEnvFile
|
|
795
850
|
: typeof process.loadEnvFile === 'function'
|
|
@@ -807,7 +862,7 @@ function loadDotEnvForCli(deps = {}) {
|
|
|
807
862
|
loaded: false,
|
|
808
863
|
unsupported: false,
|
|
809
864
|
databaseUrlProvenance: shellOrNone,
|
|
810
|
-
loadError:
|
|
865
|
+
loadError: (0, error_text_js_1.errorText)(err),
|
|
811
866
|
};
|
|
812
867
|
}
|
|
813
868
|
// `.env`-sourced only if DATABASE_URL was absent before and present after.
|
|
@@ -1206,42 +1261,94 @@ DATABASE_URL=postgres://user:password@localhost:5432/database
|
|
|
1206
1261
|
* @internal exported for tests.
|
|
1207
1262
|
*/
|
|
1208
1263
|
function scaffoldEnvForUrl(url) {
|
|
1264
|
+
// The plan is read from disk and the writes come after it, so a file can
|
|
1265
|
+
// appear in between. Creation is exclusive (see createFileExclusive); when it
|
|
1266
|
+
// loses, plan again from what is there now. The cap only ends the loop for a
|
|
1267
|
+
// name that cannot be read but cannot be created either, a dangling symlink.
|
|
1268
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1269
|
+
const plan = applyEnvScaffold(url);
|
|
1270
|
+
if (plan)
|
|
1271
|
+
return plan;
|
|
1272
|
+
}
|
|
1273
|
+
throw new errors_js_1.ValidationError('Could not write .env, .env.example and .gitignore: one of those names is taken by something that cannot be read ' +
|
|
1274
|
+
'(a dangling symlink?). Remove it and run the command again.');
|
|
1275
|
+
}
|
|
1276
|
+
/** One planning pass plus its writes; undefined when a create found its name already taken. */
|
|
1277
|
+
function applyEnvScaffold(url) {
|
|
1209
1278
|
const envPath = '.env';
|
|
1210
1279
|
const envExamplePath = '.env.example';
|
|
1211
1280
|
const gitignorePath = '.gitignore';
|
|
1212
|
-
const
|
|
1281
|
+
const env = readIfPresent(envPath);
|
|
1282
|
+
const gitignore = readIfPresent(gitignorePath);
|
|
1213
1283
|
const plan = planEnvScaffold({
|
|
1214
|
-
envExists,
|
|
1215
|
-
envHasDatabaseUrl:
|
|
1216
|
-
envExampleExists: (
|
|
1217
|
-
gitignoreExists:
|
|
1218
|
-
gitignoreIgnoresEnv:
|
|
1284
|
+
envExists: env !== undefined,
|
|
1285
|
+
envHasDatabaseUrl: env !== undefined && /^\s*(export\s+)?DATABASE_URL\s*=/m.test(env),
|
|
1286
|
+
envExampleExists: readIfPresent(envExamplePath) !== undefined,
|
|
1287
|
+
gitignoreExists: gitignore !== undefined,
|
|
1288
|
+
gitignoreIgnoresEnv: gitignore !== undefined && gitignoreIgnoresEnv(gitignore),
|
|
1219
1289
|
});
|
|
1220
1290
|
// .gitignore FIRST: the ignore rule has to exist before the file holding the
|
|
1221
1291
|
// password does, or a `git add -A` in between commits it.
|
|
1222
|
-
if (plan.gitignore === 'created')
|
|
1223
|
-
|
|
1224
|
-
|
|
1292
|
+
if (plan.gitignore === 'created') {
|
|
1293
|
+
if (!createFileExclusive(gitignorePath, '# Local environment (secrets)\n.env\n'))
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
else if (plan.gitignore === 'appended') {
|
|
1225
1297
|
(0, node_fs_1.appendFileSync)(gitignorePath, '\n# Local environment (secrets)\n.env\n');
|
|
1298
|
+
}
|
|
1299
|
+
// 0600 on CREATION only. The whole point of this scaffold is moving a live
|
|
1300
|
+
// password out of a committed file, and the default 0644 would leave it
|
|
1301
|
+
// world-readable to every account on a shared box or CI runner. `mode` is
|
|
1302
|
+
// applied by the OS only when the open() actually creates the file, so a
|
|
1303
|
+
// `.env` that exists keeps whatever mode its owner chose.
|
|
1226
1304
|
if (plan.env === 'created') {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
// applied by the OS only when the open() actually creates the file, so if
|
|
1231
|
-
// one appeared since the existsSync above we write into it without widening
|
|
1232
|
-
// or narrowing whatever mode its owner chose.
|
|
1233
|
-
(0, node_fs_1.writeFileSync)(envPath, `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`, {
|
|
1234
|
-
encoding: 'utf-8',
|
|
1235
|
-
mode: 0o600,
|
|
1236
|
-
});
|
|
1305
|
+
const body = `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`;
|
|
1306
|
+
if (!createFileExclusive(envPath, body, 0o600))
|
|
1307
|
+
return undefined;
|
|
1237
1308
|
}
|
|
1238
1309
|
else if (plan.env === 'appended') {
|
|
1239
|
-
(0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n
|
|
1310
|
+
(0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n`, { mode: 0o600 });
|
|
1240
1311
|
}
|
|
1241
|
-
if (plan.envExample === 'created')
|
|
1242
|
-
|
|
1312
|
+
if (plan.envExample === 'created' && !createFileExclusive(envExamplePath, ENV_EXAMPLE_TEMPLATE))
|
|
1313
|
+
return undefined;
|
|
1243
1314
|
return plan;
|
|
1244
1315
|
}
|
|
1316
|
+
/**
|
|
1317
|
+
* A file's text, or undefined when there is no such file. One read in place of
|
|
1318
|
+
* an existence check followed by a read, so there is no window between the two.
|
|
1319
|
+
* A directory under that name counts as present: the write step then fails on
|
|
1320
|
+
* it by name, which is the error that was always raised.
|
|
1321
|
+
*/
|
|
1322
|
+
function readIfPresent(path) {
|
|
1323
|
+
try {
|
|
1324
|
+
return (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
const code = err.code;
|
|
1328
|
+
if (code === 'ENOENT')
|
|
1329
|
+
return undefined;
|
|
1330
|
+
if (code === 'EISDIR')
|
|
1331
|
+
return '';
|
|
1332
|
+
throw err;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Create `path` only if nothing is there; false when the name is taken.
|
|
1337
|
+
* `wx` is O_CREAT | O_EXCL: the kernel refuses (EEXIST) any existing name,
|
|
1338
|
+
* a symlink included, so a file that appeared after the plan was made is never
|
|
1339
|
+
* truncated and a link planted at the name is never written through.
|
|
1340
|
+
*/
|
|
1341
|
+
function createFileExclusive(path, body, mode) {
|
|
1342
|
+
try {
|
|
1343
|
+
(0, node_fs_1.writeFileSync)(path, body, { encoding: 'utf-8', flag: 'wx', ...(mode === undefined ? {} : { mode }) });
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
catch (err) {
|
|
1347
|
+
if (err.code === 'EEXIST')
|
|
1348
|
+
return false;
|
|
1349
|
+
throw err;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1245
1352
|
/** Say exactly what was written where, so nobody has to guess where the password went. */
|
|
1246
1353
|
function reportEnvScaffold(plan, url) {
|
|
1247
1354
|
(0, ui_js_1.warn)(`The connection string passed to ${(0, ui_js_1.cyan)('--url')} contains a password.`);
|
|
@@ -1277,8 +1384,8 @@ function ensureInitScaffoldDirs(config) {
|
|
|
1277
1384
|
(0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
|
|
1278
1385
|
}
|
|
1279
1386
|
const gitignorePath = '.gitignore';
|
|
1280
|
-
|
|
1281
|
-
|
|
1387
|
+
const gitignoreContent = readIfPresent(gitignorePath);
|
|
1388
|
+
if (gitignoreContent !== undefined) {
|
|
1282
1389
|
const additions = [];
|
|
1283
1390
|
if (!gitignoreContent.includes('generated/turbine'))
|
|
1284
1391
|
additions.push('generated/turbine/');
|
|
@@ -1384,8 +1491,19 @@ async function runInitGenerate(config, url) {
|
|
|
1384
1491
|
});
|
|
1385
1492
|
const tableNames = Object.keys(schema.tables);
|
|
1386
1493
|
spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables`);
|
|
1494
|
+
// The schema file init just pushed is where `pii` / `updatedAt` live.
|
|
1495
|
+
const appliedTags = await overlayCodeFirstTags(schema, config);
|
|
1496
|
+
if (appliedTags && describeAppliedTags(appliedTags)) {
|
|
1497
|
+
(0, ui_js_1.info)(`Applied column tags from ${(0, ui_js_1.cyan)(config.schemaFile)}: ${describeAppliedTags(appliedTags)}`);
|
|
1498
|
+
}
|
|
1387
1499
|
const genSpinner = new ui_js_1.Spinner('Generating TypeScript client').start();
|
|
1388
|
-
const result = (0, generate_js_1.generate)({
|
|
1500
|
+
const result = (0, generate_js_1.generate)({
|
|
1501
|
+
schema,
|
|
1502
|
+
outDir: config.out,
|
|
1503
|
+
connectionString: url,
|
|
1504
|
+
importExtension: config.importExtension,
|
|
1505
|
+
columnTypes: await generatorColumnTypesFor(appliedTags, config, schema, url),
|
|
1506
|
+
});
|
|
1389
1507
|
genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files to ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
|
|
1390
1508
|
// The accessor name the generator emits is snakeToCamel of the table name
|
|
1391
1509
|
// (generate.ts, generateIndex), so this is the spelling that autocompletes.
|
|
@@ -1394,8 +1512,7 @@ async function runInitGenerate(config, url) {
|
|
|
1394
1512
|
}
|
|
1395
1513
|
catch (err) {
|
|
1396
1514
|
spinner.fail('Could not generate client');
|
|
1397
|
-
|
|
1398
|
-
(0, ui_js_1.errorLine)((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message)));
|
|
1515
|
+
(0, ui_js_1.errorLine)((0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err))));
|
|
1399
1516
|
(0, ui_js_1.info)(`Run generation later with: ${(0, ui_js_1.cyan)('npx turbine generate')}`);
|
|
1400
1517
|
return { tableCount: null };
|
|
1401
1518
|
}
|
|
@@ -1409,13 +1526,14 @@ async function runInitSeed(config) {
|
|
|
1409
1526
|
}
|
|
1410
1527
|
const spinner = new ui_js_1.Spinner('Running seed file').start();
|
|
1411
1528
|
try {
|
|
1412
|
-
await runSeedPlan(getSeedExecutionPlan(seedFile), config);
|
|
1529
|
+
const outcome = await runSeedPlan(getSeedExecutionPlan(seedFile), config);
|
|
1413
1530
|
spinner.succeed('Seed completed');
|
|
1531
|
+
if (!outcome.confirmed)
|
|
1532
|
+
(0, ui_js_1.warn)(UNCONFIRMED_SEED_NOTICE);
|
|
1414
1533
|
}
|
|
1415
1534
|
catch (err) {
|
|
1416
1535
|
spinner.fail('Seed failed');
|
|
1417
|
-
|
|
1418
|
-
console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
|
|
1536
|
+
console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err)))}`);
|
|
1419
1537
|
}
|
|
1420
1538
|
}
|
|
1421
1539
|
/** Human label for a step, used in prompts and skip messages. */
|
|
@@ -1726,7 +1844,7 @@ async function cmdInit(args, config) {
|
|
|
1726
1844
|
console.log(` ${(0, ui_js_1.dim)('Nothing to connect to yet?')} ${(0, ui_js_1.cyan)('npx turbine studio --demo')} ${(0, ui_js_1.dim)('needs no database.')}`);
|
|
1727
1845
|
}
|
|
1728
1846
|
else {
|
|
1729
|
-
const accessor = generated.sampleAccessor ?? 'users';
|
|
1847
|
+
const accessor = shown(generated.sampleAccessor ?? 'users');
|
|
1730
1848
|
console.log(` ${(0, ui_js_1.dim)('1.')} Import the generated client:`);
|
|
1731
1849
|
console.log(` ${(0, ui_js_1.cyan)(generatedClientImport(config))}`);
|
|
1732
1850
|
(0, ui_js_1.newline)();
|
|
@@ -1775,7 +1893,7 @@ function refuseSchemaFilePath(config, options = {}) {
|
|
|
1775
1893
|
(0, ui_js_1.errorLine)();
|
|
1776
1894
|
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Or set it once in')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
|
|
1777
1895
|
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit, reads the 'public' schema")}`);
|
|
1778
|
-
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (
|
|
1896
|
+
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (read by push and generate)')}`);
|
|
1779
1897
|
if (options.escapeHatch) {
|
|
1780
1898
|
(0, ui_js_1.errorLine)();
|
|
1781
1899
|
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)(options.escapeHatch)} ${(0, ui_js_1.dim)('to use this literal schema name anyway.')}`);
|
|
@@ -1784,6 +1902,113 @@ function refuseSchemaFilePath(config, options = {}) {
|
|
|
1784
1902
|
process.exit(1);
|
|
1785
1903
|
}
|
|
1786
1904
|
// ---------------------------------------------------------------------------
|
|
1905
|
+
// Code-first column tags
|
|
1906
|
+
// ---------------------------------------------------------------------------
|
|
1907
|
+
/**
|
|
1908
|
+
* Overlay the column tags the defineSchema() file declares (`pii`, `updatedAt`,
|
|
1909
|
+
* `maxLength`) onto an introspected schema, before it is generated.
|
|
1910
|
+
*
|
|
1911
|
+
* Introspection reads the database, and the database has no column for these
|
|
1912
|
+
* flags, so a client generated from introspection alone returns PII by default,
|
|
1913
|
+
* never stamps `updatedAt`, and leaves Studio and the MCP server with nothing to
|
|
1914
|
+
* redact. The schema file is the only place the tags exist.
|
|
1915
|
+
*
|
|
1916
|
+
* Loaded when the file was named (`--schema-file` / config `schemaFile`) or sits
|
|
1917
|
+
* at the default path. A named file that is missing, or any file that cannot be
|
|
1918
|
+
* loaded, fails the command: carrying on would generate the unredacted client
|
|
1919
|
+
* this exists to prevent. Only a missing DEFAULT file is skipped, which is a
|
|
1920
|
+
* database-first project with no schema file at all.
|
|
1921
|
+
*
|
|
1922
|
+
* Returns what was applied, or `null` when there was no schema file to read.
|
|
1923
|
+
*
|
|
1924
|
+
* @internal exported for tests.
|
|
1925
|
+
*/
|
|
1926
|
+
async function overlayCodeFirstTags(schema, config) {
|
|
1927
|
+
if (config.schemaFileConfigured !== true && !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(config.schemaFile)))
|
|
1928
|
+
return null;
|
|
1929
|
+
const def = await loadSchemaFile(config.schemaFile);
|
|
1930
|
+
const filters = {
|
|
1931
|
+
include: config.include.length ? config.include : undefined,
|
|
1932
|
+
exclude: config.exclude.length ? config.exclude : undefined,
|
|
1933
|
+
};
|
|
1934
|
+
return (0, code_first_tags_js_1.applyCodeFirstColumnTags)(schema, (0, code_first_tags_js_1.extractCodeFirstColumnTags)(def), {
|
|
1935
|
+
source: config.schemaFile,
|
|
1936
|
+
// A table the caller filtered out of the client has no client to tag.
|
|
1937
|
+
inScope: (table) => (0, introspect_js_1.applyTableFilters)([table], filters).length > 0,
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
/** "2 pii, 1 updatedAt" for the line that reports an {@link overlayCodeFirstTags} result. */
|
|
1941
|
+
function describeAppliedTags(applied) {
|
|
1942
|
+
const parts = [];
|
|
1943
|
+
if (applied.pii > 0)
|
|
1944
|
+
parts.push(`${applied.pii} pii`);
|
|
1945
|
+
if (applied.updatedAt > 0)
|
|
1946
|
+
parts.push(`${applied.updatedAt} updatedAt`);
|
|
1947
|
+
if (applied.maxLength > 0)
|
|
1948
|
+
parts.push(`${applied.maxLength} maxLength`);
|
|
1949
|
+
const tsTypes = Object.keys(applied.columnTypes).length;
|
|
1950
|
+
if (tsTypes > 0)
|
|
1951
|
+
parts.push(`${tsTypes} tsType`);
|
|
1952
|
+
return parts.join(', ');
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* The generator's `columnTypes`: the schema file's `tsType` declarations, then
|
|
1956
|
+
* the config file's `columnTypes`, which wins on the same key. A config key
|
|
1957
|
+
* whose table the run's include / exclude filters leave out is dropped, since
|
|
1958
|
+
* that table has no generated type to override; every other key goes to the
|
|
1959
|
+
* generator, which rejects one naming no table or column.
|
|
1960
|
+
*
|
|
1961
|
+
* An `include` list leaves out every table it does not name, so a misspelt
|
|
1962
|
+
* table looks exactly like a real table that was left out. `databaseTables`
|
|
1963
|
+
* (every table the schema holds, before filtering) tells them apart: only a
|
|
1964
|
+
* table the database has is dropped, and a name it does not have goes on to the
|
|
1965
|
+
* generator, which names the closest match. Without it every key outside the
|
|
1966
|
+
* filters is dropped.
|
|
1967
|
+
*
|
|
1968
|
+
* @internal exported for tests.
|
|
1969
|
+
*/
|
|
1970
|
+
function generatorColumnTypes(applied, config, schema, databaseTables) {
|
|
1971
|
+
const filters = {
|
|
1972
|
+
include: config.include.length ? config.include : undefined,
|
|
1973
|
+
exclude: config.exclude.length ? config.exclude : undefined,
|
|
1974
|
+
};
|
|
1975
|
+
const merged = { ...applied?.columnTypes };
|
|
1976
|
+
for (const [key, type] of Object.entries(config.columnTypes ?? {})) {
|
|
1977
|
+
const table = key.slice(0, key.lastIndexOf('.'));
|
|
1978
|
+
const leftOut = !Object.hasOwn(schema.tables, table) && (0, introspect_js_1.applyTableFilters)([table], filters).length === 0;
|
|
1979
|
+
if (leftOut && (databaseTables === undefined || databaseTables.has(table)))
|
|
1980
|
+
continue;
|
|
1981
|
+
merged[key] = type;
|
|
1982
|
+
}
|
|
1983
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
1984
|
+
}
|
|
1985
|
+
/** The names of every table and view in `schema`, before any include / exclude filter. */
|
|
1986
|
+
async function listDatabaseTables(url, schema) {
|
|
1987
|
+
const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
1988
|
+
const client = new pg.Client({ connectionString: url });
|
|
1989
|
+
(0, connection_guard_js_1.guardConnection)(client);
|
|
1990
|
+
await client.connect();
|
|
1991
|
+
try {
|
|
1992
|
+
const res = await client.query('SELECT table_name FROM information_schema.tables WHERE table_schema = $1', [schema]);
|
|
1993
|
+
return new Set(res.rows.map((r) => r.table_name));
|
|
1994
|
+
}
|
|
1995
|
+
finally {
|
|
1996
|
+
await client.end();
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* {@link generatorColumnTypes} for a run against a database. The catalog is read
|
|
2001
|
+
* only when an `include` list is set and a config key names a table outside the
|
|
2002
|
+
* generated set, which is the one case where a typo and a table left out look
|
|
2003
|
+
* alike.
|
|
2004
|
+
*/
|
|
2005
|
+
async function generatorColumnTypesFor(applied, config, schema, url, namespace = config.schema) {
|
|
2006
|
+
const keys = Object.keys(config.columnTypes ?? {});
|
|
2007
|
+
const outsideInclude = config.include.length > 0 && keys.some((key) => !Object.hasOwn(schema.tables, key.slice(0, key.lastIndexOf('.'))));
|
|
2008
|
+
const databaseTables = outsideInclude && url ? await listDatabaseTables(url, namespace) : undefined;
|
|
2009
|
+
return generatorColumnTypes(applied, config, schema, databaseTables);
|
|
2010
|
+
}
|
|
2011
|
+
// ---------------------------------------------------------------------------
|
|
1787
2012
|
// Command: generate (pull)
|
|
1788
2013
|
// ---------------------------------------------------------------------------
|
|
1789
2014
|
async function cmdGenerate(args, config) {
|
|
@@ -1822,6 +2047,10 @@ async function cmdGenerate(args, config) {
|
|
|
1822
2047
|
for (const t of skippedInternalTables) {
|
|
1823
2048
|
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} skipped internal table ${t} (add it to include to keep it)`)}`);
|
|
1824
2049
|
}
|
|
2050
|
+
const appliedTags = await overlayCodeFirstTags(schema, config);
|
|
2051
|
+
if (appliedTags && describeAppliedTags(appliedTags)) {
|
|
2052
|
+
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} applied column tags from ${config.schemaFile}: ${describeAppliedTags(appliedTags)}`)}`);
|
|
2053
|
+
}
|
|
1825
2054
|
// Guard: zero tables means the generated client would be empty. That is almost
|
|
1826
2055
|
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
1827
2056
|
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
@@ -1846,12 +2075,12 @@ async function cmdGenerate(args, config) {
|
|
|
1846
2075
|
for (const tbl of Object.values(schema.tables)) {
|
|
1847
2076
|
const relCount = Object.keys(tbl.relations).length;
|
|
1848
2077
|
const pk = tbl.primaryKey.join(', ') || '(none)';
|
|
1849
|
-
console.log(` ${ui_js_1.symbols.tee} ${(0, ui_js_1.bold)(tbl.name)} ${(0, ui_js_1.dim)(`${tbl.columns.length} cols, PK: ${pk}`)}${relCount > 0 ? (0, ui_js_1.dim)(`, ${relCount} rels`) : ''}`);
|
|
2078
|
+
console.log(` ${ui_js_1.symbols.tee} ${(0, ui_js_1.bold)(shown(tbl.name))} ${(0, ui_js_1.dim)(`${tbl.columns.length} cols, PK: ${shown(pk)}`)}${relCount > 0 ? (0, ui_js_1.dim)(`, ${relCount} rels`) : ''}`);
|
|
1850
2079
|
}
|
|
1851
2080
|
(0, ui_js_1.newline)();
|
|
1852
2081
|
}
|
|
1853
2082
|
if (Object.keys(schema.enums).length > 0) {
|
|
1854
|
-
(0, ui_js_1.info)(`Enums: ${Object.keys(schema.enums).join(', ')}`);
|
|
2083
|
+
(0, ui_js_1.info)(`Enums: ${Object.keys(schema.enums).map(shown).join(', ')}`);
|
|
1855
2084
|
}
|
|
1856
2085
|
// Generate
|
|
1857
2086
|
const genSpinner = new ui_js_1.Spinner('Generating TypeScript client').start();
|
|
@@ -1863,6 +2092,7 @@ async function cmdGenerate(args, config) {
|
|
|
1863
2092
|
noTimestamp: args.noTimestamp,
|
|
1864
2093
|
importExtension: config.importExtension,
|
|
1865
2094
|
keepColumnNames: config.keepColumnNames,
|
|
2095
|
+
columnTypes: await generatorColumnTypesFor(appliedTags, config, schema, url),
|
|
1866
2096
|
});
|
|
1867
2097
|
genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
|
|
1868
2098
|
// List files
|
|
@@ -1879,7 +2109,12 @@ async function cmdGenerate(args, config) {
|
|
|
1879
2109
|
console.log(` ${(0, ui_js_1.dim)('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
|
|
1880
2110
|
// Name a table the client just emitted rather than a hardcoded `users`.
|
|
1881
2111
|
const sampleAccessor = (0, schema_js_1.snakeToCamel)(tableNames[0] ?? 'users');
|
|
1882
|
-
console.log(` ${(0, ui_js_1.dim)(`const row = await db.${sampleAccessor}.findFirst();`)}`);
|
|
2112
|
+
console.log(` ${(0, ui_js_1.dim)(`const row = await db.${shown(sampleAccessor)}.findFirst();`)}`);
|
|
2113
|
+
if (config.importExtension === 'ts') {
|
|
2114
|
+
(0, ui_js_1.newline)();
|
|
2115
|
+
console.log(` ${(0, ui_js_1.dim)('Run it with Node type stripping:')} ${(0, ui_js_1.cyan)('node --env-file=.env app.ts')} ${(0, ui_js_1.dim)('(Node 22.18+ or 23.6+; earlier 22.x needs --experimental-strip-types)')}`);
|
|
2116
|
+
console.log(` ${(0, ui_js_1.dim)('A type checker needs')} ${(0, ui_js_1.cyan)('allowImportingTsExtensions')} ${(0, ui_js_1.dim)('(or')} ${(0, ui_js_1.cyan)('rewriteRelativeImportExtensions')}${(0, ui_js_1.dim)(') in tsconfig.json.')}`);
|
|
2117
|
+
}
|
|
1883
2118
|
(0, ui_js_1.newline)();
|
|
1884
2119
|
}
|
|
1885
2120
|
/**
|
|
@@ -1917,8 +2152,15 @@ function resolveMigrateFromPrismaUrl(configUrl, ast, env) {
|
|
|
1917
2152
|
* concatenation, so each file is introduced by a comment naming it.
|
|
1918
2153
|
*/
|
|
1919
2154
|
function readPrismaSchemaSource(path) {
|
|
1920
|
-
|
|
2155
|
+
// Read first, ask what it is only if that fails: a stat followed by a read
|
|
2156
|
+
// can disagree with itself when the path is replaced in between.
|
|
2157
|
+
try {
|
|
1921
2158
|
return (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
2159
|
+
}
|
|
2160
|
+
catch (err) {
|
|
2161
|
+
if (err.code !== 'EISDIR')
|
|
2162
|
+
throw err;
|
|
2163
|
+
}
|
|
1922
2164
|
const files = (0, node_fs_1.readdirSync)(path)
|
|
1923
2165
|
.filter((f) => f.endsWith('.prisma'))
|
|
1924
2166
|
.sort();
|
|
@@ -1953,7 +2195,7 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1953
2195
|
catch (err) {
|
|
1954
2196
|
(0, ui_js_1.newline)();
|
|
1955
2197
|
(0, ui_js_1.error)(`Could not read ${(0, ui_js_1.cyan)(prismaPath)}`);
|
|
1956
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.red)(
|
|
2198
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.red)((0, error_text_js_1.errorText)(err))}`);
|
|
1957
2199
|
(0, ui_js_1.errorLine)();
|
|
1958
2200
|
process.exit(1);
|
|
1959
2201
|
}
|
|
@@ -2076,6 +2318,8 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
2076
2318
|
noTimestamp: args.noTimestamp,
|
|
2077
2319
|
importExtension: config.importExtension,
|
|
2078
2320
|
keepColumnNames: config.keepColumnNames,
|
|
2321
|
+
// No schema file is read here, so only the config file's overrides apply.
|
|
2322
|
+
columnTypes: await generatorColumnTypesFor(null, config, schemaMeta, url, 'public'),
|
|
2079
2323
|
});
|
|
2080
2324
|
for (const file of gen.files) {
|
|
2081
2325
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)((0, node_path_1.join)(outDir, file))} ${(0, ui_js_1.dim)('(client)')}`);
|
|
@@ -2108,9 +2352,26 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
2108
2352
|
// ---------------------------------------------------------------------------
|
|
2109
2353
|
// Command: push
|
|
2110
2354
|
// ---------------------------------------------------------------------------
|
|
2355
|
+
/**
|
|
2356
|
+
* Print what a diff did NOT compare, under an "in sync" style message.
|
|
2357
|
+
*
|
|
2358
|
+
* "Already in sync" only means the compared surface matches. The rest is listed
|
|
2359
|
+
* so the sentence is not read as a guarantee about the whole database.
|
|
2360
|
+
*/
|
|
2361
|
+
function printNotCompared(items) {
|
|
2362
|
+
if (!items || items.length === 0)
|
|
2363
|
+
return;
|
|
2364
|
+
console.log(` ${(0, ui_js_1.dim)('Not compared, so this says nothing about:')}`);
|
|
2365
|
+
for (const item of items)
|
|
2366
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.dot)} ${(0, ui_js_1.dim)(item)}`);
|
|
2367
|
+
(0, ui_js_1.newline)();
|
|
2368
|
+
}
|
|
2111
2369
|
async function cmdPush(args, config) {
|
|
2112
2370
|
(0, ui_js_1.banner)();
|
|
2113
2371
|
refuseSchemaFilePath(config);
|
|
2372
|
+
if (args.exitCode && !args.dryRun) {
|
|
2373
|
+
failArg(`${(0, ui_js_1.cyan)('--exit-code')} needs ${(0, ui_js_1.cyan)('--dry-run')} on push.`, 'It reports whether the database differs from the schema file (exit 2) without changing it.', 'Example: npx turbine push --dry-run --exit-code');
|
|
2374
|
+
}
|
|
2114
2375
|
const url = requireUrl(config);
|
|
2115
2376
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2116
2377
|
(0, ui_js_1.label)('Schema file', config.schemaFile);
|
|
@@ -2122,8 +2383,32 @@ async function cmdPush(args, config) {
|
|
|
2122
2383
|
const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
|
|
2123
2384
|
const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
|
|
2124
2385
|
if (diff.statements.length === 0 && diff.drop.length === 0) {
|
|
2125
|
-
|
|
2386
|
+
// "In sync" is a claim about everything the diff looked at, so it is made
|
|
2387
|
+
// only when the diff also has nothing to say. A difference it cannot apply
|
|
2388
|
+
// (an undeclared index, a reordered enum) is reported as exactly that.
|
|
2389
|
+
const advisories = diff.warnings ?? [];
|
|
2390
|
+
if (advisories.length === 0) {
|
|
2391
|
+
diffSpinner.succeed('Database is already in sync');
|
|
2392
|
+
}
|
|
2393
|
+
else {
|
|
2394
|
+
diffSpinner.succeed('Nothing to apply, but the database is not fully in sync');
|
|
2395
|
+
}
|
|
2126
2396
|
(0, ui_js_1.newline)();
|
|
2397
|
+
for (const w of advisories)
|
|
2398
|
+
(0, ui_js_1.warn)(w);
|
|
2399
|
+
if (advisories.length > 0)
|
|
2400
|
+
(0, ui_js_1.newline)();
|
|
2401
|
+
printNotCompared(diff.notCompared);
|
|
2402
|
+
// A warning is a difference the diff found and cannot apply (a unique index
|
|
2403
|
+
// that is not unique, a removed enum label), so a gate fails on it. What the
|
|
2404
|
+
// diff only discloses as "not compared" is not a difference it found, and a
|
|
2405
|
+
// CHECK expression that merely reads differently is a warning it cannot
|
|
2406
|
+
// confirm (driftWarnings leaves it out).
|
|
2407
|
+
if (args.exitCode && (0, schema_sql_js_1.driftWarnings)(diff).length > 0) {
|
|
2408
|
+
(0, ui_js_1.warn)(`The database differs from the schema file ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
|
|
2409
|
+
(0, ui_js_1.newline)();
|
|
2410
|
+
process.exit(2);
|
|
2411
|
+
}
|
|
2127
2412
|
return;
|
|
2128
2413
|
}
|
|
2129
2414
|
diffSpinner.succeed('Found changes');
|
|
@@ -2180,6 +2465,11 @@ async function cmdPush(args, config) {
|
|
|
2180
2465
|
if (args.dryRun) {
|
|
2181
2466
|
(0, ui_js_1.info)('Dry run, no changes applied.');
|
|
2182
2467
|
(0, ui_js_1.newline)();
|
|
2468
|
+
if (args.exitCode) {
|
|
2469
|
+
(0, ui_js_1.warn)(`The database differs from the schema file ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
|
|
2470
|
+
(0, ui_js_1.newline)();
|
|
2471
|
+
process.exit(2);
|
|
2472
|
+
}
|
|
2183
2473
|
return;
|
|
2184
2474
|
}
|
|
2185
2475
|
if (args.allowDestructive) {
|
|
@@ -2253,8 +2543,12 @@ async function cmdMigrate(args, config) {
|
|
|
2253
2543
|
console.log(` ${(0, ui_js_1.cyan)('--recipe <name>')} Scaffold a sanctioned migration pattern`);
|
|
2254
2544
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
|
|
2255
2545
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
2546
|
+
console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 when ${(0, ui_js_1.cyan)('status')} finds pending, modified, missing or misnamed migrations`);
|
|
2547
|
+
console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Make ${(0, ui_js_1.cyan)('deploy')} wait for a concurrent deploy's lock`);
|
|
2548
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Let ${(0, ui_js_1.cyan)('--auto')} / ${(0, ui_js_1.cyan)('--from-diff')} run with migrations pending`);
|
|
2256
2549
|
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting`);
|
|
2257
2550
|
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
|
|
2551
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Run ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} against a transaction pooler anyway ${(0, ui_js_1.dim)('(refused by default: the migration lock is session-level and a pooler can release it on another backend)')}`);
|
|
2258
2552
|
(0, ui_js_1.newline)();
|
|
2259
2553
|
console.log(` ${(0, ui_js_1.bold)('Recipes')} ${(0, ui_js_1.dim)('(--recipe):')}`);
|
|
2260
2554
|
for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
|
|
@@ -2272,6 +2566,23 @@ async function cmdMigrate(args, config) {
|
|
|
2272
2566
|
(0, ui_js_1.newline)();
|
|
2273
2567
|
return;
|
|
2274
2568
|
}
|
|
2569
|
+
// Flags that change what a subcommand does are refused where they do nothing,
|
|
2570
|
+
// rather than accepted and ignored: a CI gate that silently never gates is
|
|
2571
|
+
// worse than one that will not start.
|
|
2572
|
+
if (args.exitCode && sub !== 'status' && sub !== 'list') {
|
|
2573
|
+
failArg(`${(0, ui_js_1.cyan)('--exit-code')} only applies to ${(0, ui_js_1.cyan)('migrate status')} (and ${(0, ui_js_1.cyan)('push --dry-run')}).`, 'Example: npx turbine migrate status --exit-code');
|
|
2574
|
+
}
|
|
2575
|
+
if (args.wait !== undefined && sub !== 'deploy') {
|
|
2576
|
+
failArg(`${(0, ui_js_1.cyan)('--wait')} only applies to ${(0, ui_js_1.cyan)('migrate deploy')}.`, 'Example: npx turbine migrate deploy --wait=120');
|
|
2577
|
+
}
|
|
2578
|
+
// The commands that take the migration lock, before the banner and before any
|
|
2579
|
+
// connection. A dry run takes no lock, so it may use any endpoint.
|
|
2580
|
+
if ((sub === 'up' || sub === 'deploy' || sub === 'down') && args.dryRun !== true && config.url) {
|
|
2581
|
+
refusePoolerConnection(config.url, args, {
|
|
2582
|
+
command: `turbine migrate ${sub}`,
|
|
2583
|
+
explanation: exports.MIGRATE_POOLER_EXPLANATION,
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2275
2586
|
switch (sub) {
|
|
2276
2587
|
case 'create':
|
|
2277
2588
|
await cmdMigrateCreate(args, config);
|
|
@@ -2295,6 +2606,58 @@ async function cmdMigrate(args, config) {
|
|
|
2295
2606
|
process.exit(1);
|
|
2296
2607
|
}
|
|
2297
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Stop `migrate create --auto` / `--from-diff` while migrations are pending.
|
|
2611
|
+
*
|
|
2612
|
+
* Both diff the schema file against the live database, and a pending migration
|
|
2613
|
+
* is a change the file already describes and the database does not have yet. The
|
|
2614
|
+
* new migration would repeat it and fail on the first database that runs the
|
|
2615
|
+
* pending one first. See {@link formatPendingAutoRefusal}.
|
|
2616
|
+
*/
|
|
2617
|
+
async function refuseWhenMigrationsPending(args, config, url, flag) {
|
|
2618
|
+
if ((0, migrate_js_1.listMigrationFiles)(config.migrationsDir).length === 0)
|
|
2619
|
+
return;
|
|
2620
|
+
const pending = (0, migrate_js_1.pendingMigrationFiles)(await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema }));
|
|
2621
|
+
if (pending.length === 0)
|
|
2622
|
+
return;
|
|
2623
|
+
if (args.allowPending) {
|
|
2624
|
+
(0, ui_js_1.warn)(`--allow-pending is set: ${(0, ui_js_1.bold)(String(pending.length))} pending migration(s) are NOT reflected in the diff.`);
|
|
2625
|
+
(0, ui_js_1.newline)();
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
const [first, ...rest] = (0, migrate_js_1.formatPendingAutoRefusal)(pending, flag).split('\n');
|
|
2629
|
+
(0, ui_js_1.error)(first);
|
|
2630
|
+
for (const line of rest)
|
|
2631
|
+
(0, ui_js_1.errorLine)(line);
|
|
2632
|
+
(0, ui_js_1.errorLine)();
|
|
2633
|
+
process.exit(1);
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Explain an empty `--auto` / `--from-diff` diff without claiming more than it
|
|
2637
|
+
* knows: "in sync" only when nothing, extra tables and advisories included, was
|
|
2638
|
+
* left over.
|
|
2639
|
+
*/
|
|
2640
|
+
function reportNothingToMigrate(diff, spinner) {
|
|
2641
|
+
const advisories = diff.warnings ?? [];
|
|
2642
|
+
if (diff.drop.length === 0 && advisories.length === 0) {
|
|
2643
|
+
spinner.succeed('Database is already in sync: nothing to migrate');
|
|
2644
|
+
(0, ui_js_1.newline)();
|
|
2645
|
+
printNotCompared(diff.notCompared);
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
spinner.succeed('Nothing to migrate: no statement to write, but the database is not fully in sync');
|
|
2649
|
+
(0, ui_js_1.newline)();
|
|
2650
|
+
if (diff.drop.length > 0) {
|
|
2651
|
+
(0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(diff.drop.length))} table(s) exist in the database but not in the schema file ` +
|
|
2652
|
+
`(${diff.drop.join(', ')}). A migration never drops a table on its own: write that one by hand.`);
|
|
2653
|
+
(0, ui_js_1.newline)();
|
|
2654
|
+
}
|
|
2655
|
+
for (const w of advisories)
|
|
2656
|
+
(0, ui_js_1.warn)(w);
|
|
2657
|
+
if (advisories.length > 0)
|
|
2658
|
+
(0, ui_js_1.newline)();
|
|
2659
|
+
printNotCompared(diff.notCompared);
|
|
2660
|
+
}
|
|
2298
2661
|
async function cmdMigrateCreate(args, config) {
|
|
2299
2662
|
(0, ui_js_1.banner)();
|
|
2300
2663
|
// `--auto` / `--from-diff` diff the schema FILE against the namespace, so both
|
|
@@ -2324,18 +2687,21 @@ async function cmdMigrateCreate(args, config) {
|
|
|
2324
2687
|
(0, ui_js_1.errorLine)();
|
|
2325
2688
|
process.exit(1);
|
|
2326
2689
|
}
|
|
2690
|
+
if (args.allowPending && !args.auto && !args.fromDiff) {
|
|
2691
|
+
failArg(`${(0, ui_js_1.cyan)('--allow-pending')} only applies to ${(0, ui_js_1.cyan)('--auto')} and ${(0, ui_js_1.cyan)('--from-diff')}.`, 'It lets a diff-generated migration proceed while earlier migrations are still pending.');
|
|
2692
|
+
}
|
|
2327
2693
|
if (args.auto) {
|
|
2328
2694
|
// Auto-generate migration from schema diff
|
|
2329
2695
|
const url = requireUrl(config);
|
|
2330
2696
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2331
2697
|
(0, ui_js_1.label)('Schema file', config.schemaFile);
|
|
2332
2698
|
(0, ui_js_1.newline)();
|
|
2699
|
+
await refuseWhenMigrationsPending(args, config, url, '--auto');
|
|
2333
2700
|
const schemaDef = await loadSchemaFile(config.schemaFile);
|
|
2334
2701
|
const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
|
|
2335
2702
|
const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
|
|
2336
2703
|
if (diff.statements.length === 0) {
|
|
2337
|
-
|
|
2338
|
-
(0, ui_js_1.newline)();
|
|
2704
|
+
reportNothingToMigrate(diff, diffSpinner);
|
|
2339
2705
|
return;
|
|
2340
2706
|
}
|
|
2341
2707
|
diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
|
|
@@ -2387,7 +2753,9 @@ async function cmdMigrateCreate(args, config) {
|
|
|
2387
2753
|
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
|
|
2388
2754
|
}
|
|
2389
2755
|
(0, ui_js_1.newline)();
|
|
2390
|
-
console.log(
|
|
2756
|
+
console.log(body.destructiveUp.length > 0
|
|
2757
|
+
? ` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`
|
|
2758
|
+
: ` ${(0, ui_js_1.dim)('Only the DOWN section is destructive: `migrate up` runs this file without a prompt, `migrate down` asks first.')}`);
|
|
2391
2759
|
(0, ui_js_1.newline)();
|
|
2392
2760
|
}
|
|
2393
2761
|
if (diff.warnings && diff.warnings.length > 0) {
|
|
@@ -2407,12 +2775,12 @@ async function cmdMigrateCreate(args, config) {
|
|
|
2407
2775
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2408
2776
|
(0, ui_js_1.label)('Schema file', config.schemaFile);
|
|
2409
2777
|
(0, ui_js_1.newline)();
|
|
2778
|
+
await refuseWhenMigrationsPending(args, config, url, '--from-diff');
|
|
2410
2779
|
const schemaDef = await loadSchemaFile(config.schemaFile);
|
|
2411
2780
|
const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
|
|
2412
2781
|
const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
|
|
2413
2782
|
if (diff.statements.length === 0) {
|
|
2414
|
-
|
|
2415
|
-
(0, ui_js_1.newline)();
|
|
2783
|
+
reportNothingToMigrate(diff, diffSpinner);
|
|
2416
2784
|
return;
|
|
2417
2785
|
}
|
|
2418
2786
|
diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
|
|
@@ -2459,7 +2827,9 @@ async function cmdMigrateCreate(args, config) {
|
|
|
2459
2827
|
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
|
|
2460
2828
|
}
|
|
2461
2829
|
(0, ui_js_1.newline)();
|
|
2462
|
-
console.log(
|
|
2830
|
+
console.log(body.destructiveUp.length > 0
|
|
2831
|
+
? ` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`
|
|
2832
|
+
: ` ${(0, ui_js_1.dim)('Only the DOWN section is destructive: `migrate up` runs this file without a prompt, `migrate down` asks first.')}`);
|
|
2463
2833
|
(0, ui_js_1.newline)();
|
|
2464
2834
|
}
|
|
2465
2835
|
if (diff.warnings && diff.warnings.length > 0) {
|
|
@@ -2780,7 +3150,18 @@ async function cmdMigrateDeploy(args, config) {
|
|
|
2780
3150
|
if (destructive.length > 0)
|
|
2781
3151
|
printDestructiveNotice(destructive);
|
|
2782
3152
|
const runSpinner = new ui_js_1.Spinner('Deploying migrations').start();
|
|
2783
|
-
const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, {
|
|
3153
|
+
const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, {
|
|
3154
|
+
allowDrift: args.allowDrift,
|
|
3155
|
+
schema: config.schema,
|
|
3156
|
+
lockWaitMs: args.wait !== undefined ? args.wait * 1000 : undefined,
|
|
3157
|
+
onLockWait: () => {
|
|
3158
|
+
runSpinner.stop();
|
|
3159
|
+
(0, ui_js_1.warn)(`Another migration holds the lock: waiting up to ${(0, ui_js_1.bold)(String(args.wait))}s for it to finish.`);
|
|
3160
|
+
console.log(` ${(0, ui_js_1.dim)('Whatever it applies is then no longer pending, so this run will skip it.')}`);
|
|
3161
|
+
(0, ui_js_1.newline)();
|
|
3162
|
+
runSpinner.start();
|
|
3163
|
+
},
|
|
3164
|
+
});
|
|
2784
3165
|
if (result.applied.length > 0) {
|
|
2785
3166
|
runSpinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
|
|
2786
3167
|
for (const file of result.applied) {
|
|
@@ -2907,6 +3288,15 @@ async function cmdMigrateDown(args, config) {
|
|
|
2907
3288
|
for (const file of result.rolledBack) {
|
|
2908
3289
|
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.arrowRight)} ${file.filename}`);
|
|
2909
3290
|
}
|
|
3291
|
+
const noSql = (0, migrate_js_1.rollbacksWithoutSql)(result.rolledBack);
|
|
3292
|
+
if (noSql.length > 0) {
|
|
3293
|
+
(0, ui_js_1.newline)();
|
|
3294
|
+
(0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(noSql.length))} rolled-back migration(s) have a DOWN section that runs no SQL:`);
|
|
3295
|
+
for (const file of noSql)
|
|
3296
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
|
|
3297
|
+
console.log(` ${(0, ui_js_1.dim)('Their tracking rows are removed, but nothing they changed was undone (a dropped column')}`);
|
|
3298
|
+
console.log(` ${(0, ui_js_1.dim)('cannot be restored automatically). Restore that data from a backup.')}`);
|
|
3299
|
+
}
|
|
2910
3300
|
}
|
|
2911
3301
|
else if (result.errors.length === 0) {
|
|
2912
3302
|
spinner.succeed('No migrations to roll back');
|
|
@@ -2930,20 +3320,24 @@ async function cmdMigrateDown(args, config) {
|
|
|
2930
3320
|
}
|
|
2931
3321
|
(0, ui_js_1.newline)();
|
|
2932
3322
|
}
|
|
2933
|
-
async function cmdMigrateStatus(
|
|
3323
|
+
async function cmdMigrateStatus(args, config) {
|
|
2934
3324
|
(0, ui_js_1.banner)();
|
|
2935
3325
|
const url = requireUrl(config);
|
|
2936
3326
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2937
3327
|
(0, ui_js_1.label)('Migrations', config.migrationsDir);
|
|
2938
3328
|
(0, ui_js_1.newline)();
|
|
2939
3329
|
const allFiles = (0, migrate_js_1.listMigrationFiles)(config.migrationsDir);
|
|
2940
|
-
|
|
3330
|
+
// A gate still has to ask the database when there are no files: applied
|
|
3331
|
+
// migrations whose files are gone are exactly the drift it exists to catch.
|
|
3332
|
+
if (allFiles.length === 0 && !args.exitCode) {
|
|
2941
3333
|
(0, ui_js_1.warn)('No migration files found.');
|
|
2942
3334
|
console.log(` ${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
|
|
2943
3335
|
(0, ui_js_1.newline)();
|
|
2944
3336
|
return;
|
|
2945
3337
|
}
|
|
2946
3338
|
warnIgnoredMigrationFiles(config.migrationsDir);
|
|
3339
|
+
if (allFiles.length === 0)
|
|
3340
|
+
(0, ui_js_1.warn)('No migration files found.');
|
|
2947
3341
|
const statuses = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema });
|
|
2948
3342
|
const appliedCount = statuses.filter((s) => s.applied).length;
|
|
2949
3343
|
const pendingCount = statuses.filter((s) => !s.applied).length;
|
|
@@ -2998,6 +3392,14 @@ async function cmdMigrateStatus(_args, config) {
|
|
|
2998
3392
|
console.log(` ${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine migrate up')} ${(0, ui_js_1.dim)('to apply pending migrations.')}`);
|
|
2999
3393
|
(0, ui_js_1.newline)();
|
|
3000
3394
|
}
|
|
3395
|
+
if (args.exitCode) {
|
|
3396
|
+
const problems = (0, migrate_js_1.statusGateProblems)((0, migrate_js_1.summarizeMigrationStatus)(statuses), (0, migrate_js_1.listIgnoredSqlFiles)(config.migrationsDir).map(introspect_js_1.printableCatalogName));
|
|
3397
|
+
if (problems.length > 0) {
|
|
3398
|
+
(0, ui_js_1.warn)(`Migration history is not clean: ${problems.join(', ')} ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
|
|
3399
|
+
(0, ui_js_1.newline)();
|
|
3400
|
+
process.exit(2);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3001
3403
|
}
|
|
3002
3404
|
function getSeedExecutionPlan(seedFile) {
|
|
3003
3405
|
const ext = (0, node_path_1.extname)(seedFile).toLowerCase();
|
|
@@ -3056,6 +3458,9 @@ async function seedConnectionString(config) {
|
|
|
3056
3458
|
}
|
|
3057
3459
|
return (0, migrate_js_1.connectionStringForSchema)(config.url, schema, inherited);
|
|
3058
3460
|
}
|
|
3461
|
+
/** The notice for a {@link SeedRunOutcome} that could not be confirmed. */
|
|
3462
|
+
const UNCONFIRMED_SEED_NOTICE = 'The seed file ran to completion, but it does not call defineSeed(), so Turbine cannot tell whether it wrote anything. ' +
|
|
3463
|
+
'Wrap the work in defineSeed(async (db) => { ... }) to have it confirmed.';
|
|
3059
3464
|
async function runSeedPlan(plan, config) {
|
|
3060
3465
|
const oldDatabaseUrl = process.env.DATABASE_URL;
|
|
3061
3466
|
// The seed connects through DATABASE_URL, whichever way it runs: `defineSeed`
|
|
@@ -3075,9 +3480,11 @@ async function runSeedPlan(plan, config) {
|
|
|
3075
3480
|
}
|
|
3076
3481
|
// The seed runs in a child process, so we cannot observe its callback
|
|
3077
3482
|
// directly. Hand it a sentinel path: `defineSeed`'s runner writes the file
|
|
3078
|
-
// only after a callback executes to completion.
|
|
3079
|
-
//
|
|
3080
|
-
//
|
|
3483
|
+
// only after a callback executes to completion. A non-zero exit is a
|
|
3484
|
+
// failure. A clean exit with no sentinel is a script that did its work at
|
|
3485
|
+
// the top level (a typed-client seed), which succeeds and says so, unless
|
|
3486
|
+
// the file names `defineSeed`: then its callback was expected to run and
|
|
3487
|
+
// did not, which is a no-op to report as a failure.
|
|
3081
3488
|
const sentinelDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'turbine-seed-'));
|
|
3082
3489
|
const sentinel = (0, node_path_1.join)(sentinelDir, 'ran');
|
|
3083
3490
|
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
@@ -3090,15 +3497,18 @@ async function runSeedPlan(plan, config) {
|
|
|
3090
3497
|
TURBINE_SEED_SENTINEL: sentinel,
|
|
3091
3498
|
},
|
|
3092
3499
|
});
|
|
3093
|
-
if (
|
|
3500
|
+
if ((0, node_fs_1.existsSync)(sentinel))
|
|
3501
|
+
return { confirmed: true };
|
|
3502
|
+
const seedFile = plan.args[plan.args.length - 1] ?? '';
|
|
3503
|
+
if (/\bdefineSeed\b/.test((0, node_fs_1.readFileSync)(seedFile, 'utf-8'))) {
|
|
3094
3504
|
throw new Error('The seed file completed without running a seed callback. ' +
|
|
3095
3505
|
'Make sure it calls defineSeed(async (db) => { ... }) at the top level.');
|
|
3096
3506
|
}
|
|
3507
|
+
return { confirmed: false };
|
|
3097
3508
|
}
|
|
3098
3509
|
finally {
|
|
3099
3510
|
(0, node_fs_1.rmSync)(sentinelDir, { recursive: true, force: true });
|
|
3100
3511
|
}
|
|
3101
|
-
return;
|
|
3102
3512
|
}
|
|
3103
3513
|
if (plan.kind === 'js') {
|
|
3104
3514
|
const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(plan.file).href}`).then(s => __importStar(require(s)));
|
|
@@ -3107,7 +3517,7 @@ async function runSeedPlan(plan, config) {
|
|
|
3107
3517
|
'Export your seed with `export default defineSeed(async (db) => { ... })`.');
|
|
3108
3518
|
}
|
|
3109
3519
|
await mod.default();
|
|
3110
|
-
return;
|
|
3520
|
+
return { confirmed: true };
|
|
3111
3521
|
}
|
|
3112
3522
|
// `seedUrl` is already pinned and already schema-checked; `requireUrl` is
|
|
3113
3523
|
// only reached when no url was configured at all, in which case there is no
|
|
@@ -3123,6 +3533,7 @@ async function runSeedPlan(plan, config) {
|
|
|
3123
3533
|
finally {
|
|
3124
3534
|
await client.end();
|
|
3125
3535
|
}
|
|
3536
|
+
return { confirmed: true };
|
|
3126
3537
|
}
|
|
3127
3538
|
finally {
|
|
3128
3539
|
if (oldDatabaseUrl === undefined) {
|
|
@@ -3148,14 +3559,14 @@ async function cmdSeed(_args, config) {
|
|
|
3148
3559
|
}
|
|
3149
3560
|
const spinner = new ui_js_1.Spinner('Running seed file').start();
|
|
3150
3561
|
try {
|
|
3151
|
-
await runSeedPlan(getSeedExecutionPlan(seedFile), config);
|
|
3562
|
+
const outcome = await runSeedPlan(getSeedExecutionPlan(seedFile), config);
|
|
3152
3563
|
spinner.succeed('Seed completed');
|
|
3564
|
+
if (!outcome.confirmed)
|
|
3565
|
+
(0, ui_js_1.warn)(UNCONFIRMED_SEED_NOTICE);
|
|
3153
3566
|
}
|
|
3154
3567
|
catch (err) {
|
|
3155
3568
|
spinner.fail('Seed failed');
|
|
3156
|
-
|
|
3157
|
-
console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
|
|
3158
|
-
}
|
|
3569
|
+
console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err)))}`);
|
|
3159
3570
|
(0, ui_js_1.newline)();
|
|
3160
3571
|
process.exit(1);
|
|
3161
3572
|
}
|
|
@@ -3186,7 +3597,7 @@ async function cmdStatus(_args, config) {
|
|
|
3186
3597
|
for (const tbl of Object.values(schema.tables)) {
|
|
3187
3598
|
const relCount = Object.keys(tbl.relations).length;
|
|
3188
3599
|
const _pk = tbl.primaryKey.join(', ') || (0, ui_js_1.dim)('(none)');
|
|
3189
|
-
console.log(` ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(tbl.name))}`);
|
|
3600
|
+
console.log(` ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(tbl.name)))}`);
|
|
3190
3601
|
for (let i = 0; i < tbl.columns.length; i++) {
|
|
3191
3602
|
const col = tbl.columns[i];
|
|
3192
3603
|
const isLast = i === tbl.columns.length - 1 && relCount === 0;
|
|
@@ -3194,7 +3605,7 @@ async function cmdStatus(_args, config) {
|
|
|
3194
3605
|
const nullable = col.nullable ? (0, ui_js_1.dim)('?') : '';
|
|
3195
3606
|
const def = col.hasDefault ? (0, ui_js_1.dim)(' (default)') : '';
|
|
3196
3607
|
const pkLabel = tbl.primaryKey.includes(col.name) ? ` ${(0, ui_js_1.magenta)('PK')}` : '';
|
|
3197
|
-
console.log(` ${(0, ui_js_1.dim)(prefix)} ${col.field}${nullable}: ${(0, ui_js_1.green)(col.tsType)}${pkLabel}${def} ${(0, ui_js_1.gray)(`${ui_js_1.symbols.arrow} ${col.pgType}`)}`);
|
|
3608
|
+
console.log(` ${(0, ui_js_1.dim)(prefix)} ${shown(col.field)}${nullable}: ${(0, ui_js_1.green)(shown(col.tsType))}${pkLabel}${def} ${(0, ui_js_1.gray)(`${ui_js_1.symbols.arrow} ${shown(col.pgType)}`)}`);
|
|
3198
3609
|
}
|
|
3199
3610
|
const rels = Object.entries(tbl.relations);
|
|
3200
3611
|
if (rels.length > 0) {
|
|
@@ -3204,7 +3615,7 @@ async function cmdStatus(_args, config) {
|
|
|
3204
3615
|
const prefix = isLast ? ui_js_1.symbols.teeEnd : ui_js_1.symbols.tee;
|
|
3205
3616
|
const relColor = rel.type === 'hasMany' ? ui_js_1.blue : ui_js_1.yellow;
|
|
3206
3617
|
const fkDisplay = Array.isArray(rel.foreignKey) ? rel.foreignKey.join(', ') : rel.foreignKey;
|
|
3207
|
-
console.log(` ${(0, ui_js_1.dim)(prefix)} ${relColor(relName)} ${(0, ui_js_1.dim)(ui_js_1.symbols.arrow)} ${rel.to} ${(0, ui_js_1.dim)(`(${rel.type}, FK: ${fkDisplay})`)}`);
|
|
3618
|
+
console.log(` ${(0, ui_js_1.dim)(prefix)} ${relColor(shown(relName))} ${(0, ui_js_1.dim)(ui_js_1.symbols.arrow)} ${shown(rel.to)} ${(0, ui_js_1.dim)(`(${rel.type}, FK: ${shown(fkDisplay)})`)}`);
|
|
3208
3619
|
}
|
|
3209
3620
|
}
|
|
3210
3621
|
(0, ui_js_1.newline)();
|
|
@@ -3212,7 +3623,7 @@ async function cmdStatus(_args, config) {
|
|
|
3212
3623
|
if (Object.keys(schema.enums).length > 0) {
|
|
3213
3624
|
console.log(` ${(0, ui_js_1.bold)('Enums:')}`);
|
|
3214
3625
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
3215
|
-
console.log(` ${(0, ui_js_1.cyan)(enumName)}: ${labels.map((l) => (0, ui_js_1.green)(`'${l}'`)).join((0, ui_js_1.dim)(' | '))}`);
|
|
3626
|
+
console.log(` ${(0, ui_js_1.cyan)(shown(enumName))}: ${labels.map((l) => (0, ui_js_1.green)(`'${shown(l)}'`)).join((0, ui_js_1.dim)(' | '))}`);
|
|
3216
3627
|
}
|
|
3217
3628
|
(0, ui_js_1.newline)();
|
|
3218
3629
|
}
|
|
@@ -3270,6 +3681,23 @@ const CONCURRENTLY_RECIPE_COMMENT = [
|
|
|
3270
3681
|
'-- makes "migrate up" look hung. For bounded waits, SET lock_timeout /',
|
|
3271
3682
|
'-- statement_timeout in a psql session.',
|
|
3272
3683
|
].join('\n');
|
|
3684
|
+
/**
|
|
3685
|
+
* Why `migrate up`, `deploy` and `down` refuse a pooled endpoint. The reason is
|
|
3686
|
+
* the migration lock, not the doctor's session-state argument.
|
|
3687
|
+
*/
|
|
3688
|
+
exports.MIGRATE_POOLER_EXPLANATION = [
|
|
3689
|
+
'turbine migrate serializes runs with a session-level advisory lock (pg_try_advisory_lock).',
|
|
3690
|
+
'That lock belongs to one server backend and only means something while the same backend',
|
|
3691
|
+
'serves every statement of the run. A transaction pooler (PgBouncer, a Neon "-pooler"',
|
|
3692
|
+
"endpoint, Supabase's pooler) hands each transaction to whichever backend is free, so:",
|
|
3693
|
+
'',
|
|
3694
|
+
' 1. The lock is not tied to this run. It can be taken on one backend and released on',
|
|
3695
|
+
' another, which leaves it held on a shared backend (later runs are told another',
|
|
3696
|
+
' migration is in progress) and lets two runs apply the same file.',
|
|
3697
|
+
' 2. Session state a migration file sets (SET lock_timeout, SET search_path) is left on',
|
|
3698
|
+
' a shared backend for other clients, and state an earlier client left there applies',
|
|
3699
|
+
' to this run.',
|
|
3700
|
+
];
|
|
3273
3701
|
/**
|
|
3274
3702
|
* Refuse a `doctor` run whose connection string points at a transaction pooler.
|
|
3275
3703
|
*
|
|
@@ -3291,13 +3719,13 @@ const CONCURRENTLY_RECIPE_COMMENT = [
|
|
|
3291
3719
|
* Exported for the unit test only (like {@link isLoopbackHost}): a gate whose
|
|
3292
3720
|
* failure path is `process.exit` is untestable through the command itself.
|
|
3293
3721
|
*/
|
|
3294
|
-
function refusePoolerConnection(url, args) {
|
|
3722
|
+
function refusePoolerConnection(url, args, refusal = { command: 'turbine doctor' }) {
|
|
3295
3723
|
if (args.allowPooler === true)
|
|
3296
3724
|
return;
|
|
3297
3725
|
const detection = (0, connection_url_js_1.detectPooler)(url);
|
|
3298
3726
|
if (!detection.pooled)
|
|
3299
3727
|
return;
|
|
3300
|
-
const lines = (0, connection_url_js_1.poolerRefusalMessage)(detection, {
|
|
3728
|
+
const lines = (0, connection_url_js_1.poolerRefusalMessage)(detection, { ...refusal, allowFlag: '--allow-pooler' });
|
|
3301
3729
|
console.error('');
|
|
3302
3730
|
console.error(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${lines[0]}`);
|
|
3303
3731
|
for (const line of lines.slice(1))
|
|
@@ -3365,7 +3793,7 @@ async function cmdDoctor(args, config) {
|
|
|
3365
3793
|
tables: {},
|
|
3366
3794
|
indexes: [],
|
|
3367
3795
|
nullFrac: {},
|
|
3368
|
-
notices: [`statistics collection failed: ${
|
|
3796
|
+
notices: [`statistics collection failed: ${(0, error_text_js_1.errorText)(err)}`],
|
|
3369
3797
|
};
|
|
3370
3798
|
}
|
|
3371
3799
|
// Table-heat boost: read _turbine_metrics (app DB or --metrics-url) and use
|
|
@@ -3590,6 +4018,12 @@ function renderUnusedCaveats(minScans, snapshot) {
|
|
|
3590
4018
|
}
|
|
3591
4019
|
(0, ui_js_1.newline)();
|
|
3592
4020
|
}
|
|
4021
|
+
/**
|
|
4022
|
+
* Catalog names go to the terminal through this. A database lets its creator
|
|
4023
|
+
* pick any characters for a table or index name, and a raw ESC or bidi control
|
|
4024
|
+
* in one rewrites the line it is printed on.
|
|
4025
|
+
*/
|
|
4026
|
+
const shown = introspect_js_1.printableCatalogName;
|
|
3593
4027
|
/** doctor --unused: never-scanned indexes with DROP suggestions (report-only). */
|
|
3594
4028
|
function renderUnusedIndexes(unused, minScans, snapshot) {
|
|
3595
4029
|
if (unused.length === 0)
|
|
@@ -3598,12 +4032,12 @@ function renderUnusedIndexes(unused, minScans, snapshot) {
|
|
|
3598
4032
|
(0, ui_js_1.warn)(`Found ${(0, ui_js_1.bold)(String(unused.length))} never-scanned index(es) (${(0, index_stats_js_1.formatBytes)(total)} reclaimable).`);
|
|
3599
4033
|
renderUnusedCaveats(minScans, snapshot);
|
|
3600
4034
|
for (const u of unused) {
|
|
3601
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(u.table))} ${(0, ui_js_1.dim)(`(${u.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(`${u.indexName} · ${u.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(u.sizeBytes)}`)}`);
|
|
4035
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(u.table)))} ${(0, ui_js_1.dim)(`(${shown(u.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(`${shown(u.indexName)} · ${u.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(u.sizeBytes)}`)}`);
|
|
3602
4036
|
// A functional / partial / non-btree index is not a plain `(col)` rebuild:
|
|
3603
4037
|
// say what it actually is before anyone runs the DROP.
|
|
3604
4038
|
if (u.caveat)
|
|
3605
4039
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.yellow)(u.caveat)}`);
|
|
3606
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(u.dropSql)}`);
|
|
4040
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(u.dropSql))}`);
|
|
3607
4041
|
(0, ui_js_1.newline)();
|
|
3608
4042
|
}
|
|
3609
4043
|
}
|
|
@@ -3616,9 +4050,9 @@ function renderRedundantIndexes(redundant) {
|
|
|
3616
4050
|
console.log(` ${(0, ui_js_1.dim)('Each is a leading prefix of a wider index that already serves the same lookups. Report-only, never auto-dropped.')}`);
|
|
3617
4051
|
(0, ui_js_1.newline)();
|
|
3618
4052
|
for (const r of redundant) {
|
|
3619
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(r.table))} ${(0, ui_js_1.dim)(`(${r.columns.join(', ')})`)} ${(0, ui_js_1.gray)(`${r.indexName} · ${(0, index_stats_js_1.formatBytes)(r.sizeBytes)}`)}`);
|
|
3620
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} covered by ${(0, ui_js_1.blue)(r.coveredBy)} ${(0, ui_js_1.dim)(`(${r.coveredByColumns.join(', ')})`)}`);
|
|
3621
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(r.dropSql)}`);
|
|
4053
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(r.table)))} ${(0, ui_js_1.dim)(`(${shown(r.columns.join(', '))})`)} ${(0, ui_js_1.gray)(`${shown(r.indexName)} · ${(0, index_stats_js_1.formatBytes)(r.sizeBytes)}`)}`);
|
|
4054
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} covered by ${(0, ui_js_1.blue)(shown(r.coveredBy))} ${(0, ui_js_1.dim)(`(${shown(r.coveredByColumns.join(', '))})`)}`);
|
|
4055
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(r.dropSql))}`);
|
|
3622
4056
|
(0, ui_js_1.newline)();
|
|
3623
4057
|
}
|
|
3624
4058
|
}
|
|
@@ -3633,7 +4067,7 @@ function renderDoctorAudit(audit, minScans, snapshot) {
|
|
|
3633
4067
|
renderUnusedCaveats(minScans, snapshot);
|
|
3634
4068
|
for (const a of audit) {
|
|
3635
4069
|
const tag = a.ambiguous ? (0, ui_js_1.red)(' [ambiguous: truncated name collides with another column set]') : '';
|
|
3636
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(a.table))} ${(0, ui_js_1.dim)(`(${a.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(`${a.indexName} · ${a.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(a.sizeBytes)}`)}${tag}`);
|
|
4070
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(a.table)))} ${(0, ui_js_1.dim)(`(${shown(a.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(`${shown(a.indexName)} · ${a.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(a.sizeBytes)}`)}${tag}`);
|
|
3637
4071
|
if (a.ambiguous) {
|
|
3638
4072
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)('the 63-byte name maps to more than one probe column set; confirm before dropping')}`);
|
|
3639
4073
|
}
|
|
@@ -3642,7 +4076,7 @@ function renderDoctorAudit(audit, minScans, snapshot) {
|
|
|
3642
4076
|
console.log(` ${(0, ui_js_1.dim)('reappear as a missing-index finding in this same report.')}`);
|
|
3643
4077
|
}
|
|
3644
4078
|
else if (a.dropSql) {
|
|
3645
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(a.dropSql)}`);
|
|
4079
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(a.dropSql))}`);
|
|
3646
4080
|
}
|
|
3647
4081
|
(0, ui_js_1.newline)();
|
|
3648
4082
|
}
|
|
@@ -3696,12 +4130,12 @@ function divergenceAmplificationLines(d) {
|
|
|
3696
4130
|
const amp = divInt(d.worstCaseAmplification ?? 0);
|
|
3697
4131
|
const corr = d.orderColumnCorrelation;
|
|
3698
4132
|
const corrLabel = corr === null || corr === undefined
|
|
3699
|
-
? `no pg_stats correlation available for "${d.orderColumn}"`
|
|
3700
|
-
: `correlation ${corr.toFixed(5)} on "${d.orderColumn}"`;
|
|
4133
|
+
? `no pg_stats correlation available for "${shown(d.orderColumn)}"`
|
|
4134
|
+
: `correlation ${corr.toFixed(5)} on "${shown(d.orderColumn)}"`;
|
|
3701
4135
|
if (d.heapNearlyOrdered === true) {
|
|
3702
4136
|
return [
|
|
3703
4137
|
`The size of that flip turns on the heap's physical order, and THIS heap is in near-exact`,
|
|
3704
|
-
`"${d.orderColumn}" order (${corrLabel}), so consecutive index entries hit the`,
|
|
4138
|
+
`"${shown(d.orderColumn)}" order (${corrLabel}), so consecutive index entries hit the`,
|
|
3705
4139
|
`same pinned page: measured ~1x, not the ~${amp}x an unordered heap reads. Most likely this`,
|
|
3706
4140
|
`one is not costing you anything today. It is also one sampled statistic away from the`,
|
|
3707
4141
|
`much worse reading, so measure rather than assume in either direction.`,
|
|
@@ -3710,7 +4144,7 @@ function divergenceAmplificationLines(d) {
|
|
|
3710
4144
|
return [
|
|
3711
4145
|
`That costs ~${amp}x the buffers of the seq scan, because each index entry is its own heap`,
|
|
3712
4146
|
`fetch (${corrLabel}). The one shape that reads ~1x instead is a heap`,
|
|
3713
|
-
`in near-exact "${d.orderColumn}" order; two pages of local disorder already reads ~41x.`,
|
|
4147
|
+
`in near-exact "${shown(d.orderColumn)}" order; two pages of local disorder already reads ~41x.`,
|
|
3714
4148
|
];
|
|
3715
4149
|
}
|
|
3716
4150
|
function renderDivergenceEvidence(d) {
|
|
@@ -3718,7 +4152,7 @@ function renderDivergenceEvidence(d) {
|
|
|
3718
4152
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.yellow)('cached-plan risk:')} this unindexed filter column can also flip a cached plan.`);
|
|
3719
4153
|
console.log(` ${(0, ui_js_1.dim)(`${divInt(d.rows)} rows in ${divInt(d.pages)} pages, rarest value ~${divInt(d.rarestBucket)} rows, below the assumed LIMIT ${d.assumedLimit}.`)}`);
|
|
3720
4154
|
console.log(` ${(0, ui_js_1.dim)(`Without the index the good plan is a seq scan (${divInt(d.pages)} pages); a promoted generic`)}`);
|
|
3721
|
-
console.log(` ${(0, ui_js_1.dim)(`plan cannot see the value is rare, keeps the ordered "${d.orderColumn}" walk, and reads`)}`);
|
|
4155
|
+
console.log(` ${(0, ui_js_1.dim)(`plan cannot see the value is rare, keeps the ordered "${shown(d.orderColumn)}" walk, and reads`)}`);
|
|
3722
4156
|
console.log(` ${(0, ui_js_1.dim)(`up to ~${tuples} tuples before it fills the LIMIT.`)}`);
|
|
3723
4157
|
console.log(` ${(0, ui_js_1.dim)('Postgres promotes this shape exactly when the workload keeps asking for the rare')}`);
|
|
3724
4158
|
console.log(` ${(0, ui_js_1.dim)('value: that is the case where the custom plan is expensive enough for the generic')}`);
|
|
@@ -3727,7 +4161,7 @@ function renderDivergenceEvidence(d) {
|
|
|
3727
4161
|
console.log(` ${(0, ui_js_1.dim)(line)}`);
|
|
3728
4162
|
console.log(` ${(0, ui_js_1.dim)('Adding the index above is the fix. Confirm first if you want to:')}`);
|
|
3729
4163
|
for (const line of d.diagnosticSql.split('\n')) {
|
|
3730
|
-
console.log(` ${(0, ui_js_1.green)(line)}`);
|
|
4164
|
+
console.log(` ${(0, ui_js_1.green)(shown(line))}`);
|
|
3731
4165
|
}
|
|
3732
4166
|
console.log(` ${(0, ui_js_1.dim)('After adding this index, re-run doctor. This column is expected to reappear as a')}`);
|
|
3733
4167
|
console.log(` ${(0, ui_js_1.dim)('sparse-value finding in the cached-plan section. That later finding is exposure, not')}`);
|
|
@@ -3781,9 +4215,9 @@ function renderFinding(f, opts) {
|
|
|
3781
4215
|
const rows = f.score.metrics.rows;
|
|
3782
4216
|
const rowsLabel = rows !== null ? `~${rows.toLocaleString()} rows` : 'row count unknown';
|
|
3783
4217
|
const sizeLabel = f.score.metrics.sizeBytes !== null ? `, ${(0, index_stats_js_1.formatBytes)(f.score.metrics.sizeBytes)}` : '';
|
|
3784
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(m.table))} ${(0, ui_js_1.dim)(`(${m.columns.join(', ')})`)} ${(0, ui_js_1.gray)(`${rowsLabel}${sizeLabel}`)}`);
|
|
4218
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(m.table)))} ${(0, ui_js_1.dim)(`(${shown(m.columns.join(', '))})`)} ${(0, ui_js_1.gray)(`${rowsLabel}${sizeLabel}`)}`);
|
|
3785
4219
|
for (const p of m.probes) {
|
|
3786
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${p.from}.${(0, ui_js_1.blue)(p.relation)} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
|
|
4220
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${shown(p.from)}.${(0, ui_js_1.blue)(shown(p.relation))} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
|
|
3787
4221
|
}
|
|
3788
4222
|
if (opts.withReasons) {
|
|
3789
4223
|
for (const reason of f.score.reasons) {
|
|
@@ -3791,7 +4225,7 @@ function renderFinding(f, opts) {
|
|
|
3791
4225
|
}
|
|
3792
4226
|
}
|
|
3793
4227
|
const last = opts.divergence ? ui_js_1.symbols.tee : ui_js_1.symbols.teeEnd;
|
|
3794
|
-
console.log(` ${(0, ui_js_1.dim)(last)} ${(0, ui_js_1.green)(doctorCreateSql(f, { concurrently: opts.concurrently }))}`);
|
|
4228
|
+
console.log(` ${(0, ui_js_1.dim)(last)} ${(0, ui_js_1.green)(shown(doctorCreateSql(f, { concurrently: opts.concurrently })))}`);
|
|
3795
4229
|
if (opts.divergence)
|
|
3796
4230
|
renderDivergenceEvidence(opts.divergence);
|
|
3797
4231
|
(0, ui_js_1.newline)();
|
|
@@ -3804,8 +4238,8 @@ function renderInvalidIndexes(invalid) {
|
|
|
3804
4238
|
console.log(` ${(0, ui_js_1.dim)('IF NOT EXISTS skips an invalid index on rerun, so it never rebuilds. Drop it, then rerun:')}`);
|
|
3805
4239
|
(0, ui_js_1.newline)();
|
|
3806
4240
|
for (const idx of invalid) {
|
|
3807
|
-
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(idx.table))} ${(0, ui_js_1.dim)(`(${idx.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(idx.indexName)}`);
|
|
3808
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(idx.dropSql)}`);
|
|
4241
|
+
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(idx.table)))} ${(0, ui_js_1.dim)(`(${shown(idx.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(shown(idx.indexName))}`);
|
|
4242
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(idx.dropSql))}`);
|
|
3809
4243
|
(0, ui_js_1.newline)();
|
|
3810
4244
|
}
|
|
3811
4245
|
}
|
|
@@ -3898,10 +4332,10 @@ function renderPlanDivergence(divergence, attached) {
|
|
|
3898
4332
|
// on (an index that exists but cannot serve the equality: partial,
|
|
3899
4333
|
// expression, or non-btree). The remedy is still an index, not a
|
|
3900
4334
|
// plan-cache setting, so this entry never suggests forceCustomPlan.
|
|
3901
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${f.table}.${f.column}`))} ${(0, ui_js_1.gray)('UNINDEXED-FILTER FLIP')}`);
|
|
4335
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${shown(f.table)}.${shown(f.column)}`))} ${(0, ui_js_1.gray)('UNINDEXED-FILTER FLIP')}`);
|
|
3902
4336
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${divInt(f.rows)} rows in ${divInt(f.pages)} pages, rarest value ~${(0, ui_js_1.bold)(divInt(f.rarestBucket))} rows, below the assumed LIMIT ${f.assumedLimit}`);
|
|
3903
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} no index serves ${f.column} = $1, so the good plan is a seq scan (${divInt(f.pages)} pages);`);
|
|
3904
|
-
console.log(` ${(0, ui_js_1.dim)(`a promoted generic plan keeps the ordered "${f.orderColumn}" walk and reads up to ~${divInt(f.tuplesWalked ?? f.rows)} tuples`)}`);
|
|
4337
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} no index serves ${shown(f.column)} = $1, so the good plan is a seq scan (${divInt(f.pages)} pages);`);
|
|
4338
|
+
console.log(` ${(0, ui_js_1.dim)(`a promoted generic plan keeps the ordered "${shown(f.orderColumn)}" walk and reads up to ~${divInt(f.tuplesWalked ?? f.rows)} tuples`)}`);
|
|
3905
4339
|
console.log(` ${(0, ui_js_1.dim)('before it fills the LIMIT.')}`);
|
|
3906
4340
|
for (const line of divergenceAmplificationLines(f))
|
|
3907
4341
|
console.log(` ${(0, ui_js_1.dim)(line)}`);
|
|
@@ -3916,13 +4350,13 @@ function renderPlanDivergence(divergence, attached) {
|
|
|
3916
4350
|
(0, ui_js_1.newline)();
|
|
3917
4351
|
continue;
|
|
3918
4352
|
}
|
|
3919
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${f.table}.${f.column}`))} ${(0, ui_js_1.gray)('SPARSE-VALUE FLIP')}`);
|
|
4353
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${shown(f.table)}.${shown(f.column)}`))} ${(0, ui_js_1.gray)('SPARSE-VALUE FLIP')}`);
|
|
3920
4354
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} generic estimate ${(0, ui_js_1.bold)(divInt(f.genericEstimate))} rows ${(0, ui_js_1.dim)(`(${divInt(f.rows)} rows / ${divInt(f.distinctValues)} distinct values)`)}`);
|
|
3921
4355
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} rarest value bucket ${(0, ui_js_1.bold)(divInt(f.rarestBucket))} rows ${(0, ui_js_1.dim)('(pg_stats most_common_freqs / residual bucket)')}`);
|
|
3922
4356
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} crossover ${(0, ui_js_1.bold)(divInt(f.crossoverRows ?? 0))} rows ${(0, ui_js_1.dim)(`(sqrt(limit ${f.assumedLimit} x ${divInt(f.pages)} pages); ${divInt(f.crossoverRowsWide ?? 0)} at limit ${f.thresholds.wideLimit})`)}`);
|
|
3923
4357
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)(`values below the crossover: ${divInt(f.valuesBelowCrossover ?? 0)} of ${divInt(f.distinctValues)}, filter-column correlation ${f.correlation.toFixed(2)}, ${analyzedLabel(f)}`)}`);
|
|
3924
4358
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} for such a value the generic plan walks ~${(0, ui_js_1.bold)(divInt(f.walkPages ?? 0))} of ${divInt(f.pages)} pages ${(0, ui_js_1.dim)(`(${Math.round((f.walkFraction ?? 0) * 100)}% of the table)`)}`);
|
|
3925
|
-
console.log(` ${(0, ui_js_1.dim)(`for reads shaped WHERE ${f.column} = $1 ORDER BY ${f.orderColumn} LIMIT $n,`)}`);
|
|
4359
|
+
console.log(` ${(0, ui_js_1.dim)(`for reads shaped WHERE ${shown(f.column)} = $1 ORDER BY ${shown(f.orderColumn)} LIMIT $n,`)}`);
|
|
3926
4360
|
console.log(` ${(0, ui_js_1.dim)("where the custom plan reads only that value's own rows.")}`);
|
|
3927
4361
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)('No amplification figure is printed, deliberately. This models how many rows a')}`);
|
|
3928
4362
|
console.log(` ${(0, ui_js_1.dim)('value has, not WHERE they sit in the heap, and the second half can move the')}`);
|
|
@@ -3984,7 +4418,7 @@ function renderPlanDivergence(divergence, attached) {
|
|
|
3984
4418
|
console.log(` ${(0, ui_js_1.dim)('in one direction trades this finding for its mirror image. That applies equally to')}`);
|
|
3985
4419
|
console.log(` ${(0, ui_js_1.dim)("Turbine's client-level `planCacheMode` and to a SET or ALTER ROLE applied outside")}`);
|
|
3986
4420
|
console.log(` ${(0, ui_js_1.dim)('Turbine.')}`);
|
|
3987
|
-
console.log(` 5. A composite index on (${first.column}, ${first.orderColumn}) makes the GOOD plan better. It`);
|
|
4421
|
+
console.log(` 5. A composite index on (${shown(first.column)}, ${shown(first.orderColumn)}) makes the GOOD plan better. It`);
|
|
3988
4422
|
console.log(` ${(0, ui_js_1.dim)('does NOT stop the generic plan from choosing the other one, and it can widen the gap.')}`);
|
|
3989
4423
|
console.log(` ${(0, ui_js_1.dim)('Add it for the custom-plan win, not as a fix for this finding.')}`);
|
|
3990
4424
|
}
|
|
@@ -4009,7 +4443,7 @@ function renderPlanDivergence(divergence, attached) {
|
|
|
4009
4443
|
if (notices.length > 0) {
|
|
4010
4444
|
console.log(` ${(0, ui_js_1.dim)('Not scored for cached-plan divergence (statistics missing):')}`);
|
|
4011
4445
|
for (const n of notices) {
|
|
4012
|
-
console.log(` ${(0, ui_js_1.dim)(`- ${n.table}.${n.column}: ${n.reason}`)}`);
|
|
4446
|
+
console.log(` ${(0, ui_js_1.dim)(`- ${shown(n.table)}.${shown(n.column)}: ${n.reason}`)}`);
|
|
4013
4447
|
}
|
|
4014
4448
|
(0, ui_js_1.newline)();
|
|
4015
4449
|
}
|
|
@@ -4107,7 +4541,7 @@ async function cmdStudio(args, config) {
|
|
|
4107
4541
|
spinner.succeed(demo ? 'Demo Studio is running' : 'Studio is running');
|
|
4108
4542
|
}
|
|
4109
4543
|
catch (err) {
|
|
4110
|
-
spinner.fail(`Failed to start Studio: ${
|
|
4544
|
+
spinner.fail(`Failed to start Studio: ${(0, error_text_js_1.errorText)(err)}`);
|
|
4111
4545
|
process.exit(1);
|
|
4112
4546
|
}
|
|
4113
4547
|
if (demo) {
|
|
@@ -4240,7 +4674,7 @@ async function cmdObserve(args) {
|
|
|
4240
4674
|
spinner.succeed('Observe dashboard is running');
|
|
4241
4675
|
}
|
|
4242
4676
|
catch (err) {
|
|
4243
|
-
spinner.fail(`Failed to start Observe: ${
|
|
4677
|
+
spinner.fail(`Failed to start Observe: ${(0, error_text_js_1.errorText)(err)}`);
|
|
4244
4678
|
process.exit(1);
|
|
4245
4679
|
}
|
|
4246
4680
|
(0, ui_js_1.newline)();
|
|
@@ -4348,10 +4782,12 @@ function showGenerateHelp() {
|
|
|
4348
4782
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
4349
4783
|
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
4350
4784
|
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
4785
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file, whose ${(0, ui_js_1.cyan)('pii')} / ${(0, ui_js_1.cyan)('updatedAt')} tags are applied ${(0, ui_js_1.dim)('(default: ./turbine/schema.ts)')}`);
|
|
4351
4786
|
console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
|
|
4352
4787
|
console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
|
|
4353
4788
|
console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated at:')} header line ${(0, ui_js_1.dim)('(reproducible, diff-stable output)')}`);
|
|
4354
|
-
console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
|
|
4789
|
+
console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('ts')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
|
|
4790
|
+
console.log(` ${(0, ui_js_1.dim)('ts emits ./types.ts so')} ${(0, ui_js_1.cyan)('node app.ts')} ${(0, ui_js_1.dim)('can run the client with native type stripping')}`);
|
|
4355
4791
|
console.log(` ${(0, ui_js_1.cyan)('--keep-column-names')} Keep raw DB column names as field names ${(0, ui_js_1.dim)('(snake_case, not camelCase)')}`);
|
|
4356
4792
|
console.log(` ${(0, ui_js_1.cyan)('--legacy-to-many-uniques')} Emit ${(0, ui_js_1.cyan)('hasMany')} for unique-FK children ${(0, ui_js_1.dim)('(pre-0.41 shape; default flips to hasOne)')}`);
|
|
4357
4793
|
console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
@@ -4408,6 +4844,7 @@ function showPushHelp() {
|
|
|
4408
4844
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
4409
4845
|
console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file ${(0, ui_js_1.dim)('(default: ./turbine/schema.ts)')}`);
|
|
4410
4846
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
4847
|
+
console.log(` ${(0, ui_js_1.cyan)('--exit-code')} With ${(0, ui_js_1.cyan)('--dry-run')}: exit 2 when the database differs from the schema file, 0 when in sync ${(0, ui_js_1.dim)('(CI)')}`);
|
|
4411
4848
|
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Skip the interactive confirmation for data-destroying statements ${(0, ui_js_1.dim)('(CI)')}`);
|
|
4412
4849
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
4413
4850
|
(0, ui_js_1.newline)();
|
|
@@ -4434,7 +4871,11 @@ function showMigrateHelp() {
|
|
|
4434
4871
|
console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a sanctioned migration pattern ${(0, ui_js_1.dim)('(create only, e.g. backfill)')}`);
|
|
4435
4872
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
4436
4873
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
4874
|
+
console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 when migrations are pending, modified, missing or misnamed ${(0, ui_js_1.dim)('(status only, CI)')}`);
|
|
4875
|
+
console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Wait for a concurrent deploy to release the lock, default ${exports.DEFAULT_LOCK_WAIT_SECONDS}s ${(0, ui_js_1.dim)('(deploy only)')}`);
|
|
4876
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Let ${(0, ui_js_1.cyan)('--auto')} / ${(0, ui_js_1.cyan)('--from-diff')} run while earlier migrations are still pending ${(0, ui_js_1.dim)('(create only)')}`);
|
|
4437
4877
|
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums, REWRITING history ${(0, ui_js_1.dim)('(up / deploy / down)')}`);
|
|
4878
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Run ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} through a transaction pooler ${(0, ui_js_1.dim)('(refused: the migration lock is session-level)')}`);
|
|
4438
4879
|
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying migration statements without the interactive confirm`);
|
|
4439
4880
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema the migrations run in ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
4440
4881
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
@@ -4446,8 +4887,10 @@ function showMigrateHelp() {
|
|
|
4446
4887
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create backfill_full_name --recipe backfill`);
|
|
4447
4888
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
|
|
4448
4889
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --dry-run`);
|
|
4890
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --wait=120`);
|
|
4449
4891
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate down --step 2`);
|
|
4450
4892
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status`);
|
|
4893
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status --exit-code`);
|
|
4451
4894
|
(0, ui_js_1.newline)();
|
|
4452
4895
|
}
|
|
4453
4896
|
function showSeedHelp() {
|
|
@@ -4671,8 +5114,12 @@ function showHelp() {
|
|
|
4671
5114
|
console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged ${(0, ui_js_1.dim)('(create)')}`);
|
|
4672
5115
|
console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a named migration recipe, e.g. backfill ${(0, ui_js_1.dim)('(create)')}`);
|
|
4673
5116
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
5117
|
+
console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 on drift ${(0, ui_js_1.dim)('(push --dry-run, migrate status)')}`);
|
|
5118
|
+
console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Wait for a concurrent deploy's lock instead of failing ${(0, ui_js_1.dim)('(deploy)')}`);
|
|
5119
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Diff even while migrations are pending ${(0, ui_js_1.dim)('(create --auto / --from-diff)')}`);
|
|
4674
5120
|
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting ${(0, ui_js_1.dim)('(up/down/push)')}`);
|
|
4675
5121
|
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
|
|
5122
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Let ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} and ${(0, ui_js_1.cyan)('doctor')} use a transaction pooler ${(0, ui_js_1.dim)('(refused by default)')}`);
|
|
4676
5123
|
(0, ui_js_1.newline)();
|
|
4677
5124
|
console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
|
|
4678
5125
|
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
@@ -4789,9 +5236,19 @@ function cmdSkill(args) {
|
|
|
4789
5236
|
}
|
|
4790
5237
|
const skillsDir = args.dir ?? (0, node_path_1.join)('.claude', 'skills');
|
|
4791
5238
|
const target = (0, node_path_1.resolve)(process.cwd(), skillsDir, 'turbine-orm', 'SKILL.md');
|
|
4792
|
-
const existed = (0, node_fs_1.existsSync)(target);
|
|
4793
5239
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
4794
|
-
|
|
5240
|
+
// Exclusive create first: only the wording depends on whether the file was
|
|
5241
|
+
// there, and asking separately would let the answer go stale before the write.
|
|
5242
|
+
let existed = false;
|
|
5243
|
+
try {
|
|
5244
|
+
(0, node_fs_1.writeFileSync)(target, body, { flag: 'wx' });
|
|
5245
|
+
}
|
|
5246
|
+
catch (err) {
|
|
5247
|
+
if (err.code !== 'EEXIST')
|
|
5248
|
+
throw err;
|
|
5249
|
+
existed = true;
|
|
5250
|
+
(0, node_fs_1.writeFileSync)(target, body);
|
|
5251
|
+
}
|
|
4795
5252
|
(0, ui_js_1.newline)();
|
|
4796
5253
|
(0, ui_js_1.success)(`${existed ? 'Updated' : 'Installed'} the Turbine query skill`);
|
|
4797
5254
|
console.log(` ${(0, ui_js_1.dim)((0, node_path_1.relative)(process.cwd(), target))}`);
|
|
@@ -4854,7 +5311,7 @@ async function main() {
|
|
|
4854
5311
|
// expected here; anything that does arrive is a bug in the parser rather
|
|
4855
5312
|
// than in the caller's command line, and printing it beats swallowing it.
|
|
4856
5313
|
(0, ui_js_1.errorLine)();
|
|
4857
|
-
(0, ui_js_1.error)(
|
|
5314
|
+
(0, ui_js_1.error)((0, error_text_js_1.errorText)(err));
|
|
4858
5315
|
(0, ui_js_1.errorLine)();
|
|
4859
5316
|
process.exit(1);
|
|
4860
5317
|
}
|
|
@@ -4987,6 +5444,9 @@ async function main() {
|
|
|
4987
5444
|
}
|
|
4988
5445
|
catch (err) {
|
|
4989
5446
|
if (err instanceof Error) {
|
|
5447
|
+
// `errorText`, not `message`: a refused `localhost` is an
|
|
5448
|
+
// AggregateError with an empty message (see cli/error-text.ts).
|
|
5449
|
+
const message = (0, error_text_js_1.errorText)(err);
|
|
4990
5450
|
// TYPE first, message second. These branches sniff for substrings, and
|
|
4991
5451
|
// `ValidationError` beat them by accident: a refused `--schema` value says
|
|
4992
5452
|
// "used as a connection parameter", which contains "connection", so a bad
|
|
@@ -4994,35 +5454,35 @@ async function main() {
|
|
|
4994
5454
|
// firewall hints. A typed error already knows what it is.
|
|
4995
5455
|
if (err instanceof errors_js_1.ValidationError) {
|
|
4996
5456
|
(0, ui_js_1.errorLine)();
|
|
4997
|
-
(0, ui_js_1.error)((0, ui_js_1.redactUrl)(
|
|
5457
|
+
(0, ui_js_1.error)((0, ui_js_1.redactUrl)(message));
|
|
4998
5458
|
if (args.verbose && err.stack) {
|
|
4999
5459
|
(0, ui_js_1.errorLine)();
|
|
5000
5460
|
console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
|
|
5001
5461
|
}
|
|
5002
5462
|
}
|
|
5003
|
-
else if (
|
|
5463
|
+
else if (message.includes('ECONNREFUSED') || message.includes('connection')) {
|
|
5004
5464
|
(0, ui_js_1.errorLine)();
|
|
5005
5465
|
(0, ui_js_1.error)(`Could not connect to database`);
|
|
5006
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(
|
|
5466
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
|
|
5007
5467
|
(0, ui_js_1.errorLine)();
|
|
5008
5468
|
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Check that:')}`);
|
|
5009
5469
|
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('1.')} Your database is running`);
|
|
5010
5470
|
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('2.')} The connection string is correct`);
|
|
5011
5471
|
(0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('3.')} Network/firewall allows the connection`);
|
|
5012
5472
|
}
|
|
5013
|
-
else if (
|
|
5473
|
+
else if (message.includes('authentication')) {
|
|
5014
5474
|
(0, ui_js_1.errorLine)();
|
|
5015
5475
|
(0, ui_js_1.error)(`Authentication failed`);
|
|
5016
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(
|
|
5476
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
|
|
5017
5477
|
}
|
|
5018
|
-
else if (
|
|
5478
|
+
else if (message.includes('does not exist')) {
|
|
5019
5479
|
(0, ui_js_1.errorLine)();
|
|
5020
5480
|
(0, ui_js_1.error)(`Database or schema not found`);
|
|
5021
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(
|
|
5481
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
|
|
5022
5482
|
}
|
|
5023
5483
|
else {
|
|
5024
5484
|
(0, ui_js_1.errorLine)();
|
|
5025
|
-
(0, ui_js_1.error)((0, ui_js_1.redactUrl)(
|
|
5485
|
+
(0, ui_js_1.error)((0, ui_js_1.redactUrl)(message));
|
|
5026
5486
|
if (args.verbose && err.stack) {
|
|
5027
5487
|
(0, ui_js_1.errorLine)();
|
|
5028
5488
|
console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
|