turbine-orm 0.27.1 → 0.28.1

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 (64) hide show
  1. package/README.md +19 -15
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/index.js +273 -71
  4. package/dist/cjs/cli/mcp.js +788 -0
  5. package/dist/cjs/cli/migrate.js +95 -20
  6. package/dist/cjs/cli/studio.js +3 -2
  7. package/dist/cjs/client.js +267 -34
  8. package/dist/cjs/dialect.js +2 -0
  9. package/dist/cjs/errors.js +15 -1
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/powdb.js +1 -1
  14. package/dist/cjs/powql.js +1 -1
  15. package/dist/cjs/query/batched-loader.js +148 -0
  16. package/dist/cjs/query/builder.js +763 -401
  17. package/dist/cjs/query/deferred.js +7 -0
  18. package/dist/cjs/query/filters.js +251 -0
  19. package/dist/cjs/schema-builder.js +59 -4
  20. package/dist/cjs/schema-sql.js +315 -6
  21. package/dist/cjs/seed.js +66 -0
  22. package/dist/cli/config.d.ts +9 -2
  23. package/dist/cli/config.js +19 -3
  24. package/dist/cli/index.d.ts +52 -1
  25. package/dist/cli/index.js +272 -74
  26. package/dist/cli/mcp.d.ts +17 -0
  27. package/dist/cli/mcp.js +781 -0
  28. package/dist/cli/migrate.d.ts +37 -0
  29. package/dist/cli/migrate.js +92 -20
  30. package/dist/cli/studio.d.ts +3 -2
  31. package/dist/cli/studio.js +3 -2
  32. package/dist/client.d.ts +136 -1
  33. package/dist/client.js +267 -34
  34. package/dist/dialect.d.ts +17 -0
  35. package/dist/dialect.js +2 -0
  36. package/dist/errors.js +15 -1
  37. package/dist/generate.d.ts +17 -0
  38. package/dist/generate.js +171 -10
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +2 -0
  41. package/dist/introspect.d.ts +20 -1
  42. package/dist/introspect.js +175 -4
  43. package/dist/powdb.d.ts +1 -1
  44. package/dist/powdb.js +1 -1
  45. package/dist/powql.d.ts +1 -1
  46. package/dist/powql.js +1 -1
  47. package/dist/query/batched-loader.d.ts +29 -2
  48. package/dist/query/batched-loader.js +148 -1
  49. package/dist/query/builder.d.ts +151 -122
  50. package/dist/query/builder.js +701 -339
  51. package/dist/query/deferred.d.ts +130 -0
  52. package/dist/query/deferred.js +6 -0
  53. package/dist/query/filters.d.ts +120 -0
  54. package/dist/query/filters.js +232 -0
  55. package/dist/query/index.d.ts +1 -1
  56. package/dist/query/types.d.ts +113 -8
  57. package/dist/schema-builder.d.ts +73 -8
  58. package/dist/schema-builder.js +59 -4
  59. package/dist/schema-sql.d.ts +67 -0
  60. package/dist/schema-sql.js +310 -6
  61. package/dist/schema.d.ts +53 -0
  62. package/dist/seed.d.ts +4 -0
  63. package/dist/seed.js +63 -0
  64. package/package.json +4 -5
@@ -9,13 +9,39 @@ 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.referentialActionToSql = referentialActionToSql;
12
13
  exports.schemaToSQL = schemaToSQL;
14
+ exports.buildAddForeignKeyStatement = buildAddForeignKeyStatement;
15
+ exports.diffReferentialAction = diffReferentialAction;
16
+ exports.diffEnumValues = diffEnumValues;
17
+ exports.diffCheckConstraints = diffCheckConstraints;
13
18
  exports.schemaDiff = schemaDiff;
14
19
  exports.schemaPush = schemaPush;
15
20
  exports.schemaToSQLString = schemaToSQLString;
16
21
  const pg_1 = __importDefault(require("pg"));
17
22
  const dialect_js_1 = require("./dialect.js");
23
+ const errors_js_1 = require("./errors.js");
24
+ const introspect_js_1 = require("./introspect.js");
18
25
  const schema_js_1 = require("./schema.js");
