uql-orm 0.86.0 → 0.88.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/dialect/abstractSqlDialect.d.ts +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +1 -3
- package/dist/dialect/pgLikeSqlDialect.js +1 -3
- package/dist/migrate/ddl/tableDdl.d.ts +5 -0
- package/dist/migrate/ddl/tableDdl.js +18 -7
- package/dist/migrate/ddl/tableRebuild.d.ts +17 -0
- package/dist/migrate/ddl/tableRebuild.js +56 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +3 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +7 -1
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +29 -4
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +3 -3
- package/dist/migrate/introspection/sqliteIntrospector.js +9 -7
- package/dist/migrate/migrationTarget.js +27 -1
- package/dist/migrate/migrator.d.ts +20 -3
- package/dist/migrate/migrator.js +108 -17
- package/dist/migrate/schemaChange.d.ts +12 -1
- package/dist/migrate/schemaChange.js +28 -0
- package/dist/migrate/schemaGenerator.d.ts +19 -9
- package/dist/migrate/schemaGenerator.js +81 -42
- package/dist/migrate/triggerSql.js +19 -14
- package/dist/mongo/mongoDialect.js +1 -3
- package/dist/mssql/mssqlDialect.js +1 -3
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +2 -1
- package/dist/schema/matchByKey.d.ts +9 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.js +1 -0
- package/dist/schema/schemaASTDiffer.d.ts +13 -0
- package/dist/schema/schemaASTDiffer.js +35 -4
- package/dist/schema/types.d.ts +5 -1
- package/dist/sqlite/sqliteDialect.d.ts +0 -1
- package/dist/sqlite/sqliteDialect.js +1 -4
- package/dist/type/dialect.d.ts +4 -10
- package/dist/type/migration.d.ts +45 -4
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +1 -1
package/dist/migrate/migrator.js
CHANGED
|
@@ -3,13 +3,15 @@ import { basename, extname, join } from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { getEntities, getMeta } from '../entity/index.js';
|
|
5
5
|
import { SchemaAST } from '../schema/index.js';
|
|
6
|
+
import { columnRenames, tableRenameCandidates } from '../schema/schemaASTDiffer.js';
|
|
6
7
|
import { hasTriggers } from '../util/field.util.js';
|
|
7
8
|
import { LoggerWrapper } from '../util/index.js';
|
|
8
9
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
10
|
+
import { withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
|
|
9
11
|
import { buildMigrationModule } from './codegen/migrationFile.js';
|
|
10
12
|
import { introspectorFor } from './introspection/registry.js';
|
|
11
13
|
import { migrationBuilderFor, migrationTargetFor } from './migrationTarget.js';
|
|
12
|
-
import { dropped, nonEmpty, reverseDiff, sides } from './schemaChange.js';
|
|
14
|
+
import { dropped, lacksValue, newlyRequired, nonEmpty, reverseDiff, sides, withoutRebuild } from './schemaChange.js';
|
|
13
15
|
/**
|
|
14
16
|
* Main class for managing database migrations
|
|
15
17
|
*/
|
|
@@ -183,8 +185,10 @@ export class Migrator {
|
|
|
183
185
|
*/
|
|
184
186
|
async generateFromEntities(name) {
|
|
185
187
|
const generator = await this.getSchemaGenerator();
|
|
186
|
-
const { created, altered } = await this.pendingChanges();
|
|
188
|
+
const { created, altered } = await this.pendingChanges({ renames: true });
|
|
189
|
+
await this.assertFillable(altered);
|
|
187
190
|
const plan = this.alterPlan(generator, altered, await this.installedTriggers(created));
|
|
191
|
+
await this.noteChanges(generator, created, altered);
|
|
188
192
|
const up = [...this.createSchema(generator, created), ...plan.up];
|
|
189
193
|
if (up.length === 0) {
|
|
190
194
|
this.logger.logInfo('No schema changes detected.');
|
|
@@ -234,18 +238,23 @@ export class Migrator {
|
|
|
234
238
|
* its entity declares goes back on after. `down` is lazy: SQLite cannot express every alter's inverse.
|
|
235
239
|
*/
|
|
236
240
|
alterPlan(generator, altered, state) {
|
|
237
|
-
const changing = new Set(altered
|
|
241
|
+
const changing = new Set(altered
|
|
242
|
+
.filter((diff) => sides(diff.columns, 'from').length || diff.renamedColumns?.length || diff.rebuild)
|
|
243
|
+
.map((diff) => diff.tableName));
|
|
238
244
|
const cleared = state.filter(({ entity }) => changing.has(this.tableOf(entity)));
|
|
239
245
|
const after = state.map((it) => (cleared.includes(it) ? { entity: it.entity, installed: new Map() } : it));
|
|
240
246
|
return {
|
|
241
247
|
up: [
|
|
242
248
|
...cleared.flatMap(({ entity, installed }) => generator.generateTriggerDrops(entity, [...installed.keys()])),
|
|
243
|
-
...altered.flatMap((diff) => generator.generateAlterTable(diff)),
|
|
249
|
+
...altered.flatMap((diff) => [...renameStatements(generator, diff), ...generator.generateAlterTable(diff)]),
|
|
244
250
|
...this.reconcileTriggers(generator, after),
|
|
245
251
|
],
|
|
246
252
|
down: () => [
|
|
247
253
|
...this.revertedTriggers(generator, after),
|
|
248
|
-
...altered.toReversed().flatMap((diff) =>
|
|
254
|
+
...altered.toReversed().flatMap((diff) => {
|
|
255
|
+
const reversed = reverseDiff(diff);
|
|
256
|
+
return [...generator.generateAlterTable(reversed), ...renameStatements(generator, reversed)];
|
|
257
|
+
}),
|
|
249
258
|
...cleared.flatMap(({ installed }) => [...installed.values()].flat().map((sql) => `${sql};`)),
|
|
250
259
|
],
|
|
251
260
|
};
|
|
@@ -265,6 +274,65 @@ export class Migrator {
|
|
|
265
274
|
revertedTriggers(generator, state) {
|
|
266
275
|
return state.flatMap(({ entity, installed }) => generator.generateTriggersDown(entity, installed));
|
|
267
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* What a generated migration does that its reader must not miss: each column it drops or retypes, which
|
|
279
|
+
* can lose data, and each table it creates empty while the database holds one no entity names with the
|
|
280
|
+
* same columns, which may be the table renamed. That one is never renamed here: it may be another's.
|
|
281
|
+
*/
|
|
282
|
+
async noteChanges(generator, created, altered) {
|
|
283
|
+
for (const { tableName, columns = [] } of altered) {
|
|
284
|
+
for (const { from, to } of columns.filter((change) => change.isBreaking)) {
|
|
285
|
+
this.logger.logWarn(to
|
|
286
|
+
? `Retypes "${tableName}"."${to.name}" from ${from?.type} to ${to.type}: a value that does not fit is lost or refused.`
|
|
287
|
+
: `Drops "${tableName}"."${from?.name}", losing what it holds.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const { from, to } of await this.renamedTables(generator, created)) {
|
|
291
|
+
this.logger.logWarn(`Creates "${to}" empty, while "${from}", which no entity names, holds the same columns. If it was ` +
|
|
292
|
+
`renamed, replace its creation in this migration with \`renameTable('${from}', '${to}')\`.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Refuses, before anything runs, each column the changes require with no default while rows would hold
|
|
297
|
+
* none: every engine fails on one but MySQL, which fills in a zero. Counted, since an empty table is fine.
|
|
298
|
+
*/
|
|
299
|
+
async assertFillable(altered) {
|
|
300
|
+
// A renamed column is identical but for its name, so the one counted is never renamed too.
|
|
301
|
+
const counts = altered.flatMap(({ tableName, columns }) => newlyRequired(columns)
|
|
302
|
+
.filter(({ to }) => lacksValue(to))
|
|
303
|
+
.map(({ from, to }) => ({ tableName, column: to.name, nullable: from })));
|
|
304
|
+
if (!counts.length) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const unfilled = await withSqlQuerierForMigrations(this.pool, 'Migrator', async (querier) => {
|
|
308
|
+
const escapeId = (name) => querier.dialect.escapeId(name);
|
|
309
|
+
const found = [];
|
|
310
|
+
for (const { tableName, column, nullable } of counts) {
|
|
311
|
+
const empty = nullable ? ` WHERE ${escapeId(column)} IS NULL` : '';
|
|
312
|
+
const [{ rows }] = await querier.all(`SELECT COUNT(*) AS ${escapeId('rows')} FROM ${escapeId(tableName)}${empty}`);
|
|
313
|
+
const count = Number(rows);
|
|
314
|
+
if (count) {
|
|
315
|
+
found.push(`"${tableName}"."${column}" is required with no default, and ${count} ${count === 1 ? 'row holds' : 'rows hold'} none`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return found;
|
|
319
|
+
});
|
|
320
|
+
if (unfilled.length) {
|
|
321
|
+
throw new UqlUsageError(`${unfilled.join('; ')}. Declare a default, or add the column nullable, fill it, then require it.`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** The tables the database holds that no entity names, each paired with a new one it is identical to. */
|
|
325
|
+
async renamedTables(generator, created) {
|
|
326
|
+
const createdEntities = this.createdEntities(created);
|
|
327
|
+
const diffOptions = generator.diffOptions?.();
|
|
328
|
+
if (!createdEntities.length || !generator.buildAST || !diffOptions) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const owned = new Set(this.entities.map((entity) => this.tableOf(entity)));
|
|
332
|
+
const unowned = (await this.schemaIntrospector.getTableNames()).filter((table) => !owned.has(table));
|
|
333
|
+
const current = await this.schemaIntrospector.introspect(unowned);
|
|
334
|
+
return tableRenameCandidates(generator.buildAST(createdEntities), current, diffOptions);
|
|
335
|
+
}
|
|
268
336
|
/** The entities whose tables are among `created`. */
|
|
269
337
|
createdEntities(created) {
|
|
270
338
|
const fresh = new Set(created);
|
|
@@ -275,18 +343,25 @@ export class Migrator {
|
|
|
275
343
|
return this.pool.dialect.resolveTableName(getMeta(entity));
|
|
276
344
|
}
|
|
277
345
|
/**
|
|
278
|
-
*
|
|
346
|
+
* The differences between the entities and the database. With `renames`, a column identical to one the
|
|
347
|
+
* entity no longer names is renamed in place rather than dropped and added, as a generated migration wants.
|
|
279
348
|
*/
|
|
280
|
-
async getDiffs() {
|
|
349
|
+
async getDiffs(options = {}) {
|
|
281
350
|
const generator = await this.getSchemaGenerator();
|
|
282
|
-
|
|
283
|
-
// Both sides built once: the database's above, the entities' here. Left to `diffSchema`, each
|
|
351
|
+
// Both sides built once: the database's here, the entities' below. Left to `diffSchema`, each
|
|
284
352
|
// entity would rebuild the whole AST, which is quadratic in the number of entities. Absent on a
|
|
285
353
|
// generator that compares no schema of its own - MongoDB, which reads only indexes.
|
|
286
354
|
const desiredAst = generator.buildAST?.(this.entities);
|
|
355
|
+
let ast = await this.introspectEntities(this.entities);
|
|
356
|
+
const diffOptions = generator.diffOptions?.();
|
|
357
|
+
const renames = options.renames && desiredAst && diffOptions ? columnRenames(desiredAst, ast, diffOptions) : new Map();
|
|
358
|
+
if (renames.size) {
|
|
359
|
+
// Read again under the names the entities give them, so the rest compares as the columns they become.
|
|
360
|
+
ast = await this.introspectEntities(this.entities, renames);
|
|
361
|
+
}
|
|
287
362
|
return this.entities.flatMap((entity) => {
|
|
288
|
-
const
|
|
289
|
-
const diff = generator.diffSchema(entity,
|
|
363
|
+
const tableName = generator.resolveTableName(getMeta(entity));
|
|
364
|
+
const diff = generator.diffSchema(entity, ast.getTable(tableName), desiredAst, renames.get(tableName));
|
|
290
365
|
return diff ? [diff] : [];
|
|
291
366
|
});
|
|
292
367
|
}
|
|
@@ -294,13 +369,13 @@ export class Migrator {
|
|
|
294
369
|
* The tables `entities` name, read a schema at a time so each is keyed as its entity spells it. Those
|
|
295
370
|
* alone: nothing else is diffed, and another table can be dropped mid-scan by whatever else is running.
|
|
296
371
|
*/
|
|
297
|
-
async introspectEntities(entities) {
|
|
372
|
+
async introspectEntities(entities, renames) {
|
|
298
373
|
const { dialect } = this.pool;
|
|
299
374
|
const bySchema = Map.groupBy(new Set(entities), (entity) => dialect.resolveSchema(getMeta(entity)));
|
|
300
375
|
const merged = new SchemaAST();
|
|
301
376
|
for (const [schema, members] of bySchema) {
|
|
302
377
|
const tables = members.map((entity) => dialect.resolveTableAlias(getMeta(entity)));
|
|
303
|
-
for (const table of (await this.schemaIntrospectorFor(schema).introspect(tables)).getTables()) {
|
|
378
|
+
for (const table of (await this.schemaIntrospectorFor(schema).introspect(tables, renames)).getTables()) {
|
|
304
379
|
merged.addTable(table);
|
|
305
380
|
}
|
|
306
381
|
}
|
|
@@ -342,6 +417,7 @@ export class Migrator {
|
|
|
342
417
|
const ast = await this.introspectEntities([entity, ...referencedEntities(meta)]);
|
|
343
418
|
// The table is already there, so its triggers are reconciled rather than carried by a `CREATE`.
|
|
344
419
|
const altered = this.alterFromEntity(generator, entity, ast.getTable(tableName), options);
|
|
420
|
+
await this.assertFillable(altered);
|
|
345
421
|
return this.alterPlan(generator, altered, await this.installedTriggers([], [entity])).up;
|
|
346
422
|
}
|
|
347
423
|
/** The diff for one entity against the table it already has, and none where the two agree. */
|
|
@@ -374,6 +450,7 @@ export class Migrator {
|
|
|
374
450
|
}
|
|
375
451
|
const { created, altered } = await this.pendingChanges();
|
|
376
452
|
const filtered = altered.map((diff) => this.filterDiff(diff, options));
|
|
453
|
+
await this.assertFillable(filtered);
|
|
377
454
|
return [
|
|
378
455
|
...this.createSchema(generator, created),
|
|
379
456
|
...this.alterPlan(generator, filtered, await this.installedTriggers(created)).up,
|
|
@@ -388,8 +465,8 @@ export class Migrator {
|
|
|
388
465
|
* alter. What to emit for each stays with the caller: a sync narrows an alter to what it allows and
|
|
389
466
|
* never asks for the rollback, which on SQLite cannot even be expressed (no `ALTER COLUMN`).
|
|
390
467
|
*/
|
|
391
|
-
async pendingChanges() {
|
|
392
|
-
const diffs = await this.getDiffs();
|
|
468
|
+
async pendingChanges(options = {}) {
|
|
469
|
+
const diffs = await this.getDiffs(options);
|
|
393
470
|
return {
|
|
394
471
|
created: diffs.filter((diff) => diff.type === 'create').map((diff) => diff.tableName),
|
|
395
472
|
altered: diffs.filter((diff) => diff.type === 'alter'),
|
|
@@ -398,12 +475,15 @@ export class Migrator {
|
|
|
398
475
|
/**
|
|
399
476
|
* Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
|
|
400
477
|
* and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
|
|
401
|
-
* Without `drop`, a column's drop is held too.
|
|
478
|
+
* Without `drop`, a column's drop is held too. A rebuilt table applies its diff whole, so holding any
|
|
479
|
+
* part of it holds the rebuild, and only what an `ALTER` adds goes ahead: a plain column, an index.
|
|
402
480
|
*/
|
|
403
481
|
filterDiff(diff, options) {
|
|
404
482
|
const safe = options.safe !== false;
|
|
483
|
+
let held = false;
|
|
405
484
|
const skip = (what, names, fix) => {
|
|
406
485
|
if (names.length) {
|
|
486
|
+
held = true;
|
|
407
487
|
this.logger.logSkippedMigration(`[AutoSync] Skipped ${names.length} ${what} in table '${diff.tableName}': ${names.join(', ')} (${fix}).`);
|
|
408
488
|
}
|
|
409
489
|
};
|
|
@@ -422,13 +502,18 @@ export class Migrator {
|
|
|
422
502
|
if (!options.drop) {
|
|
423
503
|
skip('column drops', dropped(columns).map((column) => column.name), 'drop: false. Use { drop: true } to apply');
|
|
424
504
|
}
|
|
425
|
-
|
|
505
|
+
const filtered = {
|
|
426
506
|
...diff,
|
|
427
507
|
primaryKey: safe ? undefined : diff.primaryKey,
|
|
428
508
|
columns: options.drop ? columns : nonEmpty((columns ?? []).filter((change) => change.to !== undefined)),
|
|
429
509
|
indexes: additive('index', diff.indexes, (index) => index.name),
|
|
430
510
|
foreignKeys: additive('foreign key', diff.foreignKeys, (foreignKey) => foreignKey.name ?? foreignKey.columns.join(', ')),
|
|
431
511
|
};
|
|
512
|
+
if (!diff.rebuild || !held) {
|
|
513
|
+
return filtered;
|
|
514
|
+
}
|
|
515
|
+
skip('rebuild', [diff.tableName], 'it applies the whole diff, and part of it is held');
|
|
516
|
+
return withoutRebuild(filtered);
|
|
432
517
|
}
|
|
433
518
|
/** Runs the statements a generator wrote, in one transaction where the engine takes DDL in one. */
|
|
434
519
|
async executeSyncStatements(statements, options) {
|
|
@@ -554,3 +639,9 @@ function referencedEntities(meta) {
|
|
|
554
639
|
const relations = Object.values(meta.relations).flatMap((relation) => relation?.entity?.() ?? []);
|
|
555
640
|
return [...fields, ...relations];
|
|
556
641
|
}
|
|
642
|
+
/** A diff's column renames, through the builder operation every SQL generator already renders. */
|
|
643
|
+
function renameStatements(generator, { tableName, renamedColumns = [], rebuild }) {
|
|
644
|
+
return rebuild
|
|
645
|
+
? []
|
|
646
|
+
: renamedColumns.flatMap(({ from, to }) => generator.generateOperation({ type: 'renameColumn', tableName, oldName: from, newName: to }));
|
|
647
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Change, SchemaDiff } from '../type/index.js';
|
|
1
|
+
import type { Change, ColumnSchema, SchemaDiff } from '../type/index.js';
|
|
2
2
|
/** Each change's end on `side`, where it has one: what a drop half removes (`from`), or an add half creates (`to`). */
|
|
3
3
|
export declare function sides<T>(changes: readonly Change<T>[] | undefined, side: 'from' | 'to'): T[];
|
|
4
4
|
/** What the changes add: each `to` with no `from`. */
|
|
@@ -10,6 +10,17 @@ export declare function alterations<T>(changes?: readonly Change<T>[]): {
|
|
|
10
10
|
readonly from: T;
|
|
11
11
|
readonly to: T;
|
|
12
12
|
}[];
|
|
13
|
+
/** Each column `changes` make required on the rows already there: added so, or no longer nullable. */
|
|
14
|
+
export declare function newlyRequired(changes?: readonly Change<ColumnSchema>[]): {
|
|
15
|
+
readonly from?: ColumnSchema;
|
|
16
|
+
readonly to: ColumnSchema;
|
|
17
|
+
}[];
|
|
18
|
+
/** Whether a row already in the table would hold nothing in `column`: required, with no default, and not one the engine fills. */
|
|
19
|
+
export declare function lacksValue(column: ColumnSchema): boolean;
|
|
20
|
+
/** Whether `diff` holds anything an engine that rebuilds tables makes no other way, a key or a foreign key included. */
|
|
21
|
+
export declare function needsRebuild(diff: SchemaDiff): boolean;
|
|
22
|
+
/** `diff` less its rebuild and everything only a rebuild makes: what an `ALTER` can still apply alone. */
|
|
23
|
+
export declare function withoutRebuild(diff: SchemaDiff): SchemaDiff;
|
|
13
24
|
/** `items`, or nothing where it has none, so an empty change list is left off a diff. */
|
|
14
25
|
export declare function nonEmpty<T>(items: readonly T[]): readonly T[] | undefined;
|
|
15
26
|
/** `change` undone: an add becomes a drop, a drop an add, and an alter runs the other way. */
|
|
@@ -17,6 +17,32 @@ export function dropped(changes = []) {
|
|
|
17
17
|
export function alterations(changes = []) {
|
|
18
18
|
return changes.flatMap(({ from, to }) => (from === undefined || to === undefined ? [] : [{ from, to }]));
|
|
19
19
|
}
|
|
20
|
+
/** Each column `changes` make required on the rows already there: added so, or no longer nullable. */
|
|
21
|
+
export function newlyRequired(changes = []) {
|
|
22
|
+
return changes.flatMap(({ from, to }) => (to && !to.nullable && (!from || from.nullable) ? [{ from, to }] : []));
|
|
23
|
+
}
|
|
24
|
+
/** Whether a row already in the table would hold nothing in `column`: required, with no default, and not one the engine fills. */
|
|
25
|
+
export function lacksValue(column) {
|
|
26
|
+
return !column.nullable && column.defaultValue === undefined && !column.generatedAs && !column.isAutoIncrement;
|
|
27
|
+
}
|
|
28
|
+
/** Whether an engine that rebuilds tables makes `change` no other way: a column changed in place, or a stored generated one added. */
|
|
29
|
+
function onlyRebuilt({ from, to }) {
|
|
30
|
+
return from === undefined ? Boolean(to?.generatedAs) : to !== undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Whether `diff` holds anything an engine that rebuilds tables makes no other way, a key or a foreign key included. */
|
|
33
|
+
export function needsRebuild(diff) {
|
|
34
|
+
return Boolean(diff.primaryKey || diff.foreignKeys || diff.columns?.some(onlyRebuilt));
|
|
35
|
+
}
|
|
36
|
+
/** `diff` less its rebuild and everything only a rebuild makes: what an `ALTER` can still apply alone. */
|
|
37
|
+
export function withoutRebuild(diff) {
|
|
38
|
+
return {
|
|
39
|
+
...diff,
|
|
40
|
+
primaryKey: undefined,
|
|
41
|
+
foreignKeys: undefined,
|
|
42
|
+
columns: nonEmpty((diff.columns ?? []).filter((change) => !onlyRebuilt(change))),
|
|
43
|
+
rebuild: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
20
46
|
/** `items`, or nothing where it has none, so an empty change list is left off a diff. */
|
|
21
47
|
export function nonEmpty(items) {
|
|
22
48
|
return items.length ? items : undefined;
|
|
@@ -33,5 +59,7 @@ export function reverseDiff(diff) {
|
|
|
33
59
|
columns: diff.columns?.map(swap),
|
|
34
60
|
indexes: diff.indexes?.map(swap),
|
|
35
61
|
foreignKeys: diff.foreignKeys?.map(swap),
|
|
62
|
+
renamedColumns: diff.renamedColumns?.map(({ from, to }) => ({ from: to, to: from })),
|
|
63
|
+
rebuild: diff.rebuild && { from: diff.rebuild.to, to: diff.rebuild.from },
|
|
36
64
|
};
|
|
37
65
|
}
|
|
@@ -3,7 +3,7 @@ import type { SchemaAST } from '../schema/schemaAST.js';
|
|
|
3
3
|
import { type BuildSchemaASTOptions } from '../schema/schemaASTBuilder.js';
|
|
4
4
|
import { type DiffOptions } from '../schema/schemaASTDiffer.js';
|
|
5
5
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
6
|
-
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, InstalledTriggers, EntityWhereMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../type/index.js';
|
|
6
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, InstalledTriggers, EntityWhereMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, Rename, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../type/index.js';
|
|
7
7
|
import type { AnyMigrationOperation, FullColumnDefinition, IndexDefinition, TableDefinition } from './builder/types.js';
|
|
8
8
|
import { type IndexDdl, type TableDdl } from './ddl/index.js';
|
|
9
9
|
/**
|
|
@@ -75,6 +75,11 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
75
75
|
* index along with its column, which would leave nothing to name. An alter is its drop, then its add.
|
|
76
76
|
*/
|
|
77
77
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Each column the changes make required while declaring a default, and that default as SQL: the rows
|
|
80
|
+
* already there hold a null it has to replace, and it is the only value the entity says it may take.
|
|
81
|
+
*/
|
|
82
|
+
private defaultFills;
|
|
78
83
|
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
79
84
|
private addForeignKeyStatements;
|
|
80
85
|
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
@@ -106,8 +111,6 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
106
111
|
* one column of a composite key, which its table never makes serial.
|
|
107
112
|
*/
|
|
108
113
|
getSqlType(field: FieldMeta): string;
|
|
109
|
-
/** The statements that alter `column` in place, as this dialect spells them. */
|
|
110
|
-
generateAlterColumnStatements(tableName: string, column: ColumnSchema, newDefinition: string): string[];
|
|
111
114
|
/** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
|
|
112
115
|
generateColumnComment(comment: string): string;
|
|
113
116
|
/** The `COMMENT ON` statements a table and its columns need, after the `CREATE TABLE`, where the engine uses them. */
|
|
@@ -121,8 +124,14 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
121
124
|
* How the entity differs from the table the database reported, compared by {@link diffTable}, the one
|
|
122
125
|
* drift detection runs, with types normalized as the engine stores them.
|
|
123
126
|
*/
|
|
124
|
-
diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
|
|
125
|
-
|
|
127
|
+
diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST, renamedColumns?: readonly Rename[]): SchemaDiff | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Both ends of rebuilding `actual` as `desired`. The new table is the entity's, keeping what it cannot
|
|
130
|
+
* know of: the indexes and triggers uql did not make, and foreign keys to tables no entity names. The
|
|
131
|
+
* old one is the engine's own statements, so a rollback restores it exactly, checks included.
|
|
132
|
+
*/
|
|
133
|
+
private rebuildOf;
|
|
134
|
+
diffOptions(): DiffOptions;
|
|
126
135
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
127
136
|
private columnNodeToSchema;
|
|
128
137
|
/** Whether a column's stored default is the one the entity declares, as this engine reprints it. */
|
|
@@ -174,12 +183,13 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
174
183
|
* generator added it. MySQL takes no name.
|
|
175
184
|
*/
|
|
176
185
|
generateDropPrimaryKeySql(tableName: string, constraintName?: string): string;
|
|
186
|
+
/** A stored generated column, which an engine that rebuilds tables takes only in a `CREATE TABLE`. */
|
|
187
|
+
private assertColumnAddable;
|
|
177
188
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
189
|
+
* Refuses `what` where the engine makes it only by rebuilding the table, which a migration generated
|
|
190
|
+
* from the entities does and a lone statement of the builder cannot.
|
|
180
191
|
*/
|
|
181
|
-
private
|
|
182
|
-
private assertPrimaryKeyAlterable;
|
|
192
|
+
private assertAlterable;
|
|
183
193
|
}
|
|
184
194
|
/**
|
|
185
195
|
* The entities as an AST, named by `generator`'s resolvers rather than a naming strategy, which would
|
|
@@ -6,14 +6,15 @@ import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
|
|
|
6
6
|
import { isAutoIncrement, qualifyName } from '../util/index.js';
|
|
7
7
|
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName, isOwnedName } from '../util/sql.util.js';
|
|
8
8
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
9
|
-
import { sameDefault } from './builder/expressions.js';
|
|
9
|
+
import { formatDefaultValue, sameDefault } from './builder/expressions.js';
|
|
10
10
|
import { splitSqlStatements } from './builder/splitSqlStatements.js';
|
|
11
11
|
import { indexDdlFor, tableDdlFor } from './ddl/index.js';
|
|
12
12
|
import { sizedType } from './ddl/tableDdl.js';
|
|
13
|
+
import { rebuildTable } from './ddl/tableRebuild.js';
|
|
13
14
|
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
14
15
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
15
16
|
import { assertIndexPredicate } from './indexPredicate.js';
|
|
16
|
-
import { added, alterations, dropped, nonEmpty, sides } from './schemaChange.js';
|
|
17
|
+
import { added, alterations, dropped, needsRebuild, newlyRequired, nonEmpty, sides } from './schemaChange.js';
|
|
17
18
|
import { dropTrigger, renderTrigger, stampTriggers } from './triggerSql.js';
|
|
18
19
|
/**
|
|
19
20
|
* Unified SQL schema generator.
|
|
@@ -92,7 +93,7 @@ export class SqlSchemaGenerator {
|
|
|
92
93
|
const withForeignKeys = options.foreignKeys ?? true;
|
|
93
94
|
// Inline only where a constraint cannot be added afterwards, which is what makes the cyclic case
|
|
94
95
|
// work everywhere else.
|
|
95
|
-
const inline = withForeignKeys &&
|
|
96
|
+
const inline = withForeignKeys && this.features.rebuildsTables;
|
|
96
97
|
// Namespaces first: a qualified `CREATE TABLE` fails against a schema nobody created, and the
|
|
97
98
|
// schema is the one part of the layout a migration cannot infer from the table it is making.
|
|
98
99
|
const statements = this.generateCreateSchemas(tables);
|
|
@@ -183,13 +184,21 @@ export class SqlSchemaGenerator {
|
|
|
183
184
|
* index along with its column, which would leave nothing to name. An alter is its drop, then its add.
|
|
184
185
|
*/
|
|
185
186
|
generateAlterTable(diff) {
|
|
186
|
-
const { tableName, schema, primaryKey } = diff;
|
|
187
|
-
const
|
|
187
|
+
const { tableName, schema, primaryKey, columns, rebuild } = diff;
|
|
188
|
+
const fills = this.defaultFills(columns);
|
|
189
|
+
if (rebuild) {
|
|
190
|
+
return rebuildTable(this.dialect, tableName, rebuild, { renames: diff.renamedColumns ?? [], fills });
|
|
191
|
+
}
|
|
192
|
+
const target = this.escapeId(tableName);
|
|
188
193
|
return [
|
|
189
194
|
...sides(diff.foreignKeys, 'from').map((foreignKey) => this.generateDropForeignKeySql(tableName, constraintNameOf(tableName, foreignKey))),
|
|
190
195
|
...(primaryKey?.from ? [this.generateDropPrimaryKeySql(tableName, primaryKey.from.name)] : []),
|
|
191
196
|
...sides(diff.indexes, 'from').map((index) => this.generateDropIndex(tableName, index.name, schema)),
|
|
192
197
|
...added(columns).flatMap((column) => this.addColumnStatements(tableName, column, schema)),
|
|
198
|
+
...[...fills].map(([column, value]) => {
|
|
199
|
+
const name = this.escapeId(column);
|
|
200
|
+
return `UPDATE ${target} SET ${name} = ${value} WHERE ${name} IS NULL;`;
|
|
201
|
+
}),
|
|
193
202
|
...alterations(columns).flatMap(({ from, to }) => this.tableDdl.alterColumn(tableName, to, this.generateColumnDefinitionFromSchema(to), from)),
|
|
194
203
|
...dropped(columns).flatMap((column) => this.tableDdl.dropColumn(tableName, column.name)),
|
|
195
204
|
...this.addIndexStatements(tableName, sides(diff.indexes, 'to')),
|
|
@@ -197,6 +206,15 @@ export class SqlSchemaGenerator {
|
|
|
197
206
|
...this.addForeignKeyStatements(tableName, sides(diff.foreignKeys, 'to')),
|
|
198
207
|
];
|
|
199
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Each column the changes make required while declaring a default, and that default as SQL: the rows
|
|
211
|
+
* already there hold a null it has to replace, and it is the only value the entity says it may take.
|
|
212
|
+
*/
|
|
213
|
+
defaultFills(columns) {
|
|
214
|
+
return new Map(newlyRequired(columns)
|
|
215
|
+
.filter(({ from, to }) => from && to.defaultValue !== undefined)
|
|
216
|
+
.map(({ to }) => [to.name, formatDefaultValue(to.defaultValue, this.dialect, to.type)]));
|
|
217
|
+
}
|
|
200
218
|
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
201
219
|
addForeignKeyStatements(tableName, foreignKeys) {
|
|
202
220
|
return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
|
|
@@ -212,7 +230,7 @@ export class SqlSchemaGenerator {
|
|
|
212
230
|
addColumnStatements(tableName, column, schema) {
|
|
213
231
|
this.assertColumnAddable(tableName, column);
|
|
214
232
|
return [
|
|
215
|
-
this.tableDdl.
|
|
233
|
+
...this.tableDdl.addColumnStatements(tableName, column, (it) => this.generateColumnDefinitionFromSchema(it)),
|
|
216
234
|
...this.generateColumnCommentStatement(tableName, column, schema),
|
|
217
235
|
];
|
|
218
236
|
}
|
|
@@ -270,10 +288,6 @@ export class SqlSchemaGenerator {
|
|
|
270
288
|
? this.serialType(canonical)
|
|
271
289
|
: this.canonicalTypeToSql(canonical);
|
|
272
290
|
}
|
|
273
|
-
/** The statements that alter `column` in place, as this dialect spells them. */
|
|
274
|
-
generateAlterColumnStatements(tableName, column, newDefinition) {
|
|
275
|
-
return this.tableDdl.alterColumn(tableName, column, newDefinition);
|
|
276
|
-
}
|
|
277
291
|
/** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
|
|
278
292
|
generateColumnComment(comment) {
|
|
279
293
|
return this.features.commentSyntax === 'inline' ? ` COMMENT ${this.dialect.escape(comment)}` : '';
|
|
@@ -302,7 +316,7 @@ export class SqlSchemaGenerator {
|
|
|
302
316
|
* How the entity differs from the table the database reported, compared by {@link diffTable}, the one
|
|
303
317
|
* drift detection runs, with types normalized as the engine stores them.
|
|
304
318
|
*/
|
|
305
|
-
diffSchema(entity, currentTable, desiredAst) {
|
|
319
|
+
diffSchema(entity, currentTable, desiredAst, renamedColumns) {
|
|
306
320
|
const meta = getMeta(entity);
|
|
307
321
|
const tableName = this.resolveTableName(meta);
|
|
308
322
|
const schema = this.resolveSchema(meta);
|
|
@@ -326,11 +340,12 @@ export class SqlSchemaGenerator {
|
|
|
326
340
|
return { to: this.columnNodeToSchema(it.expected) };
|
|
327
341
|
}
|
|
328
342
|
if (it.type === 'drop') {
|
|
329
|
-
return { from: this.columnNodeToSchema(it.actual) };
|
|
343
|
+
return { from: this.columnNodeToSchema(it.actual), isBreaking: true };
|
|
330
344
|
}
|
|
331
345
|
return {
|
|
332
346
|
from: this.columnNodeToSchema(it.actual),
|
|
333
347
|
to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
|
|
348
|
+
isBreaking: it.isBreaking,
|
|
334
349
|
};
|
|
335
350
|
});
|
|
336
351
|
const keyDiff = tableDiff?.primaryKeyDiff;
|
|
@@ -342,11 +357,7 @@ export class SqlSchemaGenerator {
|
|
|
342
357
|
name: derivedPrimaryKeyName(tableName, keyDiff.expected.columns),
|
|
343
358
|
},
|
|
344
359
|
};
|
|
345
|
-
|
|
346
|
-
// the table), since a difference nothing can apply would throw on every sync; `drift:check` names it.
|
|
347
|
-
const relationDiffs = this.features.foreignKeyAlter
|
|
348
|
-
? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
|
|
349
|
-
: [];
|
|
360
|
+
const relationDiffs = diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions());
|
|
350
361
|
const foreignKeys = relationDiffs.map(({ actual, expected }) => ({
|
|
351
362
|
from: actual && foreignKeyOf(actual),
|
|
352
363
|
to: expected && foreignKeyOf(expected),
|
|
@@ -363,8 +374,40 @@ export class SqlSchemaGenerator {
|
|
|
363
374
|
...indexes.toAlter.map(({ from, to }) => ({ from: indexNodeToSchema(from), to: indexNodeToSchema(to) })),
|
|
364
375
|
]),
|
|
365
376
|
foreignKeys: nonEmpty(foreignKeys),
|
|
377
|
+
renamedColumns: nonEmpty(renamedColumns ?? []),
|
|
378
|
+
};
|
|
379
|
+
if (!(alter.primaryKey || alter.columns || alter.indexes || alter.foreignKeys || alter.renamedColumns)) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return this.features.rebuildsTables && needsRebuild(alter)
|
|
383
|
+
? { ...alter, rebuild: this.rebuildOf(desired, currentTable, indexes.kept, renamedColumns ?? []) }
|
|
384
|
+
: alter;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Both ends of rebuilding `actual` as `desired`. The new table is the entity's, keeping what it cannot
|
|
388
|
+
* know of: the indexes and triggers uql did not make, and foreign keys to tables no entity names. The
|
|
389
|
+
* old one is the engine's own statements, so a rollback restores it exactly, checks included.
|
|
390
|
+
*/
|
|
391
|
+
rebuildOf(desired, actual, kept, renames) {
|
|
392
|
+
const definition = actual.definition ?? [];
|
|
393
|
+
const read = new Set(actual.indexes.map((index) => index.name));
|
|
394
|
+
const own = (entry) => entry.kind === 'trigger' && isOwnedName(entry.name);
|
|
395
|
+
const verbatim = (entries) => entries.map((entry) => `${entry.sql};`);
|
|
396
|
+
const stored = (table) => [...table.columns.values()].filter((column) => !column.generatedAs).map((column) => column.name);
|
|
397
|
+
return {
|
|
398
|
+
from: {
|
|
399
|
+
statements: verbatim(definition.filter((entry) => !own(entry))),
|
|
400
|
+
columns: stored(actual).map((name) => renames.find((rename) => rename.to === name)?.from ?? name),
|
|
401
|
+
},
|
|
402
|
+
to: {
|
|
403
|
+
statements: [
|
|
404
|
+
...this.generateCreateTableFromNode({ ...desired, externalForeignKeys: actual.externalForeignKeys }),
|
|
405
|
+
...kept.map((index) => this.generateCreateIndexFromNode(index)),
|
|
406
|
+
...verbatim(definition.filter((entry) => (entry.kind === 'index' && !read.has(entry.name)) || (entry.kind === 'trigger' && !own(entry)))),
|
|
407
|
+
],
|
|
408
|
+
columns: stored(desired),
|
|
409
|
+
},
|
|
366
410
|
};
|
|
367
|
-
return alter.primaryKey || alter.columns || alter.indexes || alter.foreignKeys ? alter : undefined;
|
|
368
411
|
}
|
|
369
412
|
diffOptions() {
|
|
370
413
|
return {
|
|
@@ -406,6 +449,9 @@ export class SqlSchemaGenerator {
|
|
|
406
449
|
const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
|
|
407
450
|
constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
|
|
408
451
|
}
|
|
452
|
+
for (const foreignKey of table.externalForeignKeys) {
|
|
453
|
+
constraints.push(this.foreignKeyConstraint(table.name, foreignKey, this.escapeId(foreignKey.references.table)));
|
|
454
|
+
}
|
|
409
455
|
const target = this.dialect.escapeQualifiedId(table.name, table.schema);
|
|
410
456
|
let createSql = `${this.tableDdl.createTable(target, !!options.ifNotExists)} (\n`;
|
|
411
457
|
createSql += columns.map((col) => ` ${col}`).join(',\n');
|
|
@@ -497,9 +543,7 @@ export class SqlSchemaGenerator {
|
|
|
497
543
|
}
|
|
498
544
|
/** `ADD COLUMN`, plus the foreign key and index the column declares, as `CREATE TABLE` lifts them. */
|
|
499
545
|
generateAddColumnSql(tableName, column) {
|
|
500
|
-
this.
|
|
501
|
-
const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
|
|
502
|
-
const statements = [this.tableDdl.addColumn(tableName, colSql)];
|
|
546
|
+
const statements = this.addColumnStatements(tableName, this.columnNodeToSchema(fullColumnDefinitionToNode(column, tableName)));
|
|
503
547
|
const foreignKey = columnForeignKey(column);
|
|
504
548
|
if (foreignKey) {
|
|
505
549
|
statements.push(...this.addForeignKeyStatements(tableName, [foreignKey]));
|
|
@@ -508,12 +552,12 @@ export class SqlSchemaGenerator {
|
|
|
508
552
|
if (index) {
|
|
509
553
|
statements.push(this.generateCreateIndex(tableName, index));
|
|
510
554
|
}
|
|
511
|
-
statements.push(...this.generateColumnCommentStatement(tableName, column));
|
|
512
555
|
return statements;
|
|
513
556
|
}
|
|
514
557
|
generateAlterColumnSql(tableName, columnName, column) {
|
|
558
|
+
this.assertAlterable(`Altering the column "${columnName}" of "${tableName}"`);
|
|
515
559
|
const node = fullColumnDefinitionToNode(column, tableName);
|
|
516
|
-
return this.
|
|
560
|
+
return this.tableDdl.alterColumn(tableName, { ...this.columnNodeToSchema(node), name: columnName }, this.generateColumnFromNode(node));
|
|
517
561
|
}
|
|
518
562
|
generateDropColumnSql(tableName, columnName) {
|
|
519
563
|
return this.tableDdl.dropColumn(tableName, columnName);
|
|
@@ -536,9 +580,7 @@ export class SqlSchemaGenerator {
|
|
|
536
580
|
`ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction}`);
|
|
537
581
|
}
|
|
538
582
|
generateAddForeignKeySql(tableName, foreignKey) {
|
|
539
|
-
|
|
540
|
-
throw new UqlUsageError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
541
|
-
}
|
|
583
|
+
this.assertAlterable(`Adding a foreign key to "${tableName}"`);
|
|
542
584
|
const constraint = this.foreignKeyConstraint(tableName, foreignKey, this.escapeId(foreignKey.references.table));
|
|
543
585
|
return `ALTER TABLE ${this.escapeId(tableName)} ADD ${constraint};`;
|
|
544
586
|
}
|
|
@@ -551,7 +593,7 @@ export class SqlSchemaGenerator {
|
|
|
551
593
|
* rather than emitting DDL it will reject.
|
|
552
594
|
*/
|
|
553
595
|
generateAddPrimaryKeySql(tableName, columns, name) {
|
|
554
|
-
this.
|
|
596
|
+
this.assertAlterable(`Changing the primary key of "${tableName}"`);
|
|
555
597
|
const constraintName = this.escapeId(name ?? derivedPrimaryKeyName(tableName, columns));
|
|
556
598
|
const pkCols = columns.map((c) => this.escapeId(c)).join(', ');
|
|
557
599
|
return `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} PRIMARY KEY (${pkCols});`;
|
|
@@ -561,7 +603,7 @@ export class SqlSchemaGenerator {
|
|
|
561
603
|
* generator added it. MySQL takes no name.
|
|
562
604
|
*/
|
|
563
605
|
generateDropPrimaryKeySql(tableName, constraintName) {
|
|
564
|
-
this.
|
|
606
|
+
this.assertAlterable(`Changing the primary key of "${tableName}"`);
|
|
565
607
|
const table = this.escapeId(tableName);
|
|
566
608
|
if (this.dialect.dropPrimaryKeySyntax === 'DROP PRIMARY KEY') {
|
|
567
609
|
return `ALTER TABLE ${table} DROP PRIMARY KEY;`;
|
|
@@ -572,24 +614,21 @@ export class SqlSchemaGenerator {
|
|
|
572
614
|
}
|
|
573
615
|
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
574
616
|
}
|
|
575
|
-
/**
|
|
576
|
-
* A column an `ALTER` can carry. Only a generated one is ever refused, and only where the engine
|
|
577
|
-
* takes it in a `CREATE TABLE` but not afterwards.
|
|
578
|
-
*/
|
|
617
|
+
/** A stored generated column, which an engine that rebuilds tables takes only in a `CREATE TABLE`. */
|
|
579
618
|
assertColumnAddable(tableName, column) {
|
|
580
|
-
if (
|
|
581
|
-
|
|
619
|
+
if (column.generatedAs) {
|
|
620
|
+
this.assertAlterable(`Adding the stored column "${column.name}" to "${tableName}"`);
|
|
582
621
|
}
|
|
583
|
-
throw new UqlUsageError(`${this.dialect}: Cannot add the computed column "${column.name}" to the existing table ` +
|
|
584
|
-
`"${tableName}" - this database only accepts one in a CREATE TABLE. Drop \`stored\` to have the ` +
|
|
585
|
-
'expression spliced into each statement instead, or recreate the table in a written migration.');
|
|
586
622
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Refuses `what` where the engine makes it only by rebuilding the table, which a migration generated
|
|
625
|
+
* from the entities does and a lone statement of the builder cannot.
|
|
626
|
+
*/
|
|
627
|
+
assertAlterable(what) {
|
|
628
|
+
if (this.features.rebuildsTables) {
|
|
629
|
+
throw new UqlUsageError(`${this.dialect}: ${what} rebuilds the table, which a migration generated from the entities does ` +
|
|
630
|
+
'(`uql-migrate generate:entities`) and a hand-written one cannot.');
|
|
590
631
|
}
|
|
591
|
-
throw new UqlUsageError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
|
|
592
|
-
'for it. Recreate the table in a written migration.');
|
|
593
632
|
}
|
|
594
633
|
}
|
|
595
634
|
/**
|