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/cjs/schema-sql.js
CHANGED
|
@@ -9,17 +9,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.DestructivePushRefusal = void 0;
|
|
12
|
+
exports.DestructivePushRefusal = exports.NEVER_COMPARED = void 0;
|
|
13
13
|
exports.referentialActionToSql = referentialActionToSql;
|
|
14
14
|
exports.schemaToSQL = schemaToSQL;
|
|
15
15
|
exports.describeIndexDefMismatch = describeIndexDefMismatch;
|
|
16
16
|
exports.undeclaredIndexWarnings = undeclaredIndexWarnings;
|
|
17
|
+
exports.driftWarnings = driftWarnings;
|
|
17
18
|
exports.buildAddForeignKeyStatement = buildAddForeignKeyStatement;
|
|
18
19
|
exports.diffReferentialAction = diffReferentialAction;
|
|
19
20
|
exports.diffEnumValues = diffEnumValues;
|
|
20
21
|
exports.diffCheckConstraints = diffCheckConstraints;
|
|
21
22
|
exports.schemaDiff = schemaDiff;
|
|
22
23
|
exports.planTypeChange = planTypeChange;
|
|
24
|
+
exports.isLosslessWidening = isLosslessWidening;
|
|
23
25
|
exports.findDestructivePushStatements = findDestructivePushStatements;
|
|
24
26
|
exports.schemaPush = schemaPush;
|
|
25
27
|
exports.schemaToSQLString = schemaToSQLString;
|
|
@@ -127,13 +129,18 @@ function schemaToSQL(schema, options) {
|
|
|
127
129
|
: 'CREATE EXTENSION IF NOT EXISTS vector;');
|
|
128
130
|
}
|
|
129
131
|
// CREATE TYPE for every schema-level enum, before the tables that use them.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
// Only where enums are named types (PostgreSQL): every other engine declares
|
|
133
|
+
// the labels on the column itself (see `enumColumnType`), and has no CREATE
|
|
134
|
+
// TYPE to run.
|
|
135
|
+
if (dialect.capabilities.namedEnumTypes) {
|
|
136
|
+
for (const [enumName, labels] of Object.entries(schema.enums ?? {})) {
|
|
137
|
+
statements.push(generateCreateEnumType(enumName, labels, dialect));
|
|
138
|
+
}
|
|
132
139
|
}
|
|
133
140
|
// Generate CREATE TABLE statements
|
|
134
141
|
for (const tableName of sorted) {
|
|
135
142
|
const table = schema.tables[tableName];
|
|
136
|
-
statements.push(generateCreateTable(table, resolveRef, dialect));
|
|
143
|
+
statements.push(generateCreateTable(table, resolveRef, dialect, schema.enums));
|
|
137
144
|
}
|
|
138
145
|
// Generate CREATE INDEX for foreign key columns
|
|
139
146
|
for (const tableName of sorted) {
|
|
@@ -239,13 +246,22 @@ function topologicalSort(schema) {
|
|
|
239
246
|
* to their snake_case DDL form, so users can write either camelCase JS
|
|
240
247
|
* accessor names or snake_case DDL names.
|
|
241
248
|
*/
|
|
242
|
-
function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresDialect) {
|
|
249
|
+
function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresDialect, enums) {
|
|
243
250
|
const tableName = table.name;
|
|
244
251
|
const columnDefs = [];
|
|
245
252
|
const compositePk = table.primaryKey && table.primaryKey.length > 0 ? table.primaryKey : null;
|
|
253
|
+
const keyed = keyedColumnNames(table);
|
|
254
|
+
const inline = dialect.capabilities.inlineForeignKeys;
|
|
255
|
+
const foreignKeys = [];
|
|
246
256
|
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
247
|
-
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect));
|
|
257
|
+
columnDefs.push(generateColumnDef(fieldName, config, resolveRef, dialect, keyed.has((0, schema_js_1.camelToSnake)(fieldName)), inline, enums));
|
|
258
|
+
if (!inline) {
|
|
259
|
+
const fk = generateForeignKeyConstraint(fieldName, config, resolveRef, dialect);
|
|
260
|
+
if (fk)
|
|
261
|
+
foreignKeys.push(fk);
|
|
262
|
+
}
|
|
248
263
|
}
|
|
264
|
+
columnDefs.push(...foreignKeys);
|
|
249
265
|
// Append a table-level PRIMARY KEY constraint when a composite PK is set.
|
|
250
266
|
if (compositePk) {
|
|
251
267
|
const cols = compositePk.map((c) => dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(c)));
|
|
@@ -265,11 +281,80 @@ function generateCreateTable(table, resolveRef, dialect = dialect_js_1.postgresD
|
|
|
265
281
|
definitions: columnDefs,
|
|
266
282
|
});
|
|
267
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The snake_case names of the columns of `table` that sit in a key: a primary
|
|
286
|
+
* key (single or composite), a UNIQUE column, a foreign key (Turbine indexes
|
|
287
|
+
* those), or a member of a declared index.
|
|
288
|
+
*/
|
|
289
|
+
function keyedColumnNames(table) {
|
|
290
|
+
const keyed = new Set();
|
|
291
|
+
for (const [fieldName, config] of Object.entries(table.columns)) {
|
|
292
|
+
if (config.isPrimaryKey || config.isUnique || config.referencesTarget)
|
|
293
|
+
keyed.add((0, schema_js_1.camelToSnake)(fieldName));
|
|
294
|
+
}
|
|
295
|
+
for (const pk of table.primaryKey ?? [])
|
|
296
|
+
keyed.add((0, schema_js_1.camelToSnake)(pk));
|
|
297
|
+
for (const idx of table.indexes ?? []) {
|
|
298
|
+
if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx))
|
|
299
|
+
continue;
|
|
300
|
+
for (const col of idx.columns)
|
|
301
|
+
keyed.add((0, schema_js_1.camelToSnake)(col));
|
|
302
|
+
}
|
|
303
|
+
return keyed;
|
|
304
|
+
}
|
|
305
|
+
/** A table-level `FOREIGN KEY` clause for a column's `references`, or null. */
|
|
306
|
+
function generateForeignKeyConstraint(fieldName, config, resolveRef, dialect) {
|
|
307
|
+
if (!config.referencesTarget)
|
|
308
|
+
return null;
|
|
309
|
+
const refParts = config.referencesTarget.split('.');
|
|
310
|
+
if (refParts.length !== 2)
|
|
311
|
+
return null;
|
|
312
|
+
const refTable = resolveRef ? resolveRef(refParts[0]) : refParts[0];
|
|
313
|
+
let clause = `FOREIGN KEY (${dialect.quoteIdentifier((0, schema_js_1.camelToSnake)(fieldName))}) REFERENCES ${dialect.quoteIdentifier(refTable)}(${dialect.quoteIdentifier(refParts[1])})`;
|
|
314
|
+
if (config.onDelete)
|
|
315
|
+
clause += ` ON DELETE ${referentialActionToSql(config.onDelete)}`;
|
|
316
|
+
if (config.onUpdate)
|
|
317
|
+
clause += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
|
|
318
|
+
return clause;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The type (and CHECK) of an enum column on an engine with no named enum type,
|
|
322
|
+
* from the dialect's `buildEnumColumn`, or null on PostgreSQL, where the column
|
|
323
|
+
* names the `CREATE TYPE`. The labels come from the schema's `enums`: off
|
|
324
|
+
* PostgreSQL there is no existing type a column could name instead.
|
|
325
|
+
*/
|
|
326
|
+
function enumColumnType(config, dialect, column, enums) {
|
|
327
|
+
if (!config.enumName || dialect.capabilities.namedEnumTypes)
|
|
328
|
+
return null;
|
|
329
|
+
if (!dialect.buildEnumColumn) {
|
|
330
|
+
throw new errors_js_1.UnsupportedFeatureError('enum columns', dialect.name, `Column "${column}" is of enum type "${config.enumName}". Declare it as text with a CHECK constraint instead.`);
|
|
331
|
+
}
|
|
332
|
+
if (config.isArray) {
|
|
333
|
+
throw new errors_js_1.UnsupportedFeatureError('enum array columns', dialect.name, `Column "${column}" is an array of enum "${config.enumName}"; ${dialect.name} has no array columns.`);
|
|
334
|
+
}
|
|
335
|
+
const labels = enums?.[config.enumName];
|
|
336
|
+
if (!labels) {
|
|
337
|
+
throw new errors_js_1.ValidationError(`Column "${column}" is of enum type "${config.enumName}", which the schema does not declare. ` +
|
|
338
|
+
`${dialect.name} has no enum type to refer to, so the labels must be in the schema: ` +
|
|
339
|
+
`defineSchema(tables, { enums: { ${JSON.stringify(config.enumName)}: ['a', 'b'] } }).`);
|
|
340
|
+
}
|
|
341
|
+
if (labels.length === 0) {
|
|
342
|
+
throw new errors_js_1.ValidationError(`Enum "${config.enumName}" declares no labels, so column "${column}" could hold nothing.`);
|
|
343
|
+
}
|
|
344
|
+
return dialect.buildEnumColumn(dialect.quoteIdentifier(column), labels);
|
|
345
|
+
}
|
|
268
346
|
/**
|
|
269
347
|
* Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
|
|
348
|
+
*
|
|
349
|
+
* `keyed` marks a column that sits in a key (see {@link keyedColumnNames}); it
|
|
350
|
+
* only matters to dialects that cannot key an unbounded text type. `inlineReferences`
|
|
351
|
+
* is false for a dialect that declares foreign keys at table level instead.
|
|
352
|
+
* `enums` is the schema's enum declarations, needed off PostgreSQL to declare
|
|
353
|
+
* an enum column's labels on the column itself.
|
|
270
354
|
*/
|
|
271
|
-
function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1.postgresDialect) {
|
|
355
|
+
function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1.postgresDialect, keyed = false, inlineReferences = true, enums) {
|
|
272
356
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
357
|
+
const enumColumn = enumColumnType(config, dialect, snakeName, enums);
|
|
273
358
|
// NOT NULL, serial types are implicitly NOT NULL, but explicit is fine.
|
|
274
359
|
// A column is NOT NULL if:
|
|
275
360
|
// 1. Explicitly marked .notNull(), OR
|
|
@@ -279,15 +364,13 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
|
|
|
279
364
|
const isSerial = isSerialType(config.type);
|
|
280
365
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
281
366
|
const notNull = config.isNotNull && !implicitNotNull;
|
|
282
|
-
// DEFAULT
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
defaultValue = normalizeDefault(config.defaultValue);
|
|
286
|
-
}
|
|
367
|
+
// DEFAULT, normalized here and spelled for the dialect once the column's
|
|
368
|
+
// final type is known (a MySQL default depends on it).
|
|
369
|
+
const normalizedDefault = config.defaultValue != null ? normalizeDefault(config.defaultValue) : undefined;
|
|
287
370
|
// REFERENCES, resolve the raw table name through the optional resolver so
|
|
288
371
|
// both camelCase accessor names and snake_case DDL names work.
|
|
289
372
|
let references;
|
|
290
|
-
if (config.referencesTarget) {
|
|
373
|
+
if (config.referencesTarget && inlineReferences) {
|
|
291
374
|
const refParts = config.referencesTarget.split('.');
|
|
292
375
|
if (refParts.length === 2) {
|
|
293
376
|
const rawTable = refParts[0];
|
|
@@ -298,14 +381,43 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
|
|
|
298
381
|
};
|
|
299
382
|
}
|
|
300
383
|
}
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
384
|
+
// The type is mapped ONCE, by the dialect's own `buildColumnDefinition`. A
|
|
385
|
+
// column that is a plain scalar hands it the schema-level token (`SERIAL`,
|
|
386
|
+
// `VARCHAR` + maxLength, `TEXT`), because every non-Postgres dialect keeps
|
|
387
|
+
// its auto-increment / identity marker, its length and its bounded string
|
|
388
|
+
// type INSIDE that mapping. Handing it the already-mapped token (`INT
|
|
389
|
+
// AUTO_INCREMENT`, `VARCHAR(12)`) made the dialect map the output of its own
|
|
390
|
+
// mapper, which turned `INT AUTO_INCREMENT` into `INT`, `VARCHAR(12)` into
|
|
391
|
+
// `VARCHAR(255)` and `NVARCHAR(MAX)` into `NVARCHAR(255)`. An enum name, a
|
|
392
|
+
// `vector(n)` and an array are Postgres tokens with no schema-level spelling,
|
|
393
|
+
// so they are passed through resolved.
|
|
394
|
+
const plainScalar = !config.enumName && config.vectorDimensions == null && !config.isArray;
|
|
395
|
+
let typeInput;
|
|
396
|
+
if (enumColumn) {
|
|
397
|
+
typeInput = { type: enumColumn.type, maxLength: enumColumn.maxLength ?? null };
|
|
398
|
+
}
|
|
399
|
+
else if (!plainScalar) {
|
|
400
|
+
typeInput = { type: resolveDdlType(config, dialect, snakeName), maxLength: null };
|
|
401
|
+
}
|
|
402
|
+
else if (keyed && config.type === 'TEXT' && !dialect.capabilities.unboundedTextKeys) {
|
|
403
|
+
// A keyed text / bytea column takes the dialect's bounded type
|
|
404
|
+
// (`VARCHAR(255)` / `NVARCHAR(255)`, `VARBINARY(255)`) where an unbounded
|
|
405
|
+
// one cannot be a key, or its own `maxLength` when it declares one.
|
|
406
|
+
typeInput = { type: 'VARCHAR', maxLength: config.maxLength };
|
|
407
|
+
}
|
|
408
|
+
else if (keyed && config.type === 'BYTEA' && !dialect.capabilities.unboundedTextKeys) {
|
|
409
|
+
typeInput = { type: 'VARBINARY', maxLength: config.maxLength };
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
typeInput = { type: config.type, maxLength: config.maxLength };
|
|
413
|
+
}
|
|
414
|
+
const defaultValue = normalizedDefault === undefined
|
|
415
|
+
? undefined
|
|
416
|
+
: dialectDefault(normalizedDefault, dialect, dialect.name === 'mysql' ? dialect.buildColumnType(typeInput) : '');
|
|
305
417
|
let def = dialect.buildColumnDefinition({
|
|
306
418
|
name: dialect.quoteIdentifier(snakeName),
|
|
307
|
-
type:
|
|
308
|
-
maxLength:
|
|
419
|
+
type: typeInput.type,
|
|
420
|
+
maxLength: typeInput.maxLength,
|
|
309
421
|
primaryKey: config.isPrimaryKey,
|
|
310
422
|
unique: config.isUnique,
|
|
311
423
|
notNull,
|
|
@@ -326,6 +438,10 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
|
|
|
326
438
|
if (config.check) {
|
|
327
439
|
def += ` CHECK (${config.check})`;
|
|
328
440
|
}
|
|
441
|
+
// The enum's labels, on an engine whose column type does not hold them.
|
|
442
|
+
if (enumColumn?.check) {
|
|
443
|
+
def += ` CHECK (${enumColumn.check})`;
|
|
444
|
+
}
|
|
329
445
|
return def;
|
|
330
446
|
}
|
|
331
447
|
/**
|
|
@@ -352,15 +468,136 @@ function normalizeDefault(val) {
|
|
|
352
468
|
if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
|
|
353
469
|
return val.trim();
|
|
354
470
|
}
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
471
|
+
// Single-quoted string literals, a quote inside written doubled (`'it''s'`),
|
|
472
|
+
// optionally followed by a PostgreSQL cast as the catalog prints a default
|
|
473
|
+
// (`'x'::text`, `'{}'::jsonb`). The cast is dropped: the literal is assigned
|
|
474
|
+
// to the column, which converts it to the column's type on every engine,
|
|
475
|
+
// and the other engines have no `::`. No semicolons, no statement keywords.
|
|
476
|
+
const literal = defaultStringLiteral(val.trim());
|
|
477
|
+
if (literal !== null) {
|
|
478
|
+
const inner = literal.slice(1, -1);
|
|
358
479
|
if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
|
|
359
|
-
throw new
|
|
480
|
+
throw new errors_js_1.ValidationError(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
|
|
360
481
|
}
|
|
361
|
-
return
|
|
482
|
+
return literal;
|
|
483
|
+
}
|
|
484
|
+
throw new errors_js_1.ValidationError(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal (a quote inside it written ` +
|
|
485
|
+
`twice, as in 'it''s'), or NULL.`);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* The string literal a default is, with any trailing PostgreSQL cast removed,
|
|
489
|
+
* or null when it is not one. Both patterns are linear: `[^']` and `''` cannot
|
|
490
|
+
* match the same text, and a cast's words are separated by single spaces.
|
|
491
|
+
*/
|
|
492
|
+
function defaultStringLiteral(val) {
|
|
493
|
+
const cast = /^('(?:[^']|'')*')::[a-z_][a-z0-9_]*(?: [a-z_][a-z0-9_]*)*(?:\(\d+(?:, ?\d+)?\))?(?:\[\])?$/i.exec(val);
|
|
494
|
+
if (cast)
|
|
495
|
+
return cast[1];
|
|
496
|
+
return /^'(?:[^']|'')*'$/.test(val) ? val : null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* MySQL column types that accept no literal default: a default has to be written
|
|
500
|
+
* as an expression, in parentheses (8.0.13+), even when its value is a constant.
|
|
501
|
+
*/
|
|
502
|
+
const MYSQL_EXPRESSION_DEFAULT_TYPES = /^(TINY|MEDIUM|LONG)?(TEXT|BLOB)\b|^JSON\b/i;
|
|
503
|
+
/**
|
|
504
|
+
* A MySQL expression default for a string literal, as its UTF-8 bytes in hex:
|
|
505
|
+
* `(X'...')` on a BLOB column, `(CONVERT(X'...' USING utf8mb4))` on a TEXT or
|
|
506
|
+
* JSON one. `normalized` is a single-quoted literal with its quotes doubled.
|
|
507
|
+
*/
|
|
508
|
+
function mysqlHexDefault(normalized, columnType) {
|
|
509
|
+
const value = normalized.slice(1, -1).replace(/''/g, "'");
|
|
510
|
+
let hex = '';
|
|
511
|
+
for (const byte of new TextEncoder().encode(value))
|
|
512
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
513
|
+
return /BLOB/i.test(columnType) ? `(X'${hex}')` : `(CONVERT(X'${hex}' USING utf8mb4))`;
|
|
514
|
+
}
|
|
515
|
+
/** What a clock default means on a MySQL date or time column. */
|
|
516
|
+
function mysqlClockDefault(key, ddlType) {
|
|
517
|
+
const isClock = key === 'NOW()' || key === 'CURRENT_TIMESTAMP' || key === 'CURRENT_DATE' || key === 'CURRENT_TIME';
|
|
518
|
+
if (!isClock)
|
|
519
|
+
return null;
|
|
520
|
+
const datetime = /^(?:DATETIME|TIMESTAMP)(?:\((\d)\))?$/i.exec(ddlType);
|
|
521
|
+
if (datetime) {
|
|
522
|
+
// MySQL 8 rejects a default whose fractional precision differs from the
|
|
523
|
+
// column's, and a bare CURRENT_DATE / CURRENT_TIME is not a valid default
|
|
524
|
+
// for a DATETIME (only the parenthesized expression is).
|
|
525
|
+
if (key === 'NOW()' || key === 'CURRENT_TIMESTAMP') {
|
|
526
|
+
return datetime[1] ? `CURRENT_TIMESTAMP(${datetime[1]})` : 'CURRENT_TIMESTAMP';
|
|
527
|
+
}
|
|
528
|
+
return `(${key})`;
|
|
529
|
+
}
|
|
530
|
+
// DATE and TIME columns accept neither NOW() nor a bare CURRENT_*, so the
|
|
531
|
+
// default is the matching clock function as an expression.
|
|
532
|
+
if (/^DATE$/i.test(ddlType))
|
|
533
|
+
return '(CURRENT_DATE)';
|
|
534
|
+
if (/^TIME(?:\(\d\))?$/i.test(ddlType))
|
|
535
|
+
return '(CURRENT_TIME)';
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The spelling of an already-normalized default for `dialect`. The schema
|
|
540
|
+
* vocabulary (`now()`, `true`, `gen_random_uuid()`) is Postgres's, and some of
|
|
541
|
+
* it is not SQL on the other engines: SQLite has no `NOW()` default, SQLite and
|
|
542
|
+
* MySQL have no `GEN_RANDOM_UUID()`, and SQL Server has none of `NOW()`, boolean
|
|
543
|
+
* literals or `CURRENT_DATE`. A literal (number, string, NULL) is the same
|
|
544
|
+
* everywhere and passes through, except on a MySQL TEXT, BLOB or JSON column,
|
|
545
|
+
* where it has to be an expression. `columnType` is the column's MySQL type as
|
|
546
|
+
* the dialect spells it (`DATETIME(3)`, `TEXT`); other dialects ignore it.
|
|
547
|
+
*/
|
|
548
|
+
function dialectDefault(normalized, dialect, columnType = '') {
|
|
549
|
+
const key = normalized.toUpperCase();
|
|
550
|
+
switch (dialect.name) {
|
|
551
|
+
case 'sqlite':
|
|
552
|
+
if (key === 'NOW()')
|
|
553
|
+
return 'CURRENT_TIMESTAMP';
|
|
554
|
+
if (key === 'GEN_RANDOM_UUID()') {
|
|
555
|
+
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))))";
|
|
556
|
+
}
|
|
557
|
+
return normalized;
|
|
558
|
+
case 'mysql': {
|
|
559
|
+
if (key === 'GEN_RANDOM_UUID()')
|
|
560
|
+
return '(UUID())';
|
|
561
|
+
const clock = mysqlClockDefault(key, columnType);
|
|
562
|
+
if (clock)
|
|
563
|
+
return clock;
|
|
564
|
+
// `DEFAULT NULL` is the one default these types accept as a literal.
|
|
565
|
+
const expression = key !== 'NULL' && MYSQL_EXPRESSION_DEFAULT_TYPES.test(columnType);
|
|
566
|
+
if (normalized.startsWith("'") && /['\\]/.test(normalized.slice(1, -1))) {
|
|
567
|
+
// A quote or a backslash inside the literal. Whether `\` escapes depends
|
|
568
|
+
// on the session's NO_BACKSLASH_ESCAPES, which Turbine's own connections
|
|
569
|
+
// set and other clients do not. An EXPRESSION default is worse: MySQL
|
|
570
|
+
// stores it re-escaped with backslashes and parses it again on every
|
|
571
|
+
// insert under the inserting session's mode, so `('it''s')` created
|
|
572
|
+
// fine and then failed every insert on a Turbine connection (measured
|
|
573
|
+
// on MySQL 8). Spelled as hex bytes, the value has no quote or
|
|
574
|
+
// backslash to read either way.
|
|
575
|
+
if (expression)
|
|
576
|
+
return mysqlHexDefault(normalized, columnType);
|
|
577
|
+
if (normalized.includes('\\')) {
|
|
578
|
+
throw new errors_js_1.ValidationError(`The default ${normalized} holds a backslash, which MySQL reads differently with and without ` +
|
|
579
|
+
'NO_BACKSLASH_ESCAPES. Use a default without one.');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (expression)
|
|
583
|
+
return `(${normalized})`;
|
|
584
|
+
return normalized;
|
|
585
|
+
}
|
|
586
|
+
case 'mssql':
|
|
587
|
+
if (key === 'NOW()')
|
|
588
|
+
return 'CURRENT_TIMESTAMP';
|
|
589
|
+
if (key === 'GEN_RANDOM_UUID()')
|
|
590
|
+
return 'NEWID()';
|
|
591
|
+
if (key === 'TRUE')
|
|
592
|
+
return '1';
|
|
593
|
+
if (key === 'FALSE')
|
|
594
|
+
return '0';
|
|
595
|
+
if (key === 'CURRENT_DATE')
|
|
596
|
+
return '(CAST(SYSDATETIME() AS DATE))';
|
|
597
|
+
return normalized;
|
|
598
|
+
default:
|
|
599
|
+
return normalized;
|
|
362
600
|
}
|
|
363
|
-
throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
|
|
364
601
|
}
|
|
365
602
|
/**
|
|
366
603
|
* Generate CREATE INDEX statements for foreign key columns.
|
|
@@ -515,6 +752,16 @@ function generateDeclaredIndexes(table, dialect = dialect_js_1.postgresDialect)
|
|
|
515
752
|
}
|
|
516
753
|
return out;
|
|
517
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* The warnings in a diff that report a real difference between the database and
|
|
757
|
+
* the schema file: everything in `warnings` except the entries the diff marked
|
|
758
|
+
* as possibly cosmetic ({@link DiffResult.inexactWarnings}). `push --dry-run
|
|
759
|
+
* --exit-code` fails on these.
|
|
760
|
+
*/
|
|
761
|
+
function driftWarnings(diff) {
|
|
762
|
+
const inexact = new Set(diff.inexactWarnings ?? []);
|
|
763
|
+
return (diff.warnings ?? []).filter((w) => !inexact.has(w));
|
|
764
|
+
}
|
|
518
765
|
/**
|
|
519
766
|
* Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
|
|
520
767
|
* referential actions. Default (`no action`) clauses are omitted, matching how
|
|
@@ -545,18 +792,49 @@ function diffReferentialAction(table, db, schemaOnDelete, schemaOnUpdate, dialec
|
|
|
545
792
|
return { statements: [drop, add], reverseStatements: [drop, reverseAdd] };
|
|
546
793
|
}
|
|
547
794
|
/**
|
|
548
|
-
* Compute
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
795
|
+
* Compute the enum changes needed to make the database's labels match the
|
|
796
|
+
* schema's, KEEPING THE DECLARED ORDER.
|
|
797
|
+
*
|
|
798
|
+
* An enum's order is observable: it decides `ORDER BY`, `<` and `MIN`/`MAX` on
|
|
799
|
+
* the column. A new label therefore goes where the schema declares it, not
|
|
800
|
+
* wherever the end of the type happens to be: `ADD VALUE 'x' AFTER 'prev'`, or
|
|
801
|
+
* `BEFORE 'next'` when it leads. A label that belongs at the end emits the bare
|
|
802
|
+
* `ADD VALUE`, which is what every earlier version emitted for it.
|
|
803
|
+
*
|
|
804
|
+
* Postgres cannot remove or reorder values in place, so a database label the
|
|
805
|
+
* schema dropped, and a declared order the database does not already have, are
|
|
806
|
+
* warnings (never statements): the type has to be recreated by hand.
|
|
552
807
|
*/
|
|
553
808
|
function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = dialect_js_1.postgresDialect) {
|
|
554
809
|
const statements = [];
|
|
555
810
|
const warnings = [];
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
811
|
+
const type = dialect.quoteIdentifier(enumName);
|
|
812
|
+
// The type's label order as the statements below will leave it.
|
|
813
|
+
const order = [...dbLabels];
|
|
814
|
+
const schemaIndex = schemaLabels.map((label, i) => ({ label, i }));
|
|
815
|
+
for (const { label, i } of schemaIndex) {
|
|
816
|
+
if (order.includes(label))
|
|
817
|
+
continue;
|
|
818
|
+
const previous = i > 0 ? schemaLabels[i - 1] : undefined;
|
|
819
|
+
const next = schemaLabels.slice(i + 1).find((l) => order.includes(l));
|
|
820
|
+
const prevAt = previous === undefined ? -1 : order.indexOf(previous);
|
|
821
|
+
if (previous !== undefined && prevAt !== -1) {
|
|
822
|
+
if (prevAt === order.length - 1) {
|
|
823
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
|
|
824
|
+
order.push(label);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} AFTER ${quoteEnumLabel(previous)};`);
|
|
828
|
+
order.splice(prevAt + 1, 0, label);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else if (next !== undefined) {
|
|
832
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)} BEFORE ${quoteEnumLabel(next)};`);
|
|
833
|
+
order.splice(order.indexOf(next), 0, label);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
statements.push(`ALTER TYPE ${type} ADD VALUE ${quoteEnumLabel(label)};`);
|
|
837
|
+
order.push(label);
|
|
560
838
|
}
|
|
561
839
|
}
|
|
562
840
|
const schemaSet = new Set(schemaLabels);
|
|
@@ -565,6 +843,14 @@ function diffEnumValues(enumName, schemaLabels, dbLabels, dialect = dialect_js_1
|
|
|
565
843
|
warnings.push(`Enum "${enumName}": labels [${removed.join(', ')}] exist in the database but not the schema. ` +
|
|
566
844
|
`Postgres cannot remove enum values in place, recreate the type manually if intended.`);
|
|
567
845
|
}
|
|
846
|
+
// After the adds, the labels both sides share must already be in declared
|
|
847
|
+
// order; if not, no ADD VALUE can fix it.
|
|
848
|
+
const shared = order.filter((l) => schemaSet.has(l));
|
|
849
|
+
if (shared.some((label, i) => label !== schemaLabels[i])) {
|
|
850
|
+
warnings.push(`Enum "${enumName}": the schema declares the order [${schemaLabels.join(', ')}] but the database has ` +
|
|
851
|
+
`[${shared.join(', ')}]. Postgres cannot reorder enum values in place, recreate the type manually if the ` +
|
|
852
|
+
`order matters (it decides ORDER BY and comparisons on the column).`);
|
|
853
|
+
}
|
|
568
854
|
return { statements, warnings };
|
|
569
855
|
}
|
|
570
856
|
/**
|
|
@@ -756,7 +1042,17 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
756
1042
|
const schemaDdlNames = new Set();
|
|
757
1043
|
for (const def of Object.values(schema.tables))
|
|
758
1044
|
schemaDdlNames.add(def.name);
|
|
759
|
-
const result = {
|
|
1045
|
+
const result = {
|
|
1046
|
+
create: [],
|
|
1047
|
+
alter: [],
|
|
1048
|
+
drop: [],
|
|
1049
|
+
statements: [],
|
|
1050
|
+
reverseStatements: [],
|
|
1051
|
+
warnings: [],
|
|
1052
|
+
inexactWarnings: [],
|
|
1053
|
+
widenings: [],
|
|
1054
|
+
notCompared: [],
|
|
1055
|
+
};
|
|
760
1056
|
const resolveRef = makeRefResolver(schema);
|
|
761
1057
|
// --- Enums: CREATE TYPE for new enums (before tables), ADD VALUE for grown
|
|
762
1058
|
// ones, and a warning for any destructive removal/reorder. New CREATE
|
|
@@ -790,9 +1086,10 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
790
1086
|
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${dialect.quoteIdentifier(ddlName)} CASCADE;`);
|
|
791
1087
|
}
|
|
792
1088
|
}
|
|
793
|
-
// Tables to drop (in DB but not in schema)
|
|
1089
|
+
// Tables to drop (in DB but not in schema). Turbine's own bookkeeping tables
|
|
1090
|
+
// (`_turbine_migrations`, ...) are never the user's to drop.
|
|
794
1091
|
for (const existingTable of existingTables) {
|
|
795
|
-
if (!schemaDdlNames.has(existingTable)) {
|
|
1092
|
+
if (!schemaDdlNames.has(existingTable) && !existingTable.startsWith('_turbine_')) {
|
|
796
1093
|
result.drop.push(existingTable);
|
|
797
1094
|
// We don't auto-generate DROP statements for safety
|
|
798
1095
|
}
|
|
@@ -806,6 +1103,7 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
806
1103
|
const dbCols = dbColumns[tableName] ?? {};
|
|
807
1104
|
const tableUniques = dbUniques[tableName] ?? {};
|
|
808
1105
|
const alterDef = { table: tableName, columns: [] };
|
|
1106
|
+
const sequenceDefaults = [];
|
|
809
1107
|
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
810
1108
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
811
1109
|
const dbCol = dbCols[snakeName];
|
|
@@ -817,6 +1115,13 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
817
1115
|
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
818
1116
|
result.statements.push(sql);
|
|
819
1117
|
result.reverseStatements.unshift(reverseSql);
|
|
1118
|
+
// NOT NULL with nothing to fill existing rows with: Postgres runs the
|
|
1119
|
+
// statement fine on an empty table and rejects it on any other.
|
|
1120
|
+
if ((config.isNotNull || config.isPrimaryKey) && !isSerialType(config.type) && config.defaultValue == null) {
|
|
1121
|
+
result.warnings.push(`column "${tableName}"."${snakeName}" is added NOT NULL with no default: the ALTER fails on a table ` +
|
|
1122
|
+
`that already has rows. Give it a default, add it nullable first and backfill it, or scaffold the ` +
|
|
1123
|
+
`two-phase pattern with \`npx turbine migrate create <name> --recipe backfill\`.`);
|
|
1124
|
+
}
|
|
820
1125
|
continue;
|
|
821
1126
|
}
|
|
822
1127
|
// Check type mismatch. Serial/bigserial columns are exempt: their
|
|
@@ -860,6 +1165,9 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
860
1165
|
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
861
1166
|
result.statements.push(sql);
|
|
862
1167
|
result.reverseStatements.unshift(reverseSql);
|
|
1168
|
+
if (!typePlan.needsUsing && !typePlan.loss && isLosslessWidening(oldSqlType, sqlType)) {
|
|
1169
|
+
result.widenings.push({ statement: sql, from: oldSqlType, to: sqlType });
|
|
1170
|
+
}
|
|
863
1171
|
// The destructive gate sees the statement and refuses by default; its
|
|
864
1172
|
// label is necessarily generic ("cast may truncate or fail"). This
|
|
865
1173
|
// warning names what THIS conversion does to THESE rows.
|
|
@@ -889,6 +1197,8 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
889
1197
|
if (!isSerial) {
|
|
890
1198
|
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
891
1199
|
const dbDefault = dbCol.columnDefault;
|
|
1200
|
+
if (dbDefault && isSequenceDefault(dbDefault))
|
|
1201
|
+
sequenceDefaults.push(snakeName);
|
|
892
1202
|
if (schemaDefault && !dbDefault) {
|
|
893
1203
|
// Schema has default, DB doesn't
|
|
894
1204
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
@@ -946,18 +1256,23 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
946
1256
|
// gate, not by hiding the statement: withholding it here previously made
|
|
947
1257
|
// `push` report "already in sync" for a destructive-only diff and print a
|
|
948
1258
|
// false "Altered" for a mixed one, since the 0.36 guard never received it.
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
1259
|
+
// A dropped column and its data cannot be recreated automatically, so the
|
|
1260
|
+
// reverse is irreversibleDropPlaceholder: an IRREVERSIBLE note in the DOWN
|
|
1261
|
+
// section describing the column, never SQL that pretends to restore it.
|
|
952
1262
|
for (const dbColName of Object.keys(dbCols)) {
|
|
953
1263
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
|
|
954
1264
|
if (!hasField) {
|
|
955
1265
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
956
|
-
const reverseSql =
|
|
1266
|
+
const reverseSql = irreversibleDropPlaceholder(tableName, dbColName, dbCols[dbColName], dialect);
|
|
957
1267
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
958
1268
|
result.statements.push(sql);
|
|
1269
|
+
result.reverseStatements.unshift(reverseSql);
|
|
959
1270
|
}
|
|
960
1271
|
}
|
|
1272
|
+
if (sequenceDefaults.length > 0) {
|
|
1273
|
+
result.notCompared.push(`sequence-backed defaults on "${tableName}" (${sequenceDefaults.join(', ')}): the database default is a ` +
|
|
1274
|
+
`sequence, which is never compared or changed`);
|
|
1275
|
+
}
|
|
961
1276
|
if (alterDef.columns.length > 0) {
|
|
962
1277
|
result.alter.push(alterDef);
|
|
963
1278
|
}
|
|
@@ -994,6 +1309,17 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
994
1309
|
.map((c) => ({ name: c.name, expression: c.expression }));
|
|
995
1310
|
const tableDbChecks = dbChecks[tableName] ?? [];
|
|
996
1311
|
const dbCheckByName = new Map(tableDbChecks.map((c) => [c.name, c]));
|
|
1312
|
+
const namedCheckNames = new Set(namedSchemaChecks.map((c) => c.name));
|
|
1313
|
+
const unmatchedChecks = tableDbChecks.filter((c) => !namedCheckNames.has(c.name)).map((c) => c.name);
|
|
1314
|
+
if (unmatchedChecks.length > 0) {
|
|
1315
|
+
result.notCompared.push(`CHECK constraints on "${tableName}" (${unmatchedChecks.join(', ')}): the schema does not name them, so ` +
|
|
1316
|
+
`they are not compared and never dropped`);
|
|
1317
|
+
}
|
|
1318
|
+
const unnamedSchemaChecks = (tableDef.checks ?? []).filter((c) => !c.name).length;
|
|
1319
|
+
if (unnamedSchemaChecks > 0) {
|
|
1320
|
+
result.notCompared.push(`${unnamedSchemaChecks} unnamed CHECK constraint(s) declared on "${tableName}": with no name there is nothing ` +
|
|
1321
|
+
`to match them against the database`);
|
|
1322
|
+
}
|
|
997
1323
|
const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
|
|
998
1324
|
for (const sc of namedSchemaChecks) {
|
|
999
1325
|
const existing = dbCheckByName.get(sc.name);
|
|
@@ -1006,9 +1332,11 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
1006
1332
|
result.reverseStatements.unshift(dropSql);
|
|
1007
1333
|
}
|
|
1008
1334
|
else if (normExpr(existing.expression) !== normExpr(sc.expression)) {
|
|
1009
|
-
|
|
1335
|
+
const checkWarning = `check constraint "${sc.name}" on "${tableName}": stored expression differs from the schema text ` +
|
|
1010
1336
|
`(Postgres canonicalizes CHECK expressions, so this is usually cosmetic). ` +
|
|
1011
|
-
`To intentionally replace it, rename the constraint or drop/re-add it in a manual migration
|
|
1337
|
+
`To intentionally replace it, rename the constraint or drop/re-add it in a manual migration.`;
|
|
1338
|
+
result.warnings.push(checkWarning);
|
|
1339
|
+
result.inexactWarnings.push(checkWarning);
|
|
1012
1340
|
}
|
|
1013
1341
|
}
|
|
1014
1342
|
// --- User-declared indexes (TableDef.indexes) ---
|
|
@@ -1025,6 +1353,18 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
1025
1353
|
// worth surfacing. Deleting the last declared index must NOT silence the
|
|
1026
1354
|
// warning pass. Tables with no `indexes` key stay silent (not managed).
|
|
1027
1355
|
const declaredPlain = (tableDef.indexes ?? []).filter((i) => !(0, schema_builder_js_1.isDocFieldIndexDef)(i));
|
|
1356
|
+
if (tableDef.indexes === undefined) {
|
|
1357
|
+
const dbIdxNames = [...(dbIndexes[tableName]?.keys() ?? [])];
|
|
1358
|
+
const recognized = new Set(Object.values(dbUniques[tableName] ?? {}));
|
|
1359
|
+
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
1360
|
+
if (config.referencesTarget)
|
|
1361
|
+
recognized.add(`idx_${tableName}_${(0, schema_js_1.camelToSnake)(fieldName)}`);
|
|
1362
|
+
}
|
|
1363
|
+
const unmanaged = dbIdxNames.filter((n) => !recognized.has(n) && !n.endsWith('_pkey'));
|
|
1364
|
+
if (unmanaged.length > 0) {
|
|
1365
|
+
result.notCompared.push(`indexes on "${tableName}" (${unmanaged.join(', ')}): the table declares no \`indexes\`, so they are not compared`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1028
1368
|
if (tableDef.indexes !== undefined) {
|
|
1029
1369
|
const dbIdx = dbIndexes[tableName] ?? new Map();
|
|
1030
1370
|
const dbIdxNames = new Set(dbIdx.keys());
|
|
@@ -1073,12 +1413,34 @@ async function schemaDiff(schema, connectionString, options = {}) {
|
|
|
1073
1413
|
}
|
|
1074
1414
|
}
|
|
1075
1415
|
}
|
|
1416
|
+
result.notCompared.push(exports.NEVER_COMPARED);
|
|
1076
1417
|
return result;
|
|
1077
1418
|
}
|
|
1078
1419
|
finally {
|
|
1079
1420
|
await client.end();
|
|
1080
1421
|
}
|
|
1081
1422
|
}
|
|
1423
|
+
/**
|
|
1424
|
+
* What {@link schemaDiff} never looks at, whatever the database holds. Kept
|
|
1425
|
+
* beside the diff so the sentence cannot outlive the code it describes.
|
|
1426
|
+
*/
|
|
1427
|
+
exports.NEVER_COMPARED = 'primary keys, multi-column UNIQUE constraints, and whether a foreign key exists on an existing column ' +
|
|
1428
|
+
'(only the referential actions of foreign keys that already exist are compared)';
|
|
1429
|
+
/**
|
|
1430
|
+
* The DOWN-side stand-in for a `DROP COLUMN`: a comment block, because the data
|
|
1431
|
+
* the column held cannot be put back by any statement. It names the column and
|
|
1432
|
+
* its old type so the person writing the real rollback has the facts, and it is
|
|
1433
|
+
* a comment rather than SQL so a rollback never invents a column of NULLs and
|
|
1434
|
+
* calls it restored.
|
|
1435
|
+
*/
|
|
1436
|
+
function irreversibleDropPlaceholder(table, column, dbCol, dialect = dialect_js_1.postgresDialect) {
|
|
1437
|
+
const q = (name) => dialect.quoteIdentifier(name);
|
|
1438
|
+
return [
|
|
1439
|
+
`-- IRREVERSIBLE: this migration drops ${q(table)}.${q(column)}, and the data in it cannot be restored automatically.`,
|
|
1440
|
+
'-- To roll back, restore the data from a backup and re-add the column by hand, for example:',
|
|
1441
|
+
`-- ALTER TABLE ${q(table)} ADD COLUMN ${q(column)} ${udtToSqlType(dbCol)};`,
|
|
1442
|
+
].join('\n');
|
|
1443
|
+
}
|
|
1082
1444
|
/**
|
|
1083
1445
|
* Rough type families. Within a family Postgres has an ASSIGNMENT cast between
|
|
1084
1446
|
* every pair the schema builder can produce, which is what lets the diff emit a
|
|
@@ -1367,6 +1729,89 @@ function planTypeChange(config, dbCol, column) {
|
|
|
1367
1729
|
}
|
|
1368
1730
|
return { kind: 'none' };
|
|
1369
1731
|
}
|
|
1732
|
+
const WIDENING_BASES = {
|
|
1733
|
+
VARCHAR: 'varchar',
|
|
1734
|
+
'CHARACTER VARYING': 'varchar',
|
|
1735
|
+
TEXT: 'text',
|
|
1736
|
+
SMALLINT: 'int2',
|
|
1737
|
+
INT2: 'int2',
|
|
1738
|
+
INTEGER: 'int4',
|
|
1739
|
+
INT: 'int4',
|
|
1740
|
+
INT4: 'int4',
|
|
1741
|
+
BIGINT: 'int8',
|
|
1742
|
+
INT8: 'int8',
|
|
1743
|
+
NUMERIC: 'numeric',
|
|
1744
|
+
DECIMAL: 'numeric',
|
|
1745
|
+
};
|
|
1746
|
+
/** Digits an integer type can hold, for the integer-to-numeric(p, s) rule. */
|
|
1747
|
+
const INTEGER_DIGITS = { int2: 5, int4: 10, int8: 19 };
|
|
1748
|
+
/** Parse a SQL type string, or null for any type outside the widening vocabulary. */
|
|
1749
|
+
function parseSqlType(raw) {
|
|
1750
|
+
const m = /^([A-Za-z][A-Za-z0-9 ]*?)\s*(?:\(\s*(\d+)\s*(?:,\s*(\d+)\s*)?\))?\s*(\[\])?$/.exec(raw.trim().replace(/\s+/g, ' '));
|
|
1751
|
+
if (!m)
|
|
1752
|
+
return null;
|
|
1753
|
+
const base = WIDENING_BASES[m[1].toUpperCase()];
|
|
1754
|
+
if (!base)
|
|
1755
|
+
return null;
|
|
1756
|
+
const a = m[2] === undefined ? null : Number(m[2]);
|
|
1757
|
+
const b = m[3] === undefined ? (a === null ? null : 0) : Number(m[3]);
|
|
1758
|
+
if ((base === 'text' || base.startsWith('int')) && a !== null)
|
|
1759
|
+
return null;
|
|
1760
|
+
if (base === 'varchar' && m[3] !== undefined)
|
|
1761
|
+
return null;
|
|
1762
|
+
return { base, a, b: base === 'numeric' ? b : null, array: m[4] !== undefined };
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* True when changing a column from `from` to `to` can never lose, round,
|
|
1766
|
+
* truncate or reject a value: every value of the old type is representable
|
|
1767
|
+
* unchanged in the new one. The vocabulary is deliberately small, and anything
|
|
1768
|
+
* outside it answers false (the destructive gate then treats the statement as
|
|
1769
|
+
* it always has):
|
|
1770
|
+
*
|
|
1771
|
+
* - `varchar(n)` to `varchar(m)` with m >= n, to bare `varchar`, to `text`
|
|
1772
|
+
* - `smallint` to `integer` to `bigint`
|
|
1773
|
+
* - any integer type to `numeric`, or to `numeric(p, s)` when it still holds
|
|
1774
|
+
* the integer's digits
|
|
1775
|
+
* - `numeric(p, s)` to a numeric with at least as many integer digits and at
|
|
1776
|
+
* least as many decimal places (a bare `numeric` has no bound)
|
|
1777
|
+
* - arrays of the above, element by element
|
|
1778
|
+
*
|
|
1779
|
+
* Not a widening, on purpose: `text` to `varchar(n)`, anything to a smaller
|
|
1780
|
+
* type, `timestamp` to `timestamptz` (re-reads the value in the session zone),
|
|
1781
|
+
* `real` to `double precision` (the stored digits change when printed), and
|
|
1782
|
+
* `json` to `jsonb` (drops duplicate keys).
|
|
1783
|
+
*/
|
|
1784
|
+
function isLosslessWidening(from, to) {
|
|
1785
|
+
const f = parseSqlType(from);
|
|
1786
|
+
const t = parseSqlType(to);
|
|
1787
|
+
if (!f || !t || f.array !== t.array)
|
|
1788
|
+
return false;
|
|
1789
|
+
if (f.base === 'varchar') {
|
|
1790
|
+
if (t.base === 'text')
|
|
1791
|
+
return true;
|
|
1792
|
+
if (t.base !== 'varchar')
|
|
1793
|
+
return false;
|
|
1794
|
+
return t.a === null || (f.a !== null && t.a >= f.a);
|
|
1795
|
+
}
|
|
1796
|
+
if (f.base === 'text')
|
|
1797
|
+
return t.base === 'text';
|
|
1798
|
+
if (f.base === 'numeric') {
|
|
1799
|
+
if (t.base !== 'numeric')
|
|
1800
|
+
return false;
|
|
1801
|
+
if (t.a === null)
|
|
1802
|
+
return true;
|
|
1803
|
+
if (f.a === null)
|
|
1804
|
+
return false;
|
|
1805
|
+
return t.b >= f.b && t.a - t.b >= f.a - f.b;
|
|
1806
|
+
}
|
|
1807
|
+
// Integer source.
|
|
1808
|
+
const digits = INTEGER_DIGITS[f.base];
|
|
1809
|
+
if (t.base === 'numeric')
|
|
1810
|
+
return t.a === null || t.a - t.b >= digits;
|
|
1811
|
+
if (!t.base.startsWith('int'))
|
|
1812
|
+
return false;
|
|
1813
|
+
return INTEGER_DIGITS[t.base] >= digits;
|
|
1814
|
+
}
|
|
1370
1815
|
/**
|
|
1371
1816
|
* Map a schema column type to its expected PostgreSQL UDT name.
|
|
1372
1817
|
*/
|
|
@@ -1490,10 +1935,17 @@ function defaultsMatch(schemaDefault, dbDefault) {
|
|
|
1490
1935
|
* `ALTER COLUMN ... TYPE` cast, exactly the kind of silent data loss `push`
|
|
1491
1936
|
* must never apply without an explicit opt-in.
|
|
1492
1937
|
*/
|
|
1493
|
-
function findDestructivePushStatements(statements) {
|
|
1938
|
+
function findDestructivePushStatements(statements, widenings = []) {
|
|
1939
|
+
// A lossless widening is the one `ALTER COLUMN ... TYPE` that destroys nothing.
|
|
1940
|
+
// It is skipped by statement identity, so a hand-edited or look-alike
|
|
1941
|
+
// statement is still scanned.
|
|
1942
|
+
const lossless = new Set(widenings.map((w) => w.statement));
|
|
1494
1943
|
const hits = [];
|
|
1495
|
-
for (const stmt of statements)
|
|
1944
|
+
for (const stmt of statements) {
|
|
1945
|
+
if (lossless.has(stmt))
|
|
1946
|
+
continue;
|
|
1496
1947
|
hits.push(...(0, destructive_js_1.scanDestructiveSql)(stmt));
|
|
1948
|
+
}
|
|
1497
1949
|
return hits;
|
|
1498
1950
|
}
|
|
1499
1951
|
/**
|
|
@@ -1633,7 +2085,7 @@ async function schemaPush(schema, connectionString, options = {}) {
|
|
|
1633
2085
|
}
|
|
1634
2086
|
// Destructive-statement gate: refuse silent data loss unless opted in.
|
|
1635
2087
|
if (!options.allowDestructive) {
|
|
1636
|
-
const destructive = findDestructivePushStatements(diff.statements);
|
|
2088
|
+
const destructive = findDestructivePushStatements(diff.statements, diff.widenings);
|
|
1637
2089
|
if (destructive.length > 0) {
|
|
1638
2090
|
throw new DestructivePushRefusal(destructive);
|
|
1639
2091
|
}
|