turbine-orm 0.39.0 → 0.40.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.
@@ -24,6 +24,7 @@ exports.getPendingMigrations = getPendingMigrations;
24
24
  exports.listMigrationFiles = listMigrationFiles;
25
25
  exports.parseMigrationContent = parseMigrationContent;
26
26
  exports.parseMigrationSQL = parseMigrationSQL;
27
+ exports.buildDiffMigrationBody = buildDiffMigrationBody;
27
28
  exports.createMigration = createMigration;
28
29
  exports.deriveLockId = deriveLockId;
29
30
  exports.planMigrationDeploy = planMigrationDeploy;
@@ -223,6 +224,84 @@ exports.MIGRATION_RECIPES = {
223
224
  build: buildBackfillRecipe,
224
225
  },
225
226
  };
227
+ /** Loud file-level banner prepended when a diff migration contains destructive statements. */
228
+ const DESTRUCTIVE_MIGRATION_BANNER = [
229
+ '-- ============================================================',
230
+ '-- WARNING: this migration contains DESTRUCTIVE statement(s).',
231
+ '-- Each one is flagged inline below. `turbine migrate up` refuses',
232
+ '-- destructive statements by default: it asks you to confirm',
233
+ '-- interactively, or you must pass --allow-destructive. Review',
234
+ '-- every flagged statement carefully before running.',
235
+ '-- ============================================================',
236
+ ];
237
+ /**
238
+ * Annotate a list of SQL statements: scan each for data-destroying operations
239
+ * (via {@link scanDestructiveSql}) and prefix any offender with loud, commented
240
+ * warnings. Statements are left intact so the existing `migrate up` gate still
241
+ * refuses them by default; the comments just make the danger visible on review.
242
+ */
243
+ function annotateDiffStatements(statements) {
244
+ const destructive = [];
245
+ const out = [];
246
+ for (const raw of statements) {
247
+ const stmt = raw.trim();
248
+ if (!stmt)
249
+ continue;
250
+ const hits = (0, destructive_js_1.scanDestructiveSql)(stmt);
251
+ if (hits.length > 0) {
252
+ destructive.push(...hits);
253
+ for (const h of hits) {
254
+ out.push(`-- !! DESTRUCTIVE [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
255
+ }
256
+ out.push('-- !! Refused by default. Confirm interactively or pass --allow-destructive to run it.');
257
+ }
258
+ out.push(stmt.endsWith(';') ? stmt : `${stmt};`);
259
+ }
260
+ return { text: out.join('\n'), destructive };
261
+ }
262
+ /**
263
+ * Build a migration UP/DOWN body from a `schemaDiff()` result.
264
+ *
265
+ * - UP is the diff's forward statements. DOWN is the diff's reverse statements
266
+ * when derivable, otherwise a clearly-commented "irreversible" placeholder.
267
+ * - Destructive statements in EITHER direction (a lossy `ALTER COLUMN ... TYPE`
268
+ * in UP, a `DROP TABLE`/`DROP COLUMN` reverse in DOWN) are flagged inline and,
269
+ * when any exist, a loud file-level banner is prepended to UP.
270
+ * - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
271
+ * enum value removals) are surfaced as `-- NOTE:` comments in UP.
272
+ *
273
+ * Pure and DB-free, so it is unit-testable from a synthesized diff.
274
+ */
275
+ function buildDiffMigrationBody(diff) {
276
+ const up = annotateDiffStatements(diff.statements);
277
+ const hasReverse = diff.reverseStatements.length > 0;
278
+ const down = hasReverse
279
+ ? annotateDiffStatements(diff.reverseStatements)
280
+ : { text: '', destructive: [] };
281
+ const upParts = [];
282
+ if (up.destructive.length > 0 || down.destructive.length > 0) {
283
+ upParts.push(...DESTRUCTIVE_MIGRATION_BANNER, '');
284
+ }
285
+ if (diff.warnings && diff.warnings.length > 0) {
286
+ for (const w of diff.warnings)
287
+ upParts.push(`-- NOTE: ${w}`);
288
+ upParts.push('');
289
+ }
290
+ upParts.push(up.text || '-- (no statements: schema already matches the database)');
291
+ const downText = hasReverse
292
+ ? down.text
293
+ : [
294
+ '-- irreversible, write manually',
295
+ '-- The diff produced no reversible statements for this change. Write the',
296
+ '-- rollback SQL by hand, or leave this section empty for a one-way migration.',
297
+ ].join('\n');
298
+ return {
299
+ up: upParts.join('\n'),
300
+ down: downText,
301
+ destructiveUp: up.destructive,
302
+ destructiveDown: down.destructive,
303
+ };
304
+ }
226
305
  // ---------------------------------------------------------------------------
227
306
  // Commands
228
307
  // ---------------------------------------------------------------------------
@@ -374,6 +374,7 @@ class TurbineClient {
374
374
  globalFilters: config.globalFilters,
375
375
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
376
376
  sqlCache: config.sqlCache ?? true,
377
+ sqlCacheSize: config.sqlCacheSize,
377
378
  dialect: config.dialect,
378
379
  // Non-SQL backends (PowDB) inject a factory that builds their own query
379
380
  // interface (PowqlInterface) instead of the SQL QueryInterface. SQL engines
package/dist/cjs/powql.js CHANGED
@@ -749,7 +749,7 @@ class PowqlInterface {
749
749
  buildOrder(orderBy, params, alias) {
750
750
  if (!orderBy)
751
751
  return '';
752
- const keys = Object.entries(orderBy).filter(([, dir]) => dir !== undefined);
752
+ const keys = (0, filters_js_1.orderByEntries)(orderBy).filter(([, dir]) => dir !== undefined);
753
753
  if (!keys.length)
754
754
  return '';
755
755
  const parts = keys.map(([field, dir]) => {
@@ -2204,7 +2204,11 @@ class PowqlInterface {
2204
2204
  }
2205
2205
  const having = this.buildHaving(args.having, params, aggInner);
2206
2206
  const order = this.buildGroupOrder(args.orderBy, byOrderExprs, aggOrderExprs);
2207
- const powql = `${this.qt}${filter} group ${groupExprs.join(', ')}${having}${order} { ${proj.join(', ')} }`;
2207
+ // LIMIT / OFFSET over the result groups, applied after ORDER BY (mirrors
2208
+ // the SQL groupBy). offset 0 is a no-op, matching findMany.
2209
+ const limitClause = args.limit !== undefined ? ` limit ${this.param(args.limit, params)}` : '';
2210
+ const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
2211
+ const powql = `${this.qt}${filter} group ${groupExprs.join(', ')}${having}${order}${limitClause}${offsetClause} { ${proj.join(', ')} }`;
2208
2212
  const { rows, native: resultNative } = await this.exec(powql, params, args.timeout, 'groupBy');
2209
2213
  // Reshape: group keys → user fields (coerced / null-disambiguated),
2210
2214
  // aggregates → nested `{ _sum: { field } }`; discriminators are stripped.
@@ -2310,7 +2314,7 @@ class PowqlInterface {
2310
2314
  return keys.join(', ') || '(none)';
2311
2315
  };
2312
2316
  const parts = [];
2313
- for (const [key, value] of Object.entries(orderBy)) {
2317
+ for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
2314
2318
  if (value === undefined)
2315
2319
  continue;
2316
2320
  if (aggBlocks.has(key)) {
@@ -210,6 +210,16 @@ function buildGroupBy(qi, args) {
210
210
  if (orderSql)
211
211
  sql += ` ORDER BY ${orderSql}`;
212
212
  }
213
+ // LIMIT / OFFSET over the result groups, applied AFTER ORDER BY. Routed
214
+ // through the dialect pagination hook (parameterized on PG/SQLite/SQL Server,
215
+ // inlined on MySQL); params append after the WHERE/HAVING/ORDER BY params, so
216
+ // no `$n` renumbering. `offset` without a deterministic `orderBy` yields an
217
+ // arbitrary window (same caveat as findMany).
218
+ if (args.limit !== undefined || args.offset !== undefined) {
219
+ const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params) : undefined;
220
+ const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params) : undefined;
221
+ sql += qi.buildPagination(limitPh, offsetPh, args.orderBy !== undefined);
222
+ }
213
223
  return {
214
224
  sql,
215
225
  params,
@@ -297,7 +307,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
297
307
  return keys.join(', ') || '(none)';
298
308
  };
299
309
  const parts = [];
300
- for (const [key, value] of Object.entries(orderBy)) {
310
+ for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
301
311
  if (value === undefined)
302
312
  continue;
303
313
  // Aggregate ordering blocks.
@@ -185,8 +185,12 @@ class QueryInterface {
185
185
  table;
186
186
  schema;
187
187
  tableMeta;
188
- /** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
189
- sqlTemplateCache = new utils_js_1.LRUCache(1000);
188
+ /**
189
+ * SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name).
190
+ * Capacity is set once in the constructor from `options.sqlCacheSize`
191
+ * (default 1000). See {@link QueryInterfaceOptions.sqlCacheSize}.
192
+ */
193
+ sqlTemplateCache;
190
194
  /**
191
195
  * Whether the most recent {@link acquireSql} call was a cache HIT. Read by
192
196
  * {@link crossCheckCache} to decide whether to run the dev-mode lockstep
@@ -314,7 +318,13 @@ class QueryInterface {
314
318
  : warnOpt !== false;
315
319
  this.utcTimestamps = options?.utcTimestamps !== false;
316
320
  this.preparedStatementsEnabled = options?.preparedStatements ?? true;
317
- this.sqlCacheEnabled = options?.sqlCache !== false;
321
+ // SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
322
+ // (mirrors `sqlCache: false`); any positive integer sets the LRU bound;
323
+ // undefined keeps the historical 1000-entry default. A negative value is
324
+ // treated as the default rather than throwing.
325
+ const sqlCacheSize = options?.sqlCacheSize;
326
+ this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
327
+ this.sqlTemplateCache = new utils_js_1.LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
318
328
  this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
319
329
  this.relationLoadStrategy = options?.relationLoadStrategy ?? 'join';
320
330
  this.jsonEncoding = options?.jsonEncoding ?? 'object';
@@ -1110,7 +1120,7 @@ class QueryInterface {
1110
1120
  // Checked BEFORE the SQL cache so build and warm-cache paths throw
1111
1121
  // identically (same rule as the vector guard inside the distinct branch).
1112
1122
  if (args?.distinct && args.distinct.length > 0 && args.orderBy) {
1113
- for (const d of Object.values(args.orderBy)) {
1123
+ for (const [, d] of (0, filters_js_1.orderByEntries)(args.orderBy)) {
1114
1124
  if (this.isRelationOrderByValue(d)) {
1115
1125
  throw new errors_js_1.ValidationError('[turbine] `distinct` cannot be combined with relation orderBy (pick-row, `_count`, or ' +
1116
1126
  'to-one relation ordering): the outer re-order cannot reference the parent table.');
@@ -1129,8 +1139,13 @@ class QueryInterface {
1129
1139
  // Build fingerprint for cache lookup
1130
1140
  const whereFp = hasWhere ? this.fingerprintWhere(whereObj) : '';
1131
1141
  const withFp = args?.with ? this.withFingerprint(args.with) : '';
1142
+ // Flatten via orderByEntries so the object form (`{ a, b }`) and the
1143
+ // Prisma-style array form (`[{ a }, { b }]`) fingerprint from the SAME
1144
+ // ordered entry list the build/collect paths consume. An array's element
1145
+ // order is authoritative and preserved here, so a permuted array is a
1146
+ // distinct key (correct, it emits a different ORDER BY).
1132
1147
  const orderFp = args?.orderBy
1133
- ? Object.entries(args.orderBy)
1148
+ ? (0, filters_js_1.orderByEntries)(args.orderBy)
1134
1149
  .map(([k, d]) => `${k}:${this.orderByEntryFingerprint(d, (0, utils_js_1.ownLookup)(this.tableMeta.relations, k)?.to)}`)
1135
1150
  .join(',')
1136
1151
  : '';
@@ -1199,11 +1214,15 @@ class QueryInterface {
1199
1214
  // Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
1200
1215
  const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
1201
1216
  if (cursorEntries.length > 0) {
1217
+ // Resolve the seek direction per cursor field from the flattened
1218
+ // orderBy entries (last wins, matching object-key semantics), so both
1219
+ // the object and array orderBy forms drive the cursor comparison.
1220
+ const orderDirByKey = new Map((0, filters_js_1.orderByEntries)(args.orderBy));
1202
1221
  const cursorConditions = cursorEntries.map(([k, v]) => {
1203
1222
  const col = this.toSqlColumn(k);
1204
1223
  // orderBy values can be the { sort, nulls } spec form: normalize
1205
1224
  // before comparing, or a desc spec would seek the ascending side.
1206
- const dir = args.orderBy?.[k];
1225
+ const dir = orderDirByKey.get(k);
1207
1226
  const desc = (0, filters_js_1.isOrderBySpec)(dir) ? dir.sort === 'desc' : dir === 'desc';
1208
1227
  const op = desc ? '<' : '>';
1209
1228
  freshParams.push(v);
@@ -1224,7 +1243,7 @@ class QueryInterface {
1224
1243
  // need two levels: inner DISTINCT ON ordered by the distinct columns then
1225
1244
  // the user's order (picks the right representative row), outer re-ordered
1226
1245
  // by the user's order alone.
1227
- if (Object.values(args.orderBy).some((d) => (0, filters_js_1.isVectorOrderBy)(d))) {
1246
+ if ((0, filters_js_1.orderByEntries)(args.orderBy).some(([, d]) => (0, filters_js_1.isVectorOrderBy)(d))) {
1228
1247
  throw new errors_js_1.ValidationError('[turbine] `distinct` cannot be combined with vector distance ordering.');
1229
1248
  }
1230
1249
  const userOrder = this.buildOrderBy(args.orderBy, freshParams);
@@ -1638,6 +1657,14 @@ class QueryInterface {
1638
1657
  // -------------------------------------------------------------------------
1639
1658
  // groupBy (with aggregate functions)
1640
1659
  // -------------------------------------------------------------------------
1660
+ /**
1661
+ * Group rows and compute per-group aggregates (Prisma-style). The result row
1662
+ * type is INFERRED from the args: each `by` field carries its entity field
1663
+ * type, `_count` is always a number, and each requested `_sum` / `_avg` /
1664
+ * `_min` / `_max` block maps its fields to properly typed values (see
1665
+ * {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
1666
+ * that cannot be typed, so those columns are not projected onto the row type.
1667
+ */
1641
1668
  async groupBy(args) {
1642
1669
  return this.executeWithMiddleware('groupBy', args, async () => {
1643
1670
  const deferred = this.buildGroupBy(args);
@@ -1804,7 +1831,7 @@ class QueryInterface {
1804
1831
  * order exactly so the cached-SQL param re-collection stays in lockstep.
1805
1832
  */
1806
1833
  collectOrderByParams(orderBy, params) {
1807
- for (const [key, dir] of Object.entries(orderBy)) {
1834
+ for (const [key, dir] of (0, filters_js_1.orderByEntries)(orderBy)) {
1808
1835
  if ((0, filters_js_1.isVectorOrderBy)(dir)) {
1809
1836
  const rawColumn = this.toColumn(key);
1810
1837
  // Re-run the same validation as buildOrderBy so the collect path can
@@ -28,6 +28,7 @@ exports.isVectorOrderBy = isVectorOrderBy;
28
28
  exports.isOrderBySpec = isOrderBySpec;
29
29
  exports.isJsonPathOrderBy = isJsonPathOrderBy;
30
30
  exports.isRelationPickOrderBy = isRelationPickOrderBy;
31
+ exports.orderByEntries = orderByEntries;
31
32
  exports.normalizeOrderBy = normalizeOrderBy;
32
33
  const errors_js_1 = require("../errors.js");
33
34
  const utils_js_1 = require("./utils.js");
@@ -356,6 +357,38 @@ function isRelationPickOrderBy(value) {
356
357
  }
357
358
  return true;
358
359
  }
360
+ /**
361
+ * Flatten an orderBy input into an ordered list of `[field, value]` entries.
362
+ *
363
+ * Accepts BOTH the classic single-object form (`{ a: 'asc', b: 'desc' }`,
364
+ * whose insertion order is authoritative) and the Prisma-style array form
365
+ * (`[{ a: 'asc' }, { b: 'desc' }]`, whose array order is authoritative). The
366
+ * array form removes the reliance on JS object key iteration order for
367
+ * multi-key sorts. Each array element may carry one or more keys; they expand
368
+ * left-to-right. `undefined`/non-object elements are skipped.
369
+ *
370
+ * Undefined-VALUED entries are preserved (mirroring `Object.entries`) so each
371
+ * consumer keeps its own `dir !== undefined` filtering exactly as before. This
372
+ * is THE single flattening authority: every ORDER BY compile / collect /
373
+ * fingerprint path routes through it so the array and object forms stay in
374
+ * lockstep across build, param-collect, and cache-key fingerprint.
375
+ */
376
+ function orderByEntries(orderBy) {
377
+ if (Array.isArray(orderBy)) {
378
+ const entries = [];
379
+ for (const element of orderBy) {
380
+ if (element !== null && typeof element === 'object' && !Array.isArray(element)) {
381
+ for (const kv of Object.entries(element))
382
+ entries.push(kv);
383
+ }
384
+ }
385
+ return entries;
386
+ }
387
+ if (orderBy !== null && typeof orderBy === 'object') {
388
+ return Object.entries(orderBy);
389
+ }
390
+ return [];
391
+ }
359
392
  /**
360
393
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
361
394
  * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
@@ -199,7 +199,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
199
199
  // orderBy shape (OrderBySpec nulls placement changes the SQL, so fingerprint it)
200
200
  if (opts.orderBy) {
201
201
  const targetRels = qi.schema.tables[relDef.to]?.relations;
202
- const oEntries = Object.entries(opts.orderBy).map(([k, d]) => `${k}:${orderByEntryFingerprint(qi, d, targetRels?.[k]?.to)}`);
202
+ const oEntries = (0, filters_js_1.orderByEntries)(opts.orderBy).map(([k, d]) => `${k}:${orderByEntryFingerprint(qi, d, targetRels?.[k]?.to)}`);
203
203
  subParts.push(`o=${oEntries.join(',')}`);
204
204
  }
205
205
  // limit presence, but on inline-pagination engines (MySQL) the literal
@@ -262,7 +262,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
262
262
  // orderBy params → where params → limit param → nested-with params
263
263
  // (always, both paths).
264
264
  if (relDef.type === 'manyToMany') {
265
- const m2mOrderEntries = spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
265
+ const m2mOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
266
266
  if (nativeOrderPath && m2mOrderEntries.length > 0) {
267
267
  collectRelationOrderParams(qi, targetTable, targetMeta, m2mOrderEntries, params);
268
268
  }
@@ -284,7 +284,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
284
284
  return;
285
285
  }
286
286
  // Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
287
- const relOrderEntries = spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
287
+ const relOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
288
288
  const hasOrder = relOrderEntries.length > 0;
289
289
  const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
290
290
  // Non-wrapped path: nested relations BEFORE where/limit
@@ -388,7 +388,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
388
388
  // orderBy keys (object values that are neither a vector nor an OrderBySpec)
389
389
  // are validated in the relation branch below, so skip them here.
390
390
  if (process.env.NODE_ENV !== 'production') {
391
- for (const [key, value] of Object.entries(orderBy)) {
391
+ for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
392
392
  if (isRelationOrderByValue(qi, value) && (0, utils_js_1.ownLookup)(qi.tableMeta.relations, key))
393
393
  continue;
394
394
  const snakeKey = (0, schema_js_1.camelToSnake)(key);
@@ -400,7 +400,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
400
400
  }
401
401
  const meta = qi.schema.tables[qi.table];
402
402
  let relOrdCounter = 0;
403
- return Object.entries(orderBy)
403
+ return (0, filters_js_1.orderByEntries)(orderBy)
404
404
  .map(([key, value]) => {
405
405
  // Vector KNN ordering: { distance: { to, metric, direction? } }
406
406
  if ((0, filters_js_1.isVectorOrderBy)(value)) {
@@ -1452,7 +1452,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
1452
1452
  // An orderBy with no defined entries (`orderBy: {}`) is treated as absent —
1453
1453
  // it must neither trigger the wrap (dropping nested relations) nor render a
1454
1454
  // dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
1455
- const relOrderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
1455
+ const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
1456
1456
  const willWrap = relDef.type === 'hasMany' && spec !== true && (spec.limit !== undefined || relOrderEntries.length > 0);
1457
1457
  // manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
1458
1458
  // where, orderBy, and select/omit are handled there (the target alias is the
@@ -1623,7 +1623,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
1623
1623
  // `orderBy: {}` (no defined entries) is treated as absent: it must not
1624
1624
  // render a dangling `ORDER BY `. Param pushes here land BEFORE the
1625
1625
  // spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
1626
- const relOrderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
1626
+ const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
1627
1627
  let orderClause = '';
1628
1628
  if (relOrderEntries.length > 0) {
1629
1629
  orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
@@ -6,7 +6,7 @@
6
6
  * turbine init — Initialize a Turbine project
7
7
  * turbine generate | pull — Introspect database and generate TypeScript types
8
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>)
9
+ * turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --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
@@ -38,12 +38,24 @@ export interface CliArgs {
38
38
  verbose?: boolean;
39
39
  help?: boolean;
40
40
  auto?: boolean;
41
+ /** `migrate create --from-diff`: scaffold UP/DOWN from the schema diff, destructive statements flagged. */
42
+ fromDiff?: boolean;
41
43
  allowDrift?: boolean;
42
44
  allowEmpty?: boolean;
43
45
  allowDestructive?: boolean;
44
46
  /** `migrate create --recipe <name>` scaffold selector. */
45
47
  recipe?: string;
46
48
  fix?: boolean;
49
+ /** `init --yes`/`-y`: accept every step's default non-interactively. */
50
+ yes?: boolean;
51
+ /** `init --skip-schema`: don't scaffold the schema file. */
52
+ skipSchema?: boolean;
53
+ /** `init --skip-seed`: don't scaffold the seed file or offer to run it. */
54
+ skipSeed?: boolean;
55
+ /** `init --skip-push`: don't offer to push the schema to the database. */
56
+ skipPush?: boolean;
57
+ /** `init --skip-generate`: don't offer to generate the typed client. */
58
+ skipGenerate?: boolean;
47
59
  zod?: boolean;
48
60
  includeViews?: boolean;
49
61
  /** Omit the `Generated at:` header line for reproducible (diff-stable) output. */
@@ -129,6 +141,54 @@ export declare function dotEnvUrlConflictWarning(input: {
129
141
  * project, and `'none'` when there is no readable/parseable package.json.
130
142
  */
131
143
  export declare function detectConsumerModuleType(cwd?: string): 'module' | 'commonjs' | 'none';
144
+ /** A single step in the `turbine init` flow. */
145
+ export type InitStepId = 'config' | 'schema' | 'seed-file' | 'push' | 'generate' | 'seed-run';
146
+ /** What the planner decided to do with a step. */
147
+ export type InitStepAction = 'run' | 'prompt' | 'skip';
148
+ /** Why a step was skipped (only set when `action` is `skip`). */
149
+ export type InitStepSkipReason = 'exists' | 'flag' | 'no-url' | 'unreachable' | 'no-seed-file' | 'non-interactive' | 'default-no';
150
+ export interface InitPlanStep {
151
+ id: InitStepId;
152
+ action: InitStepAction;
153
+ /** Prompt default; also the value used to decide auto-run under `--yes`. */
154
+ defaultYes: boolean;
155
+ skipReason?: InitStepSkipReason;
156
+ }
157
+ /** Detected project state (all IO done by the caller). */
158
+ export interface InitPlanState {
159
+ configExists: boolean;
160
+ schemaExists: boolean;
161
+ seedFileExists: boolean;
162
+ hasUrl: boolean;
163
+ dbReachable: boolean;
164
+ }
165
+ /** Effective flags for the planner. */
166
+ export interface InitPlanFlags {
167
+ yes: boolean;
168
+ force: boolean;
169
+ interactive: boolean;
170
+ skipSchema: boolean;
171
+ skipSeed: boolean;
172
+ skipPush: boolean;
173
+ skipGenerate: boolean;
174
+ }
175
+ /**
176
+ * Pure step planner for `turbine init`. Given the detected project state and the
177
+ * effective flags, decide for each step whether to run it, prompt for it, or
178
+ * skip it (and why). No IO: every input is precomputed by the caller: so the
179
+ * whole decision matrix is unit-testable without a TTY or a database.
180
+ *
181
+ * Three modes:
182
+ * - `prompt` (interactive TTY, no `--yes`): scaffold + DB steps are prompted.
183
+ * - `auto-yes` (`--yes`): accept each step's default; the yes-defaults run.
184
+ * - `auto-legacy` (non-TTY, no `--yes`): reproduce the pre-existing init
185
+ * behavior. Scaffold files + generate run; push + seed-run do not.
186
+ *
187
+ * Steps that create files (config, schema, seed) are skipped when the file
188
+ * already exists, so re-runs are safe. DB steps (push, generate, seed-run) are
189
+ * skipped when there is no URL or the database is unreachable.
190
+ */
191
+ export declare function planInitSteps(state: InitPlanState, flags: InitPlanFlags): InitPlanStep[];
132
192
  export declare function buildMigrateDeployOptions(_args: CliArgs): {
133
193
  allowDrift: false;
134
194
  allowDestructive: true;