uql-orm 0.79.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/uql-browser.min.js.map +2 -2
- package/dist/cockroachdb/cockroachDialect.js +5 -1
- package/dist/dialect/abstractDialect.d.ts +1 -31
- package/dist/dialect/abstractDialect.js +3 -27
- package/dist/dialect/abstractSqlDialect.d.ts +25 -56
- package/dist/dialect/abstractSqlDialect.js +78 -145
- package/dist/dialect/aliases.d.ts +5 -0
- package/dist/dialect/aliases.js +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
- package/dist/dialect/operators.d.ts +66 -0
- package/dist/dialect/operators.js +129 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
- package/dist/dialect/pgLikeSqlDialect.js +16 -3
- package/dist/dialect/vectorSqlDialect.d.ts +2 -0
- package/dist/dialect/vectorSqlDialect.js +4 -0
- package/dist/entity/decorator/entity.d.ts +6 -1
- package/dist/entity/decorator/entity.js +12 -1
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +6 -1
- package/dist/entity/metadata/definition.js +19 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +24 -0
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/codegen/entityTypes.js +1 -2
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
- package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
- package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
- package/dist/migrate/ddl/pgIndexDdl.js +8 -6
- package/dist/migrate/ddl/tableDdl.d.ts +5 -2
- package/dist/migrate/ddl/tableDdl.js +13 -7
- package/dist/migrate/drift/driftDetector.d.ts +4 -5
- package/dist/migrate/drift/driftDetector.js +21 -21
- package/dist/migrate/generator/definitionToNode.d.ts +1 -1
- package/dist/migrate/generator/definitionToNode.js +9 -20
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
- package/dist/migrate/index.d.ts +2 -1
- package/dist/migrate/index.js +1 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
- package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
- package/dist/migrate/introspection/mongoIntrospector.js +7 -3
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
- package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
- package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
- package/dist/migrate/introspection/postgresIntrospector.js +70 -43
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
- package/dist/migrate/migrator.d.ts +33 -1
- package/dist/migrate/migrator.js +111 -43
- package/dist/migrate/schemaChange.d.ts +18 -0
- package/dist/migrate/schemaChange.js +37 -0
- package/dist/migrate/schemaGenerator.d.ts +25 -15
- package/dist/migrate/schemaGenerator.js +130 -182
- package/dist/migrate/storage/databaseStorage.d.ts +4 -0
- package/dist/migrate/storage/databaseStorage.js +14 -8
- package/dist/migrate/triggerSql.d.ts +24 -0
- package/dist/migrate/triggerSql.js +229 -0
- package/dist/mongo/mongoDialect.d.ts +0 -21
- package/dist/mongo/mongoDialect.js +105 -100
- package/dist/mongo/mongodbQuerier.js +17 -1
- package/dist/mssql/mssqlDialect.d.ts +18 -7
- package/dist/mssql/mssqlDialect.js +77 -33
- package/dist/mssql/mssqlQuerier.js +2 -2
- package/dist/schema/canonicalType.d.ts +6 -1
- package/dist/schema/canonicalType.js +14 -0
- package/dist/schema/indexDifferences.d.ts +22 -6
- package/dist/schema/indexDifferences.js +23 -8
- package/dist/schema/matchByKey.d.ts +10 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.d.ts +6 -2
- package/dist/schema/schemaAST.js +7 -3
- package/dist/schema/schemaASTBuilder.d.ts +4 -8
- package/dist/schema/schemaASTBuilder.js +20 -29
- package/dist/schema/schemaASTDiffer.d.ts +2 -3
- package/dist/schema/schemaASTDiffer.js +15 -36
- package/dist/schema/types.d.ts +14 -15
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +12 -3
- package/dist/type/dialect.d.ts +69 -9
- package/dist/type/entity.d.ts +96 -3
- package/dist/type/migration.d.ts +43 -44
- package/dist/type/query.d.ts +13 -4
- package/dist/type/queryWhere.d.ts +4 -2
- package/dist/util/ddlExpression.util.d.ts +5 -1
- package/dist/util/ddlExpression.util.js +6 -2
- package/dist/util/field.util.d.ts +9 -1
- package/dist/util/field.util.js +14 -2
- package/dist/util/fieldOption.util.d.ts +2 -2
- package/dist/util/fieldOption.util.js +2 -2
- package/dist/util/raw.d.ts +9 -1
- package/dist/util/raw.js +36 -11
- package/dist/util/sql.util.d.ts +12 -0
- package/dist/util/sql.util.js +24 -3
- package/dist/util/uqlError.d.ts +2 -0
- package/dist/util/uqlError.js +4 -0
- package/package.json +4 -4
- package/skills/uql-orm/SKILL.md +11 -6
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { canonicalToSql, engineType, isVectorCategory } from '../schema/canonicalType.js';
|
|
2
|
+
import { canonicalToSql, engineType, isVectorCategory, resolveColumnCanonicalType } from '../schema/canonicalType.js';
|
|
3
3
|
import { indexChanges } from '../schema/indexDifferences.js';
|
|
4
|
-
import { buildSchemaAST
|
|
4
|
+
import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
5
5
|
import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
|
|
6
6
|
import { isAutoIncrement, qualifyName } from '../util/index.js';
|
|
7
|
-
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
|
|
8
|
-
import {
|
|
7
|
+
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName, isOwnedName } from '../util/sql.util.js';
|
|
8
|
+
import { sameDefault } from './builder/expressions.js';
|
|
9
9
|
import { splitSqlStatements } from './builder/splitSqlStatements.js';
|
|
10
10
|
import { indexDdlFor, tableDdlFor } from './ddl/index.js';
|
|
11
11
|
import { sizedType } from './ddl/tableDdl.js';
|
|
12
12
|
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
13
13
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
14
14
|
import { assertIndexPredicate } from './indexPredicate.js';
|
|
15
|
+
import { added, alterations, dropped, nonEmpty, sides } from './schemaChange.js';
|
|
16
|
+
import { dropTrigger, renderTrigger, stampTriggers } from './triggerSql.js';
|
|
15
17
|
/**
|
|
16
18
|
* Unified SQL schema generator.
|
|
17
19
|
* Parameterized by dialect to handle Postgres, MySQL, MariaDB, and SQLite.
|
|
@@ -77,6 +79,7 @@ export class SqlSchemaGenerator {
|
|
|
77
79
|
return buildEntityAST(this, entities, {
|
|
78
80
|
defaultForeignKeyAction: this.defaultForeignKeyAction,
|
|
79
81
|
textScoreIndexes: this.dialect.features.textScoreIndexes,
|
|
82
|
+
vectorIndexRequiresNotNull: this.features.vectorIndexRequiresNotNull,
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
@@ -98,8 +101,49 @@ export class SqlSchemaGenerator {
|
|
|
98
101
|
statements.push(...this.addForeignKeyStatements(qualifyName(table.name, table.schema), table.outgoingRelations.map(foreignKeyOf)));
|
|
99
102
|
}
|
|
100
103
|
}
|
|
104
|
+
// Triggers last: each needs its own table, and a body may read any other the same schema just made.
|
|
105
|
+
const made = new Set(tables.map((table) => qualifyName(table.name, table.schema)));
|
|
106
|
+
statements.push(...entities
|
|
107
|
+
.filter((entity) => made.has(this.resolveTableName(getMeta(entity))))
|
|
108
|
+
.flatMap((entity) => this.generateTriggers(entity)));
|
|
101
109
|
return statements;
|
|
102
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The installed triggers on `entity`'s table it does not declare, and the declared ones not installed,
|
|
113
|
+
* compared by name alone: a name carries a hash of the trigger's SQL, so an edited one is a new name.
|
|
114
|
+
* Every trigger counts - the ones it authored, and one per event each stamp names.
|
|
115
|
+
*/
|
|
116
|
+
triggerChanges(entity, installed) {
|
|
117
|
+
const meta = getMeta(entity);
|
|
118
|
+
const triggers = [...(meta.triggers ?? []), ...stampTriggers(this.dialect, meta)];
|
|
119
|
+
const rendered = triggers.map((trigger, i) => renderTrigger(this.dialect, meta, trigger, i));
|
|
120
|
+
const declared = new Set(rendered.map((trigger) => trigger.name));
|
|
121
|
+
return {
|
|
122
|
+
stale: [...installed]
|
|
123
|
+
.filter(([name]) => isOwnedName(name) && !declared.has(name))
|
|
124
|
+
.map(([name, statements]) => ({ name, statements })),
|
|
125
|
+
missing: rendered.filter((trigger) => !installed.has(trigger.name)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
generateTriggers(entity, installed = new Map()) {
|
|
129
|
+
const { stale, missing } = this.triggerChanges(entity, installed);
|
|
130
|
+
return this.swapTriggers(entity, stale, missing);
|
|
131
|
+
}
|
|
132
|
+
generateTriggersDown(entity, installed = new Map()) {
|
|
133
|
+
const { stale, missing } = this.triggerChanges(entity, installed);
|
|
134
|
+
return this.swapTriggers(entity, missing, stale);
|
|
135
|
+
}
|
|
136
|
+
/** `dropped` taken off `entity`'s table and `created` put on, which is a reconcile read either way. */
|
|
137
|
+
swapTriggers(entity, dropped, created) {
|
|
138
|
+
return [
|
|
139
|
+
...this.generateTriggerDrops(entity, dropped.map((trigger) => trigger.name)),
|
|
140
|
+
...created.flatMap((trigger) => trigger.statements.map((sql) => `${sql};`)),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
generateTriggerDrops(entity, names) {
|
|
144
|
+
const meta = getMeta(entity);
|
|
145
|
+
return names.filter(isOwnedName).flatMap((name) => dropTrigger(this.dialect, meta, name).map((sql) => `${sql};`));
|
|
146
|
+
}
|
|
103
147
|
/**
|
|
104
148
|
* One statement per distinct schema the tables being created live in, in first-seen order. Only
|
|
105
149
|
* the tables actually being created, so a narrowed `only` does not declare namespaces it is not
|
|
@@ -131,97 +175,45 @@ export class SqlSchemaGenerator {
|
|
|
131
175
|
const cascade = options.cascade && this.features.dropTableCascade ? ' CASCADE' : '';
|
|
132
176
|
return `DROP TABLE ${ifExists}${this.escapeId(tableName)}${cascade};`;
|
|
133
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The statements taking a table through `diff`, in the one order both directions need: whatever holds
|
|
180
|
+
* something down goes before it and comes back after it. A foreign key holds its columns and the key it
|
|
181
|
+
* points at, so it goes first and comes back last; the key holds its columns; and some engines drop an
|
|
182
|
+
* index along with its column, which would leave nothing to name. An alter is its drop, then its add.
|
|
183
|
+
*/
|
|
134
184
|
generateAlterTable(diff) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
...(diff.
|
|
145
|
-
...(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// drop an index along with its column, which would leave nothing here to name.
|
|
149
|
-
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
|
|
150
|
-
for (const column of diff.columnsToAdd ?? []) {
|
|
151
|
-
this.assertColumnAddable(diff.tableName, column);
|
|
152
|
-
statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
|
|
153
|
-
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
154
|
-
}
|
|
155
|
-
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
|
|
156
|
-
for (const columnName of diff.columnsToDrop ?? []) {
|
|
157
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
|
|
158
|
-
}
|
|
159
|
-
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
|
|
160
|
-
// Last, so every column it names exists by now.
|
|
161
|
-
if (diff.primaryKey?.to.length) {
|
|
162
|
-
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
163
|
-
}
|
|
164
|
-
// After the columns, for the same reason the key is: a constraint cannot name one that is not
|
|
165
|
-
// there yet. The add half of an alter rides along, its drop having gone out above.
|
|
166
|
-
statements.push(...this.addForeignKeyStatements(diff.tableName, [
|
|
167
|
-
...(diff.foreignKeysToAdd ?? []),
|
|
168
|
-
...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
|
|
169
|
-
]));
|
|
170
|
-
return statements;
|
|
185
|
+
const { tableName, schema, primaryKey } = diff;
|
|
186
|
+
const { columns } = diff;
|
|
187
|
+
return [
|
|
188
|
+
...sides(diff.foreignKeys, 'from').map((foreignKey) => this.generateDropForeignKeySql(tableName, constraintNameOf(tableName, foreignKey))),
|
|
189
|
+
...(primaryKey?.from ? [this.generateDropPrimaryKeySql(tableName, primaryKey.from.name)] : []),
|
|
190
|
+
...sides(diff.indexes, 'from').map((index) => this.generateDropIndex(tableName, index.name, schema)),
|
|
191
|
+
...added(columns).flatMap((column) => this.addColumnStatements(tableName, column, schema)),
|
|
192
|
+
...alterations(columns).flatMap(({ from, to }) => this.tableDdl.alterColumn(tableName, to, this.generateColumnDefinitionFromSchema(to), from)),
|
|
193
|
+
...dropped(columns).flatMap((column) => this.tableDdl.dropColumn(tableName, column.name)),
|
|
194
|
+
...this.addIndexStatements(tableName, sides(diff.indexes, 'to')),
|
|
195
|
+
...(primaryKey?.to ? [this.generateAddPrimaryKeySql(tableName, primaryKey.to.columns, primaryKey.to.name)] : []),
|
|
196
|
+
...this.addForeignKeyStatements(tableName, sides(diff.foreignKeys, 'to')),
|
|
197
|
+
];
|
|
171
198
|
}
|
|
172
199
|
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
173
200
|
addForeignKeyStatements(tableName, foreignKeys) {
|
|
174
201
|
return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
|
|
175
202
|
}
|
|
176
|
-
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
177
|
-
dropForeignKeyStatements(tableName, constraintNames) {
|
|
178
|
-
return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
|
|
179
|
-
}
|
|
180
|
-
/** The `ALTER COLUMN` restating each of `columns`. */
|
|
181
|
-
alterColumnStatements(tableName, columns) {
|
|
182
|
-
return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
|
|
183
|
-
}
|
|
184
203
|
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
185
|
-
addIndexStatements(tableName, indexes
|
|
204
|
+
addIndexStatements(tableName, indexes) {
|
|
186
205
|
return indexes.flatMap((index) => [
|
|
187
206
|
this.generateCreateIndex(tableName, index),
|
|
188
207
|
...this.indexDdl.settleStatements(tableName, index),
|
|
189
208
|
]);
|
|
190
209
|
}
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// first, and a column it is about to drop is then free of anything naming it.
|
|
199
|
-
statements.push(...this.dropForeignKeyStatements(diff.tableName, [
|
|
200
|
-
...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
|
|
201
|
-
...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
|
|
202
|
-
]));
|
|
203
|
-
// The key next, for the same reason: a column the up added cannot be dropped below while the new
|
|
204
|
-
// key still names it. Restored under the name the database gave it, which is what the table had
|
|
205
|
-
// before, rather than a derived one that was never on it.
|
|
206
|
-
if (diff.primaryKey?.to.length) {
|
|
207
|
-
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
208
|
-
}
|
|
209
|
-
for (const column of diff.columnsToAdd ?? []) {
|
|
210
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
|
|
211
|
-
}
|
|
212
|
-
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
|
|
213
|
-
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
|
|
214
|
-
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
|
|
215
|
-
if (diff.primaryKey?.from.length) {
|
|
216
|
-
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
217
|
-
}
|
|
218
|
-
// The constraint the up replaced, back under the name the database had for it. A foreign key the
|
|
219
|
-
// up *dropped* is not restored: only its name survived the diff, never what it pointed at.
|
|
220
|
-
statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
|
|
221
|
-
if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
|
|
222
|
-
statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
|
|
223
|
-
}
|
|
224
|
-
return statements;
|
|
210
|
+
/** A column added to a table that exists, and its comment where the engine keeps one apart. */
|
|
211
|
+
addColumnStatements(tableName, column, schema) {
|
|
212
|
+
this.assertColumnAddable(tableName, column);
|
|
213
|
+
return [
|
|
214
|
+
this.tableDdl.addColumn(tableName, this.generateColumnDefinitionFromSchema(column)),
|
|
215
|
+
...this.generateColumnCommentStatement(tableName, column, schema),
|
|
216
|
+
];
|
|
225
217
|
}
|
|
226
218
|
generateCreateIndex(tableName, index, options = {}) {
|
|
227
219
|
return this.indexDdl.getCreateIndexStatement(tableName, index, options);
|
|
@@ -246,8 +238,8 @@ export class SqlSchemaGenerator {
|
|
|
246
238
|
/**
|
|
247
239
|
* The one place a column definition is spelled, so the `ColumnSchema` and `ColumnNode` paths cannot
|
|
248
240
|
* drift. A key column states `NOT NULL` rather than leave it to the key: SQLite lets a key column hold
|
|
249
|
-
* NULL otherwise, and SQL Server adds no key over a nullable column. `UNIQUE
|
|
250
|
-
* enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
241
|
+
* NULL otherwise, and SQL Server adds no key over a nullable column. Never `UNIQUE`: a unique column is
|
|
242
|
+
* a unique index, which the table creates beside it. An enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
251
243
|
*/
|
|
252
244
|
renderColumn(column) {
|
|
253
245
|
const type = column.generatedAs
|
|
@@ -257,9 +249,6 @@ export class SqlSchemaGenerator {
|
|
|
257
249
|
if (!column.nullable) {
|
|
258
250
|
def += ' NOT NULL';
|
|
259
251
|
}
|
|
260
|
-
if (column.isUnique && !column.isPrimaryKey) {
|
|
261
|
-
def += ' UNIQUE';
|
|
262
|
-
}
|
|
263
252
|
def += this.tableDdl.defaultClause(column);
|
|
264
253
|
if (column.comment) {
|
|
265
254
|
def += this.generateColumnComment(column.comment);
|
|
@@ -325,72 +314,61 @@ export class SqlSchemaGenerator {
|
|
|
325
314
|
if (!desired) {
|
|
326
315
|
return undefined;
|
|
327
316
|
}
|
|
328
|
-
// Indexes are matched here rather than by the differ, which pairs them by name so that a changed
|
|
329
|
-
// one reads as one index that altered. A migration needs the opposite: an index already in the
|
|
330
|
-
// table, under whatever name, must not be created again, and one whose shape differs is dropped
|
|
331
|
-
// and created anew - no engine alters an index's columns or uniqueness.
|
|
332
317
|
const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
|
|
333
|
-
const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
318
|
+
const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes, currentTable.indexFacets);
|
|
319
|
+
// An alter's `to` without its values: an alter restates the whole column, and MySQL answers a
|
|
320
|
+
// restated `CHECK` by adding a *second* constraint rather than replacing the first, so the column
|
|
321
|
+
// would accumulate one per alter. An enum's values reach the database with the column and are never
|
|
322
|
+
// restated - which is also why changing them is a hand-written migration. See architecture/roadmap.md.
|
|
323
|
+
const columns = (tableDiff?.columnDiffs ?? []).map((it) => {
|
|
324
|
+
if (it.type === 'add') {
|
|
325
|
+
return { to: this.columnNodeToSchema(it.expected) };
|
|
326
|
+
}
|
|
327
|
+
if (it.type === 'drop') {
|
|
328
|
+
return { from: this.columnNodeToSchema(it.actual) };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
from: this.columnNodeToSchema(it.actual),
|
|
332
|
+
to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
const keyDiff = tableDiff?.primaryKeyDiff;
|
|
336
|
+
// The key added named as this generator names it, so the rollback can drop it by that name.
|
|
337
|
+
const primaryKey = keyDiff && {
|
|
338
|
+
from: keyDiff.actual,
|
|
339
|
+
to: keyDiff.expected && {
|
|
340
|
+
columns: keyDiff.expected.columns,
|
|
341
|
+
name: derivedPrimaryKeyName(tableName, keyDiff.expected.columns),
|
|
342
|
+
},
|
|
355
343
|
};
|
|
356
344
|
// This table's own foreign keys. None where the engine cannot alter one (SQLite, short of rebuilding
|
|
357
345
|
// the table), since a difference nothing can apply would throw on every sync; `drift:check` names it.
|
|
358
346
|
const relationDiffs = this.features.foreignKeyAlter
|
|
359
347
|
? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
|
|
360
348
|
: [];
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
!columnsToDrop.length &&
|
|
367
|
-
!indexesToAdd.length &&
|
|
368
|
-
!indexesToDrop.length &&
|
|
369
|
-
!foreignKeysToAdd.length &&
|
|
370
|
-
!foreignKeysToDrop.length &&
|
|
371
|
-
!foreignKeysToAlter.length &&
|
|
372
|
-
!primaryKey) {
|
|
373
|
-
return undefined;
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
349
|
+
const foreignKeys = relationDiffs.map(({ actual, expected }) => ({
|
|
350
|
+
from: actual && foreignKeyOf(actual),
|
|
351
|
+
to: expected && foreignKeyOf(expected),
|
|
352
|
+
}));
|
|
353
|
+
const alter = {
|
|
376
354
|
tableName,
|
|
377
355
|
schema,
|
|
378
356
|
type: 'alter',
|
|
379
357
|
primaryKey,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
|
|
358
|
+
columns: nonEmpty(columns),
|
|
359
|
+
indexes: nonEmpty([
|
|
360
|
+
...indexes.toAdd.map((to) => ({ to: indexNodeToSchema(to) })),
|
|
361
|
+
...indexes.toDrop.map((from) => ({ from: indexNodeToSchema(from) })),
|
|
362
|
+
...indexes.toAlter.map(({ from, to }) => ({ from: indexNodeToSchema(from), to: indexNodeToSchema(to) })),
|
|
363
|
+
]),
|
|
364
|
+
foreignKeys: nonEmpty(foreignKeys),
|
|
388
365
|
};
|
|
366
|
+
return alter.primaryKey || alter.columns || alter.indexes || alter.foreignKeys ? alter : undefined;
|
|
389
367
|
}
|
|
390
368
|
diffOptions() {
|
|
391
369
|
return {
|
|
392
370
|
normalizeType: engineType(this.dialect),
|
|
393
|
-
defaultsEqual:
|
|
371
|
+
defaultsEqual: this.defaultsEqual,
|
|
394
372
|
};
|
|
395
373
|
}
|
|
396
374
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
@@ -398,58 +376,28 @@ export class SqlSchemaGenerator {
|
|
|
398
376
|
const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
|
|
399
377
|
return { ...column, type: this.columnSqlType(col) };
|
|
400
378
|
}
|
|
401
|
-
/**
|
|
402
|
-
|
|
403
|
-
*/
|
|
404
|
-
isDefaultValueEqual(current, desired) {
|
|
405
|
-
if (current === desired)
|
|
406
|
-
return true;
|
|
407
|
-
// Both spellings of "no default" are the same fact, and engines disagree on which they report:
|
|
408
|
-
// MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
|
|
409
|
-
// `MODIFY` every nullable column, on every sync, forever.
|
|
410
|
-
if (current == null || desired == null)
|
|
411
|
-
return current == null && desired == null;
|
|
412
|
-
const normalize = (value) => {
|
|
413
|
-
// Render first: the desired side may be a symbolic expression, the current side is always the
|
|
414
|
-
// engine's own text, and `{"kind":"now"}` matches no spelling of `CURRENT_TIMESTAMP`.
|
|
415
|
-
const val = SqlExpression.isExpression(value) ? formatDefaultValue(value, this.dialect) : value;
|
|
416
|
-
if (typeof val === 'string') {
|
|
417
|
-
let s = val.replace(/::[a-z_]+(\s+[a-z_]+)*(\[\])?$/i, '');
|
|
418
|
-
s = s.replace(/^'(.*)'$/, '$1');
|
|
419
|
-
if (s.toLowerCase() === 'null')
|
|
420
|
-
return 'null';
|
|
421
|
-
return s;
|
|
422
|
-
}
|
|
423
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
424
|
-
};
|
|
425
|
-
return normalize(current) === normalize(desired);
|
|
426
|
-
}
|
|
379
|
+
/** Whether a column's stored default is the one the entity declares, as this engine reprints it. */
|
|
380
|
+
defaultsEqual = (desired, current) => sameDefault(desired, current, this.dialect);
|
|
427
381
|
generateCreateTableFromNode(table, options = {}) {
|
|
428
382
|
const columns = [];
|
|
429
383
|
const constraints = [];
|
|
430
|
-
// MariaDB rejects a `VECTOR INDEX` whose column is nullable ("All parts of a VECTOR index must
|
|
431
|
-
// be NOT NULL"), so being indexed decides it rather than the entity's own nullability.
|
|
432
|
-
const indexedVectorColumns = new Set(this.features.vectorIndexRequiresNotNull
|
|
433
|
-
? table.indexes.filter((index) => index.type === 'vector').flatMap((idx) => idx.entries.map((e) => e.column))
|
|
434
|
-
: []);
|
|
435
384
|
for (const col of table.columns.values()) {
|
|
436
|
-
|
|
437
|
-
columns.push(colDef);
|
|
385
|
+
columns.push(this.generateColumnFromNode(col));
|
|
438
386
|
}
|
|
439
387
|
// Every key, of any width, as one named constraint beside the checks and foreign keys - so a
|
|
440
388
|
// later `DROP` has something to name. The exception is a dialect whose serial type states the key
|
|
441
389
|
// itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
|
|
442
390
|
// has already declared it, and saying it again is a second primary key.
|
|
391
|
+
const key = table.primaryKey;
|
|
443
392
|
const declaredByColumn = this.dialect.features.serialDeclaresPrimaryKey &&
|
|
444
|
-
|
|
445
|
-
table.
|
|
446
|
-
if (
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
(table.checks ?? []).forEach((check, i) => {
|
|
393
|
+
key?.columns.length === 1 &&
|
|
394
|
+
table.columns.get(key.columns[0])?.isAutoIncrement;
|
|
395
|
+
if (key && !declaredByColumn) {
|
|
396
|
+
const name = key.name ?? derivedPrimaryKeyName(table.name, key.columns);
|
|
397
|
+
const columns = key.columns.map((column) => this.escapeId(column)).join(', ');
|
|
398
|
+
constraints.push(`CONSTRAINT ${this.escapeId(name)} PRIMARY KEY (${columns})`);
|
|
399
|
+
}
|
|
400
|
+
table.checks.forEach((check, i) => {
|
|
453
401
|
const name = check.name ?? derivedCheckName(table.name, i + 1);
|
|
454
402
|
constraints.push(`CONSTRAINT ${this.escapeId(name)} CHECK (${check.expression})`);
|
|
455
403
|
});
|
|
@@ -457,8 +405,8 @@ export class SqlSchemaGenerator {
|
|
|
457
405
|
const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
|
|
458
406
|
constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
|
|
459
407
|
}
|
|
460
|
-
const
|
|
461
|
-
let createSql =
|
|
408
|
+
const target = this.dialect.escapeQualifiedId(table.name, table.schema);
|
|
409
|
+
let createSql = `${this.tableDdl.createTable(target, !!options.ifNotExists)} (\n`;
|
|
462
410
|
createSql += columns.map((col) => ` ${col}`).join(',\n');
|
|
463
411
|
if (constraints.length > 0) {
|
|
464
412
|
createSql += ',\n';
|
|
@@ -13,6 +13,10 @@ export declare class DatabaseMigrationStorage implements MigrationStorage {
|
|
|
13
13
|
tableName?: string;
|
|
14
14
|
});
|
|
15
15
|
ensureStorage(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Rendered by the schema generator rather than written out, so each engine gets its own spelling: SQL
|
|
18
|
+
* Server has no `CREATE TABLE IF NOT EXISTS`, and its `TIMESTAMP` is a row version that takes no default.
|
|
19
|
+
*/
|
|
16
20
|
private createTableIfNotExists;
|
|
17
21
|
executed(): Promise<string[]>;
|
|
18
22
|
/**
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { withSqlQuerierForMigrations } from '../acquireQuerierForMigrations.js';
|
|
2
|
+
import { expr } from '../builder/expressions.js';
|
|
3
|
+
import { TableBuilder } from '../builder/tableBuilder.js';
|
|
4
|
+
import { SqlSchemaGenerator } from '../schemaGenerator.js';
|
|
2
5
|
/** Where executed migrations are recorded when the config does not name a table. */
|
|
3
6
|
export const DEFAULT_MIGRATIONS_TABLE = 'uql_migrations';
|
|
4
7
|
/**
|
|
@@ -22,15 +25,18 @@ export class DatabaseMigrationStorage {
|
|
|
22
25
|
this.storageInitialized = true;
|
|
23
26
|
});
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Rendered by the schema generator rather than written out, so each engine gets its own spelling: SQL
|
|
30
|
+
* Server has no `CREATE TABLE IF NOT EXISTS`, and its `TIMESTAMP` is a row version that takes no default.
|
|
31
|
+
*/
|
|
25
32
|
async createTableIfNotExists(querier) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await querier.run(sql);
|
|
33
|
+
const table = new TableBuilder(this.tableName);
|
|
34
|
+
table.string('name', { length: 255, primaryKey: true });
|
|
35
|
+
table.timestamp('executed_at', { defaultValue: expr.now() });
|
|
36
|
+
const generator = new SqlSchemaGenerator(querier.dialect);
|
|
37
|
+
for (const sql of generator.generateCreateTableFromDefinition(table.build(), { ifNotExists: true })) {
|
|
38
|
+
await querier.run(sql);
|
|
39
|
+
}
|
|
34
40
|
}
|
|
35
41
|
async executed() {
|
|
36
42
|
await this.ensureStorage();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
+
import type { EntityMeta, EntityTriggerMeta } from '../type/index.js';
|
|
3
|
+
/** A trigger as uql installs it: the identifier, and the statements creating it under that identifier. */
|
|
4
|
+
export type RenderedTrigger = {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly statements: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* One trigger for `dialect`, named for its table and label and ending in a hash of its own SQL. That hash
|
|
10
|
+
* is the whole of change detection: a trigger is in place exactly when its name is installed, and an
|
|
11
|
+
* edited one is a new name, created while the old one drops as no longer declared.
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderTrigger<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>, trigger: EntityTriggerMeta<E>, position: number): RenderedTrigger;
|
|
14
|
+
/**
|
|
15
|
+
* What removes one trigger: the trigger, named with its table only where the engine scopes the name to
|
|
16
|
+
* one, and on the Postgres family the function holding its body, which dropping a trigger leaves behind.
|
|
17
|
+
*/
|
|
18
|
+
export declare function dropTrigger<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>, name: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* The triggers a stamp needs: one per event it names, each assigning the field's expression to its
|
|
21
|
+
* column. Generated rather than authored, so unlike an authored body it renders on every engine from
|
|
22
|
+
* one declaration - and not at all on the MySQL family, whose columns stamp themselves.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stampTriggers<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>): EntityTriggerMeta<E>[];
|