turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/schema-sql.js
CHANGED
|
@@ -108,13 +108,18 @@ export function schemaToSQL(schema, options) {
|
|
|
108
108
|
: 'CREATE EXTENSION IF NOT EXISTS vector;');
|
|
109
109
|
}
|
|
110
110
|
// CREATE TYPE for every schema-level enum, before the tables that use them.
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// Only where enums are named types (PostgreSQL): every other engine declares
|
|
112
|
+
// the labels on the column itself (see `enumColumnType`), and has no CREATE
|
|
113
|
+
// TYPE to run.
|
|
114
|
+
if (dialect.capabilities.namedEnumTypes) {
|
|
115
|
+
for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
|
|
116
|
+
statements.push(generateCreateEnumType(enumName, labels, dialect));
|
|
117
|
+
}
|
|
113
118
|
}
|
|
114
119
|
// Generate CREATE TABLE statements
|
|
115
120
|
for (const tableName of sorted) {
|
|
116
121
|
const table = schema.tables[tableName];
|
|
117
|
-
statements.push(generateCreateTable(table, resolveRef, dialect));
|
|
122
|
+
statements.push(generateCreateTable(table, resolveRef, dialect, schema.enums));
|
|
118
123
|
}
|
|
119
124
|
// Generate CREATE INDEX for foreign key columns
|
|
120
125
|
for (const tableName of sorted) {
|
|
@@ -220,13 +225,22 @@ function topologicalSort(schema) {
|
|
|
220
225
|
* to their snake_case DDL form, so users can write either camelCase JS
|
|
221
226
|
* accessor names or snake_case DDL names.
|
|
222
227
|
*/
|
|
223
|
-
function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
|
|
228
|
+
function generateCreateTable(table, resolveRef, dialect = postgresDialect, enums) {
|
|
224
229
|
const tableName = table.name;
|
|
225
230
|
const columnDefs = [];
|
|
226
231
|
const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
|
|
232
|
+
const keyed = keyedColumnNames(table);
|
|
233
|
+
const inline = dialect.capabilities.inlineForeignKeys;
|
|
234
|
+
const foreignKeys = [];
|
|
227
235
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
228
|
-
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
|
|
236
|
+
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect, keyed.has(camelToSnake(fieldName)), inline, enums));
|
|
237
|
+
if (!inline) {
|
|
238
|
+
const fk = generateForeignKeyConstraint(fieldName, config, resolveRef, dialect);
|
|
239
|
+
if (fk)
|
|
240
|
+
foreignKeys.push(fk);
|
|
241
|
+
}
|
|
229
242
|
}
|
|
243
|
+
columnDefs.push(...foreignKeys);
|
|
230
244
|
// Append a table-level PRIMARY KEY constraint when a composite PK is set.
|
|
231
245
|
if (compositePk) {
|
|
232
246
|
const cols = compositePk.map((c) => dialect.quoteIdentifier(camelToSnake(c)));
|
|
@@ -246,11 +260,80 @@ function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
|
|
|
246
260
|
definitions: columnDefs,
|
|
247
261
|
});
|
|
248
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The snake_case names of the columns of `table` that sit in a key: a primary
|
|
265
|
+
* key (single or composite), a UNIQUE column, a foreign key (Turbine indexes
|
|
266
|
+
* those), or a member of a declared index.
|
|
267
|
+
*/
|
|
268
|
+
function keyedColumnNames(table) {
|
|
269
|
+
const keyed = new Set();
|
|
270
|
+
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
271
|
+
if (config.isPrimaryKey || config.isUnique || config.referencesTarget)
|
|
272
|
+
keyed.add(camelToSnake(fieldName));
|
|
273
|
+
}
|
|
274
|
+
for (const pk of table.primaryKey ?? [])
|
|
275
|
+
keyed.add(camelToSnake(pk));
|
|
276
|
+
for (const idx of table.indexes ?? []) {
|
|
277
|
+
if (isDocFieldIndexDef(idx))
|
|
278
|
+
continue;
|
|
279
|
+
for (const col of idx.columns)
|
|
280
|
+
keyed.add(camelToSnake(col));
|
|
281
|
+
}
|
|
282
|
+
return keyed;
|
|
283
|
+
}
|
|
284
|
+
/** A table-level `FOREIGN KEY` clause for a column's `references`, or null. */
|
|
285
|
+
function generateForeignKeyConstraint(fieldName, config, resolveRef, dialect) {
|
|
286
|
+
if (!config.referencesTarget)
|
|
287
|
+
return null;
|
|
288
|
+
const refParts = config.referencesTarget.split('.');
|
|
289
|
+
if (refParts.length !== 2)
|
|
290
|
+
return null;
|
|
291
|
+
const refTable = resolveRef ? resolveRef(refParts[0]) : refParts[0];
|
|
292
|
+
let clause = `FOREIGN KEY (${dialect.quoteIdentifier(camelToSnake(fieldName))}) REFERENCES ${dialect.quoteIdentifier(refTable)}(${dialect.quoteIdentifier(refParts[1])})`;
|
|
293
|
+
if (config.onDelete)
|
|
294
|
+
clause += ` ON DELETE ${referentialActionToSql(config.onDelete)}`;
|
|
295
|
+
if (config.onUpdate)
|
|
296
|
+
clause += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
|
|
297
|
+
return clause;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The type (and CHECK) of an enum column on an engine with no named enum type,
|
|
301
|
+
* from the dialect's `buildEnumColumn`, or null on PostgreSQL, where the column
|
|
302
|
+
* names the `CREATE TYPE`. The labels come from the schema's `enums`: off
|
|
303
|
+
* PostgreSQL there is no existing type a column could name instead.
|
|
304
|
+
*/
|
|
305
|
+
function enumColumnType(config, dialect, column, enums) {
|
|
306
|
+
if (!config.enumName || dialect.capabilities.namedEnumTypes)
|
|
307
|
+
return null;
|
|
308
|
+
if (!dialect.buildEnumColumn) {
|
|
309
|
+
throw new UnsupportedFeatureError('enum columns', dialect.name, `Column "${column}" is of enum type "${config.enumName}". Declare it as text with a CHECK constraint instead.`);
|
|
310
|
+
}
|
|
311
|
+
if (config.isArray) {
|
|
312
|
+
throw new UnsupportedFeatureError('enum array columns', dialect.name, `Column "${column}" is an array of enum "${config.enumName}"; ${dialect.name} has no array columns.`);
|
|
313
|
+
}
|
|
314
|
+
const labels = enums?.[config.enumName];
|
|
315
|
+
if (!labels) {
|
|
316
|
+
throw new ValidationError(`Column "${column}" is of enum type "${config.enumName}", which the schema does not declare. ` +
|
|
317
|
+
`${dialect.name} has no enum type to refer to, so the labels must be in the schema: ` +
|
|
318
|
+
`defineSchema(tables, { enums: { ${JSON.stringify(config.enumName)}: ['a', 'b'] } }).`);
|
|
319
|
+
}
|
|
320
|
+
if (labels.length === 0) {
|
|
321
|
+
throw new ValidationError(`Enum "${config.enumName}" declares no labels, so column "${column}" could hold nothing.`);
|
|
322
|
+
}
|
|
323
|
+
return dialect.buildEnumColumn(dialect.quoteIdentifier(column), labels);
|
|
324
|
+
}
|
|
249
325
|
/**
|
|
250
326
|
* Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
|
|
327
|
+
*
|
|
328
|
+
* `keyed` marks a column that sits in a key (see {@link keyedColumnNames}); it
|
|
329
|
+
* only matters to dialects that cannot key an unbounded text type. `inlineReferences`
|
|
330
|
+
* is false for a dialect that declares foreign keys at table level instead.
|
|
331
|
+
* `enums` is the schema's enum declarations, needed off PostgreSQL to declare
|
|
332
|
+
* an enum column's labels on the column itself.
|
|
251
333
|
*/
|
|
252
|
-
function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDialect) {
|
|
334
|
+
function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDialect, keyed = false, inlineReferences = true, enums) {
|
|
253
335
|
const snakeName = camelToSnake(fieldName);
|
|
336
|
+
const enumColumn = enumColumnType(config, dialect, snakeName, enums);
|
|
254
337
|
// NOT NULL, serial types are implicitly NOT NULL, but explicit is fine.
|
|
255
338
|
// A column is NOT NULL if:
|
|
256
339
|
// 1. Explicitly marked .notNull(), OR
|
|
@@ -260,15 +343,13 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
260
343
|
const isSerial = isSerialType(config.type);
|
|
261
344
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
262
345
|
const notNull = config.isNotNull && !implicitNotNull;
|
|
263
|
-
// DEFAULT
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
defaultValue = normalizeDefault(config.defaultValue);
|
|
267
|
-
}
|
|
346
|
+
// DEFAULT, normalized here and spelled for the dialect once the column's
|
|
347
|
+
// final type is known (a MySQL default depends on it).
|
|
348
|
+
const normalizedDefault = config.defaultValue != null ? normalizeDefault(config.defaultValue) : undefined;
|
|
268
349
|
// REFERENCES, resolve the raw table name through the optional resolver so
|
|
269
350
|
// both camelCase accessor names and snake_case DDL names work.
|
|
270
351
|
let references;
|
|
271
|
-
if (config.referencesTarget) {
|
|
352
|
+
if (config.referencesTarget && inlineReferences) {
|
|
272
353
|
const refParts = config.referencesTarget.split('.');
|
|
273
354
|
if (refParts.length === 2) {
|
|
274
355
|
const rawTable = refParts[0];
|
|
@@ -279,14 +360,43 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
279
360
|
};
|
|
280
361
|
}
|
|
281
362
|
}
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
363
|
+
// The type is mapped ONCE, by the dialect's own `buildColumnDefinition`. A
|
|
364
|
+
// column that is a plain scalar hands it the schema-level token (`SERIAL`,
|
|
365
|
+
// `VARCHAR` + maxLength, `TEXT`), because every non-Postgres dialect keeps
|
|
366
|
+
// its auto-increment / identity marker, its length and its bounded string
|
|
367
|
+
// type INSIDE that mapping. Handing it the already-mapped token (`INT
|
|
368
|
+
// AUTO_INCREMENT`, `VARCHAR(12)`) made the dialect map the output of its own
|
|
369
|
+
// mapper, which turned `INT AUTO_INCREMENT` into `INT`, `VARCHAR(12)` into
|
|
370
|
+
// `VARCHAR(255)` and `NVARCHAR(MAX)` into `NVARCHAR(255)`. An enum name, a
|
|
371
|
+
// `vector(n)` and an array are Postgres tokens with no schema-level spelling,
|
|
372
|
+
// so they are passed through resolved.
|
|
373
|
+
const plainScalar = !config.enumName && config.vectorDimensions == null && !config.isArray;
|
|
374
|
+
let typeInput;
|
|
375
|
+
if (enumColumn) {
|
|
376
|
+
typeInput = { type: enumColumn.type, maxLength: enumColumn.maxLength ?? null };
|
|
377
|
+
}
|
|
378
|
+
else if (!plainScalar) {
|
|
379
|
+
typeInput = { type: resolveDdlType(config, dialect, snakeName), maxLength: null };
|
|
380
|
+
}
|
|
381
|
+
else if (keyed && config.type === 'TEXT' && !dialect.capabilities.unboundedTextKeys) {
|
|
382
|
+
// A keyed text / bytea column takes the dialect's bounded type
|
|
383
|
+
// (`VARCHAR(255)` / `NVARCHAR(255)`, `VARBINARY(255)`) where an unbounded
|
|
384
|
+
// one cannot be a key, or its own `maxLength` when it declares one.
|
|
385
|
+
typeInput = { type: 'VARCHAR', maxLength: config.maxLength };
|
|
386
|
+
}
|
|
387
|
+
else if (keyed && config.type === 'BYTEA' && !dialect.capabilities.unboundedTextKeys) {
|
|
388
|
+
typeInput = { type: 'VARBINARY', maxLength: config.maxLength };
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
typeInput = { type: config.type, maxLength: config.maxLength };
|
|
392
|
+
}
|
|
393
|
+
const defaultValue = normalizedDefault === undefined
|
|
394
|
+
? undefined
|
|
395
|
+
: dialectDefault(normalizedDefault, dialect, dialect.name === 'mysql' ? dialect.buildColumnType(typeInput) : '');
|
|
286
396
|
let def = dialect.buildColumnDefinition({
|
|
287
397
|
name: dialect.quoteIdentifier(snakeName),
|
|
288
|
-
type:
|
|
289
|
-
maxLength:
|
|
398
|
+
type: typeInput.type,
|
|
399
|
+
maxLength: typeInput.maxLength,
|
|
290
400
|
primaryKey: config.isPrimaryKey,
|
|
291
401
|
unique: config.isUnique,
|
|
292
402
|
notNull,
|
|
@@ -307,6 +417,10 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
307
417
|
if (config.check) {
|
|
308
418
|
def += ` CHECK (${config.check})`;
|
|
309
419
|
}
|
|
420
|
+
// The enum's labels, on an engine whose column type does not hold them.
|
|
421
|
+
if (enumColumn?.check) {
|
|
422
|
+
def += ` CHECK (${enumColumn.check})`;
|
|
423
|
+
}
|
|
310
424
|
return def;
|
|
311
425
|
}
|
|
312
426
|
/**
|
|
@@ -333,15 +447,136 @@ function normalizeDefault(val) {
|
|
|
333
447
|
if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
|
|
334
448
|
return val.trim();
|
|
335
449
|
}
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
450
|
+
// Single-quoted string literals, a quote inside written doubled (`'it''s'`),
|
|
451
|
+
// optionally followed by a PostgreSQL cast as the catalog prints a default
|
|
452
|
+
// (`'x'::text`, `'{}'::jsonb`). The cast is dropped: the literal is assigned
|
|
453
|
+
// to the column, which converts it to the column's type on every engine,
|
|
454
|
+
// and the other engines have no `::`. No semicolons, no statement keywords.
|
|
455
|
+
const literal = defaultStringLiteral(val.trim());
|
|
456
|
+
if (literal !== null) {
|
|
457
|
+
const inner = literal.slice(1, -1);
|
|
339
458
|
if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
|
|
340
|
-
throw new
|
|
459
|
+
throw new ValidationError(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
|
|
341
460
|
}
|
|
342
|
-
return
|
|
461
|
+
return literal;
|
|
462
|
+
}
|
|
463
|
+
throw new ValidationError(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal (a quote inside it written ` +
|
|
464
|
+
`twice, as in 'it''s'), or NULL.`);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The string literal a default is, with any trailing PostgreSQL cast removed,
|
|
468
|
+
* or null when it is not one. Both patterns are linear: `[^']` and `''` cannot
|
|
469
|
+
* match the same text, and a cast's words are separated by single spaces.
|
|
470
|
+
*/
|
|
471
|
+
function defaultStringLiteral(val) {
|
|
472
|
+
const cast = /^('(?:[^']|'')*')::[a-z_][a-z0-9_]*(?: [a-z_][a-z0-9_]*)*(?:\(\d+(?:, ?\d+)?\))?(?:\[\])?$/i.exec(val);
|
|
473
|
+
if (cast)
|
|
474
|
+
return cast[1];
|
|
475
|
+
return /^'(?:[^']|'')*'$/.test(val) ? val : null;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* MySQL column types that accept no literal default: a default has to be written
|
|
479
|
+
* as an expression, in parentheses (8.0.13+), even when its value is a constant.
|
|
480
|
+
*/
|
|
481
|
+
const MYSQL_EXPRESSION_DEFAULT_TYPES = /^(TINY|MEDIUM|LONG)?(TEXT|BLOB)\b|^JSON\b/i;
|
|
482
|
+
/**
|
|
483
|
+
* A MySQL expression default for a string literal, as its UTF-8 bytes in hex:
|
|
484
|
+
* `(X'...')` on a BLOB column, `(CONVERT(X'...' USING utf8mb4))` on a TEXT or
|
|
485
|
+
* JSON one. `normalized` is a single-quoted literal with its quotes doubled.
|
|
486
|
+
*/
|
|
487
|
+
function mysqlHexDefault(normalized, columnType) {
|
|
488
|
+
const value = normalized.slice(1, -1).replace(/''/g, "'");
|
|
489
|
+
let hex = '';
|
|
490
|
+
for (const byte of new TextEncoder().encode(value))
|
|
491
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
492
|
+
return /BLOB/i.test(columnType) ? `(X'${hex}')` : `(CONVERT(X'${hex}' USING utf8mb4))`;
|
|
493
|
+
}
|
|
494
|
+
/** What a clock default means on a MySQL date or time column. */
|
|
495
|
+
function mysqlClockDefault(key, ddlType) {
|
|
496
|
+
const isClock = key === 'NOW()' || key === 'CURRENT_TIMESTAMP' || key === 'CURRENT_DATE' || key === 'CURRENT_TIME';
|
|
497
|
+
if (!isClock)
|
|
498
|
+
return null;
|
|
499
|
+
const datetime = /^(?:DATETIME|TIMESTAMP)(?:\((\d)\))?$/i.exec(ddlType);
|
|
500
|
+
if (datetime) {
|
|
501
|
+
// MySQL 8 rejects a default whose fractional precision differs from the
|
|
502
|
+
// column's, and a bare CURRENT_DATE / CURRENT_TIME is not a valid default
|
|
503
|
+
// for a DATETIME (only the parenthesized expression is).
|
|
504
|
+
if (key === 'NOW()' || key === 'CURRENT_TIMESTAMP') {
|
|
505
|
+
return datetime[1] ? `CURRENT_TIMESTAMP(${datetime[1]})` : 'CURRENT_TIMESTAMP';
|
|
506
|
+
}
|
|
507
|
+
return `(${key})`;
|
|
508
|
+
}
|
|
509
|
+
// DATE and TIME columns accept neither NOW() nor a bare CURRENT_*, so the
|
|
510
|
+
// default is the matching clock function as an expression.
|
|
511
|
+
if (/^DATE$/i.test(ddlType))
|
|
512
|
+
return '(CURRENT_DATE)';
|
|
513
|
+
if (/^TIME(?:\(\d\))?$/i.test(ddlType))
|
|
514
|
+
return '(CURRENT_TIME)';
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The spelling of an already-normalized default for `dialect`. The schema
|
|
519
|
+
* vocabulary (`now()`, `true`, `gen_random_uuid()`) is Postgres's, and some of
|
|
520
|
+
* it is not SQL on the other engines: SQLite has no `NOW()` default, SQLite and
|
|
521
|
+
* MySQL have no `GEN_RANDOM_UUID()`, and SQL Server has none of `NOW()`, boolean
|
|
522
|
+
* literals or `CURRENT_DATE`. A literal (number, string, NULL) is the same
|
|
523
|
+
* everywhere and passes through, except on a MySQL TEXT, BLOB or JSON column,
|
|
524
|
+
* where it has to be an expression. `columnType` is the column's MySQL type as
|
|
525
|
+
* the dialect spells it (`DATETIME(3)`, `TEXT`); other dialects ignore it.
|
|
526
|
+
*/
|
|
527
|
+
function dialectDefault(normalized, dialect, columnType = '') {
|
|
528
|
+
const key = normalized.toUpperCase();
|
|
529
|
+
switch (dialect.name) {
|
|
530
|
+
case 'sqlite':
|
|
531
|
+
if (key === 'NOW()')
|
|
532
|
+
return 'CURRENT_TIMESTAMP';
|
|
533
|
+
if (key === 'GEN_RANDOM_UUID()') {
|
|
534
|
+
return "(lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)), 2) || '-' || hex(randomblob(6))))";
|
|
535
|
+
}
|
|
536
|
+
return normalized;
|
|
537
|
+
case 'mysql': {
|
|
538
|
+
if (key === 'GEN_RANDOM_UUID()')
|
|
539
|
+
return '(UUID())';
|
|
540
|
+
const clock = mysqlClockDefault(key, columnType);
|
|
541
|
+
if (clock)
|
|
542
|
+
return clock;
|
|
543
|
+
// `DEFAULT NULL` is the one default these types accept as a literal.
|
|
544
|
+
const expression = key !== 'NULL' && MYSQL_EXPRESSION_DEFAULT_TYPES.test(columnType);
|
|
545
|
+
if (normalized.startsWith("'") && /['\\]/.test(normalized.slice(1, -1))) {
|
|
546
|
+
// A quote or a backslash inside the literal. Whether `\` escapes depends
|
|
547
|
+
// on the session's NO_BACKSLASH_ESCAPES, which Turbine's own connections
|
|
548
|
+
// set and other clients do not. An EXPRESSION default is worse: MySQL
|
|
549
|
+
// stores it re-escaped with backslashes and parses it again on every
|
|
550
|
+
// insert under the inserting session's mode, so `('it''s')` created
|
|
551
|
+
// fine and then failed every insert on a Turbine connection (measured
|
|
552
|
+
// on MySQL 8). Spelled as hex bytes, the value has no quote or
|
|
553
|
+
// backslash to read either way.
|
|
554
|
+
if (expression)
|
|
555
|
+
return mysqlHexDefault(normalized, columnType);
|
|
556
|
+
if (normalized.includes('\\')) {
|
|
557
|
+
throw new ValidationError(`The default ${normalized} holds a backslash, which MySQL reads differently with and without ` +
|
|
558
|
+
'NO_BACKSLASH_ESCAPES. Use a default without one.');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (expression)
|
|
562
|
+
return `(${normalized})`;
|
|
563
|
+
return normalized;
|
|
564
|
+
}
|
|
565
|
+
case 'mssql':
|
|
566
|
+
if (key === 'NOW()')
|
|
567
|
+
return 'CURRENT_TIMESTAMP';
|
|
568
|
+
if (key === 'GEN_RANDOM_UUID()')
|
|
569
|
+
return 'NEWID()';
|
|
570
|
+
if (key === 'TRUE')
|
|
571
|
+
return '1';
|
|
572
|
+
if (key === 'FALSE')
|
|
573
|
+
return '0';
|
|
574
|
+
if (key === 'CURRENT_DATE')
|
|
575
|
+
return '(CAST(SYSDATETIME() AS DATE))';
|
|
576
|
+
return normalized;
|
|
577
|
+
default:
|
|
578
|
+
return normalized;
|
|
343
579
|
}
|
|
344
|
-
throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
|
|
345
580
|
}
|
|
346
581
|
/**
|
|
347
582
|
* Generate CREATE INDEX statements for foreign key columns.
|
|
@@ -496,6 +731,16 @@ function generateDeclaredIndexes(table, dialect = postgresDialect) {
|
|
|
496
731
|
}
|
|
497
732
|
return out;
|
|
498
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* The warnings in a diff that report a real difference between the database and
|
|
736
|
+
* the schema file: everything in `warnings` except the entries the diff marked
|
|
737
|
+
* as possibly cosmetic ({@link DiffResult.inexactWarnings}). `push --dry-run
|
|
738
|
+
* --exit-code` fails on these.
|
|
739
|
+
*/
|
|
740
|
+
export function driftWarnings(diff) {
|
|
741
|
+
const inexact = new Set(diff.inexactWarnings ?? []);
|
|
742
|
+
return (diff.warnings ?? []).filter((w) => !inexact.has(w));
|
|
743
|
+
}
|
|
499
744
|
/**
|
|
500
745
|
* Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
|
|
501
746
|
* referential actions. Default (`no action`) clauses are omitted, matching how
|
|
@@ -526,18 +771,49 @@ export function diffReferentialAction(table, db, schemaOnDelete, schemaOnUpdate,
|
|
|
526
771
|
return { statements: [drop, add], reverseStatements: [drop, reverseAdd] };
|
|
527
772
|
}
|
|
528
773
|
/**
|
|
529
|
-
* Compute
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
774
|
+
* Compute the enum changes needed to make the database's labels match the
|
|
775
|
+
* schema's, KEEPING THE DECLARED ORDER.
|
|
776
|
+
*
|
|
777
|
+
* An enum's order is observable: it decides `ORDER BY`, `<` and `MIN`/`MAX` on
|
|
778
|
+
* the column. A new label therefore goes where the schema declares it, not
|
|
779
|
+
* wherever the end of the type happens to be: `ADD VALUE 'x' AFTER 'prev'`, or
|
|
780
|
+
* `BEFORE 'next'` when it leads. A label that belongs at the end emits the bare
|
|
781
|
+
* `ADD VALUE`, which is what every earlier version emitted for it.
|
|
782
|
+
*
|
|
783
|
+
* Postgres cannot remove or reorder values in place, so a database label the
|
|
784
|
+
* schema dropped, and a declared order the database does not already have, are
|
|
785
|
+
* warnings (never statements): the type has to be recreated by hand.
|
|
533
786
|
*/
|
|
534
787
|
export function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = postgresDialect) {
|
|
535
788
|
const statements = [];
|
|
536
789
|
const warnings = [];
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
790
|
+
const type = dialect.quoteIdentifier(enumName);
|
|
791
|
+
// The type's label order as the statements below will leave it.
|
|
792
|
+
const order = [...dbLabels];
|
|
793
|
+
const schemaIndex = schemaLabels.map((label, i) => ({ label, i }));
|
|
794
|
+
for (const { label, i } of schemaIndex) {
|
|
795
|
+
if (order.includes(label))
|
|
796
|
+
continue;
|
|
797
|
+
const previous = i > 0 ? schemaLabels[i - 1] : undefined;
|
|
798
|
+
const next = schemaLabels.slice(i + 1).find((l) => order.includes(l));
|
|
799
|
+
const prevAt = previous === undefined ? -1 : order.indexOf(previous);
|
|
800
|
+
if (previous !== undefined && prevAt !== -1) {
|
|
801
|
+
if (prevAt === order.length - 1) {
|
|
802
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
|
|
803
|
+
order.push(label);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} AFTER ${quoteEnumLabel(previous)};`);
|
|
807
|
+
order.splice(prevAt + 1, 0, label);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else if (next !== undefined) {
|
|
811
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} BEFORE ${quoteEnumLabel(next)};`);
|
|
812
|
+
order.splice(order.indexOf(next), 0, label);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
|
|
816
|
+
order.push(label);
|
|
541
817
|
}
|
|
542
818
|
}
|
|
543
819
|
const schemaSet = new Set(schemaLabels);
|
|
@@ -546,6 +822,14 @@ export function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = postg
|
|
|
546
822
|
warnings.push(`Enum "${enumName}": labels [${removed.join(', ')}] exist in the database but not the schema. ` +
|
|
547
823
|
`Postgres cannot remove enum values in place, recreate the type manually if intended.`);
|
|
548
824
|
}
|
|
825
|
+
// After the adds, the labels both sides share must already be in declared
|
|
826
|
+
// order; if not, no ADD VALUE can fix it.
|
|
827
|
+
const shared = order.filter((l) => schemaSet.has(l));
|
|
828
|
+
if (shared.some((label, i) => label !== schemaLabels[i])) {
|
|
829
|
+
warnings.push(`Enum "${enumName}": the schema declares the order [${schemaLabels.join(', ')}] but the database has ` +
|
|
830
|
+
`[${shared.join(', ')}]. Postgres cannot reorder enum values in place, recreate the type manually if the ` +
|
|
831
|
+
`order matters (it decides ORDER BY and comparisons on the column).`);
|
|
832
|
+
}
|
|
549
833
|
return { statements, warnings };
|
|
550
834
|
}
|
|
551
835
|
/**
|
|
@@ -737,7 +1021,17 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
737
1021
|
const schemaDdlNames = new Set();
|
|
738
1022
|
for (const def of Object.values(schema.tables))
|
|
739
1023
|
schemaDdlNames.add(def.name);
|
|
740
|
-
const result = {
|
|
1024
|
+
const result = {
|
|
1025
|
+
create: [],
|
|
1026
|
+
alter: [],
|
|
1027
|
+
drop: [],
|
|
1028
|
+
statements: [],
|
|
1029
|
+
reverseStatements: [],
|
|
1030
|
+
warnings: [],
|
|
1031
|
+
inexactWarnings: [],
|
|
1032
|
+
widenings: [],
|
|
1033
|
+
notCompared: [],
|
|
1034
|
+
};
|
|
741
1035
|
const resolveRef = makeRefResolver(schema);
|
|
742
1036
|
// --- Enums: CREATE TYPE for new enums (before tables), ADD VALUE for grown
|
|
743
1037
|
// ones, and a warning for any destructive removal/reorder. New CREATE
|
|
@@ -771,9 +1065,10 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
771
1065
|
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
772
1066
|
}
|
|
773
1067
|
}
|
|
774
|
-
// Tables to drop (in DB but not in schema)
|
|
1068
|
+
// Tables to drop (in DB but not in schema). Turbine's own bookkeeping tables
|
|
1069
|
+
// (`_turbine_migrations`, ...) are never the user's to drop.
|
|
775
1070
|
for (const existingTable of existingTables) {
|
|
776
|
-
if (!schemaDdlNames.has(existingTable)) {
|
|
1071
|
+
if (!schemaDdlNames.has(existingTable) && !existingTable.startsWith('_turbine_')) {
|
|
777
1072
|
result.drop.push(existingTable);
|
|
778
1073
|
// We don't auto-generate DROP statements for safety
|
|
779
1074
|
}
|
|
@@ -787,6 +1082,7 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
787
1082
|
const dbCols = dbColumns[tableName] ?? {};
|
|
788
1083
|
const tableUniques = dbUniques[tableName] ?? {};
|
|
789
1084
|
const alterDef = { table: tableName, columns: [] };
|
|
1085
|
+
const sequenceDefaults = [];
|
|
790
1086
|
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
791
1087
|
const snakeName = camelToSnake(fieldName);
|
|
792
1088
|
const dbCol = dbCols[snakeName];
|
|
@@ -798,6 +1094,13 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
798
1094
|
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
799
1095
|
result.statements.push(sql);
|
|
800
1096
|
result.reverseStatements.unshift(reverseSql);
|
|
1097
|
+
// NOT NULL with nothing to fill existing rows with: Postgres runs the
|
|
1098
|
+
// statement fine on an empty table and rejects it on any other.
|
|
1099
|
+
if ((config.isNotNull || config.isPrimaryKey) && !isSerialType(config.type) && config.defaultValue == null) {
|
|
1100
|
+
result.warnings.push(`column "${tableName}"."${snakeName}" is added NOT NULL with no default: the ALTER fails on a table ` +
|
|
1101
|
+
`that already has rows. Give it a default, add it nullable first and backfill it, or scaffold the ` +
|
|
1102
|
+
`two-phase pattern with \`npx turbine migrate create <name> --recipe backfill\`.`);
|
|
1103
|
+
}
|
|
801
1104
|
continue;
|
|
802
1105
|
}
|
|
803
1106
|
// Check type mismatch. Serial/bigserial columns are exempt: their
|
|
@@ -841,6 +1144,9 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
841
1144
|
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
842
1145
|
result.statements.push(sql);
|
|
843
1146
|
result.reverseStatements.unshift(reverseSql);
|
|
1147
|
+
if (!typePlan.needsUsing && !typePlan.loss && isLosslessWidening(oldSqlType, sqlType)) {
|
|
1148
|
+
result.widenings.push({ statement: sql, from: oldSqlType, to: sqlType });
|
|
1149
|
+
}
|
|
844
1150
|
// The destructive gate sees the statement and refuses by default; its
|
|
845
1151
|
// label is necessarily generic ("cast may truncate or fail"). This
|
|
846
1152
|
// warning names what THIS conversion does to THESE rows.
|
|
@@ -870,6 +1176,8 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
870
1176
|
if (!isSerial) {
|
|
871
1177
|
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
872
1178
|
const dbDefault = dbCol.columnDefault;
|
|
1179
|
+
if (dbDefault && isSequenceDefault(dbDefault))
|
|
1180
|
+
sequenceDefaults.push(snakeName);
|
|
873
1181
|
if (schemaDefault && !dbDefault) {
|
|
874
1182
|
// Schema has default, DB doesn't
|
|
875
1183
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
@@ -927,18 +1235,23 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
927
1235
|
// gate, not by hiding the statement: withholding it here previously made
|
|
928
1236
|
// `push` report "already in sync" for a destructive-only diff and print a
|
|
929
1237
|
// false "Altered" for a mixed one, since the 0.36 guard never received it.
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
1238
|
+
// A dropped column and its data cannot be recreated automatically, so the
|
|
1239
|
+
// reverse is irreversibleDropPlaceholder: an IRREVERSIBLE note in the DOWN
|
|
1240
|
+
// section describing the column, never SQL that pretends to restore it.
|
|
933
1241
|
for (const dbColName of Object.keys(dbCols)) {
|
|
934
1242
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => camelToSnake(fieldName) === dbColName);
|
|
935
1243
|
if (!hasField) {
|
|
936
1244
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
937
|
-
const reverseSql =
|
|
1245
|
+
const reverseSql = irreversibleDropPlaceholder(tableName, dbColName, dbCols[dbColName], dialect);
|
|
938
1246
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
939
1247
|
result.statements.push(sql);
|
|
1248
|
+
result.reverseStatements.unshift(reverseSql);
|
|
940
1249
|
}
|
|
941
1250
|
}
|
|
1251
|
+
if (sequenceDefaults.length > 0) {
|
|
1252
|
+
result.notCompared.push(`sequence-backed defaults on "${tableName}" (${sequenceDefaults.join(', ')}): the database default is a ` +
|
|
1253
|
+
`sequence, which is never compared or changed`);
|
|
1254
|
+
}
|
|
942
1255
|
if (alterDef.columns.length > 0) {
|
|
943
1256
|
result.alter.push(alterDef);
|
|
944
1257
|
}
|
|
@@ -975,6 +1288,17 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
975
1288
|
.map((c) => ({ name: c.name, expression: c.expression }));
|
|
976
1289
|
const tableDbChecks = dbChecks[tableName] ?? [];
|
|
977
1290
|
const dbCheckByName = new Map(tableDbChecks.map((c) => [c.name, c]));
|
|
1291
|
+
const namedCheckNames = new Set(namedSchemaChecks.map((c) => c.name));
|
|
1292
|
+
const unmatchedChecks = tableDbChecks.filter((c) => !namedCheckNames.has(c.name)).map((c) => c.name);
|
|
1293
|
+
if (unmatchedChecks.length > 0) {
|
|
1294
|
+
result.notCompared.push(`CHECK constraints on "${tableName}" (${unmatchedChecks.join(', ')}): the schema does not name them, so ` +
|
|
1295
|
+
`they are not compared and never dropped`);
|
|
1296
|
+
}
|
|
1297
|
+
const unnamedSchemaChecks = (tableDef.checks ?? []).filter((c) => !c.name).length;
|
|
1298
|
+
if (unnamedSchemaChecks > 0) {
|
|
1299
|
+
result.notCompared.push(`${unnamedSchemaChecks} unnamed CHECK constraint(s) declared on "${tableName}": with no name there is nothing ` +
|
|
1300
|
+
`to match them against the database`);
|
|
1301
|
+
}
|
|
978
1302
|
const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
|
|
979
1303
|
for (const sc of namedSchemaChecks) {
|
|
980
1304
|
const existing = dbCheckByName.get(sc.name);
|
|
@@ -987,9 +1311,11 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
987
1311
|
result.reverseStatements.unshift(dropSql);
|
|
988
1312
|
}
|
|
989
1313
|
else if (normExpr(existing.expression) !== normExpr(sc.expression)) {
|
|
990
|
-
|
|
1314
|
+
const checkWarning = `check constraint "${sc.name}" on "${tableName}": stored expression differs from the schema text ` +
|
|
991
1315
|
`(Postgres canonicalizes CHECK expressions, so this is usually cosmetic). ` +
|
|
992
|
-
`To intentionally replace it, rename the constraint or drop/re-add it in a manual migration
|
|
1316
|
+
`To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`;
|
|
1317
|
+
result.warnings.push(checkWarning);
|
|
1318
|
+
result.inexactWarnings.push(checkWarning);
|
|
993
1319
|
}
|
|
994
1320
|
}
|
|
995
1321
|
// --- User-declared indexes (TableDef.indexes) ---
|
|
@@ -1006,6 +1332,18 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
1006
1332
|
// worth surfacing. Deleting the last declared index must NOT silence the
|
|
1007
1333
|
// warning pass. Tables with no `indexes` key stay silent (not managed).
|
|
1008
1334
|
const declaredPlain = (tableDef.indexes ?? []).filter((i) => !isDocFieldIndexDef(i));
|
|
1335
|
+
if (tableDef.indexes === undefined) {
|
|
1336
|
+
const dbIdxNames = [...(dbIndexes[tableName]?.keys() ?? [])];
|
|
1337
|
+
const recognized = new Set(Object.values(dbUniques[tableName] ?? {}));
|
|
1338
|
+
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
1339
|
+
if (config.referencesTarget)
|
|
1340
|
+
recognized.add(`idx_${tableName}_${camelToSnake(fieldName)}`);
|
|
1341
|
+
}
|
|
1342
|
+
const unmanaged = dbIdxNames.filter((n) => !recognized.has(n) && !n.endsWith('_pkey'));
|
|
1343
|
+
if (unmanaged.length > 0) {
|
|
1344
|
+
result.notCompared.push(`indexes on "${tableName}" (${unmanaged.join(', ')}): the table declares no \`indexes\`, so they are not compared`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1009
1347
|
if (tableDef.indexes !== undefined) {
|
|
1010
1348
|
const dbIdx = dbIndexes[tableName] ?? new Map();
|
|
1011
1349
|
const dbIdxNames = new Set(dbIdx.keys());
|
|
@@ -1054,12 +1392,34 @@ export async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
1054
1392
|
}
|
|
1055
1393
|
}
|
|
1056
1394
|
}
|
|
1395
|
+
result.notCompared.push(NEVER_COMPARED);
|
|
1057
1396
|
return result;
|
|
1058
1397
|
}
|
|
1059
1398
|
finally {
|
|
1060
1399
|
await client.end();
|
|
1061
1400
|
}
|
|
1062
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* What {@link schemaDiff} never looks at, whatever the database holds. Kept
|
|
1404
|
+
* beside the diff so the sentence cannot outlive the code it describes.
|
|
1405
|
+
*/
|
|
1406
|
+
export const NEVER_COMPARED = 'primary keys, multi-column UNIQUE constraints, and whether a foreign key exists on an existing column ' +
|
|
1407
|
+
'(only the referential actions of foreign keys that already exist are compared)';
|
|
1408
|
+
/**
|
|
1409
|
+
* The DOWN-side stand-in for a `DROP COLUMN`: a comment block, because the data
|
|
1410
|
+
* the column held cannot be put back by any statement. It names the column and
|
|
1411
|
+
* its old type so the person writing the real rollback has the facts, and it is
|
|
1412
|
+
* a comment rather than SQL so a rollback never invents a column of NULLs and
|
|
1413
|
+
* calls it restored.
|
|
1414
|
+
*/
|
|
1415
|
+
function irreversibleDropPlaceholder(table, column, dbCol, dialect = postgresDialect) {
|
|
1416
|
+
const q = (name) => dialect.quoteIdentifier(name);
|
|
1417
|
+
return [
|
|
1418
|
+
`-- IRREVERSIBLE: this migration drops ${q(table)}.${q(column)}, and the data in it cannot be restored automatically.`,
|
|
1419
|
+
'-- To roll back, restore the data from a backup and re-add the column by hand, for example:',
|
|
1420
|
+
`-- ALTER TABLE ${q(table)} ADD COLUMN ${q(column)} ${udtToSqlType(dbCol)};`,
|
|
1421
|
+
].join('\n');
|
|
1422
|
+
}
|
|
1063
1423
|
/**
|
|
1064
1424
|
* Rough type families. Within a family Postgres has an ASSIGNMENT cast between
|
|
1065
1425
|
* every pair the schema builder can produce, which is what lets the diff emit a
|
|
@@ -1348,6 +1708,89 @@ export function planTypeChange(config, dbCol, column) {
|
|
|
1348
1708
|
}
|
|
1349
1709
|
return { kind: 'none' };
|
|
1350
1710
|
}
|
|
1711
|
+
const WIDENING_BASES = {
|
|
1712
|
+
VARCHAR: 'varchar',
|
|
1713
|
+
'CHARACTER VARYING': 'varchar',
|
|
1714
|
+
TEXT: 'text',
|
|
1715
|
+
SMALLINT: 'int2',
|
|
1716
|
+
INT2: 'int2',
|
|
1717
|
+
INTEGER: 'int4',
|
|
1718
|
+
INT: 'int4',
|
|
1719
|
+
INT4: 'int4',
|
|
1720
|
+
BIGINT: 'int8',
|
|
1721
|
+
INT8: 'int8',
|
|
1722
|
+
NUMERIC: 'numeric',
|
|
1723
|
+
DECIMAL: 'numeric',
|
|
1724
|
+
};
|
|
1725
|
+
/** Digits an integer type can hold, for the integer-to-numeric(p, s) rule. */
|
|
1726
|
+
const INTEGER_DIGITS = { int2: 5, int4: 10, int8: 19 };
|
|
1727
|
+
/** Parse a SQL type string, or null for any type outside the widening vocabulary. */
|
|
1728
|
+
function parseSqlType(raw) {
|
|
1729
|
+
const m = /^([A-Za-z][A-Za-z0-9 ]*?)\s*(?:\(\s*(\d+)\s*(?:,\s*(\d+)\s*)?\))?\s*(\[\])?$/.exec(raw.trim().replace(/\s+/g, ' '));
|
|
1730
|
+
if (!m)
|
|
1731
|
+
return null;
|
|
1732
|
+
const base = WIDENING_BASES[m[1].toUpperCase()];
|
|
1733
|
+
if (!base)
|
|
1734
|
+
return null;
|
|
1735
|
+
const a = m[2] === undefined ? null : Number(m[2]);
|
|
1736
|
+
const b = m[3] === undefined ? (a === null ? null : 0) : Number(m[3]);
|
|
1737
|
+
if ((base === 'text' || base.startsWith('int')) && a !== null)
|
|
1738
|
+
return null;
|
|
1739
|
+
if (base === 'varchar' && m[3] !== undefined)
|
|
1740
|
+
return null;
|
|
1741
|
+
return { base, a, b: base === 'numeric' ? b : null, array: m[4] !== undefined };
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* True when changing a column from `from` to `to` can never lose, round,
|
|
1745
|
+
* truncate or reject a value: every value of the old type is representable
|
|
1746
|
+
* unchanged in the new one. The vocabulary is deliberately small, and anything
|
|
1747
|
+
* outside it answers false (the destructive gate then treats the statement as
|
|
1748
|
+
* it always has):
|
|
1749
|
+
*
|
|
1750
|
+
* - `varchar(n)` to `varchar(m)` with m >= n, to bare `varchar`, to `text`
|
|
1751
|
+
* - `smallint` to `integer` to `bigint`
|
|
1752
|
+
* - any integer type to `numeric`, or to `numeric(p, s)` when it still holds
|
|
1753
|
+
* the integer's digits
|
|
1754
|
+
* - `numeric(p, s)` to a numeric with at least as many integer digits and at
|
|
1755
|
+
* least as many decimal places (a bare `numeric` has no bound)
|
|
1756
|
+
* - arrays of the above, element by element
|
|
1757
|
+
*
|
|
1758
|
+
* Not a widening, on purpose: `text` to `varchar(n)`, anything to a smaller
|
|
1759
|
+
* type, `timestamp` to `timestamptz` (re-reads the value in the session zone),
|
|
1760
|
+
* `real` to `double precision` (the stored digits change when printed), and
|
|
1761
|
+
* `json` to `jsonb` (drops duplicate keys).
|
|
1762
|
+
*/
|
|
1763
|
+
export function isLosslessWidening(from, to) {
|
|
1764
|
+
const f = parseSqlType(from);
|
|
1765
|
+
const t = parseSqlType(to);
|
|
1766
|
+
if (!f || !t || f.array !== t.array)
|
|
1767
|
+
return false;
|
|
1768
|
+
if (f.base === 'varchar') {
|
|
1769
|
+
if (t.base === 'text')
|
|
1770
|
+
return true;
|
|
1771
|
+
if (t.base !== 'varchar')
|
|
1772
|
+
return false;
|
|
1773
|
+
return t.a === null || (f.a !== null && t.a >= f.a);
|
|
1774
|
+
}
|
|
1775
|
+
if (f.base === 'text')
|
|
1776
|
+
return t.base === 'text';
|
|
1777
|
+
if (f.base === 'numeric') {
|
|
1778
|
+
if (t.base !== 'numeric')
|
|
1779
|
+
return false;
|
|
1780
|
+
if (t.a === null)
|
|
1781
|
+
return true;
|
|
1782
|
+
if (f.a === null)
|
|
1783
|
+
return false;
|
|
1784
|
+
return t.b >= f.b && t.a - t.b >= f.a - f.b;
|
|
1785
|
+
}
|
|
1786
|
+
// Integer source.
|
|
1787
|
+
const digits = INTEGER_DIGITS[f.base];
|
|
1788
|
+
if (t.base === 'numeric')
|
|
1789
|
+
return t.a === null || t.a - t.b >= digits;
|
|
1790
|
+
if (!t.base.startsWith('int'))
|
|
1791
|
+
return false;
|
|
1792
|
+
return INTEGER_DIGITS[t.base] >= digits;
|
|
1793
|
+
}
|
|
1351
1794
|
/**
|
|
1352
1795
|
* Map a schema column type to its expected PostgreSQL UDT name.
|
|
1353
1796
|
*/
|
|
@@ -1471,10 +1914,17 @@ function defaultsMatch(schemaDefault, dbDefault) {
|
|
|
1471
1914
|
* `ALTER COLUMN ... TYPE` cast, exactly the kind of silent data loss `push`
|
|
1472
1915
|
* must never apply without an explicit opt-in.
|
|
1473
1916
|
*/
|
|
1474
|
-
export function findDestructivePushStatements(statements) {
|
|
1917
|
+
export function findDestructivePushStatements(statements, widenings = []) {
|
|
1918
|
+
// A lossless widening is the one `ALTER COLUMN ... TYPE` that destroys nothing.
|
|
1919
|
+
// It is skipped by statement identity, so a hand-edited or look-alike
|
|
1920
|
+
// statement is still scanned.
|
|
1921
|
+
const lossless = new Set(widenings.map((w) => w.statement));
|
|
1475
1922
|
const hits = [];
|
|
1476
|
-
for (const stmt of statements)
|
|
1923
|
+
for (const stmt of statements) {
|
|
1924
|
+
if (lossless.has(stmt))
|
|
1925
|
+
continue;
|
|
1477
1926
|
hits.push(...scanDestructiveSql(stmt));
|
|
1927
|
+
}
|
|
1478
1928
|
return hits;
|
|
1479
1929
|
}
|
|
1480
1930
|
/**
|
|
@@ -1613,7 +2063,7 @@ export async function schemaPush(schema, connectionString, options = {}) {
|
|
|
1613
2063
|
}
|
|
1614
2064
|
// Destructive-statement gate: refuse silent data loss unless opted in.
|
|
1615
2065
|
if (!options.allowDestructive) {
|
|
1616
|
-
const destructive = findDestructivePushStatements(diff.statements);
|
|
2066
|
+
const destructive = findDestructivePushStatements(diff.statements, diff.widenings);
|
|
1617
2067
|
if (destructive.length > 0) {
|
|
1618
2068
|
throw new DestructivePushRefusal(destructive);
|
|
1619
2069
|
}
|