26
+ /** Map a {@link ReferentialAction} to its SQL keyword form. */
27
+ function referentialActionToSql(action) {
28
+ switch (action) {
29
+ case 'cascade':
30
+ return 'CASCADE';
31
+ case 'restrict':
32
+ return 'RESTRICT';
33
+ case 'set null':
34
+ return 'SET NULL';
35
+ case 'set default':
36
+ return 'SET DEFAULT';
37
+ case 'no action':
38
+ return 'NO ACTION';
39
+ }
40
+ }
41
+ /** Single-quote-escape an enum label for a `CREATE TYPE ... AS ENUM` literal. */
42
+ function quoteEnumLabel(label) {
43
+ return `'${label.replace(/'/g, "''")}'`;
44
+ }
19
45
  /**
20
46
  * Whether a resolved column type is an auto-increment pseudo-type (SERIAL /
21
47
  * BIGSERIAL). These carry an implicit sequence default and NOT NULL, and their
@@ -25,6 +51,38 @@ const schema_js_1 = require("./schema.js");
25
51
  function isSerialType(type) {
26
52
  return type === 'SERIAL' || type === 'BIGSERIAL';
27
53
  }
54
+ /** Whether any column in the schema is a pgvector column. */
55
+ function schemaHasVectorColumn(schema) {
56
+ for (const table of Object.values(schema.tables)) {
57
+ for (const col of Object.values(table.columns)) {
58
+ if (col.vectorDimensions != null)
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+ /** Build a `CREATE TYPE "<name>" AS ENUM ('a', 'b')` statement. */
65
+ function generateCreateEnumType(enumName, labels, dialect) {
66
+ const values = labels.map(quoteEnumLabel).join(', ');
67
+ return `CREATE TYPE ${dialect.quoteIdentifier(enumName)} AS ENUM (${values});`;
68
+ }
69
+ /**
70
+ * Resolve the DDL type token for a column: an enum type name, a `vector(n)`
71
+ * literal, or the dialect's scalar type — with a trailing `[]` for arrays.
72
+ */
73
+ function resolveDdlType(config, dialect) {
74
+ let base;
75
+ if (config.enumName) {
76
+ base = dialect.quoteIdentifier(config.enumName);
77
+ }
78
+ else if (config.vectorDimensions != null) {
79
+ base = `vector(${config.vectorDimensions})`;
80
+ }
81
+ else {
82
+ base = dialect.buildColumnType({ type: config.type, maxLength: config.maxLength });
83
+ }
84
+ return config.isArray ? `${base}[]` : base;
85
+ }
28
86
  // ---------------------------------------------------------------------------
29
87
  // SQL Generation — SchemaDef → CREATE TABLE statements
30
88
  // ---------------------------------------------------------------------------
@@ -36,10 +94,25 @@ function isSerialType(type) {
36
94
  */
37
95
  function schemaToSQL(schema, options) {
38
96
  const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
97
+ const extensions = options?.extensions ?? 'auto';
39
98
  const statements = [];
40
99
  // Topologically sort tables by their foreign key references
41
100
  const sorted = topologicalSort(schema);
42
101
  const resolveRef = makeRefResolver(schema);
102
+ // pgvector extension line — only when a vector column exists. Postgres-only:
103
+ // a dialect that can't do pgvector must not silently emit broken DDL.
104
+ if (schemaHasVectorColumn(schema)) {
105
+ if (!dialect.supportsVector) {
106
+ throw new errors_js_1.UnsupportedFeatureError('vector columns', dialect.name, 'pgvector is a PostgreSQL-only feature.');
107
+ }
108
+ statements.push(extensions === 'manual'
109
+ ? '-- Requires the pgvector extension: run `CREATE EXTENSION IF NOT EXISTS vector;` before applying.'
110
+ : 'CREATE EXTENSION IF NOT EXISTS vector;');
111
+ }
112
+ // CREATE TYPE for every schema-level enum, before the tables that use them.
113
+ for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
114
+ statements.push(generateCreateEnumType(enumName, labels, dialect));
115
+ }
43
116
  // Generate CREATE TABLE statements
44
117
  for (const tableName of sorted) {
45
118
  const table = schema.tables[tableName];
@@ -156,6 +229,12 @@ function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresD
156
229
  const cols = compositePk.map((c) => dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(c)));
157
230
  columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
158
231
  }
232
+ // Table-level CHECK constraints (named → CONSTRAINT "name" CHECK (...)).
233
+ for (const chk of table.checks ?? []) {
234
+ columnDefs.push(chk.name
235
+ ? `CONSTRAINT ${dialect.quoteIdentifier(chk.name)} CHECK (${chk.expression})`
236
+ : `CHECK (${chk.expression})`);
237
+ }
159
238
  return dialect.buildCreateTableStatement({
160
239
  table: dialect.quoteIdentifier(tableName),
161
240
  definitions: columnDefs,
@@ -194,16 +273,33 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
194
273
  };
195
274
  }
196
275
  }
197
- return dialect.buildColumnDefinition({
276
+ // Resolve the DDL type (enum name / vector(n) / scalar, plus [] for arrays).
277
+ // Passed as a fully-formed `type` token with no maxLength so the dialect
278
+ // doesn't re-apply VARCHAR(n) on top of it.
279
+ const ddlType = resolveDdlType(config, dialect);
280
+ let def = dialect.buildColumnDefinition({
198
281
  name: dialect.quoteIdentifier(snakeName),
199
- type: config.type,
200
- maxLength: config.maxLength,
282
+ type: ddlType,
283
+ maxLength: null,
201
284
  primaryKey: config.isPrimaryKey,
202
285
  unique: config.isUnique,
203
286
  notNull,
204
287
  defaultValue,
205
288
  references,
206
289
  });
290
+ // Referential actions follow the REFERENCES clause (which buildColumnDefinition
291
+ // emits last). Postgres omits ON DELETE/UPDATE for the default NO ACTION.
292
+ if (references) {
293
+ if (config.onDelete)
294
+ def += ` ON DELETE ${referentialActionToSql(config.onDelete)}`;
295
+ if (config.onUpdate)
296
+ def += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
297
+ }
298
+ // Column-level CHECK constraint (raw SQL expression, user-authored).
299
+ if (config.check) {
300
+ def += ` CHECK (${config.check})`;
301
+ }
302
+ return def;
207
303
  }
208
304
  /**
209
305
  * Normalize a default value from the user's schema definition to valid SQL.
@@ -258,6 +354,94 @@ function generateForeignKeyIndexes(table, dialect = dialect_js_1.postgresDialect
258
354
  }
259
355
  return indexes;
260
356
  }
357
+ /**
358
+ * Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
359
+ * referential actions. Default (`no action`) clauses are omitted, matching how
360
+ * Postgres normalizes them, so re-diffing is stable.
361
+ */
362
+ function buildAddForeignKeyStatement(table, constraintName, column, targetTable, targetColumn, onDelete, onUpdate, dialect = dialect_js_1.postgresDialect) {
363
+ const q = (s) => dialect.quoteIdentifier(s);
364
+ let sql = `ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(constraintName)} FOREIGN KEY (${q(column)}) REFERENCES ${q(targetTable)}(${q(targetColumn)})`;
365
+ if (onDelete !== 'no action')
366
+ sql += ` ON DELETE ${referentialActionToSql(onDelete)}`;
367
+ if (onUpdate !== 'no action')
368
+ sql += ` ON UPDATE ${referentialActionToSql(onUpdate)}`;
369
+ return `${sql};`;
370
+ }
371
+ /**
372
+ * Decide whether a FK's referential actions changed. When they differ, returns
373
+ * the DROP + ADD CONSTRAINT statements (and their reverse) — Postgres has no
374
+ * `ALTER CONSTRAINT` for referential actions, so drop-and-recreate is the only
375
+ * path. Returns null when the actions already match.
376
+ */
377
+ function diffReferentialAction(table, db, schemaOnDelete, schemaOnUpdate, dialect = dialect_js_1.postgresDialect) {
378
+ if (db.onDelete === schemaOnDelete && db.onUpdate === schemaOnUpdate)
379
+ return null;
380
+ const q = (s) => dialect.quoteIdentifier(s);
381
+ const drop = `ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(db.constraintName)};`;
382
+ const add = buildAddForeignKeyStatement(table, db.constraintName, db.column, db.targetTable, db.targetColumn, schemaOnDelete, schemaOnUpdate, dialect);
383
+ const reverseAdd = buildAddForeignKeyStatement(table, db.constraintName, db.column, db.targetTable, db.targetColumn, db.onDelete, db.onUpdate, dialect);
384
+ return { statements: [drop, add], reverseStatements: [drop, reverseAdd] };
385
+ }
386
+ /**
387
+ * Compute append-only enum value changes. Returns `ALTER TYPE ... ADD VALUE`
388
+ * statements for labels present in the schema but not the DB (in order), plus a
389
+ * destructive warning for any DB label the schema dropped or any reorder —
390
+ * Postgres cannot remove or reorder enum values without recreating the type.
391
+ */
392
+ function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = dialect_js_1.postgresDialect) {
393
+ const statements = [];
394
+ const warnings = [];
395
+ const dbSet = new Set(dbLabels);
396
+ for (const label of schemaLabels) {
397
+ if (!dbSet.has(label)) {
398
+ statements.push(`ALTER TYPE ${dialect.quoteIdentifier(enumName)} ADD VALUE ${quoteEnumLabel(label)};`);
399
+ }
400
+ }
401
+ const schemaSet = new Set(schemaLabels);
402
+ const removed = dbLabels.filter((l) => !schemaSet.has(l));
403
+ if (removed.length > 0) {
404
+ warnings.push(`Enum "${enumName}": labels [${removed.join(', ')}] exist in the database but not the schema. ` +
405
+ `Postgres cannot remove enum values in place — recreate the type manually if intended.`);
406
+ }
407
+ return { statements, warnings };
408
+ }
409
+ /**
410
+ * Diff a table's CHECK constraints (matched by name). Adds constraints missing
411
+ * from the DB, drops DB constraints absent from the schema, and drop+adds when a
412
+ * same-named constraint's expression changed. Expression comparison is a naive
413
+ * whitespace-insensitive match — semantically-equal-but-different-spelled
414
+ * expressions may re-emit (documented; harmless drop+add).
415
+ */
416
+ function diffCheckConstraints(table, schemaChecks, dbChecks, dialect = dialect_js_1.postgresDialect) {
417
+ const q = (s) => dialect.quoteIdentifier(s);
418
+ const statements = [];
419
+ const reverseStatements = [];
420
+ const dbByName = new Map(dbChecks.map((c) => [c.name, c]));
421
+ const schemaByName = new Map(schemaChecks.map((c) => [c.name, c]));
422
+ const norm = (e) => e.replace(/\s+/g, ' ').trim();
423
+ const addStmt = (c) => `ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(c.name)} CHECK (${c.expression});`;
424
+ const dropStmt = (name) => `ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(name)};`;
425
+ for (const sc of schemaChecks) {
426
+ const existing = dbByName.get(sc.name);
427
+ if (!existing) {
428
+ statements.push(addStmt(sc));
429
+ reverseStatements.push(dropStmt(sc.name));
430
+ }
431
+ else if (norm(existing.expression) !== norm(sc.expression)) {
432
+ // Expression changed → drop + add.
433
+ statements.push(dropStmt(sc.name), addStmt(sc));
434
+ reverseStatements.push(dropStmt(sc.name), addStmt(existing));
435
+ }
436
+ }
437
+ for (const dc of dbChecks) {
438
+ if (!schemaByName.has(dc.name)) {
439
+ statements.push(dropStmt(dc.name));
440
+ reverseStatements.push(addStmt(dc));
441
+ }
442
+ }
443
+ return { statements, reverseStatements };
444
+ }
261
445
  /**
262
446
  * Compare a SchemaDef against a live Postgres database and return the diff.
263
447
  *
@@ -312,12 +496,78 @@ async function schemaDiff(schema, connectionString) {
312
496
  dbUniques[row.table_name] = {};
313
497
  dbUniques[row.table_name][row.column_name] = row.constraint_name;
314
498
  }
499
+ // Existing enums (typname → ordered labels) for CREATE TYPE / ADD VALUE diff.
500
+ const enumResult = await client.query(`SELECT t.typname, e.enumlabel
501
+ FROM pg_type t
502
+ JOIN pg_enum e ON t.oid = e.enumtypid
503
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
504
+ WHERE n.nspname = 'public'
505
+ ORDER BY t.typname, e.enumsortorder`);
506
+ const dbEnums = {};
507
+ for (const row of enumResult.rows) {
508
+ if (!dbEnums[row.typname])
509
+ dbEnums[row.typname] = [];
510
+ dbEnums[row.typname].push(row.enumlabel);
511
+ }
512
+ // Existing single-column FK referential actions, keyed by table → column.
513
+ const fkResult = await client.query(`SELECT rel.relname AS table_name, con.conname AS constraint_name,
514
+ att.attname AS column, tgt.relname AS target_table,
515
+ tatt.attname AS target_column, con.confdeltype, con.confupdtype
516
+ FROM pg_constraint con
517
+ JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
518
+ JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
519
+ JOIN pg_catalog.pg_class tgt ON tgt.oid = con.confrelid
520
+ JOIN pg_catalog.pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = con.conkey[1]
521
+ JOIN pg_catalog.pg_attribute tatt ON tatt.attrelid = con.confrelid AND tatt.attnum = con.confkey[1]
522
+ WHERE con.contype = 'f' AND n.nspname = 'public'
523
+ AND array_length(con.conkey, 1) = 1`);
524
+ const dbForeignKeys = {};
525
+ for (const row of fkResult.rows) {
526
+ if (!dbForeignKeys[row.table_name])
527
+ dbForeignKeys[row.table_name] = {};
528
+ dbForeignKeys[row.table_name][row.column] = {
529
+ constraintName: row.constraint_name,
530
+ column: row.column,
531
+ targetTable: row.target_table,
532
+ targetColumn: row.target_column,
533
+ onDelete: (0, introspect_js_1.pgConfActionToReferential)(row.confdeltype),
534
+ onUpdate: (0, introspect_js_1.pgConfActionToReferential)(row.confupdtype),
535
+ };
536
+ }
537
+ // Existing CHECK constraints (contype='c'), keyed by table.
538
+ const checkResult = await client.query(`SELECT rel.relname AS table_name, con.conname, pg_get_constraintdef(con.oid) AS definition
539
+ FROM pg_constraint con
540
+ JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
541
+ JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
542
+ WHERE con.contype = 'c' AND n.nspname = 'public'`);
543
+ const dbChecks = {};
544
+ for (const row of checkResult.rows) {
545
+ if (!dbChecks[row.table_name])
546
+ dbChecks[row.table_name] = [];
547
+ dbChecks[row.table_name].push({ name: row.conname, expression: (0, introspect_js_1.stripCheckWrapper)(row.definition) });
548
+ }
315
549
  // Build a set of DDL-facing snake_case table names that the schema defines.
316
550
  const schemaDdlNames = new Set();
317
551
  for (const def of Object.values(schema.tables))
318
552
  schemaDdlNames.add(def.name);
319
- const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [] };
553
+ const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [], warnings: [] };
320
554
  const resolveRef = makeRefResolver(schema);
555
+ // --- Enums: CREATE TYPE for new enums (before tables), ADD VALUE for grown
556
+ // ones, and a warning for any destructive removal/reorder. New CREATE
557
+ // TYPEs go first so tables that reference them create cleanly. ---
558
+ for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
559
+ const existing = dbEnums[enumName];
560
+ if (!existing) {
561
+ result.statements.push(generateCreateEnumType(enumName, labels, dialect));
562
+ result.reverseStatements.unshift(`DROP TYPE IF EXISTS ${dialect.quoteIdentifier(enumName)};`);
563
+ }
564
+ else {
565
+ const { statements, warnings } = diffEnumValues(enumName, labels, existing, dialect);
566
+ result.statements.push(...statements);
567
+ result.warnings.push(...warnings);
568
+ // ADD VALUE cannot be reversed (Postgres can't drop enum values).
569
+ }
570
+ }
321
571
  // Tables to create (in schema but not in DB)
322
572
  const sorted = topologicalSort(schema);
323
573
  for (const tableKey of sorted) {
@@ -368,7 +618,9 @@ async function schemaDiff(schema, connectionString) {
368
618
  // BIGSERIAL (int8) before 0.24.0 — `push` won't try to shrink them.
369
619
  const expectedUdt = schemaTypeToUdt(config);
370
620
  if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
371
- const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
621
+ // resolveDdlType handles enum names, vector(n), arrays, and VARCHAR(n) —
622
+ // config.type alone would emit the internal ENUM/VECTOR sentinels here.
623
+ const sqlType = resolveDdlType(config, dialect);
372
624
  const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
373
625
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
374
626
  const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
@@ -461,6 +713,54 @@ async function schemaDiff(schema, connectionString) {
461
713
  if (alterDef.columns.length > 0) {
462
714
  result.alter.push(alterDef);
463
715
  }
716
+ // --- Referential action changes on existing single-column FKs ---
717
+ // Postgres has no ALTER CONSTRAINT for actions → DROP + ADD CONSTRAINT.
718
+ const tableFks = dbForeignKeys[tableName] ?? {};
719
+ for (const [fieldName, config] of Object.entries(tableDef.columns)) {
720
+ if (!config.referencesTarget)
721
+ continue;
722
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
723
+ const dbFk = tableFks[snakeName];
724
+ if (!dbFk)
725
+ continue; // FK not present in DB yet (or composite) — skip
726
+ const change = diffReferentialAction(tableName, dbFk, config.onDelete ?? 'no action', config.onUpdate ?? 'no action', dialect);
727
+ if (change) {
728
+ result.statements.push(...change.statements);
729
+ for (const rev of change.reverseStatements.slice().reverse())
730
+ result.reverseStatements.unshift(rev);
731
+ }
732
+ }
733
+ // --- Named table-level CHECK constraints ---
734
+ // Presence-only diffing: ADD checks whose NAME is missing from the DB.
735
+ // We do NOT auto-drop DB checks absent from the schema (column-level /
736
+ // inline checks carry auto-generated names the code-first schema never
737
+ // sees), and we do NOT drop+add on expression mismatch: pg_get_constraintdef
738
+ // canonicalizes expressions (casts, ANY(ARRAY[...]) rewrites), so authored
739
+ // text almost never string-matches the stored form — comparing would emit a
740
+ // spurious full-table-revalidating drop+add on every diff. An apparent
741
+ // mismatch surfaces as a warning instead; rename the constraint to
742
+ // intentionally replace its expression. Unnamed schema checks are skipped
743
+ // (no stable identity to diff on).
744
+ const namedSchemaChecks = (tableDef.checks ?? [])
745
+ .filter((c) => typeof c.name === 'string' && c.name.length > 0)
746
+ .map((c) => ({ name: c.name, expression: c.expression }));
747
+ const tableDbChecks = dbChecks[tableName] ?? [];
748
+ const dbCheckByName = new Map(tableDbChecks.map((c) => [c.name, c]));
749
+ const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
750
+ for (const sc of namedSchemaChecks) {
751
+ const existing = dbCheckByName.get(sc.name);
752
+ const addSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(sc.name)} CHECK (${sc.expression});`;
753
+ const dropSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(sc.name)};`;
754
+ if (!existing) {
755
+ result.statements.push(addSql);
756
+ result.reverseStatements.unshift(dropSql);
757
+ }
758
+ else if (normExpr(existing.expression) !== normExpr(sc.expression)) {
759
+ result.warnings.push(`check constraint "${sc.name}" on "${tableName}": stored expression differs from the schema text ` +
760
+ `(Postgres canonicalizes CHECK expressions, so this is usually cosmetic). ` +
761
+ `To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`);
762
+ }
763
+ }
464
764
  }
465
765
  return result;
466
766
  }
@@ -472,6 +772,11 @@ async function schemaDiff(schema, connectionString) {
472
772
  * Map a schema column type to its expected PostgreSQL UDT name.
473
773
  */
474
774
  function schemaTypeToUdt(config) {
775
+ // Enum columns: udt is the enum type name. Vector columns: udt is `vector`.
776
+ if (config.enumName)
777
+ return config.enumName;
778
+ if (config.vectorDimensions != null)
779
+ return 'vector';
475
780
  const map = {
476
781
  SERIAL: 'int4',
477
782
  BIGSERIAL: 'int8',
@@ -490,7 +795,11 @@ function schemaTypeToUdt(config) {
490
795
  NUMERIC: 'numeric',
491
796
  BYTEA: 'bytea',
492
797
  };
493
- return map[config.type] ?? null;
798
+ const base = map[config.type] ?? null;
799
+ // Postgres names an array type `_<element>` (e.g. `_text` for text[]).
800
+ if (base && config.isArray)
801
+ return `_${base}`;
802
+ return base;
494
803
  }
495
804
  /**
496
805
  * Reverse map: PostgreSQL UDT name → SQL type (for generating reverse ALTER TYPE).
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineSeed = defineSeed;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const node_url_1 = require("node:url");
7
+ const client_js_1 = require("./client.js");
8
+ const errors_js_1 = require("./errors.js");
9
+ const emptySchema = { tables: {}, enums: {} };
10
+ function entryUrl() {
11
+ const entry = process.argv[1];
12
+ if (!entry)
13
+ return null;
14
+ try {
15
+ return (0, node_url_1.pathToFileURL)((0, node_fs_1.realpathSync)(entry)).href;
16
+ }
17
+ catch {
18
+ return (0, node_url_1.pathToFileURL)((0, node_path_1.resolve)(entry)).href;
19
+ }
20
+ }
21
+ function callerUrl() {
22
+ const stack = new Error().stack;
23
+ if (!stack)
24
+ return null;
25
+ for (const line of stack.split('\n').slice(2)) {
26
+ const fileUrl = line.match(/(file:\/\/\/[^):]+):\d+:\d+/)?.[1];
27
+ if (fileUrl && !fileUrl.endsWith('/seed.ts') && !fileUrl.endsWith('/seed.js'))
28
+ return fileUrl;
29
+ const filePath = line.match(/\(?((?:\/|[A-Za-z]:\\)[^):]+):\d+:\d+\)?/)?.[1];
30
+ if (filePath && !filePath.endsWith('/src/seed.ts') && !filePath.endsWith('\\src\\seed.ts')) {
31
+ return (0, node_url_1.pathToFileURL)((0, node_path_1.resolve)(filePath)).href;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ function isDirectSeedModule() {
37
+ const entry = entryUrl();
38
+ const caller = callerUrl();
39
+ return process.env.NODE_TEST_CONTEXT === undefined && !!entry && !!caller && entry === caller;
40
+ }
41
+ async function runSeed(fn) {
42
+ const connectionString = process.env.DATABASE_URL;
43
+ if (!connectionString) {
44
+ throw new errors_js_1.ConnectionError('[turbine] DATABASE_URL is required to run this seed.');
45
+ }
46
+ const db = new client_js_1.TurbineClient({ connectionString }, emptySchema);
47
+ try {
48
+ await fn(db);
49
+ }
50
+ finally {
51
+ await db.disconnect();
52
+ }
53
+ }
54
+ function defineSeed(fn) {
55
+ const run = () => runSeed(fn);
56
+ if (isDirectSeedModule()) {
57
+ queueMicrotask(() => {
58
+ run().catch((err) => {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ console.error(message);
61
+ process.exitCode = 1;
62
+ });
63
+ });
64
+ }
65
+ return run;
66
+ }
@@ -17,7 +17,9 @@ export interface TurbineCliConfig {
17
17
  exclude?: string[];
18
18
  /** Directory for migration files (default: ./turbine/migrations) */
19
19
  migrationsDir?: string;
20
- /** Path to seed file (default: ./turbine/seed.ts) */
20
+ /** Path to seed file. Defaults are resolved from seed.ts, seed.js, then seed.sql. */
21
+ seed?: string;
22
+ /** Path to seed file. Deprecated alias for `seed`. */
21
23
  seedFile?: string;
22
24
  /** Schema builder file path (for push command) */
23
25
  schemaFile?: string;
@@ -64,7 +66,7 @@ export interface ResolvedConfig {
64
66
  include: string[];
65
67
  exclude: string[];
66
68
  migrationsDir: string;
67
- seedFile: string;
69
+ seedFile?: string;
68
70
  schemaFile: string;
69
71
  }
70
72
  export interface CliOverrides {
@@ -79,4 +81,9 @@ export interface CliOverrides {
79
81
  * Priority: CLI flags > env vars > config file > defaults.
80
82
  */
81
83
  export declare function resolveConfig(fileConfig: TurbineCliConfig, overrides: CliOverrides): ResolvedConfig;
84
+ /**
85
+ * Resolve the seed file path. An explicit config value wins even if the file
86
+ * does not exist yet; otherwise the root-level defaults are tried in order.
87
+ */
88
+ export declare function resolveSeedFile(config: Pick<TurbineCliConfig, 'seed' | 'seedFile'>, cwd?: string): string | null;
82
89
  export declare function configTemplate(connectionString?: string): string;
@@ -24,6 +24,7 @@ export function looksLikeSchemaFilePath(schema) {
24
24
  // Config file names, in priority order
25
25
  // ---------------------------------------------------------------------------
26
26
  const CONFIG_FILES = ['turbine.config.ts', 'turbine.config.mts', 'turbine.config.js', 'turbine.config.mjs'];
27
+ const DEFAULT_SEED_CANDIDATES = ['seed.ts', 'seed.js', 'seed.sql'];
27
28
  // ---------------------------------------------------------------------------
28
29
  // Load config
29
30
  // ---------------------------------------------------------------------------
@@ -81,10 +82,25 @@ export function resolveConfig(fileConfig, overrides) {
81
82
  include: overrides.include ?? fileConfig.include ?? [],
82
83
  exclude: overrides.exclude ?? fileConfig.exclude ?? [],
83
84
  migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
84
- seedFile: fileConfig.seedFile ?? './turbine/seed.ts',
85
+ seedFile: fileConfig.seed ?? fileConfig.seedFile,
85
86
  schemaFile: fileConfig.schemaFile ?? './turbine/schema.ts',
86
87
  };
87
88
  }
89
+ /**
90
+ * Resolve the seed file path. An explicit config value wins even if the file
91
+ * does not exist yet; otherwise the root-level defaults are tried in order.
92
+ */
93
+ export function resolveSeedFile(config, cwd = process.cwd()) {
94
+ const explicit = config.seed ?? config.seedFile;
95
+ if (explicit)
96
+ return resolve(cwd, explicit);
97
+ for (const candidate of DEFAULT_SEED_CANDIDATES) {
98
+ const filePath = resolve(cwd, candidate);
99
+ if (existsSync(filePath))
100
+ return filePath;
101
+ }
102
+ return null;
103
+ }
88
104
  // ---------------------------------------------------------------------------
89
105
  // Config file template (for `turbine init`)
90
106
  // ---------------------------------------------------------------------------
@@ -113,8 +129,8 @@ ${urlLine}
113
129
  /** Directory for SQL migration files */
114
130
  migrationsDir: './turbine/migrations',
115
131
 
116
- /** Path to seed file */
117
- seedFile: './turbine/seed.ts',
132
+ /** Path to seed file (defaults: ./seed.ts, ./seed.js, ./seed.sql) */
133
+ seed: './seed.ts',
118
134
 
119
135
  /** Path to schema builder file (for turbine push) */
120
136
  schemaFile: './turbine/schema.ts',
@@ -8,12 +8,14 @@
8
8
  * turbine push — Apply schema-builder definitions to database
9
9
  * turbine migrate create <name> — Create a new SQL migration file
10
10
  * turbine migrate up — Apply pending migrations
11
+ * turbine migrate deploy — Apply pending migrations without prompts
11
12
  * turbine migrate down — Rollback last migration
12
13
  * turbine migrate status — Show migration status
13
14
  * turbine seed — Run seed file
14
15
  * turbine status — Show schema summary
15
16
  * turbine doctor — Check relations for missing FK indexes (--fix emits migration)
16
17
  * turbine studio — Launch local read-only web UI
18
+ * turbine mcp — Start read-only MCP server over JSON-RPC stdio
17
19
  * turbine observe — Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
18
20
  *
19
21
  * Usage:
@@ -21,4 +23,53 @@
21
23
  * npx turbine init --url postgres://...
22
24
  * npx turbine migrate create add_users_table
23
25
  */
24
- export {};
26
+ export interface CliArgs {
27
+ command: string;
28
+ subcommand?: string;
29
+ positional: string[];
30
+ url?: string;
31
+ out?: string;
32
+ schema?: string;
33
+ include?: string[];
34
+ exclude?: string[];
35
+ step?: number;
36
+ dryRun?: boolean;
37
+ force?: boolean;
38
+ verbose?: boolean;
39
+ help?: boolean;
40
+ auto?: boolean;
41
+ allowDrift?: boolean;
42
+ allowEmpty?: boolean;
43
+ allowDestructive?: boolean;
44
+ fix?: boolean;
45
+ zod?: boolean;
46
+ includeViews?: boolean;
47
+ port?: number;
48
+ host?: string;
49
+ noOpen?: boolean;
50
+ /** Opt-in to bind Studio/Observe on a non-loopback host. */
51
+ allowRemote?: boolean;
52
+ }
53
+ export declare function parseArgs(argv?: string[]): CliArgs;
54
+ export declare function buildMigrateDeployOptions(_args: CliArgs): {
55
+ allowDrift: false;
56
+ allowDestructive: true;
57
+ step: undefined;
58
+ };
59
+ export type SeedExecutionPlan = {
60
+ kind: 'tsx';
61
+ command: 'npx';
62
+ args: string[];
63
+ } | {
64
+ kind: 'js';
65
+ file: string;
66
+ } | {
67
+ kind: 'sql';
68
+ file: string;
69
+ };
70
+ export declare function getSeedExecutionPlan(seedFile: string): SeedExecutionPlan;
71
+ /**
72
+ * True when `host` is a loopback address Studio/Observe may bind without
73
+ * `--allow-remote`. Accepts IPv4, IPv6, and the common bracket form.
74
+ */
75
+ export declare function isLoopbackHost(host: string): boolean;