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.
Files changed (37) hide show
  1. package/dist/dialect/abstractSqlDialect.d.ts +1 -1
  2. package/dist/dialect/mysqlLikeSqlDialect.js +1 -3
  3. package/dist/dialect/pgLikeSqlDialect.js +1 -3
  4. package/dist/migrate/ddl/tableDdl.d.ts +5 -0
  5. package/dist/migrate/ddl/tableDdl.js +18 -7
  6. package/dist/migrate/ddl/tableRebuild.d.ts +17 -0
  7. package/dist/migrate/ddl/tableRebuild.js +56 -0
  8. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +3 -1
  9. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +7 -1
  10. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
  11. package/dist/migrate/introspection/baseSqlIntrospector.js +29 -4
  12. package/dist/migrate/introspection/sqliteIntrospector.d.ts +3 -3
  13. package/dist/migrate/introspection/sqliteIntrospector.js +9 -7
  14. package/dist/migrate/migrationTarget.js +27 -1
  15. package/dist/migrate/migrator.d.ts +20 -3
  16. package/dist/migrate/migrator.js +108 -17
  17. package/dist/migrate/schemaChange.d.ts +12 -1
  18. package/dist/migrate/schemaChange.js +28 -0
  19. package/dist/migrate/schemaGenerator.d.ts +19 -9
  20. package/dist/migrate/schemaGenerator.js +81 -42
  21. package/dist/migrate/triggerSql.js +19 -14
  22. package/dist/mongo/mongoDialect.js +1 -3
  23. package/dist/mssql/mssqlDialect.js +1 -3
  24. package/dist/schema/indexDifferences.d.ts +2 -1
  25. package/dist/schema/indexDifferences.js +2 -1
  26. package/dist/schema/matchByKey.d.ts +9 -0
  27. package/dist/schema/matchByKey.js +18 -0
  28. package/dist/schema/schemaAST.js +1 -0
  29. package/dist/schema/schemaASTDiffer.d.ts +13 -0
  30. package/dist/schema/schemaASTDiffer.js +35 -4
  31. package/dist/schema/types.d.ts +5 -1
  32. package/dist/sqlite/sqliteDialect.d.ts +0 -1
  33. package/dist/sqlite/sqliteDialect.js +1 -4
  34. package/dist/type/dialect.d.ts +4 -10
  35. package/dist/type/migration.d.ts +45 -4
  36. package/package.json +1 -1
  37. package/skills/uql-orm/SKILL.md +1 -1
@@ -106,7 +106,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
106
106
  createSchemaSql(schema: string): string;
107
107
  readonly isolationLevelStrategy: 'inline' | 'set-before' | 'none';
108
108
  readonly alterColumnStrategy: 'separate-clauses' | 'single-statement';
109
- readonly alterColumnSyntax: 'ALTER COLUMN' | 'MODIFY COLUMN' | 'none';
109
+ readonly alterColumnSyntax: 'ALTER COLUMN' | 'MODIFY COLUMN';
110
110
  readonly dropForeignKeySyntax: 'DROP CONSTRAINT' | 'DROP FOREIGN KEY';
111
111
  /**
112
112
  * `DROP CONSTRAINT <name>` where a primary key is a named constraint like any other; MySQL spells
@@ -19,9 +19,7 @@ export const MYSQL_FEATURES = {
19
19
  indexIfNotExists: false,
20
20
  schemas: true,
21
21
  dropTableCascade: false,
22
- foreignKeyAlter: true,
23
- primaryKeyAlter: true,
24
- generatedColumnAdd: true,
22
+ rebuildsTables: false,
25
23
  commentSyntax: 'inline',
26
24
  vectorIndexRequiresNotNull: false,
27
25
  vectorSupportsLength: false,
@@ -21,9 +21,7 @@ export const PG_FEATURES = {
21
21
  indexIfNotExists: true,
22
22
  schemas: true,
23
23
  dropTableCascade: true,
24
- foreignKeyAlter: true,
25
- primaryKeyAlter: true,
26
- generatedColumnAdd: true,
24
+ rebuildsTables: false,
27
25
  commentSyntax: 'statement',
28
26
  vectorIndexRequiresNotNull: false,
29
27
  vectorSupportsLength: true,
@@ -15,6 +15,11 @@ export declare class TableDdl {
15
15
  /** A `CREATE TABLE` up to its column list, a no-op where `ifNotExists` and the table is already there. */
