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.
Files changed (68) hide show
  1. package/README.md +18 -16
  2. package/dist/cjs/cli/index.js +109 -16
  3. package/dist/cjs/cli/migrate.js +78 -3
  4. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  5. package/dist/cjs/cli/studio.js +333 -22
  6. package/dist/cjs/cli/ui.js +7 -1
  7. package/dist/cjs/dialect.js +1 -1
  8. package/dist/cjs/generate.js +23 -2
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/mssql.js +22 -5
  11. package/dist/cjs/powdb.js +41 -1
  12. package/dist/cjs/powql.js +80 -25
  13. package/dist/cjs/query/aggregates.js +683 -0
  14. package/dist/cjs/query/batched-loader.js +2 -0
  15. package/dist/cjs/query/builder.js +297 -4504
  16. package/dist/cjs/query/filters.js +12 -0
  17. package/dist/cjs/query/relations.js +1698 -0
  18. package/dist/cjs/query/where-compile.js +180 -0
  19. package/dist/cjs/query/where.js +1491 -0
  20. package/dist/cjs/query/writes.js +680 -0
  21. package/dist/cjs/schema-builder.js +6 -0
  22. package/dist/cjs/schema-metadata.js +4 -0
  23. package/dist/cjs/schema-sql.js +265 -3
  24. package/dist/cjs/sqlite.js +1 -1
  25. package/dist/cli/index.d.ts +8 -2
  26. package/dist/cli/index.js +111 -18
  27. package/dist/cli/migrate.d.ts +24 -1
  28. package/dist/cli/migrate.js +77 -3
  29. package/dist/cli/studio-ui.generated.js +1 -1
  30. package/dist/cli/studio.d.ts +46 -13
  31. package/dist/cli/studio.js +331 -23
  32. package/dist/cli/ui.js +7 -1
  33. package/dist/dialect.d.ts +15 -6
  34. package/dist/dialect.js +1 -1
  35. package/dist/generate.js +23 -2
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/mssql.js +22 -5
  39. package/dist/powdb.d.ts +20 -0
  40. package/dist/powdb.js +40 -0
  41. package/dist/powql.d.ts +33 -1
  42. package/dist/powql.js +80 -25
  43. package/dist/query/aggregates.d.ts +74 -0
  44. package/dist/query/aggregates.js +641 -0
  45. package/dist/query/batched-loader.d.ts +6 -0
  46. package/dist/query/batched-loader.js +2 -0
  47. package/dist/query/builder.d.ts +62 -829
  48. package/dist/query/builder.js +302 -4509
  49. package/dist/query/deferred.d.ts +7 -0
  50. package/dist/query/filters.d.ts +7 -0
  51. package/dist/query/filters.js +11 -0
  52. package/dist/query/relations.d.ts +441 -0
  53. package/dist/query/relations.js +1627 -0
  54. package/dist/query/types.d.ts +15 -0
  55. package/dist/query/where-compile.d.ts +139 -0
  56. package/dist/query/where-compile.js +175 -0
  57. package/dist/query/where.d.ts +494 -0
  58. package/dist/query/where.js +1431 -0
  59. package/dist/query/writes.d.ts +131 -0
  60. package/dist/query/writes.js +626 -0
  61. package/dist/schema-builder.d.ts +18 -3
  62. package/dist/schema-builder.js +6 -0
  63. package/dist/schema-metadata.js +4 -0
  64. package/dist/schema-sql.d.ts +60 -3
  65. package/dist/schema-sql.js +261 -4
  66. package/dist/schema.d.ts +10 -0
  67. package/dist/sqlite.js +1 -1
  68. 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);
@@ -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. This is a destructive operation for ADD/ALTER —
858
- * it will NOT drop tables or columns unless explicitly configured.
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
- const diff = await schemaDiff(schema, connectionString);
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();
@@ -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(', ')}) ` +
@@ -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 Apply schema-builder definitions to database
9
- * turbine migrate create <name> Create a new SQL migration file
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. */