turbine-orm 0.35.0 → 0.36.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 +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +22 -5
- package/dist/cjs/powdb.js +41 -1
- package/dist/cjs/powql.js +80 -25
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +297 -4504
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +1 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/dialect.d.ts +15 -6
- package/dist/dialect.js +1 -1
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +22 -5
- package/dist/powdb.d.ts +20 -0
- package/dist/powdb.js +40 -0
- package/dist/powql.d.ts +33 -1
- package/dist/powql.js +80 -25
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +62 -829
- package/dist/query/builder.js +302 -4509
- package/dist/query/deferred.d.ts +7 -0
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +15 -0
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +1 -1
- package/package.json +2 -2
|
@@ -101,6 +101,7 @@ function resolveColumn(def) {
|
|
|
101
101
|
vectorDimensions: def.dimensions ?? null,
|
|
102
102
|
isArray: def.array ?? false,
|
|
103
103
|
check: def.check ?? null,
|
|
104
|
+
pii: def.pii ?? false,
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
/** Type guard: is this index declaration a doc-field expression index? */
|
|
@@ -254,6 +255,7 @@ class ColumnBuilder {
|
|
|
254
255
|
vectorDimensions: null,
|
|
255
256
|
isArray: false,
|
|
256
257
|
check: null,
|
|
258
|
+
pii: false,
|
|
257
259
|
};
|
|
258
260
|
}
|
|
259
261
|
serial() {
|
|
@@ -360,6 +362,10 @@ class ColumnBuilder {
|
|
|
360
362
|
this._config.check = expression;
|
|
361
363
|
return this;
|
|
362
364
|
}
|
|
365
|
+
pii() {
|
|
366
|
+
this._config.pii = true;
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
363
369
|
array() {
|
|
364
370
|
this._config.isArray = true;
|
|
365
371
|
return this;
|
|
@@ -340,6 +340,10 @@ function schemaDefToMetadata(def) {
|
|
|
340
340
|
arrayType: (0, schema_js_1.pgArrayType)(base),
|
|
341
341
|
pgArrayType: (0, schema_js_1.pgArrayType)(base),
|
|
342
342
|
...(serial ? { isGenerated: true } : {}),
|
|
343
|
+
// PII is a code-first declaration; carry it through so query-layer
|
|
344
|
+
// default-projection exclusion and Studio redaction can honor it.
|
|
345
|
+
// Only set when true, keeping output byte-stable for untagged schemas.
|
|
346
|
+
...(config.pii ? { pii: true } : {}),
|
|
343
347
|
...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
|
|
344
348
|
};
|
|
345
349
|
columns.push(col);
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -9,20 +9,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DestructivePushRefusal = void 0;
|
|
12
13
|
exports.referentialActionToSql = referentialActionToSql;
|
|
13
14
|
exports.schemaToSQL = schemaToSQL;
|
|
15
|
+
exports.describeIndexDefMismatch = describeIndexDefMismatch;
|
|
16
|
+
exports.undeclaredIndexWarnings = undeclaredIndexWarnings;
|
|
14
17
|
exports.buildAddForeignKeyStatement = buildAddForeignKeyStatement;
|
|
15
18
|
exports.diffReferentialAction = diffReferentialAction;
|
|
16
19
|
exports.diffEnumValues = diffEnumValues;
|
|
17
20
|
exports.diffCheckConstraints = diffCheckConstraints;
|
|
18
21
|
exports.schemaDiff = schemaDiff;
|
|
22
|
+
exports.findDestructivePushStatements = findDestructivePushStatements;
|
|
19
23
|
exports.schemaPush = schemaPush;
|
|
20
24
|
exports.schemaToSQLString = schemaToSQLString;
|
|
21
25
|
const pg_1 = __importDefault(require("pg"));
|
|
26
|
+
const destructive_js_1 = require("./cli/destructive.js");
|
|
22
27
|
const dialect_js_1 = require("./dialect.js");
|
|
23
28
|
const errors_js_1 = require("./errors.js");
|
|
24
29
|
const introspect_js_1 = require("./introspect.js");
|
|
25
30
|
const schema_js_1 = require("./schema.js");
|
|
31
|
+
const schema_builder_js_1 = require("./schema-builder.js");
|
|
26
32
|
/** Map a {@link ReferentialAction} to its SQL keyword form. */
|
|
27
33
|
function referentialActionToSql(action) {
|
|
28
34
|
switch (action) {
|
|
@@ -124,6 +130,11 @@ function schemaToSQL(schema, options) {
|
|
|
124
130
|
const indexes = generateForeignKeyIndexes(table, dialect);
|
|
125
131
|
statements.push(...indexes);
|
|
126
132
|
}
|
|
133
|
+
// Generate CREATE INDEX / CREATE UNIQUE INDEX for user-declared indexes.
|
|
134
|
+
for (const tableName of sorted) {
|
|
135
|
+
const table = schema.tables[tableName];
|
|
136
|
+
statements.push(...generateDeclaredIndexes(table, dialect));
|
|
137
|
+
}
|
|
127
138
|
return statements;
|
|
128
139
|
}
|
|
129
140
|
/**
|
|
@@ -341,10 +352,16 @@ function normalizeDefault(val) {
|
|
|
341
352
|
*/
|
|
342
353
|
function generateForeignKeyIndexes(table, dialect = dialect_js_1.postgresDialect) {
|
|
343
354
|
const indexes = [];
|
|
355
|
+
// A declared index whose deterministic name matches the auto FK-index name
|
|
356
|
+
// takes precedence (it may be UNIQUE); emitting both would fail at apply time
|
|
357
|
+
// with "relation already exists".
|
|
358
|
+
const declared = declaredPlainIndexNames(table);
|
|
344
359
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
345
360
|
if (config.referencesTarget) {
|
|
346
361
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
347
362
|
const indexName = `idx_${table.name}_${snakeName}`;
|
|
363
|
+
if (declared.has(indexName))
|
|
364
|
+
continue;
|
|
348
365
|
indexes.push(dialect.buildCreateIndexStatement({
|
|
349
366
|
name: dialect.quoteIdentifier(indexName),
|
|
350
367
|
table: dialect.quoteIdentifier(table.name),
|
|
@@ -354,6 +371,116 @@ function generateForeignKeyIndexes(table, dialect = dialect_js_1.postgresDialect
|
|
|
354
371
|
}
|
|
355
372
|
return indexes;
|
|
356
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* The deterministic SQL index name for a declared plain (column-list) index:
|
|
376
|
+
* the user-supplied `name`, else `idx_<table>_<col1>_<col2>...`, mirroring the
|
|
377
|
+
* FK-index convention in {@link generateForeignKeyIndexes}.
|
|
378
|
+
*/
|
|
379
|
+
function declaredIndexName(tableName, idx) {
|
|
380
|
+
if (idx.name)
|
|
381
|
+
return idx.name;
|
|
382
|
+
const cols = idx.columns.map(schema_js_1.camelToSnake);
|
|
383
|
+
return `idx_${tableName}_${cols.join('_')}`;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Compare a declared plain index against a pg_indexes `indexdef` string.
|
|
387
|
+
* Returns a human-readable description of the first mismatch (uniqueness or
|
|
388
|
+
* column list), or null when the definitions agree. Expression/partial indexes
|
|
389
|
+
* in the DB never structurally match a plain column list, which is the
|
|
390
|
+
* intended outcome: the operator gets a warning rather than a silent skip.
|
|
391
|
+
*/
|
|
392
|
+
function describeIndexDefMismatch(idx, indexdef) {
|
|
393
|
+
const dbUnique = /^\s*CREATE\s+UNIQUE\s+INDEX\b/i.test(indexdef);
|
|
394
|
+
const wantUnique = idx.unique === true;
|
|
395
|
+
if (dbUnique !== wantUnique) {
|
|
396
|
+
return wantUnique
|
|
397
|
+
? 'declared UNIQUE, existing index is not unique'
|
|
398
|
+
: 'existing index is UNIQUE, declaration is not';
|
|
399
|
+
}
|
|
400
|
+
// pg_indexes.indexdef always reads `CREATE [UNIQUE] INDEX name ON tbl
|
|
401
|
+
// USING method (col, ...) [WHERE ...]`; anchor on the USING clause so a
|
|
402
|
+
// partial index's WHERE parentheses are never mistaken for the column list.
|
|
403
|
+
const parenMatch = indexdef.match(/USING\s+\w+\s*\(([^)]*)\)/i) ?? indexdef.match(/\(([^)]*)\)/);
|
|
404
|
+
const dbCols = parenMatch
|
|
405
|
+
? parenMatch[1].split(',').map((c) => c
|
|
406
|
+
.trim()
|
|
407
|
+
.replace(/\s+(ASC|DESC|NULLS\s+(FIRST|LAST))\b/gi, '')
|
|
408
|
+
.replace(/^"(.*)"$/, '$1')
|
|
409
|
+
.trim())
|
|
410
|
+
: [];
|
|
411
|
+
const wantCols = idx.columns.map(schema_js_1.camelToSnake);
|
|
412
|
+
if (dbCols.length !== wantCols.length || dbCols.some((c, i) => c !== wantCols[i])) {
|
|
413
|
+
return `declared columns (${wantCols.join(', ')}) differ from existing (${dbCols.join(', ') || 'unparsed'})`;
|
|
414
|
+
}
|
|
415
|
+
if (/\bWHERE\b/i.test(indexdef))
|
|
416
|
+
return 'existing index is partial (has a WHERE clause)';
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Pure decision for the "index exists in the DB but is not declared" warning
|
|
421
|
+
* pass. Extracted so the scope rule is unit-testable without a live database.
|
|
422
|
+
*
|
|
423
|
+
* The pass runs only when the table opts into index management by DEFINING an
|
|
424
|
+
* `indexes` array (`indexesDefined: true`), even when that array is empty or
|
|
425
|
+
* all-doc-field: deleting the last declared index must NOT silence the pass.
|
|
426
|
+
* Tables with no `indexes` key stay silent (the user is not managing indexes
|
|
427
|
+
* there). Recognized names (declared, unique-constraint / FK-column, `*_pkey`)
|
|
428
|
+
* are never flagged; everything else in the DB yields one warning.
|
|
429
|
+
*/
|
|
430
|
+
function undeclaredIndexWarnings(opts) {
|
|
431
|
+
if (!opts.indexesDefined)
|
|
432
|
+
return [];
|
|
433
|
+
const out = [];
|
|
434
|
+
for (const dbIdxName of opts.dbIndexNames) {
|
|
435
|
+
if (opts.declaredNames.has(dbIdxName) || opts.recognizedNames.has(dbIdxName) || dbIdxName.endsWith('_pkey')) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
out.push(`index "${dbIdxName}" on "${opts.tableName}" exists in the database but is not declared in the schema. ` +
|
|
439
|
+
`Turbine does not drop indexes automatically; drop it in a manual migration if it is no longer needed.`);
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
/** The deterministic SQL names of every declared plain index on a table. */
|
|
444
|
+
function declaredPlainIndexNames(table) {
|
|
445
|
+
const names = new Set();
|
|
446
|
+
for (const idx of table.indexes ?? []) {
|
|
447
|
+
if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx) || idx.columns.length === 0)
|
|
448
|
+
continue;
|
|
449
|
+
names.add(declaredIndexName(table.name, idx));
|
|
450
|
+
}
|
|
451
|
+
return names;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Build the `CREATE [UNIQUE] INDEX` statement for a declared plain index.
|
|
455
|
+
* `buildCreateIndexStatement` has no `unique` hook, so unique indexes are
|
|
456
|
+
* emitted directly here (still fully identifier-quoted via the dialect).
|
|
457
|
+
*/
|
|
458
|
+
function buildDeclaredIndexStatement(tableName, idx, dialect) {
|
|
459
|
+
const cols = idx.columns.map(schema_js_1.camelToSnake);
|
|
460
|
+
if (cols.length === 0)
|
|
461
|
+
return null;
|
|
462
|
+
const name = declaredIndexName(tableName, idx);
|
|
463
|
+
const quotedCols = cols.map((c) => dialect.quoteIdentifier(c)).join(', ');
|
|
464
|
+
const unique = idx.unique ? 'UNIQUE ' : '';
|
|
465
|
+
return `CREATE ${unique}INDEX ${dialect.quoteIdentifier(name)} ON ${dialect.quoteIdentifier(tableName)}(${quotedCols});`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Generate `CREATE INDEX` / `CREATE UNIQUE INDEX` for a table's user-declared
|
|
469
|
+
* plain-column indexes. PowDB doc-field expression indexes ({@link DocFieldIndexDef})
|
|
470
|
+
* are skipped here (those stay PowDB-only, emitted by `powqlSchemaDDL`) and have
|
|
471
|
+
* no SQL equivalent.
|
|
472
|
+
*/
|
|
473
|
+
function generateDeclaredIndexes(table, dialect = dialect_js_1.postgresDialect) {
|
|
474
|
+
const out = [];
|
|
475
|
+
for (const idx of table.indexes ?? []) {
|
|
476
|
+
if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx))
|
|
477
|
+
continue; // PowDB-only, no SQL emission
|
|
478
|
+
const stmt = buildDeclaredIndexStatement(table.name, idx, dialect);
|
|
479
|
+
if (stmt)
|
|
480
|
+
out.push(stmt);
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
357
484
|
/**
|
|
358
485
|
* Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
|
|
359
486
|
* referential actions. Default (`no action`) clauses are omitted, matching how
|
|
@@ -546,6 +673,16 @@ async function schemaDiff(schema, connectionString) {
|
|
|
546
673
|
dbChecks[row.table_name] = [];
|
|
547
674
|
dbChecks[row.table_name].push({ name: row.conname, expression: (0, introspect_js_1.stripCheckWrapper)(row.definition) });
|
|
548
675
|
}
|
|
676
|
+
// Existing index NAMES per table (for the declared-index diff). Covers PK,
|
|
677
|
+
// unique-constraint, FK, and user indexes alike; the diff only ADDs declared
|
|
678
|
+
// indexes whose name is missing and never auto-drops any (see below).
|
|
679
|
+
const indexResult = await client.query(`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public'`);
|
|
680
|
+
const dbIndexes = {};
|
|
681
|
+
for (const row of indexResult.rows) {
|
|
682
|
+
if (!dbIndexes[row.tablename])
|
|
683
|
+
dbIndexes[row.tablename] = new Map();
|
|
684
|
+
dbIndexes[row.tablename].set(row.indexname, row.indexdef);
|
|
685
|
+
}
|
|
549
686
|
// Build a set of DDL-facing snake_case table names that the schema defines.
|
|
550
687
|
const schemaDdlNames = new Set();
|
|
551
688
|
for (const def of Object.values(schema.tables))
|
|
@@ -578,6 +715,8 @@ async function schemaDiff(schema, connectionString) {
|
|
|
578
715
|
result.statements.push(generateCreateTable(tableDef, resolveRef, dialect));
|
|
579
716
|
const fkIndexes = generateForeignKeyIndexes(tableDef, dialect);
|
|
580
717
|
result.statements.push(...fkIndexes);
|
|
718
|
+
// User-declared indexes on a brand-new table (reversed by the DROP TABLE).
|
|
719
|
+
result.statements.push(...generateDeclaredIndexes(tableDef, dialect));
|
|
581
720
|
// Reverse: DROP TABLE (with indexes — they drop automatically)
|
|
582
721
|
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
583
722
|
}
|
|
@@ -761,6 +900,67 @@ async function schemaDiff(schema, connectionString) {
|
|
|
761
900
|
`To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`);
|
|
762
901
|
}
|
|
763
902
|
}
|
|
903
|
+
// --- User-declared indexes (TableDef.indexes) ---
|
|
904
|
+
// ADD any declared plain index whose NAME is missing from the DB (reverse:
|
|
905
|
+
// DROP INDEX). Doc-field indexes are PowDB-only and skipped. We never
|
|
906
|
+
// auto-drop DB indexes not in the schema, matching the column-drop posture.
|
|
907
|
+
// When the table declares at least one index (the user is actively managing
|
|
908
|
+
// indexes here), unrecognized extra DB indexes are surfaced as warnings so
|
|
909
|
+
// the operator can drop them by hand if intended. Recognized = PK, unique
|
|
910
|
+
// constraint, or FK-column index names, which the schema never declares.
|
|
911
|
+
// Run the whole index-management pass whenever the table DEFINES an
|
|
912
|
+
// `indexes` array (even empty or all-doc-field): a table with an `indexes`
|
|
913
|
+
// key is one the user is actively managing, so undeclared DB indexes stay
|
|
914
|
+
// worth surfacing. Deleting the last declared index must NOT silence the
|
|
915
|
+
// warning pass. Tables with no `indexes` key stay silent (not managed).
|
|
916
|
+
const declaredPlain = (tableDef.indexes ?? []).filter((i) => !(0, schema_builder_js_1.isDocFieldIndexDef)(i));
|
|
917
|
+
if (tableDef.indexes !== undefined) {
|
|
918
|
+
const dbIdx = dbIndexes[tableName] ?? new Map();
|
|
919
|
+
const dbIdxNames = new Set(dbIdx.keys());
|
|
920
|
+
const declaredNames = new Set();
|
|
921
|
+
for (const idx of declaredPlain) {
|
|
922
|
+
if (idx.columns.length === 0)
|
|
923
|
+
continue;
|
|
924
|
+
const name = declaredIndexName(tableName, idx);
|
|
925
|
+
declaredNames.add(name);
|
|
926
|
+
const existingDef = dbIdx.get(name);
|
|
927
|
+
if (existingDef === undefined) {
|
|
928
|
+
const stmt = buildDeclaredIndexStatement(tableName, idx, dialect);
|
|
929
|
+
if (!stmt)
|
|
930
|
+
continue;
|
|
931
|
+
result.statements.push(stmt);
|
|
932
|
+
result.reverseStatements.unshift(`DROP INDEX IF EXISTS ${dialect.quoteIdentifier(name)};`);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// Name matches an existing index: verify the definition agrees.
|
|
936
|
+
// Matching is by name, so a definition drift (a declared UNIQUE
|
|
937
|
+
// index colliding with the plain auto FK index, or a changed
|
|
938
|
+
// column list) would otherwise be silently skipped, leaving the
|
|
939
|
+
// declared guarantee unenforced. Warn, never drop.
|
|
940
|
+
const mismatch = describeIndexDefMismatch(idx, existingDef);
|
|
941
|
+
if (mismatch) {
|
|
942
|
+
result.warnings.push(`index "${name}" on "${tableName}": the declared definition does not match the existing ` +
|
|
943
|
+
`database index (${mismatch}). Turbine matches indexes by name and never drops them ` +
|
|
944
|
+
`automatically; drop and recreate it in a manual migration to apply the declared definition.`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Recognized (never-declared) index names: unique-constraint + FK-column.
|
|
949
|
+
const recognized = new Set(Object.values(dbUniques[tableName] ?? {}));
|
|
950
|
+
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
951
|
+
if (config.referencesTarget)
|
|
952
|
+
recognized.add(`idx_${tableName}_${(0, schema_js_1.camelToSnake)(fieldName)}`);
|
|
953
|
+
}
|
|
954
|
+
for (const w of undeclaredIndexWarnings({
|
|
955
|
+
tableName,
|
|
956
|
+
indexesDefined: tableDef.indexes !== undefined,
|
|
957
|
+
dbIndexNames: dbIdxNames,
|
|
958
|
+
declaredNames,
|
|
959
|
+
recognizedNames: recognized,
|
|
960
|
+
})) {
|
|
961
|
+
result.warnings.push(w);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
764
964
|
}
|
|
765
965
|
return result;
|
|
766
966
|
}
|
|
@@ -850,15 +1050,70 @@ function defaultsMatch(schemaDefault, dbDefault) {
|
|
|
850
1050
|
const b = normalizeDbDefault(dbDefault).toLowerCase().trim();
|
|
851
1051
|
return a === b;
|
|
852
1052
|
}
|
|
1053
|
+
// ---------------------------------------------------------------------------
|
|
1054
|
+
// Schema Push — execute the diff against a live database
|
|
1055
|
+
// ---------------------------------------------------------------------------
|
|
1056
|
+
/**
|
|
1057
|
+
* Scan a set of diff statements for data-destroying operations, using the same
|
|
1058
|
+
* conservative scanner (`scanDestructiveSql`) that gates `migrate up`/`down`.
|
|
1059
|
+
* Push mostly emits additive DDL, but a type change surfaces as a lossy
|
|
1060
|
+
* `ALTER COLUMN ... TYPE` cast, exactly the kind of silent data loss `push`
|
|
1061
|
+
* must never apply without an explicit opt-in.
|
|
1062
|
+
*/
|
|
1063
|
+
function findDestructivePushStatements(statements) {
|
|
1064
|
+
const hits = [];
|
|
1065
|
+
for (const stmt of statements)
|
|
1066
|
+
hits.push(...(0, destructive_js_1.scanDestructiveSql)(stmt));
|
|
1067
|
+
return hits;
|
|
1068
|
+
}
|
|
1069
|
+
/** Format the destructive-push refusal message (mirrors the migrate gate copy). */
|
|
1070
|
+
function formatDestructivePushError(hits) {
|
|
1071
|
+
const lines = ['[turbine] Refusing to apply schema changes containing DESTRUCTIVE statements:', ''];
|
|
1072
|
+
for (const h of hits) {
|
|
1073
|
+
lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
1074
|
+
}
|
|
1075
|
+
lines.push('');
|
|
1076
|
+
lines.push('Review the statements above. To proceed: run `npx turbine push` interactively');
|
|
1077
|
+
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
1078
|
+
return lines.join('\n');
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Thrown by {@link schemaPush} when the diff contains data-destroying statements
|
|
1082
|
+
* and `allowDestructive` was not set. A typed subclass of {@link ValidationError}
|
|
1083
|
+
* (same `TURBINE_E003` code, no new taxonomy entry) so callers can branch on
|
|
1084
|
+
* `instanceof DestructivePushRefusal` instead of sniffing the message text. The
|
|
1085
|
+
* offending statements are carried on `.destructive` for programmatic display.
|
|
1086
|
+
*/
|
|
1087
|
+
class DestructivePushRefusal extends errors_js_1.ValidationError {
|
|
1088
|
+
/** The destructive statements the push refused to apply. */
|
|
1089
|
+
destructive;
|
|
1090
|
+
constructor(destructive) {
|
|
1091
|
+
super(formatDestructivePushError(destructive));
|
|
1092
|
+
this.name = 'DestructivePushRefusal';
|
|
1093
|
+
this.destructive = destructive;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
exports.DestructivePushRefusal = DestructivePushRefusal;
|
|
853
1097
|
/**
|
|
854
1098
|
* Push a schema definition to a live database.
|
|
855
1099
|
*
|
|
856
1100
|
* Computes the diff, then executes the resulting DDL statements in a
|
|
857
|
-
* single transaction.
|
|
858
|
-
*
|
|
1101
|
+
* single transaction. It will NOT drop tables or columns.
|
|
1102
|
+
*
|
|
1103
|
+
* Data-loss gate: if the diff contains a destructive statement (e.g. a lossy
|
|
1104
|
+
* `ALTER COLUMN ... TYPE` cast), `schemaPush` throws a
|
|
1105
|
+
* {@link DestructivePushRefusal} (a {@link ValidationError} subclass carrying
|
|
1106
|
+
* the offending statements on `.destructive`) listing the statements UNLESS
|
|
1107
|
+
* `allowDestructive: true` is passed. The CLI (`turbine push`) catches this and
|
|
1108
|
+
* prompts for the same typed confirmation as `migrate up`; programmatic callers
|
|
1109
|
+
* must opt in explicitly.
|
|
859
1110
|
*/
|
|
860
1111
|
async function schemaPush(schema, connectionString, options = {}) {
|
|
861
|
-
|
|
1112
|
+
// Accept a precomputed diff so a caller (the CLI) can diff ONCE, show the
|
|
1113
|
+
// plan, confirm, and apply the EXACT statements it displayed. Without this,
|
|
1114
|
+
// schemaPush would re-diff on the post-confirmation retry, so a concurrent
|
|
1115
|
+
// schema change between confirm and apply could alter the applied set (TOCTOU).
|
|
1116
|
+
const diff = options.precomputedDiff ?? (await schemaDiff(schema, connectionString));
|
|
862
1117
|
const result = {
|
|
863
1118
|
statementsExecuted: 0,
|
|
864
1119
|
statements: diff.statements,
|
|
@@ -868,6 +1123,13 @@ async function schemaPush(schema, connectionString, options = {}) {
|
|
|
868
1123
|
if (options.dryRun || diff.statements.length === 0) {
|
|
869
1124
|
return result;
|
|
870
1125
|
}
|
|
1126
|
+
// Destructive-statement gate: refuse silent data loss unless opted in.
|
|
1127
|
+
if (!options.allowDestructive) {
|
|
1128
|
+
const destructive = findDestructivePushStatements(diff.statements);
|
|
1129
|
+
if (destructive.length > 0) {
|
|
1130
|
+
throw new DestructivePushRefusal(destructive);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
871
1133
|
// Execute all statements in a transaction
|
|
872
1134
|
const client = new pg_1.default.Client({ connectionString });
|
|
873
1135
|
await client.connect();
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -434,7 +434,7 @@ exports.sqliteDialect = {
|
|
|
434
434
|
return JSON.stringify(values ?? []);
|
|
435
435
|
},
|
|
436
436
|
buildReturningClause(selection = '*') {
|
|
437
|
-
return ` RETURNING ${selection}`;
|
|
437
|
+
return ` RETURNING ${selection === '*' ? '*' : selection.join(', ')}`;
|
|
438
438
|
},
|
|
439
439
|
buildInsertStatement(input) {
|
|
440
440
|
return (`INSERT INTO ${input.table} (${input.columns.join(', ')}) ` +
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Commands:
|
|
6
6
|
* turbine init — Initialize a Turbine project
|
|
7
7
|
* turbine generate | pull — Introspect database and generate TypeScript types
|
|
8
|
-
* turbine push
|
|
9
|
-
* turbine migrate create <name>
|
|
8
|
+
* turbine push - Apply schema-builder definitions to database (destructive ops gated)
|
|
9
|
+
* turbine migrate create <name> - Create a new SQL migration file (--auto | --recipe <name>)
|
|
10
10
|
* turbine migrate up — Apply pending migrations
|
|
11
11
|
* turbine migrate deploy — Apply pending migrations without prompts
|
|
12
12
|
* turbine migrate down — Rollback last migration
|
|
@@ -41,6 +41,8 @@ export interface CliArgs {
|
|
|
41
41
|
allowDrift?: boolean;
|
|
42
42
|
allowEmpty?: boolean;
|
|
43
43
|
allowDestructive?: boolean;
|
|
44
|
+
/** `migrate create --recipe <name>` scaffold selector. */
|
|
45
|
+
recipe?: string;
|
|
44
46
|
fix?: boolean;
|
|
45
47
|
zod?: boolean;
|
|
46
48
|
includeViews?: boolean;
|
|
@@ -51,6 +53,10 @@ export interface CliArgs {
|
|
|
51
53
|
noOpen?: boolean;
|
|
52
54
|
/** Opt-in to bind Studio/Observe on a non-loopback host. */
|
|
53
55
|
allowRemote?: boolean;
|
|
56
|
+
/** Opt-in to Studio single-row write mode (`studio --write`). */
|
|
57
|
+
write?: boolean;
|
|
58
|
+
/** Reveal PII-tagged column values in Studio instead of redacting (`--show-pii`). */
|
|
59
|
+
showPii?: boolean;
|
|
54
60
|
}
|
|
55
61
|
export declare function parseArgs(argv?: string[]): CliArgs;
|
|
56
62
|
/** Where a resolved `DATABASE_URL` came from, after the `.env` load. */
|