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
|
@@ -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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
59
|
+
const name = this.dialect.escapeId(column.name);
|
|
60
|
+
const alter = `ALTER TABLE ${target} ALTER COLUMN ${name}`;
|
|
51
61
|
return [
|
|
52
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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;
|