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.
- package/README.md +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- 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
|
|
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() {
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
200
|
-
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
|
-
|
|
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
|
-
|
|
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).
|
package/dist/cjs/seed.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|