16
16
  createTable(target: string, ifNotExists: boolean): string;
17
17
  addColumn(table: string, definition: string): string;
18
+ /**
19
+ * `column` added, spelled by `render`. MySQL fills a zero into the rows already there for a required
20
+ * column with no default, so there it is added nullable and required after, failing on them as elsewhere.
21
+ */
22
+ addColumnStatements(table: string, column: ColumnSchema, render: (column: ColumnSchema) => string): string[];
18
23
  dropColumn(table: string, column: string): string[];
19
24
  /**
20
25
  * What changes `column` to what it now declares. `definition` is the whole column, which MySQL's
@@ -1,5 +1,5 @@
1
- import { UqlUsageError } from '../../util/uqlError.js';
2
1
  import { formatDefaultValue, sameDefault } from '../builder/expressions.js';
2
+ import { lacksValue } from '../schemaChange.js';
3
3
  /**
4
4
  * A column's type with the size it was read back with, unless its spelling already carries one:
5
5
  * introspection reports `VARCHAR` and `255` apart, where a type from an entity is already whole.
@@ -30,6 +30,19 @@ export class TableDdl {
30
30
  addColumn(table, definition) {
31
31
  return `ALTER TABLE ${this.dialect.escapeId(table)} ADD COLUMN ${definition};`;
32
32
  }
33
+ /**
34
+ * `column` added, spelled by `render`. MySQL fills a zero into the rows already there for a required
35
+ * column with no default, so there it is added nullable and required after, failing on them as elsewhere.
36
+ */
37
+ addColumnStatements(table, column, render) {
38
+ if (this.dialect.alterColumnSyntax !== 'MODIFY COLUMN' || !lacksValue(column)) {
39
+ return [this.addColumn(table, render(column))];
40
+ }
41
+ return [
42
+ this.addColumn(table, render({ ...column, nullable: true })),
43
+ ...this.alterColumn(table, column, render(column)),
44
+ ];
45
+ }
33
46
  dropColumn(table, column) {
34
47
  return [`ALTER TABLE ${this.dialect.escapeId(table)} DROP COLUMN ${this.dialect.escapeId(column)};`];
35
48
  }
