turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  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/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
@@ -57,12 +57,34 @@ const TYPE_MAP = {
57
57
  double: 'DOUBLE PRECISION',
58
58
  numeric: 'NUMERIC',
59
59
  bytea: 'BYTEA',
60
+ // Sentinels — the real DDL type is derived from `enumName` / `dimensions`
61
+ // in schema-sql.ts, never from these placeholders.
62
+ enum: 'ENUM',
63
+ vector: 'VECTOR',
60
64
  };
61
65
  /** Convert a user-facing ColumnDef to the internal ColumnConfig */
62
66
  function resolveColumn(def) {
63
67
  if (!(def.type in TYPE_MAP)) {
64
68
  throw new Error(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
65
69
  }
70
+ if (def.type === 'enum' && !def.enumName) {
71
+ throw new Error(`Column of type "enum" requires an "enumName" (the CREATE TYPE name).`);
72
+ }
73
+ if (def.type === 'vector' && (def.dimensions == null || def.dimensions <= 0)) {
74
+ throw new Error(`Column of type "vector" requires a positive "dimensions" count.`);
75
+ }
76
+ // `references` is either the "table.column" string or a { target, onDelete, onUpdate } object.
77
+ let referencesTarget = null;
78
+ let onDelete = null;
79
+ let onUpdate = null;
80
+ if (typeof def.references === 'string') {
81
+ referencesTarget = def.references;
82
+ }
83
+ else if (def.references) {
84
+ referencesTarget = def.references.target;
85
+ onDelete = def.references.onDelete ?? null;
86
+ onUpdate = def.references.onUpdate ?? null;
87
+ }
66
88
  return {
67
89
  type: TYPE_MAP[def.type],
68
90
  isPrimaryKey: def.primaryKey ?? false,
@@ -70,8 +92,14 @@ function resolveColumn(def) {
70
92
  isNullable: def.nullable ?? false,
71
93
  isUnique: def.unique ?? false,
72
94
  defaultValue: def.default ?? null,
73
- referencesTarget: def.references ?? null,
95
+ referencesTarget,
74
96
  maxLength: def.maxLength ?? null,
97
+ onDelete,
98
+ onUpdate,
99
+ enumName: def.enumName ?? null,
100
+ vectorDimensions: def.dimensions ?? null,
101
+ isArray: def.array ?? false,
102
+ check: def.check ?? null,
75
103
  };
76
104
  }
77
105
  /** Check if a value is a TableDef (from legacy table() builder) */
@@ -97,7 +125,7 @@ function isTableDef(v) {
97
125
  * });
98
126
  * ```
99
127
  */
100
- function defineSchema(input) {
128
+ function defineSchema(input, options) {
101
129
  const tables = {};
102
130
  for (const [accessor, value] of Object.entries(input)) {
103
131
  // The user-facing key is the camelCase JS accessor; the DDL-facing
@@ -116,6 +144,7 @@ function defineSchema(input) {
116
144
  const columns = {};
117
145
  let pk;
118
146
  let m2m;
147
+ let checks;
119
148
  for (const [fieldName, def] of Object.entries(raw)) {
120
149
  if (fieldName === 'manyToMany') {
121
150
  if (def !== undefined) {
@@ -126,6 +155,15 @@ function defineSchema(input) {
126
155
  }
127
156
  continue;
128
157
  }
158
+ if (fieldName === 'checks') {
159
+ if (def !== undefined) {
160
+ if (!Array.isArray(def)) {
161
+ throw new Error(`Table "${accessor}": "checks" must be an array of { name?, expression } objects`);
162
+ }
163
+ checks = def;
164
+ }
165
+ continue;
166
+ }
129
167
  if (fieldName === 'primaryKey') {
130
168
  // Top-level composite primary key declaration
131
169
  if (def !== undefined) {
@@ -166,10 +204,11 @@ function defineSchema(input) {
166
204
  columns,
167
205
  ...(pk && pk.length > 0 ? { primaryKey: pk } : {}),
168
206
  ...(m2m && m2m.length > 0 ? { manyToMany: m2m } : {}),
207
+ ...(checks && checks.length > 0 ? { checks } : {}),
169
208
  };
170
209
  }
171
210
  }
172
- return { tables };
211
+ return { tables, ...(options?.enums ? { enums: options.enums } : {}) };
173
212
  }
174
213
  /**
175
214
  * Local copy of camelToSnake to avoid a circular import dependency at the
@@ -193,6 +232,12 @@ class ColumnBuilder {
193
232
  defaultValue: null,
194
233
  referencesTarget: null,
195
234
  maxLength: null,
235
+ onDelete: null,
236
+ onUpdate: null,
237
+ enumName: null,
238
+ vectorDimensions: null,
239
+ isArray: false,
240
+ check: null,
196
241
  };
197
242
  }
198
243
  serial() {
@@ -289,8 +334,18 @@ class ColumnBuilder {
289
334
  this._config.defaultValue = val;
290
335
  return this;
291
336
  }
292
- references(target) {
337
+ references(target, opts) {
293
338
  this._config.referencesTarget = target;
339
+ this._config.onDelete = opts?.onDelete ?? null;
340
+ this._config.onUpdate = opts?.onUpdate ?? null;
341
+ return this;
342
+ }
343
+ check(expression) {
344
+ this._config.check = expression;
345
+ return this;
346
+ }
347
+ array() {
348
+ this._config.isArray = true;
294
349
  return this;
295
350
  }
296
351
  build() {
@@ -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;