uql-orm 0.87.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 +14 -5
- 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.js +4 -1
- 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 +7 -1
- package/dist/migrate/migrator.js +49 -6
- package/dist/migrate/schemaChange.d.ts +12 -1
- package/dist/migrate/schemaChange.js +27 -0
- package/dist/migrate/schemaGenerator.d.ts +16 -4
- package/dist/migrate/schemaGenerator.js +76 -37
- 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/schemaAST.js +1 -0
- 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 +21 -0
- 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,10 +52,6 @@ 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};`];
|
|
@@ -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());
|
|
@@ -97,13 +97,16 @@ export class BaseSqlIntrospector {
|
|
|
97
97
|
// key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back to the flags
|
|
98
98
|
// for an introspector that reports no key of its own.
|
|
99
99
|
table.primaryKey = schema.primaryKey ?? keyOfColumns(schema.columns);
|
|
100
|
+
table.definition = schema.definition;
|
|
100
101
|
return table;
|
|
101
102
|
}
|
|
102
103
|
buildRelationships(ast, tableNodes, schema, fromTable) {
|
|
103
104
|
for (const fk of schema.foreignKeys ?? []) {
|
|
104
105
|
const toTable = tableNodes.get(fk.references.table);
|
|
105
|
-
if (!toTable)
|
|
106
|
+
if (!toTable) {
|
|
107
|
+
fromTable.externalForeignKeys.push(fk);
|
|
106
108
|
continue;
|
|
109
|
+
}
|
|
107
110
|
const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
|
|
108
111
|
const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
|
|
109
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),
|
|
@@ -92,6 +92,11 @@ export declare class Migrator {
|
|
|
92
92
|
* same columns, which may be the table renamed. That one is never renamed here: it may be another's.
|
|
93
93
|
*/
|
|
94
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;
|
|
95
100
|
/** The tables the database holds that no entity names, each paired with a new one it is identical to. */
|
|
96
101
|
private renamedTables;
|
|
97
102
|
/** The entities whose tables are among `created`. */
|
|
@@ -142,7 +147,8 @@ export declare class Migrator {
|
|
|
142
147
|
/**
|
|
143
148
|
* Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
|
|
144
149
|
* and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
|
|
145
|
-
* 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.
|
|
146
152
|
*/
|
|
147
153
|
protected filterDiff(diff: SchemaDiff, options: {
|
|
148
154
|
safe?: boolean;
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -7,10 +7,11 @@ import { columnRenames, tableRenameCandidates } from '../schema/schemaASTDiffer.
|
|
|
7
7
|
import { hasTriggers } from '../util/field.util.js';
|
|
8
8
|
import { LoggerWrapper } from '../util/index.js';
|
|
9
9
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
10
|
+
import { withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
|
|
10
11
|
import { buildMigrationModule } from './codegen/migrationFile.js';
|
|
11
12
|
import { introspectorFor } from './introspection/registry.js';
|
|
12
13
|
import { migrationBuilderFor, migrationTargetFor } from './migrationTarget.js';
|
|
13
|
-
import { dropped, nonEmpty, reverseDiff, sides } from './schemaChange.js';
|
|
14
|
+
import { dropped, lacksValue, newlyRequired, nonEmpty, reverseDiff, sides, withoutRebuild } from './schemaChange.js';
|
|
14
15
|
/**
|
|
15
16
|
* Main class for managing database migrations
|
|
16
17
|
*/
|
|
@@ -185,6 +186,7 @@ export class Migrator {
|
|
|
185
186
|
async generateFromEntities(name) {
|
|
186
187
|
const generator = await this.getSchemaGenerator();
|
|
187
188
|
const { created, altered } = await this.pendingChanges({ renames: true });
|
|
189
|
+
await this.assertFillable(altered);
|
|
188
190
|
const plan = this.alterPlan(generator, altered, await this.installedTriggers(created));
|
|
189
191
|
await this.noteChanges(generator, created, altered);
|
|
190
192
|
const up = [...this.createSchema(generator, created), ...plan.up];
|
|
@@ -237,7 +239,7 @@ export class Migrator {
|
|
|
237
239
|
*/
|
|
238
240
|
alterPlan(generator, altered, state) {
|
|
239
241
|
const changing = new Set(altered
|
|
240
|
-
.filter((diff) => sides(diff.columns, 'from').length || diff.renamedColumns?.length)
|
|
242
|
+
.filter((diff) => sides(diff.columns, 'from').length || diff.renamedColumns?.length || diff.rebuild)
|
|
241
243
|
.map((diff) => diff.tableName));
|
|
242
244
|
const cleared = state.filter(({ entity }) => changing.has(this.tableOf(entity)));
|
|
243
245
|
const after = state.map((it) => (cleared.includes(it) ? { entity: it.entity, installed: new Map() } : it));
|
|
@@ -290,6 +292,35 @@ export class Migrator {
|
|
|
290
292
|
`renamed, replace its creation in this migration with \`renameTable('${from}', '${to}')\`.`);
|
|
291
293
|
}
|
|
292
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
|
+
}
|
|
293
324
|
/** The tables the database holds that no entity names, each paired with a new one it is identical to. */
|
|
294
325
|
async renamedTables(generator, created) {
|
|
295
326
|
const createdEntities = this.createdEntities(created);
|
|
@@ -386,6 +417,7 @@ export class Migrator {
|
|
|
386
417
|
const ast = await this.introspectEntities([entity, ...referencedEntities(meta)]);
|
|
387
418
|
// The table is already there, so its triggers are reconciled rather than carried by a `CREATE`.
|
|
388
419
|
const altered = this.alterFromEntity(generator, entity, ast.getTable(tableName), options);
|
|
420
|
+
await this.assertFillable(altered);
|
|
389
421
|
return this.alterPlan(generator, altered, await this.installedTriggers([], [entity])).up;
|
|
390
422
|
}
|
|
391
423
|
/** The diff for one entity against the table it already has, and none where the two agree. */
|
|
@@ -418,6 +450,7 @@ export class Migrator {
|
|
|
418
450
|
}
|
|
419
451
|
const { created, altered } = await this.pendingChanges();
|
|
420
452
|
const filtered = altered.map((diff) => this.filterDiff(diff, options));
|
|
453
|
+
await this.assertFillable(filtered);
|
|
421
454
|
return [
|
|
422
455
|
...this.createSchema(generator, created),
|
|
423
456
|
...this.alterPlan(generator, filtered, await this.installedTriggers(created)).up,
|
|
@@ -442,12 +475,15 @@ export class Migrator {
|
|
|
442
475
|
/**
|
|
443
476
|
* Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
|
|
444
477
|
* and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
|
|
445
|
-
* 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.
|
|
446
480
|
*/
|
|
447
481
|
filterDiff(diff, options) {
|
|
448
482
|
const safe = options.safe !== false;
|
|
483
|
+
let held = false;
|
|
449
484
|
const skip = (what, names, fix) => {
|
|
450
485
|
if (names.length) {
|
|
486
|
+
held = true;
|
|
451
487
|
this.logger.logSkippedMigration(`[AutoSync] Skipped ${names.length} ${what} in table '${diff.tableName}': ${names.join(', ')} (${fix}).`);
|
|
452
488
|
}
|
|
453
489
|
};
|
|
@@ -466,13 +502,18 @@ export class Migrator {
|
|
|
466
502
|
if (!options.drop) {
|
|
467
503
|
skip('column drops', dropped(columns).map((column) => column.name), 'drop: false. Use { drop: true } to apply');
|
|
468
504
|
}
|
|
469
|
-
|
|
505
|
+
const filtered = {
|
|
470
506
|
...diff,
|
|
471
507
|
primaryKey: safe ? undefined : diff.primaryKey,
|
|
472
508
|
columns: options.drop ? columns : nonEmpty((columns ?? []).filter((change) => change.to !== undefined)),
|
|
473
509
|
indexes: additive('index', diff.indexes, (index) => index.name),
|
|
474
510
|
foreignKeys: additive('foreign key', diff.foreignKeys, (foreignKey) => foreignKey.name ?? foreignKey.columns.join(', ')),
|
|
475
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);
|
|
476
517
|
}
|
|
477
518
|
/** Runs the statements a generator wrote, in one transaction where the engine takes DDL in one. */
|
|
478
519
|
async executeSyncStatements(statements, options) {
|
|
@@ -599,6 +640,8 @@ function referencedEntities(meta) {
|
|
|
599
640
|
return [...fields, ...relations];
|
|
600
641
|
}
|
|
601
642
|
/** A diff's column renames, through the builder operation every SQL generator already renders. */
|
|
602
|
-
function renameStatements(generator, { tableName, renamedColumns = [] }) {
|
|
603
|
-
return
|
|
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 }));
|
|
604
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;
|
|
@@ -34,5 +60,6 @@ export function reverseDiff(diff) {
|
|
|
34
60
|
indexes: diff.indexes?.map(swap),
|
|
35
61
|
foreignKeys: diff.foreignKeys?.map(swap),
|
|
36
62
|
renamedColumns: diff.renamedColumns?.map(({ from, to }) => ({ from: to, to: from })),
|
|
63
|
+
rebuild: diff.rebuild && { from: diff.rebuild.to, to: diff.rebuild.from },
|
|
37
64
|
};
|
|
38
65
|
}
|
|
@@ -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. */
|
|
@@ -120,6 +125,12 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
120
125
|
* drift detection runs, with types normalized as the engine stores them.
|
|
121
126
|
*/
|
|
122
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;
|
|
123
134
|
diffOptions(): DiffOptions;
|
|
124
135
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
125
136
|
private columnNodeToSchema;
|
|
@@ -172,12 +183,13 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
172
183
|
* generator added it. MySQL takes no name.
|
|
173
184
|
*/
|
|
174
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;
|
|
175
188
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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.
|
|
178
191
|
*/
|
|
179
|
-
private
|
|
180
|
-
private assertPrimaryKeyAlterable;
|
|
192
|
+
private assertAlterable;
|
|
181
193
|
}
|
|
182
194
|
/**
|
|
183
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
|
}
|
|
@@ -339,11 +357,7 @@ export class SqlSchemaGenerator {
|
|
|
339
357
|
name: derivedPrimaryKeyName(tableName, keyDiff.expected.columns),
|
|
340
358
|
},
|
|
341
359
|
};
|
|
342
|
-
|
|
343
|
-
// the table), since a difference nothing can apply would throw on every sync; `drift:check` names it.
|
|
344
|
-
const relationDiffs = this.features.foreignKeyAlter
|
|
345
|
-
? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
|
|
346
|
-
: [];
|
|
360
|
+
const relationDiffs = diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions());
|
|
347
361
|
const foreignKeys = relationDiffs.map(({ actual, expected }) => ({
|
|
348
362
|
from: actual && foreignKeyOf(actual),
|
|
349
363
|
to: expected && foreignKeyOf(expected),
|
|
@@ -362,9 +376,38 @@ export class SqlSchemaGenerator {
|
|
|
362
376
|
foreignKeys: nonEmpty(foreignKeys),
|
|
363
377
|
renamedColumns: nonEmpty(renamedColumns ?? []),
|
|
364
378
|
};
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
},
|
|
410
|
+
};
|
|
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,10 +552,10 @@ 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
560
|
return this.tableDdl.alterColumn(tableName, { ...this.columnNodeToSchema(node), name: columnName }, this.generateColumnFromNode(node));
|
|
517
561
|
}
|
|
@@ -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
|
/**
|
|
@@ -53,8 +53,9 @@ function triggerStatements(dialect, meta, trigger, name) {
|
|
|
53
53
|
const table = dialect.escapedTableName(meta);
|
|
54
54
|
const names = rowNames(dialect);
|
|
55
55
|
const rows = [rowRefs(meta.entity, names.$new), rowRefs(meta.entity, names.$old)];
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const source = rowsFrom(dialect, meta, operation, trigger.of ?? []);
|
|
57
|
+
const sql = dialect.compileDdl(body(...rows), meta.entity, { rows: source });
|
|
58
|
+
const guard = triggerGuard(dialect, meta, trigger, rows, names, source);
|
|
58
59
|
const inBody = features.guards !== 'clause';
|
|
59
60
|
const guarded = !guard || !inBody
|
|
60
61
|
? sql
|
|
@@ -115,12 +116,16 @@ function triggerBody(dialect, meta, trigger, before) {
|
|
|
115
116
|
}
|
|
116
117
|
return body;
|
|
117
118
|
}
|
|
118
|
-
/**
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The one condition both guards reduce to: any watched column that moved, and whatever `where` asks. On a
|
|
121
|
+
* set-based engine a watched column narrows `source` already, so the guard asks whether it holds a row.
|
|
122
|
+
*/
|
|
123
|
+
function triggerGuard(dialect, meta, trigger, rows, names, source) {
|
|
120
124
|
const moved = movedColumns(dialect, meta, trigger.of ?? [], names);
|
|
125
|
+
const watched = moved && (source ? `EXISTS (SELECT 1 ${source})` : moved);
|
|
121
126
|
return [
|
|
122
|
-
...(
|
|
123
|
-
...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(
|
|
127
|
+
...(watched ? [watched] : []),
|
|
128
|
+
...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(watched)) : []),
|
|
124
129
|
].join(' AND ');
|
|
125
130
|
}
|
|
126
131
|
/** The PL/pgSQL function holding a body. `OR REPLACE`, since a dropped table leaves its function behind. */
|
|
@@ -210,7 +215,7 @@ function dollarQuote(body) {
|
|
|
210
215
|
}
|
|
211
216
|
/**
|
|
212
217
|
* Whether any watched column moved, null-safely, or `undefined` where none is watched: the two records
|
|
213
|
-
* compared on a row-based engine,
|
|
218
|
+
* compared on a row-based engine, the two tables' rows on a set-based one.
|
|
214
219
|
*/
|
|
215
220
|
function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
|
|
216
221
|
if (!of.length) {
|
|
@@ -220,16 +225,15 @@ function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
|
|
|
220
225
|
const column = dialect.escapedColumnName(meta, key);
|
|
221
226
|
return dialect.neExpr(`${oldName}.${column}`, `${newName}.${column}`);
|
|
222
227
|
});
|
|
223
|
-
|
|
224
|
-
const source = rowsFrom(dialect, meta, 'UPDATE');
|
|
225
|
-
return source ? `EXISTS (SELECT 1 ${source} WHERE ${moved})` : moved;
|
|
228
|
+
return differs.length > 1 ? `(${differs.join(' OR ')})` : differs.join('');
|
|
226
229
|
}
|
|
227
230
|
/**
|
|
228
231
|
* Where a set-based engine's body reads the rows it fires for, as the `FROM` a statement names them in:
|
|
229
|
-
* `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key
|
|
230
|
-
*
|
|
232
|
+
* `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key and on a
|
|
233
|
+
* watched column having moved, so the body writes for those rows alone, as a per-row engine fires for them.
|
|
234
|
+
* None on a row-based engine, whose body reads `NEW` and `OLD` bare.
|
|
231
235
|
*/
|
|
232
|
-
function rowsFrom(dialect, meta, operation) {
|
|
236
|
+
function rowsFrom(dialect, meta, operation, of) {
|
|
233
237
|
if (dialect.features.triggers.rows !== 'set') {
|
|
234
238
|
return undefined;
|
|
235
239
|
}
|
|
@@ -241,5 +245,6 @@ function rowsFrom(dialect, meta, operation) {
|
|
|
241
245
|
const column = dialect.escapedColumnName(meta, id);
|
|
242
246
|
return `${$new}.${column} = ${$old}.${column}`;
|
|
243
247
|
});
|
|
244
|
-
|
|
248
|
+
const moved = movedColumns(dialect, meta, of, { $new, $old });
|
|
249
|
+
return `FROM ${$new} JOIN ${$old} ON ${[...keyed, ...(moved ? [moved] : [])].join(' AND ')}`;
|
|
245
250
|
}
|
|
@@ -19,9 +19,7 @@ export const mongoDialectFeatures = {
|
|
|
19
19
|
indexIfNotExists: false,
|
|
20
20
|
schemas: false, // the connection picks the database, and a collection name takes no dot
|
|
21
21
|
dropTableCascade: false,
|
|
22
|
-
|
|
23
|
-
primaryKeyAlter: false,
|
|
24
|
-
generatedColumnAdd: false,
|
|
22
|
+
rebuildsTables: false,
|
|
25
23
|
commentSyntax: 'none',
|
|
26
24
|
vectorIndexRequiresNotNull: false,
|
|
27
25
|
vectorSupportsLength: false,
|
|
@@ -17,9 +17,7 @@ const MSSQL_FEATURES = {
|
|
|
17
17
|
indexIfNotExists: true,
|
|
18
18
|
schemas: true,
|
|
19
19
|
dropTableCascade: false,
|
|
20
|
-
|
|
21
|
-
primaryKeyAlter: true,
|
|
22
|
-
generatedColumnAdd: true,
|
|
20
|
+
rebuildsTables: false,
|
|
23
21
|
// Extended properties are out-of-band metadata with their own procedures, not comments.
|
|
24
22
|
commentSyntax: 'none',
|
|
25
23
|
vectorIndexRequiresNotNull: false,
|
|
@@ -30,7 +30,7 @@ export declare function pairIndexes<S extends ComparableIndex, T extends Compara
|
|
|
30
30
|
/**
|
|
31
31
|
* The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
|
|
32
32
|
* `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
|
|
33
|
-
* dropped: any other may have been made outside the ORM
|
|
33
|
+
* dropped: any other may have been made outside the ORM, so it is `kept`.
|
|
34
34
|
*/
|
|
35
35
|
export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
|
|
36
36
|
toAdd: I[];
|
|
@@ -39,6 +39,7 @@ export declare function indexChanges<I extends IndexSchema>(table: string, decla
|
|
|
39
39
|
from: IndexNode;
|
|
40
40
|
to: I;
|
|
41
41
|
}[];
|
|
42
|
+
kept: IndexNode[];
|
|
42
43
|
};
|
|
43
44
|
/**
|
|
44
45
|
* What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
|
|
@@ -38,7 +38,7 @@ export function pairIndexes(source, target, normalizeName = (name) => name) {
|
|
|
38
38
|
/**
|
|
39
39
|
* The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
|
|
40
40
|
* `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
|
|
41
|
-
* dropped: any other may have been made outside the ORM
|
|
41
|
+
* dropped: any other may have been made outside the ORM, so it is `kept`.
|
|
42
42
|
*/
|
|
43
43
|
export function indexChanges(table, declared, current, facets) {
|
|
44
44
|
const { created, dropped, matched } = pairIndexes(declared, current);
|
|
@@ -47,6 +47,7 @@ export function indexChanges(table, declared, current, facets) {
|
|
|
47
47
|
return {
|
|
48
48
|
toAdd: created,
|
|
49
49
|
toDrop: dropped.filter(owned),
|
|
50
|
+
kept: dropped.filter((index) => !owned(index)),
|
|
50
51
|
toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
|
|
51
52
|
};
|
|
52
53
|
}
|
package/dist/schema/schemaAST.js
CHANGED
package/dist/schema/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IndexSchema, PrimaryKeySchema } from '../type/migration.js';
|
|
1
|
+
import type { ForeignKeySchema, IndexSchema, PrimaryKeySchema, StoredDefinition } from '../type/migration.js';
|
|
2
2
|
import type { IndexFacet } from './indexDifferences.js';
|
|
3
3
|
/**
|
|
4
4
|
* Type categories universal across SQL dialects.
|
|
@@ -123,6 +123,10 @@ export interface TableNode {
|
|
|
123
123
|
readonly checks: CheckSchema[];
|
|
124
124
|
/** Optional table comment */
|
|
125
125
|
readonly comment?: string;
|
|
126
|
+
/** The statements the engine keeps for the table, where it keeps them; none on a table built from entities. */
|
|
127
|
+
definition?: readonly StoredDefinition[];
|
|
128
|
+
/** The foreign keys to tables this AST does not hold, which a rebuild keeps as they are. */
|
|
129
|
+
readonly externalForeignKeys: ForeignKeySchema[];
|
|
126
130
|
/** Relationships pointing TO this table (other tables referencing this one) */
|
|
127
131
|
incomingRelations: RelationshipNode[];
|
|
128
132
|
/** Relationships pointing FROM this table (this table referencing others) */
|
|
@@ -13,7 +13,6 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
13
13
|
readonly commitTransactionCommand = "COMMIT";
|
|
14
14
|
readonly rollbackTransactionCommand = "ROLLBACK";
|
|
15
15
|
readonly isolationLevelStrategy = "none";
|
|
16
|
-
readonly alterColumnSyntax = "none";
|
|
17
16
|
readonly booleanLiteral = "integer";
|
|
18
17
|
/** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
|
|
19
18
|
readonly maxFunctionArgs: number;
|
|
@@ -21,9 +21,7 @@ export const SQLITE_FEATURES = {
|
|
|
21
21
|
indexIfNotExists: true,
|
|
22
22
|
schemas: false, // SQLite's namespaces are attached database files, not declared objects
|
|
23
23
|
dropTableCascade: false,
|
|
24
|
-
|
|
25
|
-
primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
|
|
26
|
-
generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
|
|
24
|
+
rebuildsTables: true,
|
|
27
25
|
commentSyntax: 'none',
|
|
28
26
|
vectorIndexRequiresNotNull: false,
|
|
29
27
|
vectorSupportsLength: true,
|
|
@@ -62,7 +60,6 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
62
60
|
commitTransactionCommand = 'COMMIT';
|
|
63
61
|
rollbackTransactionCommand = 'ROLLBACK';
|
|
64
62
|
isolationLevelStrategy = 'none';
|
|
65
|
-
alterColumnSyntax = 'none';
|
|
66
63
|
booleanLiteral = 'integer';
|
|
67
64
|
/** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
|
|
68
65
|
maxFunctionArgs = 127;
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -87,18 +87,12 @@ export interface DialectFeatures {
|
|
|
87
87
|
*/
|
|
88
88
|
readonly schemas: boolean;
|
|
89
89
|
readonly dropTableCascade: boolean;
|
|
90
|
-
readonly foreignKeyAlter: boolean;
|
|
91
90
|
/**
|
|
92
|
-
* Whether a
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* Whether the engine changes a column, a key or a foreign key, or adds a stored generated column, only
|
|
92
|
+
* by rebuilding the table, as SQLite does: a generated migration copies the table into a new one, and
|
|
93
|
+
* the migration builder refuses the change by name.
|
|
95
94
|
*/
|
|
96
|
-
readonly
|
|
97
|
-
/**
|
|
98
|
-
* Whether a stored generated column can be added to an existing table. SQLite takes one only in a
|
|
99
|
-
* `CREATE TABLE`, so a sync that would add one is refused by name.
|
|
100
|
-
*/
|
|
101
|
-
readonly generatedColumnAdd: boolean;
|
|
95
|
+
readonly rebuildsTables: boolean;
|
|
102
96
|
/** Where a comment goes: in the declaration (MySQL family), a `COMMENT ON` of its own (Postgres family), or nowhere. */
|
|
103
97
|
readonly commentSyntax: 'inline' | 'statement' | 'none';
|
|
104
98
|
/**
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -122,7 +122,20 @@ export interface TableSchema {
|
|
|
122
122
|
readonly primaryKey?: PrimaryKeySchema;
|
|
123
123
|
readonly indexes?: IndexSchema[];
|
|
124
124
|
readonly foreignKeys?: ForeignKeySchema[];
|
|
125
|
+
/** The statements the engine keeps for the table, where it keeps them: SQLite's `sqlite_master`. */
|
|
126
|
+
readonly definition?: readonly StoredDefinition[];
|
|
125
127
|
}
|
|
128
|
+
/** A statement exactly as the engine keeps it, which only it can say all of: a `CHECK`, an index over an expression. */
|
|
129
|
+
export type StoredDefinition = {
|
|
130
|
+
readonly kind: 'table' | 'index' | 'trigger';
|
|
131
|
+
readonly name: string;
|
|
132
|
+
readonly sql: string;
|
|
133
|
+
};
|
|
134
|
+
/** One side of a rebuilt table: its `CREATE TABLE` and what goes back on it, and the columns holding stored values, under this side's names. */
|
|
135
|
+
export type RebuiltTable = {
|
|
136
|
+
readonly statements: readonly string[];
|
|
137
|
+
readonly columns: readonly string[];
|
|
138
|
+
};
|
|
126
139
|
/**
|
|
127
140
|
* Represents an index in a database table
|
|
128
141
|
*/
|
|
@@ -216,6 +229,14 @@ export interface SchemaDiff {
|
|
|
216
229
|
readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
|
|
217
230
|
/** Columns renamed in place, `from` the database's name `to` the entity's, which the other changes already use. */
|
|
218
231
|
readonly renamedColumns?: readonly Rename[];
|
|
232
|
+
/**
|
|
233
|
+
* The table copied into a new one, which is how an engine that {@link DialectFeatures.rebuildsTables}
|
|
234
|
+
* applies the changes above. Its column renames are carried by the copy.
|
|
235
|
+
*/
|
|
236
|
+
readonly rebuild?: {
|
|
237
|
+
readonly from: RebuiltTable;
|
|
238
|
+
readonly to: RebuiltTable;
|
|
239
|
+
};
|
|
219
240
|
}
|
|
220
241
|
/**
|
|
221
242
|
* What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.88.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
package/skills/uql-orm/SKILL.md
CHANGED
|
@@ -154,7 +154,7 @@ transaction. A querier from `pool.getQuerier()` is yours to release: bind it wit
|
|
|
154
154
|
## Migrations
|
|
155
155
|
|
|
156
156
|
`npx uql-migrate` reads `uql.config.ts`. `sync` creates what the entities imply (development only);
|
|
157
|
-
`generate:entities` writes the diff as a migration file to review, renaming a column its field was renamed from
|
|
157
|
+
`generate:entities` writes the diff as a migration file to review, renaming a column its field was renamed from, printing `renameTable` for a table that may have been, rebuilding a SQLite table for what it cannot alter, and refusing a required column with no default on a table holding rows; `up` applies migrations; `generate:from-db`
|
|
158
158
|
writes entity classes from an existing database; `drift:check` fails when the database no longer matches.
|
|
159
159
|
Triggers are part of the diff: uql installs its own under `_uql_`-prefixed names and never touches another.
|
|
160
160
|
|