@@ -39,17 +52,15 @@ export class TableDdl {
39
52
  * column was (`from`), only the clauses that changed.
40
53
  */
41
54
  alterColumn(table, column, definition, from) {
42
- if (this.dialect.alterColumnSyntax === 'none') {
43
- throw new UqlUsageError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
44
- `This database does not support ALTER COLUMN.`);
45
- }
46
55
  const target = this.dialect.escapeId(table);
47
56
  if (this.dialect.alterColumnStrategy !== 'separate-clauses') {
48
57
  return [`ALTER TABLE ${target} ${this.dialect.alterColumnSyntax} ${definition};`];
49
58
  }
50
- const alter = `ALTER TABLE ${target} ALTER COLUMN ${this.dialect.escapeId(column.name)}`;
59
+ const name = this.dialect.escapeId(column.name);
60
+ const alter = `ALTER TABLE ${target} ALTER COLUMN ${name}`;
51
61
  return [
52
- (!from || from.type !== column.type) && `${alter} TYPE ${column.type};`,
62
+ // Cast, since the engine converts only between types it deems compatible: text to integer needs saying.
63
+ (!from || from.type !== column.type) && `${alter} TYPE ${column.type} USING ${name}::${column.type};`,
53
64
  (!from || from.nullable !== column.nullable) && `${alter} ${column.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'};`,
54
65
  (!from || !sameDefault(column.defaultValue, from.defaultValue, this.dialect)) &&
55
66
  (column.defaultValue === undefined ? `${alter} DROP DEFAULT;` : `${alter} SET${this.defaultClause(column)};`),
@@ -0,0 +1,17 @@
1
+ import type { AbstractSqlDialect } from '../../dialect/index.js';
2
+ import type { RebuiltTable, Rename } from '../../type/index.js';
3
+ /** What the copy reads besides the columns both sides share: renamed ones, and a default filling a column's nulls. */
4
+ export type RebuildCopy = {
5
+ readonly renames: readonly Rename[];
6
+ readonly fills: ReadonlyMap<string, string>;
7
+ };
8
+ /**
9
+ * SQLite's documented rebuild of `table`: a new table in the shape `to` gives, the rows copied into it,
10
+ * the old one dropped and the new one renamed into its place, then its indexes and triggers. A foreign
11
+ * key pointing at the table would delete or null its rows with the drop, so the guard fails the whole
12
+ * rebuild first unless foreign keys are off, as the migrator turns them off on a SQLite connection.
13
+ */
14
+ export declare function rebuildTable(dialect: AbstractSqlDialect, table: string, { from, to }: {
15
+ readonly from: RebuiltTable;
16
+ readonly to: RebuiltTable;
17
+ }, copy: RebuildCopy): string[];
@@ -0,0 +1,56 @@
1
+ import { OWNED_PREFIX } from '../../dialect/aliases.js';
2
+ /** The table a rebuild copies into, under a name no entity's table takes. */
3
+ const NEW_TABLE_PREFIX = `${OWNED_PREFIX}_new_`;
4
+ /** The table the guard inserts into, refused while foreign keys would take rows down with the rebuilt one. */
5
+ const GUARD_TABLE = `${OWNED_PREFIX}_rebuild_guard`;
6
+ /**
7
+ * SQLite's documented rebuild of `table`: a new table in the shape `to` gives, the rows copied into it,
8
+ * the old one dropped and the new one renamed into its place, then its indexes and triggers. A foreign
9
+ * key pointing at the table would delete or null its rows with the drop, so the guard fails the whole
10
+ * rebuild first unless foreign keys are off, as the migrator turns them off on a SQLite connection.
11
+ */
12
+ export function rebuildTable(dialect, table, { from, to }, copy) {
13
+ const id = (name) => dialect.escapeId(name);
14
+ const target = id(`${NEW_TABLE_PREFIX}${table}`);
15
+ const [create, ...rest] = to.statements;
16
+ const copied = to.columns.flatMap((column) => {
17
+ const source = copy.renames.find((rename) => rename.to === column)?.from ?? column;
18
+ if (!from.columns.includes(source)) {
19
+ return [];
20
+ }
21
+ const fill = copy.fills.get(column);
22
+ return [{ column: id(column), value: fill === undefined ? id(source) : `coalesce(${id(source)}, ${fill})` }];
23
+ });
24
+ return [
25
+ ...guard(dialect, table),
26
+ renameCreatedTable(create, target),
27
+ ...(copied.length
28
+ ? [
29
+ `INSERT INTO ${target} (${copied.map((it) => it.column).join(', ')}) ` +
30
+ `SELECT ${copied.map((it) => it.value).join(', ')} FROM ${id(table)};`,
31
+ ]
32
+ : []),
33
+ `DROP TABLE ${id(table)};`,
34
+ `ALTER TABLE ${target} RENAME TO ${id(table)};`,
35
+ ...rest,
36
+ ];
37
+ }
38
+ /** Fails, naming the way out, where foreign keys are on and any table references `table`. */
39
+ function guard(dialect, table) {
40
+ const id = (name) => dialect.escapeId(name);
41
+ const guardTable = id(GUARD_TABLE);
42
+ const referencing = id('referencing');
43
+ const refusal = id(`turn foreign keys off to rebuild ${table}: the rows referencing it would be lost`);
44
+ return [
45
+ `CREATE TABLE IF NOT EXISTS ${guardTable} (${referencing} INTEGER CONSTRAINT ${refusal} CHECK (${referencing} = 0));`,
46
+ `INSERT INTO ${guardTable} SELECT count(*) FROM pragma_foreign_keys AS k, sqlite_master AS m, ` +
47
+ `pragma_foreign_key_list(m.name) AS f WHERE k.foreign_keys AND m.type = 'table' ` +
48
+ `AND f.${id('table')} = ${dialect.escape(table)} COLLATE NOCASE;`,
49
+ `DROP TABLE ${guardTable};`,
50
+ ];
51
+ }
52
+ const CREATE_TABLE_NAME = /^(\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)(?:"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[[^\]]*\]|[^\s(]+)/i;
53
+ /** A `CREATE TABLE` as the engine or the generator spelled it, creating `name` instead. */
54
+ function renameCreatedTable(sql, name) {
55
+ return sql.replace(CREATE_TABLE_NAME, `$1${name}`);
56
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ForeignKeyAction } from '../../schema/types.js';
2
- import type { ColumnSchema, InstalledTriggers, ForeignKeySchema, IndexSchema, PrimaryKeySchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, TableSchema } from '../../type/index.js';
2
+ import type { ColumnSchema, InstalledTriggers, ForeignKeySchema, IndexSchema, PrimaryKeySchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, StoredDefinition, TableSchema } from '../../type/index.js';
3
3
  import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
4
4
  /**
5
5
  * Reads the rows of one statement while introspecting a table.
@@ -37,6 +37,8 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
37
37
  */
38
38
  protected readonly defaultSchemaExpr: string;
39
39
  getTableSchema(tableName: string): Promise<TableSchema | undefined>;
40
+ /** See {@link TableSchema.definition}: none, but where the engine keeps the statements themselves. */
41
+ protected getDefinition(_read: TableRowReader, _tableName: string): Promise<StoredDefinition[] | undefined>;
40
42
  getTableNames(): Promise<string[]>;
41
43
  /**
42
44
  * Every trigger uql installed in this schema, by table and then by name, with the statements recreating
@@ -30,11 +30,12 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
30
30
  if (!exists) {
31
31
  return undefined;
32
32
  }
33
- const [columns, indexes, foreignKeys, primaryKey] = await Promise.all([
33
+ const [columns, indexes, foreignKeys, primaryKey, definition] = await Promise.all([
34
34
  this.getColumns(read, tableName),
35
35
  this.getIndexes(read, tableName),
36
36
  this.getForeignKeys(read, tableName),
37
37
  this.getPrimaryKey(read, tableName),
38
+ this.getDefinition(read, tableName),
38
39
  ]);
39
40
  return {
40
41
  name: tableName,
@@ -42,9 +43,14 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
42
43
  primaryKey,
43
44
  indexes,
44
45
  foreignKeys,
46
+ definition,
45
47
  };
46
48
  });
47
49
  }
50
+ /** See {@link TableSchema.definition}: none, but where the engine keeps the statements themselves. */
51
+ async getDefinition(_read, _tableName) {
52
+ return undefined;
53
+ }
48
54
  async getTableNames() {
49
55
  return this.withSqlQuerier(async (querier) => {
50
56
  const results = await querier.all(this.getTableNamesQuery());
@@ -1,7 +1,7 @@
1
1
  import type { AbstractSqlDialect } from '../../dialect/index.js';
2
2
  import type { IndexFacet } from '../../schema/indexDifferences.js';
3
3
  import { SchemaAST } from '../../schema/schemaAST.js';
4
- import type { TableSchema } from '../../type/migration.js';
4
+ import type { ColumnRenames, TableSchema } from '../../type/migration.js';
5
5
  /**
6
6
  * Base class for SQL introspectors with shared AST building logic.
7
7
  */
@@ -22,7 +22,9 @@ export declare abstract class BaseSqlIntrospector {
22
22
  * rather than raised: the point of naming them is to read a database other things are still
23
23
  * changing, where scanning every table is both wasted work and a relation that can vanish mid-scan.
24
24
  */
25
- introspect(tables?: readonly string[]): Promise<SchemaAST>;
25
+ introspect(tables?: readonly string[], renames?: ColumnRenames): Promise<SchemaAST>;
26
+ /** `table` with each column `renames` names under its new name, wherever the table names it. */
27
+ private renamed;
26
28
  abstract getTableNames(): Promise<string[]>;
27
29
  abstract getTableSchema(tableName: string): Promise<TableSchema | undefined>;
28
30
  /**
@@ -1,7 +1,7 @@
1
1
  import { canonicalColumnType } from '../../schema/canonicalType.js';
2
2
  import { createTableNode, keyOfColumns, SchemaAST } from '../../schema/schemaAST.js';
3
3
  import { escapeSqlId } from '../../util/index.js';
4
- import { derivedForeignKeyName } from '../../util/sql.util.js';
4
+ import { derivedForeignKeyName, qualifyName } from '../../util/sql.util.js';
5
5
  /**
6
6
  * Base class for SQL introspectors with shared AST building logic.
7
7
  */
@@ -27,17 +27,39 @@ export class BaseSqlIntrospector {
27
27
  * rather than raised: the point of naming them is to read a database other things are still
28
28
  * changing, where scanning every table is both wasted work and a relation that can vanish mid-scan.
29
29
  */
30
- async introspect(tables) {
30
+ async introspect(tables, renames) {
31
31
  const tableNames = tables ?? (await this.getTableNames());
32
32
  const tableSchemas = [];
33
33
  for (const tableName of tableNames) {
34
34
  const schema = await this.getTableSchema(tableName);
35
35
  if (schema) {
36
- tableSchemas.push(schema);
36
+ tableSchemas.push(renames ? this.renamed(schema, renames) : schema);
37
37
  }
38
38
  }
39
39
  return this.buildAST(tableSchemas);
40
40
  }
41
+ /** `table` with each column `renames` names under its new name, wherever the table names it. */
42
+ renamed(table, renames) {
43
+ const nameIn = (tableName) => (column) => renames.get(qualifyName(tableName, this.schema))?.find((rename) => rename.from === column)?.to ?? column;
44
+ const own = nameIn(table.name);
45
+ return {
46
+ ...table,
47
+ columns: table.columns.map((column) => ({ ...column, name: own(column.name) })),
48
+ primaryKey: table.primaryKey && { ...table.primaryKey, columns: table.primaryKey.columns.map(own) },
49
+ indexes: table.indexes?.map((index) => ({
50
+ ...index,
51
+ entries: index.entries.map((entry) => (entry.expression ? entry : { ...entry, column: own(entry.column) })),
52
+ })),
53
+ foreignKeys: table.foreignKeys?.map((foreignKey) => ({
54
+ ...foreignKey,
55
+ columns: foreignKey.columns.map(own),
56
+ references: {
57
+ ...foreignKey.references,
58
+ columns: foreignKey.references.columns.map(nameIn(foreignKey.references.table)),
59
+ },
60
+ })),
61
+ };
62
+ }
41
63
  /**
42
64
  * Build SchemaAST from table schemas.
43
65
  */
@@ -75,13 +97,16 @@ export class BaseSqlIntrospector {
75
97
  // key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back to the flags
76
98
  // for an introspector that reports no key of its own.
77
99
  table.primaryKey = schema.primaryKey ?? keyOfColumns(schema.columns);
100
+ table.definition = schema.definition;
78
101
  return table;
79
102
  }
80
103
  buildRelationships(ast, tableNodes, schema, fromTable) {
81
104
  for (const fk of schema.foreignKeys ?? []) {
82
105
  const toTable = tableNodes.get(fk.references.table);
83
- if (!toTable)
106
+ if (!toTable) {
107
+ fromTable.externalForeignKeys.push(fk);
84
108
  continue;
109
+ }
85
110
  const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
86
111
  const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
87
112
  if (fromColumns.length > 0 && toColumns.length > 0) {
@@ -1,5 +1,5 @@
1
1
  import type { IndexFacet } from '../../schema/indexDifferences.js';
2
- import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
+ import type { ColumnSchema, ForeignKeySchema, IndexSchema, StoredDefinition } from '../../type/index.js';
3
3
  import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
4
4
  /**
5
5
  * SQLite schema introspector
@@ -32,12 +32,12 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
32
32
  protected mapColumnsResult(read: TableRowReader, tableName: string, results: SqliteColumnRow[]): Promise<ColumnSchema[]>;
33
33
  protected mapIndexesResult(read: TableRowReader, _tableName: string, results: SqliteIndexRow[]): Promise<IndexSchema[]>;
34
34
  protected mapForeignKeysResult(_read: TableRowReader, tableName: string, results: SqliteForeignKeyRow[]): Promise<ForeignKeySchema[]>;
35
+ /** Every statement `sqlite_master` keeps for the table, its `CREATE TABLE` first. An automatic index has none. */
36
+ protected getDefinition(read: TableRowReader, tableName: string): Promise<StoredDefinition[]>;
35
37
  protected mapPrimaryKeyResult(results: SqliteColumnRow[]): string[] | undefined;
36
38
  private getUniqueColumns;
37
39
  /** libSQL's `libsql_vector_idx(col, 'metric=...')`, read back from the statement that created it. */
38
40
  private getVectorIndex;
39
- /** The statement that created the table, which is where SQLite keeps every expression it was given. */
40
- private getTableDdl;
41
41
  private getIndexColumns;
42
42
  protected normalizeType(type: string): string;
43
43
  protected extractLength(type: string): number | undefined;
@@ -69,7 +69,10 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
69
69
  const uniqueColumns = await this.getUniqueColumns(read, tableName);
70
70
  // Only a sole `INTEGER PRIMARY KEY` is the rowid, which is what numbers itself.
71
71
  const soleKey = results.filter((row) => row.pk > 0).length === 1;
72
- const ddl = results.some((row) => row.hidden === STORED_GENERATED) ? await this.getTableDdl(read, tableName) : '';
72
+ const [table] = results.some((row) => row.hidden === STORED_GENERATED)
73
+ ? await this.getDefinition(read, tableName)
74
+ : [];
75
+ const ddl = table?.sql ?? '';
73
76
  return results.map((row) => ({
74
77
  name: row.name,
75
78
  type: this.normalizeType(row.type),
@@ -136,6 +139,11 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
136
139
  };
137
140
  });
138
141
  }
142
+ /** Every statement `sqlite_master` keeps for the table, its `CREATE TABLE` first. An automatic index has none. */
143
+ getDefinition(read, tableName) {
144
+ return read(
145
+ /*sql*/ `SELECT type AS kind, name, sql FROM sqlite_master WHERE tbl_name = ? AND sql IS NOT NULL ORDER BY type <> 'table'`, [tableName]);
146
+ }
139
147
  mapPrimaryKeyResult(results) {
140
148
  const pkColumns = results.filter((r) => r.pk > 0).sort((a, b) => a.pk - b.pk);
141
149
  if (pkColumns.length === 0) {
@@ -176,12 +184,6 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
176
184
  distance: this.dialect.indexedDistance(metric),
177
185
  };
178
186
  }
179
- /** The statement that created the table, which is where SQLite keeps every expression it was given. */
180
- async getTableDdl(read, tableName) {
181
- const [row] = await read(
182
- /*sql*/ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`, [tableName]);
183
- return row.sql;
184
- }
185
187
  getIndexColumns(read, indexName) {
186
188
  return read(/*sql*/ `PRAGMA index_info(${this.escapeId(indexName)})`);
187
189
  }
@@ -10,8 +10,34 @@ import { MongoMigrationStorage } from './storage/mongoStorage.js';
10
10
  const sqlSession = (querier) => ({
11
11
  querier,
12
12
  run: (statement) => querier.run(statement),
13
- transaction: (work) => querier.transaction(work),
13
+ transaction: (work) => querier.dialect.features.rebuildsTables ? withForeignKeysOff(querier, work) : querier.transaction(work),
14
14
  });
15
+ /**
16
+ * SQLite's own procedure for a schema change: foreign keys off before the transaction, where the switch
17
+ * takes effect, and every reference checked before it commits. Otherwise dropping a table to rebuild it
18
+ * deletes the rows pointing at it. A connection whose transaction runs elsewhere keeps them on, which the
19
+ * rebuild's guard refuses.
20
+ */
21
+ async function withForeignKeysOff(querier, work) {
22
+ const [{ foreign_keys: enforced }] = await querier.all('PRAGMA foreign_keys');
23
+ if (!enforced) {
24
+ return querier.transaction(work);
25
+ }
26
+ await querier.run('PRAGMA foreign_keys = OFF');
27
+ try {
28
+ await querier.transaction(async () => {
29
+ await work();
30
+ const violations = await querier.all('PRAGMA foreign_key_check');
31
+ if (violations.length) {
32
+ const [{ table, parent }] = violations;
33
+ throw new UqlUsageError(`The migration leaves ${violations.length} row(s) referencing a missing one, the first in "${table}" pointing at "${parent}".`);
34
+ }
35
+ });
36
+ }
37
+ finally {
38
+ await querier.run('PRAGMA foreign_keys = ON');
39
+ }
40
+ }
15
41
  const mongoSession = (querier) => ({
16
42
  querier,
17
43
  run: (statement) => runMongoCommand(querier.db, statement),
@@ -86,14 +86,30 @@ export declare class Migrator {
86
86
  * it. Read off the catalogue rather than recorded by uql, and exactly right for restoring one.
87
87
  */
88
88
  private revertedTriggers;
89
+ /**
90
+ * What a generated migration does that its reader must not miss: each column it drops or retypes, which
91
+ * can lose data, and each table it creates empty while the database holds one no entity names with the
92
+ * same columns, which may be the table renamed. That one is never renamed here: it may be another's.
93
+ */
94
+ private noteChanges;
95
+ /**
96
+ * Refuses, before anything runs, each column the changes require with no default while rows would hold
97
+ * none: every engine fails on one but MySQL, which fills in a zero. Counted, since an empty table is fine.
98
+ */
99
+ private assertFillable;
100
+ /** The tables the database holds that no entity names, each paired with a new one it is identical to. */
101
+ private renamedTables;
89
102
  /** The entities whose tables are among `created`. */
90
103
  private createdEntities;
91
104
  /** The table `entity` maps to, as a diff names it. */
92
105
  private tableOf;
93
106
  /**
94
- * Get all schema differences between entities and database
107
+ * The differences between the entities and the database. With `renames`, a column identical to one the
108
+ * entity no longer names is renamed in place rather than dropped and added, as a generated migration wants.
95
109
  */
96
- getDiffs(): Promise<SchemaDiff[]>;
110
+ getDiffs(options?: {
111
+ renames?: boolean;
112
+ }): Promise<SchemaDiff[]>;
97
113
  /**
98
114
  * The tables `entities` name, read a schema at a time so each is keyed as its entity spells it. Those
99
115
  * alone: nothing else is diffed, and another table can be dropped mid-scan by whatever else is running.
@@ -131,7 +147,8 @@ export declare class Migrator {
131
147
  /**
132
148
  * Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
133
149
  * and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
134
- * Without `drop`, a column's drop is held too.
150
+ * Without `drop`, a column's drop is held too. A rebuilt table applies its diff whole, so holding any
151
+ * part of it holds the rebuild, and only what an `ALTER` adds goes ahead: a plain column, an index.
135
152
  */
136
153
  protected filterDiff(diff: SchemaDiff, options: {
137
154
  safe?: boolean;