turbine-orm 0.12.0 → 0.13.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/dist/cjs/cli/migrate.js +25 -28
- package/dist/cjs/dialect.js +52 -0
- package/dist/cjs/schema-sql.js +64 -61
- package/dist/cli/migrate.d.ts +6 -1
- package/dist/cli/migrate.js +25 -28
- package/dist/dialect.d.ts +57 -0
- package/dist/dialect.js +52 -0
- package/dist/index.d.ts +2 -2
- package/dist/query/index.d.ts +1 -1
- package/dist/schema-sql.d.ts +7 -2
- package/dist/schema-sql.js +64 -61
- package/package.json +3 -3
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -33,27 +33,21 @@ const node_fs_1 = require("node:fs");
|
|
|
33
33
|
const node_path_1 = require("node:path");
|
|
34
34
|
const pg_1 = __importDefault(require("pg"));
|
|
35
35
|
const index_js_1 = require("../adapters/index.js");
|
|
36
|
+
const dialect_js_1 = require("../dialect.js");
|
|
36
37
|
const errors_js_1 = require("../errors.js");
|
|
37
|
-
const index_js_2 = require("../query/index.js");
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
// Tracking table management
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
const TRACKING_TABLE = '_turbine_migrations';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
checksum TEXT NOT NULL,
|
|
48
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
49
|
-
);
|
|
50
|
-
`;
|
|
51
|
-
async function ensureTrackingTable(client) {
|
|
52
|
-
await client.query(CREATE_TRACKING_TABLE);
|
|
42
|
+
function quotedTrackingTable(dialect) {
|
|
43
|
+
return dialect.quoteIdentifier(TRACKING_TABLE);
|
|
44
|
+
}
|
|
45
|
+
async function ensureTrackingTable(client, dialect = dialect_js_1.postgresDialect) {
|
|
46
|
+
await client.query(dialect.buildMigrationTrackingTable(quotedTrackingTable(dialect)));
|
|
53
47
|
}
|
|
54
|
-
async function getAppliedMigrations(client) {
|
|
55
|
-
await ensureTrackingTable(client);
|
|
56
|
-
const result = await client.query(
|
|
48
|
+
async function getAppliedMigrations(client, dialect = dialect_js_1.postgresDialect) {
|
|
49
|
+
await ensureTrackingTable(client, dialect);
|
|
50
|
+
const result = await client.query(dialect.buildMigrationSelectApplied(quotedTrackingTable(dialect)));
|
|
57
51
|
return result.rows;
|
|
58
52
|
}
|
|
59
53
|
// ---------------------------------------------------------------------------
|
|
@@ -265,8 +259,8 @@ async function releaseLock(client, lockId, adapter) {
|
|
|
265
259
|
* Validate that applied migration files have not been modified or deleted since they were run.
|
|
266
260
|
* Returns an array of mismatched migrations (empty if all are clean).
|
|
267
261
|
*/
|
|
268
|
-
async function validateChecksums(client, migrationsDir) {
|
|
269
|
-
const applied = await getAppliedMigrations(client);
|
|
262
|
+
async function validateChecksums(client, migrationsDir, dialect = dialect_js_1.postgresDialect) {
|
|
263
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
270
264
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
271
265
|
const fileMap = new Map(allFiles.map((f) => [f.name, f]));
|
|
272
266
|
const mismatches = [];
|
|
@@ -286,7 +280,7 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
286
280
|
if (currentHash !== migration.checksum) {
|
|
287
281
|
// Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
|
|
288
282
|
if (isLegacyChecksum(migration.checksum)) {
|
|
289
|
-
await client.query(
|
|
283
|
+
await client.query(dialect.buildMigrationUpdateChecksum(quotedTrackingTable(dialect)), [
|
|
290
284
|
currentHash,
|
|
291
285
|
migration.name,
|
|
292
286
|
]);
|
|
@@ -321,6 +315,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
321
315
|
await client.connect();
|
|
322
316
|
// Treat `force` as an alias for `allowDrift` for backwards compatibility.
|
|
323
317
|
const allowDrift = options?.allowDrift === true || options?.force === true;
|
|
318
|
+
const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
324
319
|
try {
|
|
325
320
|
// Derive an advisory lock ID per-database so concurrent migrations in
|
|
326
321
|
// sibling databases on the same Postgres cluster do not contend.
|
|
@@ -334,7 +329,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
334
329
|
throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
335
330
|
}
|
|
336
331
|
try {
|
|
337
|
-
await ensureTrackingTable(client);
|
|
332
|
+
await ensureTrackingTable(client, dialect);
|
|
338
333
|
// Validate checksums of already-applied migrations.
|
|
339
334
|
// Drift = an APPLIED migration's on-disk file has changed (or been deleted)
|
|
340
335
|
// since it was run. Either situation means the database state and the
|
|
@@ -342,7 +337,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
342
337
|
// Users can pass `allowDrift: true` (CLI: `--allow-drift`) to force past
|
|
343
338
|
// the block when they are intentionally rewriting history.
|
|
344
339
|
if (!allowDrift) {
|
|
345
|
-
const mismatches = await validateChecksums(client, migrationsDir);
|
|
340
|
+
const mismatches = await validateChecksums(client, migrationsDir, dialect);
|
|
346
341
|
if (mismatches.length > 0) {
|
|
347
342
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
348
343
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
@@ -366,7 +361,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
366
361
|
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
367
362
|
}
|
|
368
363
|
}
|
|
369
|
-
const applied = await getAppliedMigrations(client);
|
|
364
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
370
365
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
371
366
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
372
367
|
let pending = allFiles.filter((f) => !appliedNames.has(f.name));
|
|
@@ -386,7 +381,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
386
381
|
try {
|
|
387
382
|
await client.query('BEGIN');
|
|
388
383
|
await client.query(up);
|
|
389
|
-
await client.query(
|
|
384
|
+
await client.query(dialect.buildMigrationInsertApplied(quotedTrackingTable(dialect)), [file.name, hash]);
|
|
390
385
|
await client.query('COMMIT');
|
|
391
386
|
results.push(file);
|
|
392
387
|
}
|
|
@@ -419,6 +414,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
419
414
|
async function migrateDown(connectionString, migrationsDir, options) {
|
|
420
415
|
const client = new pg_1.default.Client({ connectionString });
|
|
421
416
|
await client.connect();
|
|
417
|
+
const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
422
418
|
try {
|
|
423
419
|
// Derive a per-database advisory lock ID so concurrent migrations in
|
|
424
420
|
// sibling databases on the same cluster do not contend.
|
|
@@ -430,8 +426,8 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
430
426
|
throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
431
427
|
}
|
|
432
428
|
try {
|
|
433
|
-
await ensureTrackingTable(client);
|
|
434
|
-
const applied = await getAppliedMigrations(client);
|
|
429
|
+
await ensureTrackingTable(client, dialect);
|
|
430
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
435
431
|
if (applied.length === 0) {
|
|
436
432
|
return { rolledBack: [], errors: [] };
|
|
437
433
|
}
|
|
@@ -458,7 +454,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
458
454
|
try {
|
|
459
455
|
await client.query('BEGIN');
|
|
460
456
|
await client.query(down);
|
|
461
|
-
await client.query(
|
|
457
|
+
await client.query(dialect.buildMigrationDeleteApplied(quotedTrackingTable(dialect)), [migration.name]);
|
|
462
458
|
await client.query('COMMIT');
|
|
463
459
|
results.push(file);
|
|
464
460
|
}
|
|
@@ -483,12 +479,13 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
483
479
|
* Get the status of all migrations (applied vs pending).
|
|
484
480
|
* Includes checksum validation for applied migrations.
|
|
485
481
|
*/
|
|
486
|
-
async function migrateStatus(connectionString, migrationsDir) {
|
|
482
|
+
async function migrateStatus(connectionString, migrationsDir, options) {
|
|
487
483
|
const client = new pg_1.default.Client({ connectionString });
|
|
488
484
|
await client.connect();
|
|
485
|
+
const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
489
486
|
try {
|
|
490
|
-
await ensureTrackingTable(client);
|
|
491
|
-
const applied = await getAppliedMigrations(client);
|
|
487
|
+
await ensureTrackingTable(client, dialect);
|
|
488
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
492
489
|
const appliedMap = new Map(applied.map((m) => [m.name, m]));
|
|
493
490
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
494
491
|
return allFiles.map((file) => {
|
package/dist/cjs/dialect.js
CHANGED
|
@@ -76,4 +76,56 @@ exports.postgresDialect = {
|
|
|
76
76
|
// This hook is the package boundary MySQL/SQLite implementations will fill.
|
|
77
77
|
return 'unknown';
|
|
78
78
|
},
|
|
79
|
+
buildColumnType(input) {
|
|
80
|
+
if (input.type === 'VARCHAR' && input.maxLength != null) {
|
|
81
|
+
return `VARCHAR(${input.maxLength})`;
|
|
82
|
+
}
|
|
83
|
+
return input.type;
|
|
84
|
+
},
|
|
85
|
+
buildColumnDefinition(input) {
|
|
86
|
+
const parts = [input.name, this.buildColumnType(input)];
|
|
87
|
+
if (input.primaryKey)
|
|
88
|
+
parts.push('PRIMARY KEY');
|
|
89
|
+
if (input.unique && !input.primaryKey)
|
|
90
|
+
parts.push('UNIQUE');
|
|
91
|
+
if (input.notNull)
|
|
92
|
+
parts.push('NOT NULL');
|
|
93
|
+
if (input.defaultValue != null)
|
|
94
|
+
parts.push(`DEFAULT ${input.defaultValue}`);
|
|
95
|
+
if (input.references)
|
|
96
|
+
parts.push(`REFERENCES ${input.references.table}(${input.references.column})`);
|
|
97
|
+
return parts.join(' ');
|
|
98
|
+
},
|
|
99
|
+
buildPrimaryKeyConstraint(columns) {
|
|
100
|
+
return `PRIMARY KEY (${columns.join(', ')})`;
|
|
101
|
+
},
|
|
102
|
+
buildCreateTableStatement(input) {
|
|
103
|
+
const body = input.definitions.map((d) => ` ${d}`).join(',\n');
|
|
104
|
+
return `CREATE TABLE ${input.table} (\n${body}\n);`;
|
|
105
|
+
},
|
|
106
|
+
buildCreateIndexStatement(input) {
|
|
107
|
+
return `CREATE INDEX ${input.name} ON ${input.table}(${input.columns.join(', ')});`;
|
|
108
|
+
},
|
|
109
|
+
buildMigrationTrackingTable(table) {
|
|
110
|
+
return `
|
|
111
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
112
|
+
id SERIAL PRIMARY KEY,
|
|
113
|
+
name TEXT NOT NULL UNIQUE,
|
|
114
|
+
checksum TEXT NOT NULL,
|
|
115
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
116
|
+
);
|
|
117
|
+
`;
|
|
118
|
+
},
|
|
119
|
+
buildMigrationSelectApplied(table) {
|
|
120
|
+
return `SELECT id, name, applied_at, checksum FROM ${table} ORDER BY id ASC`;
|
|
121
|
+
},
|
|
122
|
+
buildMigrationUpdateChecksum(table) {
|
|
123
|
+
return `UPDATE ${table} SET checksum = ${this.paramPlaceholder(1)} WHERE name = ${this.paramPlaceholder(2)}`;
|
|
124
|
+
},
|
|
125
|
+
buildMigrationInsertApplied(table) {
|
|
126
|
+
return `INSERT INTO ${table} (name, checksum) VALUES (${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}) ON CONFLICT (name) DO NOTHING`;
|
|
127
|
+
},
|
|
128
|
+
buildMigrationDeleteApplied(table) {
|
|
129
|
+
return `DELETE FROM ${table} WHERE name = ${this.paramPlaceholder(1)}`;
|
|
130
|
+
},
|
|
79
131
|
};
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -14,7 +14,7 @@ exports.schemaDiff = schemaDiff;
|
|
|
14
14
|
exports.schemaPush = schemaPush;
|
|
15
15
|
exports.schemaToSQLString = schemaToSQLString;
|
|
16
16
|
const pg_1 = __importDefault(require("pg"));
|
|
17
|
-
const
|
|
17
|
+
const dialect_js_1 = require("./dialect.js");
|
|
18
18
|
const schema_js_1 = require("./schema.js");
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
@@ -25,7 +25,8 @@ const schema_js_1 = require("./schema.js");
|
|
|
25
25
|
* Returns CREATE TABLE statements (in dependency order based on references)
|
|
26
26
|
* followed by CREATE INDEX statements for foreign key columns.
|
|
27
27
|
*/
|
|
28
|
-
function schemaToSQL(schema) {
|
|
28
|
+
function schemaToSQL(schema, options) {
|
|
29
|
+
const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
29
30
|
const statements = [];
|
|
30
31
|
// Topologically sort tables by their foreign key references
|
|
31
32
|
const sorted = topologicalSort(schema);
|
|
@@ -33,12 +34,12 @@ function schemaToSQL(schema) {
|
|
|
33
34
|
// Generate CREATE TABLE statements
|
|
34
35
|
for (const tableName of sorted) {
|
|
35
36
|
const table = schema.tables[tableName];
|
|
36
|
-
statements.push(generateCreateTable(table, resolveRef));
|
|
37
|
+
statements.push(generateCreateTable(table, resolveRef, dialect));
|
|
37
38
|
}
|
|
38
39
|
// Generate CREATE INDEX for foreign key columns
|
|
39
40
|
for (const tableName of sorted) {
|
|
40
41
|
const table = schema.tables[tableName];
|
|
41
|
-
const indexes = generateForeignKeyIndexes(table);
|
|
42
|
+
const indexes = generateForeignKeyIndexes(table, dialect);
|
|
42
43
|
statements.push(...indexes);
|
|
43
44
|
}
|
|
44
45
|
return statements;
|
|
@@ -134,42 +135,28 @@ function topologicalSort(schema) {
|
|
|
134
135
|
* to their snake_case DDL form, so users can write either camelCase JS
|
|
135
136
|
* accessor names or snake_case DDL names.
|
|
136
137
|
*/
|
|
137
|
-
function generateCreateTable(table, resolveRef) {
|
|
138
|
+
function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresDialect) {
|
|
138
139
|
const tableName = table.name;
|
|
139
140
|
const columnDefs = [];
|
|
140
141
|
const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
|
|
141
142
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
142
|
-
columnDefs.push(generateColumnDef(fieldName, config, resolveRef));
|
|
143
|
+
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
|
|
143
144
|
}
|
|
144
145
|
// Append a table-level PRIMARY KEY constraint when a composite PK is set.
|
|
145
146
|
if (compositePk) {
|
|
146
|
-
const cols = compositePk.map((c) =>
|
|
147
|
-
columnDefs.push(
|
|
147
|
+
const cols = compositePk.map((c) => dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(c)));
|
|
148
|
+
columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
return dialect.buildCreateTableStatement({
|
|
151
|
+
table: dialect.quoteIdentifier(tableName),
|
|
152
|
+
definitions: columnDefs,
|
|
153
|
+
});
|
|
151
154
|
}
|
|
152
155
|
/**
|
|
153
156
|
* Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
|
|
154
157
|
*/
|
|
155
|
-
function generateColumnDef(fieldName, config, resolveRef) {
|
|
158
|
+
function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1.postgresDialect) {
|
|
156
159
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
157
|
-
const parts = [(0, index_js_1.quoteIdent)(snakeName)];
|
|
158
|
-
// Type
|
|
159
|
-
if (config.type === 'VARCHAR' && config.maxLength != null) {
|
|
160
|
-
parts.push(`VARCHAR(${config.maxLength})`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
parts.push(config.type);
|
|
164
|
-
}
|
|
165
|
-
// PRIMARY KEY
|
|
166
|
-
if (config.isPrimaryKey) {
|
|
167
|
-
parts.push('PRIMARY KEY');
|
|
168
|
-
}
|
|
169
|
-
// UNIQUE (only if not primary key — PK is implicitly unique)
|
|
170
|
-
if (config.isUnique && !config.isPrimaryKey) {
|
|
171
|
-
parts.push('UNIQUE');
|
|
172
|
-
}
|
|
173
160
|
// NOT NULL — serial types are implicitly NOT NULL, but explicit is fine.
|
|
174
161
|
// A column is NOT NULL if:
|
|
175
162
|
// 1. Explicitly marked .notNull(), OR
|
|
@@ -178,25 +165,36 @@ function generateColumnDef(fieldName, config, resolveRef) {
|
|
|
178
165
|
// A column is left nullable if .nullable() was called.
|
|
179
166
|
const isSerial = config.type === 'BIGSERIAL';
|
|
180
167
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
181
|
-
|
|
182
|
-
parts.push('NOT NULL');
|
|
183
|
-
}
|
|
168
|
+
const notNull = config.isNotNull && !implicitNotNull;
|
|
184
169
|
// DEFAULT
|
|
170
|
+
let defaultValue;
|
|
185
171
|
if (config.defaultValue != null) {
|
|
186
|
-
|
|
187
|
-
parts.push(`DEFAULT ${sqlDefault}`);
|
|
172
|
+
defaultValue = normalizeDefault(config.defaultValue);
|
|
188
173
|
}
|
|
189
174
|
// REFERENCES — resolve the raw table name through the optional resolver so
|
|
190
175
|
// both camelCase accessor names and snake_case DDL names work.
|
|
176
|
+
let references;
|
|
191
177
|
if (config.referencesTarget) {
|
|
192
178
|
const refParts = config.referencesTarget.split('.');
|
|
193
179
|
if (refParts.length === 2) {
|
|
194
180
|
const rawTable = refParts[0];
|
|
195
181
|
const refTable = resolveRef ? resolveRef(rawTable) : rawTable;
|
|
196
|
-
|
|
182
|
+
references = {
|
|
183
|
+
table: dialect.quoteIdentifier(refTable),
|
|
184
|
+
column: dialect.quoteIdentifier(refParts[1]),
|
|
185
|
+
};
|
|
197
186
|
}
|
|
198
187
|
}
|
|
199
|
-
return
|
|
188
|
+
return dialect.buildColumnDefinition({
|
|
189
|
+
name: dialect.quoteIdentifier(snakeName),
|
|
190
|
+
type: config.type,
|
|
191
|
+
maxLength: config.maxLength,
|
|
192
|
+
primaryKey: config.isPrimaryKey,
|
|
193
|
+
unique: config.isUnique,
|
|
194
|
+
notNull,
|
|
195
|
+
defaultValue,
|
|
196
|
+
references,
|
|
197
|
+
});
|
|
200
198
|
}
|
|
201
199
|
/**
|
|
202
200
|
* Normalize a default value from the user's schema definition to valid SQL.
|
|
@@ -236,13 +234,17 @@ function normalizeDefault(val) {
|
|
|
236
234
|
* Generate CREATE INDEX statements for foreign key columns.
|
|
237
235
|
* Only generates indexes for columns that have a REFERENCES clause.
|
|
238
236
|
*/
|
|
239
|
-
function generateForeignKeyIndexes(table) {
|
|
237
|
+
function generateForeignKeyIndexes(table, dialect = dialect_js_1.postgresDialect) {
|
|
240
238
|
const indexes = [];
|
|
241
239
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
242
240
|
if (config.referencesTarget) {
|
|
243
241
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
244
242
|
const indexName = `idx_${table.name}_${snakeName}`;
|
|
245
|
-
indexes.push(
|
|
243
|
+
indexes.push(dialect.buildCreateIndexStatement({
|
|
244
|
+
name: dialect.quoteIdentifier(indexName),
|
|
245
|
+
table: dialect.quoteIdentifier(table.name),
|
|
246
|
+
columns: [dialect.quoteIdentifier(snakeName)],
|
|
247
|
+
}));
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
250
|
return indexes;
|
|
@@ -254,6 +256,7 @@ function generateForeignKeyIndexes(table) {
|
|
|
254
256
|
* DDL is needed to make the database match the schema definition.
|
|
255
257
|
*/
|
|
256
258
|
async function schemaDiff(schema, connectionString) {
|
|
259
|
+
const dialect = dialect_js_1.postgresDialect;
|
|
257
260
|
const client = new pg_1.default.Client({ connectionString });
|
|
258
261
|
await client.connect();
|
|
259
262
|
try {
|
|
@@ -313,11 +316,11 @@ async function schemaDiff(schema, connectionString) {
|
|
|
313
316
|
const ddlName = tableDef.name;
|
|
314
317
|
if (!existingTables.has(ddlName)) {
|
|
315
318
|
result.create.push(tableDef);
|
|
316
|
-
result.statements.push(generateCreateTable(tableDef, resolveRef));
|
|
317
|
-
const fkIndexes = generateForeignKeyIndexes(tableDef);
|
|
319
|
+
result.statements.push(generateCreateTable(tableDef, resolveRef, dialect));
|
|
320
|
+
const fkIndexes = generateForeignKeyIndexes(tableDef, dialect);
|
|
318
321
|
result.statements.push(...fkIndexes);
|
|
319
322
|
// Reverse: DROP TABLE (with indexes — they drop automatically)
|
|
320
|
-
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${
|
|
323
|
+
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
// Tables to drop (in DB but not in schema)
|
|
@@ -341,9 +344,9 @@ async function schemaDiff(schema, connectionString) {
|
|
|
341
344
|
const dbCol = dbCols[snakeName];
|
|
342
345
|
if (!dbCol) {
|
|
343
346
|
// Column exists in schema but not in DB — ADD COLUMN
|
|
344
|
-
const colDef = generateColumnDef(fieldName, config, resolveRef);
|
|
345
|
-
const sql = `ALTER TABLE ${
|
|
346
|
-
const reverseSql = `ALTER TABLE ${
|
|
347
|
+
const colDef = generateColumnDef(fieldName, config, resolveRef, dialect);
|
|
348
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD COLUMN ${colDef};`;
|
|
349
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(snakeName)};`;
|
|
347
350
|
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
348
351
|
result.statements.push(sql);
|
|
349
352
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -354,8 +357,8 @@ async function schemaDiff(schema, connectionString) {
|
|
|
354
357
|
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
355
358
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
356
359
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
357
|
-
const sql = `ALTER TABLE ${
|
|
358
|
-
const reverseSql = `ALTER TABLE ${
|
|
360
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
361
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
|
|
359
362
|
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
360
363
|
result.statements.push(sql);
|
|
361
364
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -364,15 +367,15 @@ async function schemaDiff(schema, connectionString) {
|
|
|
364
367
|
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
|
|
365
368
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
366
369
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
367
|
-
const sql = `ALTER TABLE ${
|
|
368
|
-
const reverseSql = `ALTER TABLE ${
|
|
370
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
371
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
369
372
|
alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
|
|
370
373
|
result.statements.push(sql);
|
|
371
374
|
result.reverseStatements.unshift(reverseSql);
|
|
372
375
|
}
|
|
373
376
|
else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
|
|
374
|
-
const sql = `ALTER TABLE ${
|
|
375
|
-
const reverseSql = `ALTER TABLE ${
|
|
377
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
378
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
376
379
|
alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
|
|
377
380
|
result.statements.push(sql);
|
|
378
381
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -384,16 +387,16 @@ async function schemaDiff(schema, connectionString) {
|
|
|
384
387
|
const dbDefault = dbCol.columnDefault;
|
|
385
388
|
if (schemaDefault && !dbDefault) {
|
|
386
389
|
// Schema has default, DB doesn't
|
|
387
|
-
const sql = `ALTER TABLE ${
|
|
388
|
-
const reverseSql = `ALTER TABLE ${
|
|
390
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
391
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
389
392
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
390
393
|
result.statements.push(sql);
|
|
391
394
|
result.reverseStatements.unshift(reverseSql);
|
|
392
395
|
}
|
|
393
396
|
else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
|
|
394
397
|
// DB has a non-sequence default, schema doesn't
|
|
395
|
-
const sql = `ALTER TABLE ${
|
|
396
|
-
const reverseSql = `ALTER TABLE ${
|
|
398
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
399
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
397
400
|
alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
|
|
398
401
|
result.statements.push(sql);
|
|
399
402
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -403,8 +406,8 @@ async function schemaDiff(schema, connectionString) {
|
|
|
403
406
|
!isSequenceDefault(dbDefault) &&
|
|
404
407
|
!defaultsMatch(schemaDefault, dbDefault)) {
|
|
405
408
|
// Both have defaults but they differ
|
|
406
|
-
const sql = `ALTER TABLE ${
|
|
407
|
-
const reverseSql = `ALTER TABLE ${
|
|
409
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
410
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
408
411
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
409
412
|
result.statements.push(sql);
|
|
410
413
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -416,16 +419,16 @@ async function schemaDiff(schema, connectionString) {
|
|
|
416
419
|
const wantsUnique = config.isUnique === true;
|
|
417
420
|
if (wantsUnique && !hasDbUnique) {
|
|
418
421
|
const constraintName = `${tableName}_${snakeName}_key`;
|
|
419
|
-
const sql = `ALTER TABLE ${
|
|
420
|
-
const reverseSql = `ALTER TABLE ${
|
|
422
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
423
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
421
424
|
alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
|
|
422
425
|
result.statements.push(sql);
|
|
423
426
|
result.reverseStatements.unshift(reverseSql);
|
|
424
427
|
}
|
|
425
428
|
else if (!wantsUnique && hasDbUnique) {
|
|
426
429
|
const constraintName = tableUniques[snakeName];
|
|
427
|
-
const sql = `ALTER TABLE ${
|
|
428
|
-
const reverseSql = `ALTER TABLE ${
|
|
430
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
431
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
429
432
|
alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
|
|
430
433
|
result.statements.push(sql);
|
|
431
434
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -436,7 +439,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
436
439
|
for (const dbColName of Object.keys(dbCols)) {
|
|
437
440
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
|
|
438
441
|
if (!hasField) {
|
|
439
|
-
const sql = `ALTER TABLE ${
|
|
442
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
440
443
|
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" — add it back manually`;
|
|
441
444
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
442
445
|
// Don't auto-add drops to statements for safety — user must opt in
|
|
@@ -569,7 +572,7 @@ async function schemaPush(schema, connectionString, options = {}) {
|
|
|
569
572
|
* Generate the full DDL as a single formatted string.
|
|
570
573
|
* Useful for printing or saving to a .sql file.
|
|
571
574
|
*/
|
|
572
|
-
function schemaToSQLString(schema) {
|
|
573
|
-
const statements = schemaToSQL(schema);
|
|
575
|
+
function schemaToSQLString(schema, options) {
|
|
576
|
+
const statements = schemaToSQL(schema, options);
|
|
574
577
|
return `${statements.join('\n\n')}\n`;
|
|
575
578
|
}
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
14
|
import type { DatabaseAdapter } from '../adapters/index.js';
|
|
15
|
+
import { type Dialect } from '../dialect.js';
|
|
15
16
|
export interface MigrationFile {
|
|
16
17
|
/** Full filename (e.g. "20260325120000_create_users.sql") */
|
|
17
18
|
filename: string;
|
|
@@ -115,6 +116,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
115
116
|
allowDrift?: boolean /** @deprecated use allowDrift */;
|
|
116
117
|
force?: boolean;
|
|
117
118
|
adapter?: DatabaseAdapter;
|
|
119
|
+
dialect?: Dialect;
|
|
118
120
|
}): Promise<{
|
|
119
121
|
applied: MigrationFile[];
|
|
120
122
|
errors: Array<{
|
|
@@ -133,6 +135,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
133
135
|
export declare function migrateDown(connectionString: string, migrationsDir: string, options?: {
|
|
134
136
|
step?: number;
|
|
135
137
|
adapter?: DatabaseAdapter;
|
|
138
|
+
dialect?: Dialect;
|
|
136
139
|
}): Promise<{
|
|
137
140
|
rolledBack: MigrationFile[];
|
|
138
141
|
errors: Array<{
|
|
@@ -144,5 +147,7 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
|
|
|
144
147
|
* Get the status of all migrations (applied vs pending).
|
|
145
148
|
* Includes checksum validation for applied migrations.
|
|
146
149
|
*/
|
|
147
|
-
export declare function migrateStatus(connectionString: string, migrationsDir: string
|
|
150
|
+
export declare function migrateStatus(connectionString: string, migrationsDir: string, options?: {
|
|
151
|
+
dialect?: Dialect;
|
|
152
|
+
}): Promise<MigrationStatus[]>;
|
|
148
153
|
//# sourceMappingURL=migrate.d.ts.map
|
package/dist/cli/migrate.js
CHANGED
|
@@ -16,27 +16,21 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import pg from 'pg';
|
|
18
18
|
import { postgresql } from '../adapters/index.js';
|
|
19
|
+
import { postgresDialect } from '../dialect.js';
|
|
19
20
|
import { MigrationError } from '../errors.js';
|
|
20
|
-
import { quoteIdent } from '../query/index.js';
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Tracking table management
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
const TRACKING_TABLE = '_turbine_migrations';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
checksum TEXT NOT NULL,
|
|
31
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
32
|
-
);
|
|
33
|
-
`;
|
|
34
|
-
async function ensureTrackingTable(client) {
|
|
35
|
-
await client.query(CREATE_TRACKING_TABLE);
|
|
25
|
+
function quotedTrackingTable(dialect) {
|
|
26
|
+
return dialect.quoteIdentifier(TRACKING_TABLE);
|
|
27
|
+
}
|
|
28
|
+
async function ensureTrackingTable(client, dialect = postgresDialect) {
|
|
29
|
+
await client.query(dialect.buildMigrationTrackingTable(quotedTrackingTable(dialect)));
|
|
36
30
|
}
|
|
37
|
-
async function getAppliedMigrations(client) {
|
|
38
|
-
await ensureTrackingTable(client);
|
|
39
|
-
const result = await client.query(
|
|
31
|
+
async function getAppliedMigrations(client, dialect = postgresDialect) {
|
|
32
|
+
await ensureTrackingTable(client, dialect);
|
|
33
|
+
const result = await client.query(dialect.buildMigrationSelectApplied(quotedTrackingTable(dialect)));
|
|
40
34
|
return result.rows;
|
|
41
35
|
}
|
|
42
36
|
// ---------------------------------------------------------------------------
|
|
@@ -248,8 +242,8 @@ async function releaseLock(client, lockId, adapter) {
|
|
|
248
242
|
* Validate that applied migration files have not been modified or deleted since they were run.
|
|
249
243
|
* Returns an array of mismatched migrations (empty if all are clean).
|
|
250
244
|
*/
|
|
251
|
-
async function validateChecksums(client, migrationsDir) {
|
|
252
|
-
const applied = await getAppliedMigrations(client);
|
|
245
|
+
async function validateChecksums(client, migrationsDir, dialect = postgresDialect) {
|
|
246
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
253
247
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
254
248
|
const fileMap = new Map(allFiles.map((f) => [f.name, f]));
|
|
255
249
|
const mismatches = [];
|
|
@@ -269,7 +263,7 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
269
263
|
if (currentHash !== migration.checksum) {
|
|
270
264
|
// Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
|
|
271
265
|
if (isLegacyChecksum(migration.checksum)) {
|
|
272
|
-
await client.query(
|
|
266
|
+
await client.query(dialect.buildMigrationUpdateChecksum(quotedTrackingTable(dialect)), [
|
|
273
267
|
currentHash,
|
|
274
268
|
migration.name,
|
|
275
269
|
]);
|
|
@@ -304,6 +298,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
304
298
|
await client.connect();
|
|
305
299
|
// Treat `force` as an alias for `allowDrift` for backwards compatibility.
|
|
306
300
|
const allowDrift = options?.allowDrift === true || options?.force === true;
|
|
301
|
+
const dialect = options?.dialect ?? postgresDialect;
|
|
307
302
|
try {
|
|
308
303
|
// Derive an advisory lock ID per-database so concurrent migrations in
|
|
309
304
|
// sibling databases on the same Postgres cluster do not contend.
|
|
@@ -317,7 +312,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
317
312
|
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
318
313
|
}
|
|
319
314
|
try {
|
|
320
|
-
await ensureTrackingTable(client);
|
|
315
|
+
await ensureTrackingTable(client, dialect);
|
|
321
316
|
// Validate checksums of already-applied migrations.
|
|
322
317
|
// Drift = an APPLIED migration's on-disk file has changed (or been deleted)
|
|
323
318
|
// since it was run. Either situation means the database state and the
|
|
@@ -325,7 +320,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
325
320
|
// Users can pass `allowDrift: true` (CLI: `--allow-drift`) to force past
|
|
326
321
|
// the block when they are intentionally rewriting history.
|
|
327
322
|
if (!allowDrift) {
|
|
328
|
-
const mismatches = await validateChecksums(client, migrationsDir);
|
|
323
|
+
const mismatches = await validateChecksums(client, migrationsDir, dialect);
|
|
329
324
|
if (mismatches.length > 0) {
|
|
330
325
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
331
326
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
@@ -349,7 +344,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
349
344
|
throw new MigrationError(lines.join('\n'));
|
|
350
345
|
}
|
|
351
346
|
}
|
|
352
|
-
const applied = await getAppliedMigrations(client);
|
|
347
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
353
348
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
354
349
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
355
350
|
let pending = allFiles.filter((f) => !appliedNames.has(f.name));
|
|
@@ -369,7 +364,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
369
364
|
try {
|
|
370
365
|
await client.query('BEGIN');
|
|
371
366
|
await client.query(up);
|
|
372
|
-
await client.query(
|
|
367
|
+
await client.query(dialect.buildMigrationInsertApplied(quotedTrackingTable(dialect)), [file.name, hash]);
|
|
373
368
|
await client.query('COMMIT');
|
|
374
369
|
results.push(file);
|
|
375
370
|
}
|
|
@@ -402,6 +397,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
402
397
|
export async function migrateDown(connectionString, migrationsDir, options) {
|
|
403
398
|
const client = new pg.Client({ connectionString });
|
|
404
399
|
await client.connect();
|
|
400
|
+
const dialect = options?.dialect ?? postgresDialect;
|
|
405
401
|
try {
|
|
406
402
|
// Derive a per-database advisory lock ID so concurrent migrations in
|
|
407
403
|
// sibling databases on the same cluster do not contend.
|
|
@@ -413,8 +409,8 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
413
409
|
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
414
410
|
}
|
|
415
411
|
try {
|
|
416
|
-
await ensureTrackingTable(client);
|
|
417
|
-
const applied = await getAppliedMigrations(client);
|
|
412
|
+
await ensureTrackingTable(client, dialect);
|
|
413
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
418
414
|
if (applied.length === 0) {
|
|
419
415
|
return { rolledBack: [], errors: [] };
|
|
420
416
|
}
|
|
@@ -441,7 +437,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
441
437
|
try {
|
|
442
438
|
await client.query('BEGIN');
|
|
443
439
|
await client.query(down);
|
|
444
|
-
await client.query(
|
|
440
|
+
await client.query(dialect.buildMigrationDeleteApplied(quotedTrackingTable(dialect)), [migration.name]);
|
|
445
441
|
await client.query('COMMIT');
|
|
446
442
|
results.push(file);
|
|
447
443
|
}
|
|
@@ -466,12 +462,13 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
466
462
|
* Get the status of all migrations (applied vs pending).
|
|
467
463
|
* Includes checksum validation for applied migrations.
|
|
468
464
|
*/
|
|
469
|
-
export async function migrateStatus(connectionString, migrationsDir) {
|
|
465
|
+
export async function migrateStatus(connectionString, migrationsDir, options) {
|
|
470
466
|
const client = new pg.Client({ connectionString });
|
|
471
467
|
await client.connect();
|
|
468
|
+
const dialect = options?.dialect ?? postgresDialect;
|
|
472
469
|
try {
|
|
473
|
-
await ensureTrackingTable(client);
|
|
474
|
-
const applied = await getAppliedMigrations(client);
|
|
470
|
+
await ensureTrackingTable(client, dialect);
|
|
471
|
+
const applied = await getAppliedMigrations(client, dialect);
|
|
475
472
|
const appliedMap = new Map(applied.map((m) => [m.name, m]));
|
|
476
473
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
477
474
|
return allFiles.map((file) => {
|
package/dist/dialect.d.ts
CHANGED
|
@@ -49,6 +49,43 @@ export interface UpsertStatementInput {
|
|
|
49
49
|
/** Optional SQL-ready RETURNING selection. */
|
|
50
50
|
returning?: string;
|
|
51
51
|
}
|
|
52
|
+
export interface ColumnTypeInput {
|
|
53
|
+
/** Schema-builder column type name (PostgreSQL-native in the root package). */
|
|
54
|
+
type: string;
|
|
55
|
+
/** Optional VARCHAR length. */
|
|
56
|
+
maxLength?: number | null;
|
|
57
|
+
}
|
|
58
|
+
export interface ColumnDefinitionInput extends ColumnTypeInput {
|
|
59
|
+
/** SQL-ready quoted column name. */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Whether this column is a single-column primary key. */
|
|
62
|
+
primaryKey?: boolean;
|
|
63
|
+
/** Whether this column is unique. Ignored when primaryKey is true. */
|
|
64
|
+
unique?: boolean;
|
|
65
|
+
/** Whether this column is NOT NULL. */
|
|
66
|
+
notNull?: boolean;
|
|
67
|
+
/** SQL-ready default expression. */
|
|
68
|
+
defaultValue?: string;
|
|
69
|
+
/** SQL-ready REFERENCES clause without the leading REFERENCES keyword. */
|
|
70
|
+
references?: {
|
|
71
|
+
table: string;
|
|
72
|
+
column: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface CreateTableStatementInput {
|
|
76
|
+
/** SQL-ready quoted table name. */
|
|
77
|
+
table: string;
|
|
78
|
+
/** SQL-ready column and table constraints. */
|
|
79
|
+
definitions: string[];
|
|
80
|
+
}
|
|
81
|
+
export interface CreateIndexStatementInput {
|
|
82
|
+
/** SQL-ready quoted index name. */
|
|
83
|
+
name: string;
|
|
84
|
+
/** SQL-ready quoted table name. */
|
|
85
|
+
table: string;
|
|
86
|
+
/** SQL-ready quoted index columns. */
|
|
87
|
+
columns: string[];
|
|
88
|
+
}
|
|
52
89
|
export interface Dialect {
|
|
53
90
|
/** Dialect identifier. */
|
|
54
91
|
readonly name: DialectName;
|
|
@@ -92,6 +129,26 @@ export interface Dialect {
|
|
|
92
129
|
typeToTypeScript(dialectType: string, nullable: boolean): string;
|
|
93
130
|
/** Optional array-cast hook for bulk insert implementations. */
|
|
94
131
|
arrayType?(baseType: string): string;
|
|
132
|
+
/** Map a schema-builder column type to dialect DDL. */
|
|
133
|
+
buildColumnType(input: ColumnTypeInput): string;
|
|
134
|
+
/** Build a column definition line for CREATE/ALTER TABLE. */
|
|
135
|
+
buildColumnDefinition(input: ColumnDefinitionInput): string;
|
|
136
|
+
/** Build a table-level PRIMARY KEY constraint. */
|
|
137
|
+
buildPrimaryKeyConstraint(columns: string[]): string;
|
|
138
|
+
/** Build a CREATE TABLE statement from SQL-ready definitions. */
|
|
139
|
+
buildCreateTableStatement(input: CreateTableStatementInput): string;
|
|
140
|
+
/** Build a CREATE INDEX statement. */
|
|
141
|
+
buildCreateIndexStatement(input: CreateIndexStatementInput): string;
|
|
142
|
+
/** Build the migration tracking table DDL. */
|
|
143
|
+
buildMigrationTrackingTable(table: string): string;
|
|
144
|
+
/** Build the query that reads applied migrations. */
|
|
145
|
+
buildMigrationSelectApplied(table: string): string;
|
|
146
|
+
/** Build the query that updates an applied migration checksum. */
|
|
147
|
+
buildMigrationUpdateChecksum(table: string): string;
|
|
148
|
+
/** Build the query that records an applied migration. */
|
|
149
|
+
buildMigrationInsertApplied(table: string): string;
|
|
150
|
+
/** Build the query that deletes a rolled-back migration record. */
|
|
151
|
+
buildMigrationDeleteApplied(table: string): string;
|
|
95
152
|
}
|
|
96
153
|
export interface DialectIntrospector {
|
|
97
154
|
introspect(options: IntrospectOptions): Promise<SchemaMetadata>;
|
package/dist/dialect.js
CHANGED
|
@@ -73,5 +73,57 @@ export const postgresDialect = {
|
|
|
73
73
|
// This hook is the package boundary MySQL/SQLite implementations will fill.
|
|
74
74
|
return 'unknown';
|
|
75
75
|
},
|
|
76
|
+
buildColumnType(input) {
|
|
77
|
+
if (input.type === 'VARCHAR' && input.maxLength != null) {
|
|
78
|
+
return `VARCHAR(${input.maxLength})`;
|
|
79
|
+
}
|
|
80
|
+
return input.type;
|
|
81
|
+
},
|
|
82
|
+
buildColumnDefinition(input) {
|
|
83
|
+
const parts = [input.name, this.buildColumnType(input)];
|
|
84
|
+
if (input.primaryKey)
|
|
85
|
+
parts.push('PRIMARY KEY');
|
|
86
|
+
if (input.unique && !input.primaryKey)
|
|
87
|
+
parts.push('UNIQUE');
|
|
88
|
+
if (input.notNull)
|
|
89
|
+
parts.push('NOT NULL');
|
|
90
|
+
if (input.defaultValue != null)
|
|
91
|
+
parts.push(`DEFAULT ${input.defaultValue}`);
|
|
92
|
+
if (input.references)
|
|
93
|
+
parts.push(`REFERENCES ${input.references.table}(${input.references.column})`);
|
|
94
|
+
return parts.join(' ');
|
|
95
|
+
},
|
|
96
|
+
buildPrimaryKeyConstraint(columns) {
|
|
97
|
+
return `PRIMARY KEY (${columns.join(', ')})`;
|
|
98
|
+
},
|
|
99
|
+
buildCreateTableStatement(input) {
|
|
100
|
+
const body = input.definitions.map((d) => ` ${d}`).join(',\n');
|
|
101
|
+
return `CREATE TABLE ${input.table} (\n${body}\n);`;
|
|
102
|
+
},
|
|
103
|
+
buildCreateIndexStatement(input) {
|
|
104
|
+
return `CREATE INDEX ${input.name} ON ${input.table}(${input.columns.join(', ')});`;
|
|
105
|
+
},
|
|
106
|
+
buildMigrationTrackingTable(table) {
|
|
107
|
+
return `
|
|
108
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
109
|
+
id SERIAL PRIMARY KEY,
|
|
110
|
+
name TEXT NOT NULL UNIQUE,
|
|
111
|
+
checksum TEXT NOT NULL,
|
|
112
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
113
|
+
);
|
|
114
|
+
`;
|
|
115
|
+
},
|
|
116
|
+
buildMigrationSelectApplied(table) {
|
|
117
|
+
return `SELECT id, name, applied_at, checksum FROM ${table} ORDER BY id ASC`;
|
|
118
|
+
},
|
|
119
|
+
buildMigrationUpdateChecksum(table) {
|
|
120
|
+
return `UPDATE ${table} SET checksum = ${this.paramPlaceholder(1)} WHERE name = ${this.paramPlaceholder(2)}`;
|
|
121
|
+
},
|
|
122
|
+
buildMigrationInsertApplied(table) {
|
|
123
|
+
return `INSERT INTO ${table} (name, checksum) VALUES (${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}) ON CONFLICT (name) DO NOTHING`;
|
|
124
|
+
},
|
|
125
|
+
buildMigrationDeleteApplied(table) {
|
|
126
|
+
return `DELETE FROM ${table} WHERE name = ${this.paramPlaceholder(1)}`;
|
|
127
|
+
},
|
|
76
128
|
};
|
|
77
129
|
//# sourceMappingURL=dialect.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
export type { DatabaseAdapter, IntrospectionOverrides } from './adapters/index.js';
|
|
36
36
|
export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
|
|
37
37
|
export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, } from './client.js';
|
|
38
|
-
export type { BuiltStatement, BulkInsertStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, UpsertStatementInput, } from './dialect.js';
|
|
38
|
+
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, UpsertStatementInput, } from './dialect.js';
|
|
39
39
|
export { postgresDialect } from './dialect.js';
|
|
40
40
|
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
41
|
export { type GenerateOptions, generate } from './generate.js';
|
|
@@ -45,6 +45,6 @@ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type CountA
|
|
|
45
45
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
46
46
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
47
47
|
export { ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
48
|
-
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
48
|
+
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
49
49
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
|
50
50
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/query/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* former monolithic `import { … } from './query.js'`.
|
|
7
7
|
*/
|
|
8
8
|
export type { AggregateArgs, AggregateResult, ArrayFilter, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, OrderDirection, RelationDescriptor, RelationFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
|
|
9
|
-
export type { BuiltStatement, BulkInsertStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
|
|
9
|
+
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
|
12
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
package/dist/schema-sql.d.ts
CHANGED
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
* Converts a SchemaDef (from defineSchema) into executable DDL statements.
|
|
5
5
|
* Also provides diff and push commands for syncing schema to a live database.
|
|
6
6
|
*/
|
|
7
|
+
import { type Dialect } from './dialect.js';
|
|
7
8
|
import type { SchemaDef, TableDef } from './schema-builder.js';
|
|
9
|
+
export interface SchemaSqlOptions {
|
|
10
|
+
/** SQL dialect used for DDL generation. Defaults to PostgreSQL. */
|
|
11
|
+
dialect?: Dialect;
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Convert a SchemaDef into an ordered array of SQL DDL statements.
|
|
10
15
|
*
|
|
11
16
|
* Returns CREATE TABLE statements (in dependency order based on references)
|
|
12
17
|
* followed by CREATE INDEX statements for foreign key columns.
|
|
13
18
|
*/
|
|
14
|
-
export declare function schemaToSQL(schema: SchemaDef): string[];
|
|
19
|
+
export declare function schemaToSQL(schema: SchemaDef, options?: SchemaSqlOptions): string[];
|
|
15
20
|
export interface AlterColumnDef {
|
|
16
21
|
/** Column name in snake_case */
|
|
17
22
|
column: string;
|
|
@@ -71,5 +76,5 @@ export declare function schemaPush(schema: SchemaDef, connectionString: string,
|
|
|
71
76
|
* Generate the full DDL as a single formatted string.
|
|
72
77
|
* Useful for printing or saving to a .sql file.
|
|
73
78
|
*/
|
|
74
|
-
export declare function schemaToSQLString(schema: SchemaDef): string;
|
|
79
|
+
export declare function schemaToSQLString(schema: SchemaDef, options?: SchemaSqlOptions): string;
|
|
75
80
|
//# sourceMappingURL=schema-sql.d.ts.map
|
package/dist/schema-sql.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Also provides diff and push commands for syncing schema to a live database.
|
|
6
6
|
*/
|
|
7
7
|
import pg from 'pg';
|
|
8
|
-
import {
|
|
8
|
+
import { postgresDialect } from './dialect.js';
|
|
9
9
|
import { camelToSnake } from './schema.js';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
@@ -16,7 +16,8 @@ import { camelToSnake } from './schema.js';
|
|
|
16
16
|
* Returns CREATE TABLE statements (in dependency order based on references)
|
|
17
17
|
* followed by CREATE INDEX statements for foreign key columns.
|
|
18
18
|
*/
|
|
19
|
-
export function schemaToSQL(schema) {
|
|
19
|
+
export function schemaToSQL(schema, options) {
|
|
20
|
+
const dialect = options?.dialect ?? postgresDialect;
|
|
20
21
|
const statements = [];
|
|
21
22
|
// Topologically sort tables by their foreign key references
|
|
22
23
|
const sorted = topologicalSort(schema);
|
|
@@ -24,12 +25,12 @@ export function schemaToSQL(schema) {
|
|
|
24
25
|
// Generate CREATE TABLE statements
|
|
25
26
|
for (const tableName of sorted) {
|
|
26
27
|
const table = schema.tables[tableName];
|
|
27
|
-
statements.push(generateCreateTable(table, resolveRef));
|
|
28
|
+
statements.push(generateCreateTable(table, resolveRef, dialect));
|
|
28
29
|
}
|
|
29
30
|
// Generate CREATE INDEX for foreign key columns
|
|
30
31
|
for (const tableName of sorted) {
|
|
31
32
|
const table = schema.tables[tableName];
|
|
32
|
-
const indexes = generateForeignKeyIndexes(table);
|
|
33
|
+
const indexes = generateForeignKeyIndexes(table, dialect);
|
|
33
34
|
statements.push(...indexes);
|
|
34
35
|
}
|
|
35
36
|
return statements;
|
|
@@ -125,42 +126,28 @@ function topologicalSort(schema) {
|
|
|
125
126
|
* to their snake_case DDL form, so users can write either camelCase JS
|
|
126
127
|
* accessor names or snake_case DDL names.
|
|
127
128
|
*/
|
|
128
|
-
function generateCreateTable(table, resolveRef) {
|
|
129
|
+
function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
|
|
129
130
|
const tableName = table.name;
|
|
130
131
|
const columnDefs = [];
|
|
131
132
|
const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
|
|
132
133
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
133
|
-
columnDefs.push(generateColumnDef(fieldName, config, resolveRef));
|
|
134
|
+
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
|
|
134
135
|
}
|
|
135
136
|
// Append a table-level PRIMARY KEY constraint when a composite PK is set.
|
|
136
137
|
if (compositePk) {
|
|
137
|
-
const cols = compositePk.map((c) =>
|
|
138
|
-
columnDefs.push(
|
|
138
|
+
const cols = compositePk.map((c) => dialect.quoteIdentifier(camelToSnake(c)));
|
|
139
|
+
columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
return dialect.buildCreateTableStatement({
|
|
142
|
+
table: dialect.quoteIdentifier(tableName),
|
|
143
|
+
definitions: columnDefs,
|
|
144
|
+
});
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
147
|
* Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
|
|
145
148
|
*/
|
|
146
|
-
function generateColumnDef(fieldName, config, resolveRef) {
|
|
149
|
+
function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDialect) {
|
|
147
150
|
const snakeName = camelToSnake(fieldName);
|
|
148
|
-
const parts = [quoteIdent(snakeName)];
|
|
149
|
-
// Type
|
|
150
|
-
if (config.type === 'VARCHAR' && config.maxLength != null) {
|
|
151
|
-
parts.push(`VARCHAR(${config.maxLength})`);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
parts.push(config.type);
|
|
155
|
-
}
|
|
156
|
-
// PRIMARY KEY
|
|
157
|
-
if (config.isPrimaryKey) {
|
|
158
|
-
parts.push('PRIMARY KEY');
|
|
159
|
-
}
|
|
160
|
-
// UNIQUE (only if not primary key — PK is implicitly unique)
|
|
161
|
-
if (config.isUnique && !config.isPrimaryKey) {
|
|
162
|
-
parts.push('UNIQUE');
|
|
163
|
-
}
|
|
164
151
|
// NOT NULL — serial types are implicitly NOT NULL, but explicit is fine.
|
|
165
152
|
// A column is NOT NULL if:
|
|
166
153
|
// 1. Explicitly marked .notNull(), OR
|
|
@@ -169,25 +156,36 @@ function generateColumnDef(fieldName, config, resolveRef) {
|
|
|
169
156
|
// A column is left nullable if .nullable() was called.
|
|
170
157
|
const isSerial = config.type === 'BIGSERIAL';
|
|
171
158
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
172
|
-
|
|
173
|
-
parts.push('NOT NULL');
|
|
174
|
-
}
|
|
159
|
+
const notNull = config.isNotNull && !implicitNotNull;
|
|
175
160
|
// DEFAULT
|
|
161
|
+
let defaultValue;
|
|
176
162
|
if (config.defaultValue != null) {
|
|
177
|
-
|
|
178
|
-
parts.push(`DEFAULT ${sqlDefault}`);
|
|
163
|
+
defaultValue = normalizeDefault(config.defaultValue);
|
|
179
164
|
}
|
|
180
165
|
// REFERENCES — resolve the raw table name through the optional resolver so
|
|
181
166
|
// both camelCase accessor names and snake_case DDL names work.
|
|
167
|
+
let references;
|
|
182
168
|
if (config.referencesTarget) {
|
|
183
169
|
const refParts = config.referencesTarget.split('.');
|
|
184
170
|
if (refParts.length === 2) {
|
|
185
171
|
const rawTable = refParts[0];
|
|
186
172
|
const refTable = resolveRef ? resolveRef(rawTable) : rawTable;
|
|
187
|
-
|
|
173
|
+
references = {
|
|
174
|
+
table: dialect.quoteIdentifier(refTable),
|
|
175
|
+
column: dialect.quoteIdentifier(refParts[1]),
|
|
176
|
+
};
|
|
188
177
|
}
|
|
189
178
|
}
|
|
190
|
-
return
|
|
179
|
+
return dialect.buildColumnDefinition({
|
|
180
|
+
name: dialect.quoteIdentifier(snakeName),
|
|
181
|
+
type: config.type,
|
|
182
|
+
maxLength: config.maxLength,
|
|
183
|
+
primaryKey: config.isPrimaryKey,
|
|
184
|
+
unique: config.isUnique,
|
|
185
|
+
notNull,
|
|
186
|
+
defaultValue,
|
|
187
|
+
references,
|
|
188
|
+
});
|
|
191
189
|
}
|
|
192
190
|
/**
|
|
193
191
|
* Normalize a default value from the user's schema definition to valid SQL.
|
|
@@ -227,13 +225,17 @@ function normalizeDefault(val) {
|
|
|
227
225
|
* Generate CREATE INDEX statements for foreign key columns.
|
|
228
226
|
* Only generates indexes for columns that have a REFERENCES clause.
|
|
229
227
|
*/
|
|
230
|
-
function generateForeignKeyIndexes(table) {
|
|
228
|
+
function generateForeignKeyIndexes(table, dialect = postgresDialect) {
|
|
231
229
|
const indexes = [];
|
|
232
230
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
233
231
|
if (config.referencesTarget) {
|
|
234
232
|
const snakeName = camelToSnake(fieldName);
|
|
235
233
|
const indexName = `idx_${table.name}_${snakeName}`;
|
|
236
|
-
indexes.push(
|
|
234
|
+
indexes.push(dialect.buildCreateIndexStatement({
|
|
235
|
+
name: dialect.quoteIdentifier(indexName),
|
|
236
|
+
table: dialect.quoteIdentifier(table.name),
|
|
237
|
+
columns: [dialect.quoteIdentifier(snakeName)],
|
|
238
|
+
}));
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
return indexes;
|
|
@@ -245,6 +247,7 @@ function generateForeignKeyIndexes(table) {
|
|
|
245
247
|
* DDL is needed to make the database match the schema definition.
|
|
246
248
|
*/
|
|
247
249
|
export async function schemaDiff(schema, connectionString) {
|
|
250
|
+
const dialect = postgresDialect;
|
|
248
251
|
const client = new pg.Client({ connectionString });
|
|
249
252
|
await client.connect();
|
|
250
253
|
try {
|
|
@@ -304,11 +307,11 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
304
307
|
const ddlName = tableDef.name;
|
|
305
308
|
if (!existingTables.has(ddlName)) {
|
|
306
309
|
result.create.push(tableDef);
|
|
307
|
-
result.statements.push(generateCreateTable(tableDef, resolveRef));
|
|
308
|
-
const fkIndexes = generateForeignKeyIndexes(tableDef);
|
|
310
|
+
result.statements.push(generateCreateTable(tableDef, resolveRef, dialect));
|
|
311
|
+
const fkIndexes = generateForeignKeyIndexes(tableDef, dialect);
|
|
309
312
|
result.statements.push(...fkIndexes);
|
|
310
313
|
// Reverse: DROP TABLE (with indexes — they drop automatically)
|
|
311
|
-
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${
|
|
314
|
+
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
312
315
|
}
|
|
313
316
|
}
|
|
314
317
|
// Tables to drop (in DB but not in schema)
|
|
@@ -332,9 +335,9 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
332
335
|
const dbCol = dbCols[snakeName];
|
|
333
336
|
if (!dbCol) {
|
|
334
337
|
// Column exists in schema but not in DB — ADD COLUMN
|
|
335
|
-
const colDef = generateColumnDef(fieldName, config, resolveRef);
|
|
336
|
-
const sql = `ALTER TABLE ${
|
|
337
|
-
const reverseSql = `ALTER TABLE ${
|
|
338
|
+
const colDef = generateColumnDef(fieldName, config, resolveRef, dialect);
|
|
339
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD COLUMN ${colDef};`;
|
|
340
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(snakeName)};`;
|
|
338
341
|
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
339
342
|
result.statements.push(sql);
|
|
340
343
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -345,8 +348,8 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
345
348
|
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
346
349
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
347
350
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
348
|
-
const sql = `ALTER TABLE ${
|
|
349
|
-
const reverseSql = `ALTER TABLE ${
|
|
351
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
352
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
|
|
350
353
|
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
351
354
|
result.statements.push(sql);
|
|
352
355
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -355,15 +358,15 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
355
358
|
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
|
|
356
359
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
357
360
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
358
|
-
const sql = `ALTER TABLE ${
|
|
359
|
-
const reverseSql = `ALTER TABLE ${
|
|
361
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
362
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
360
363
|
alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
|
|
361
364
|
result.statements.push(sql);
|
|
362
365
|
result.reverseStatements.unshift(reverseSql);
|
|
363
366
|
}
|
|
364
367
|
else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
|
|
365
|
-
const sql = `ALTER TABLE ${
|
|
366
|
-
const reverseSql = `ALTER TABLE ${
|
|
368
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP NOT NULL;`;
|
|
369
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
367
370
|
alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
|
|
368
371
|
result.statements.push(sql);
|
|
369
372
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -375,16 +378,16 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
375
378
|
const dbDefault = dbCol.columnDefault;
|
|
376
379
|
if (schemaDefault && !dbDefault) {
|
|
377
380
|
// Schema has default, DB doesn't
|
|
378
|
-
const sql = `ALTER TABLE ${
|
|
379
|
-
const reverseSql = `ALTER TABLE ${
|
|
381
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
382
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
380
383
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
381
384
|
result.statements.push(sql);
|
|
382
385
|
result.reverseStatements.unshift(reverseSql);
|
|
383
386
|
}
|
|
384
387
|
else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
|
|
385
388
|
// DB has a non-sequence default, schema doesn't
|
|
386
|
-
const sql = `ALTER TABLE ${
|
|
387
|
-
const reverseSql = `ALTER TABLE ${
|
|
389
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} DROP DEFAULT;`;
|
|
390
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
388
391
|
alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
|
|
389
392
|
result.statements.push(sql);
|
|
390
393
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -394,8 +397,8 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
394
397
|
!isSequenceDefault(dbDefault) &&
|
|
395
398
|
!defaultsMatch(schemaDefault, dbDefault)) {
|
|
396
399
|
// Both have defaults but they differ
|
|
397
|
-
const sql = `ALTER TABLE ${
|
|
398
|
-
const reverseSql = `ALTER TABLE ${
|
|
400
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
401
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
399
402
|
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
400
403
|
result.statements.push(sql);
|
|
401
404
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -407,16 +410,16 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
407
410
|
const wantsUnique = config.isUnique === true;
|
|
408
411
|
if (wantsUnique && !hasDbUnique) {
|
|
409
412
|
const constraintName = `${tableName}_${snakeName}_key`;
|
|
410
|
-
const sql = `ALTER TABLE ${
|
|
411
|
-
const reverseSql = `ALTER TABLE ${
|
|
413
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
414
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
412
415
|
alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
|
|
413
416
|
result.statements.push(sql);
|
|
414
417
|
result.reverseStatements.unshift(reverseSql);
|
|
415
418
|
}
|
|
416
419
|
else if (!wantsUnique && hasDbUnique) {
|
|
417
420
|
const constraintName = tableUniques[snakeName];
|
|
418
|
-
const sql = `ALTER TABLE ${
|
|
419
|
-
const reverseSql = `ALTER TABLE ${
|
|
421
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)};`;
|
|
422
|
+
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} UNIQUE (${dialect.quoteIdentifier(snakeName)});`;
|
|
420
423
|
alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
|
|
421
424
|
result.statements.push(sql);
|
|
422
425
|
result.reverseStatements.unshift(reverseSql);
|
|
@@ -427,7 +430,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
427
430
|
for (const dbColName of Object.keys(dbCols)) {
|
|
428
431
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => camelToSnake(fieldName) === dbColName);
|
|
429
432
|
if (!hasField) {
|
|
430
|
-
const sql = `ALTER TABLE ${
|
|
433
|
+
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
431
434
|
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" — add it back manually`;
|
|
432
435
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
433
436
|
// Don't auto-add drops to statements for safety — user must opt in
|
|
@@ -560,8 +563,8 @@ export async function schemaPush(schema, connectionString, options = {}) {
|
|
|
560
563
|
* Generate the full DDL as a single formatted string.
|
|
561
564
|
* Useful for printing or saving to a .sql file.
|
|
562
565
|
*/
|
|
563
|
-
export function schemaToSQLString(schema) {
|
|
564
|
-
const statements = schemaToSQL(schema);
|
|
566
|
+
export function schemaToSQLString(schema, options) {
|
|
567
|
+
const statements = schemaToSQL(schema, options);
|
|
565
568
|
return `${statements.join('\n\n')}\n`;
|
|
566
569
|
}
|
|
567
570
|
//# sourceMappingURL=schema-sql.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"main": "./dist/cjs/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"bin": {
|
|
31
|
-
"turbine": "
|
|
31
|
+
"turbine": "dist/cli/index.js"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"homepage": "https://turbineorm.dev",
|
|
102
102
|
"repository": {
|
|
103
103
|
"type": "git",
|
|
104
|
-
"url": "https://github.com/zvndev/turbine-orm.git"
|
|
104
|
+
"url": "git+https://github.com/zvndev/turbine-orm.git"
|
|
105
105
|
},
|
|
106
106
|
"bugs": {
|
|
107
107
|
"url": "https://github.com/zvndev/turbine-orm/issues"
|