uql-orm 0.45.0 → 0.46.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/README.md +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +9 -2
- package/dist/dialect/abstractSqlDialect.js +25 -16
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/entity/metadata/definition.js +8 -1
- package/dist/migrate/builder/columnBuilder.d.ts +14 -0
- package/dist/migrate/builder/columnBuilder.js +28 -9
- package/dist/migrate/builder/tableBuilder.js +8 -19
- package/dist/migrate/builder/types.d.ts +16 -33
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
- package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
- package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
- package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
- package/dist/migrate/codegen/sourceLiteral.js +17 -0
- package/dist/migrate/generator/definitionToNode.d.ts +21 -0
- package/dist/migrate/generator/definitionToNode.js +47 -25
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +2 -2
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
- package/dist/migrate/schemaGenerator.d.ts +48 -2
- package/dist/migrate/schemaGenerator.js +120 -38
- package/dist/mongo/mongoDialect.js +2 -1
- package/dist/schema/schemaAST.d.ts +34 -3
- package/dist/schema/schemaAST.js +4 -9
- package/dist/schema/schemaASTBuilder.js +5 -2
- package/dist/schema/schemaASTDiffer.js +8 -4
- package/dist/schema/types.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +21 -2
- package/dist/type/entity.d.ts +21 -0
- package/dist/type/migration.d.ts +11 -11
- package/dist/util/dialect.util.js +5 -4
- package/dist/util/field.util.d.ts +23 -0
- package/dist/util/field.util.js +28 -0
- package/dist/util/fieldOption.util.d.ts +16 -3
- package/dist/util/fieldOption.util.js +23 -4
- package/package.json +1 -1
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import { derivedForeignKeyName } from '../../util/sql.util.js';
|
|
1
|
+
import { derivedForeignKeyName, derivedIndexName } from '../../util/sql.util.js';
|
|
2
|
+
/**
|
|
3
|
+
* A table the builder names but has not seen.
|
|
4
|
+
*
|
|
5
|
+
* A `RelationshipNode` points at a whole `TableNode` because the AST wires `incomingRelations` through
|
|
6
|
+
* it; a builder creating one table has no node for the table its foreign key targets, and the
|
|
7
|
+
* generator reads only the name. Stated once, so the three casts it replaces cannot be mistaken for a
|
|
8
|
+
* node that was resolved and lost.
|
|
9
|
+
*/
|
|
10
|
+
function unresolvedTable(name) {
|
|
11
|
+
return { name };
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
4
15
|
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
|
|
@@ -40,7 +51,7 @@ export function tableDefinitionToNode(def) {
|
|
|
40
51
|
columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
|
|
41
52
|
},
|
|
42
53
|
to: {
|
|
43
|
-
table:
|
|
54
|
+
table: unresolvedTable(fkDef.references.table),
|
|
44
55
|
columns: fkDef.references.columns.map((name) => ({ name })),
|
|
45
56
|
},
|
|
46
57
|
onDelete: fkDef.onDelete,
|
|
@@ -50,30 +61,41 @@ export function tableDefinitionToNode(def) {
|
|
|
50
61
|
}
|
|
51
62
|
return table;
|
|
52
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A builder's column as the AST node the generators render from.
|
|
66
|
+
*
|
|
67
|
+
* The shared half is spread, not copied field by field: `ColumnDefinition` *is* a `ColumnNode` minus
|
|
68
|
+
* the graph links, so spreading it and adding those back is a node by construction. Listed one by one,
|
|
69
|
+
* the copy silently dropped whatever the node gained next - `enum` first, and the type had no way to
|
|
70
|
+
* say so. The two builder-only keys are destructured off: `index` and `foreignKey` are lifted onto the
|
|
71
|
+
* table by `columnIndex`/`columnForeignKey`, which is the path that renders them.
|
|
72
|
+
*
|
|
73
|
+
* No `references` node either: `SchemaAST.addRelationship` sets that one.
|
|
74
|
+
*/
|
|
53
75
|
export function fullColumnDefinitionToNode(col, tableName) {
|
|
76
|
+
const { index: _index, foreignKey: _foreignKey, ...column } = col;
|
|
77
|
+
return { ...column, table: unresolvedTable(tableName), referencedBy: [] };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The index a column-level `index` declares, or nothing.
|
|
81
|
+
*
|
|
82
|
+
* Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
|
|
83
|
+
* `addColumn` had no lift at all and silently emitted a column with no index.
|
|
84
|
+
*/
|
|
85
|
+
export function columnIndex(tableName, col) {
|
|
86
|
+
if (!col.index) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
54
89
|
return {
|
|
55
|
-
name: col.name,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
defaultValue: col.defaultValue,
|
|
59
|
-
isPrimaryKey: col.primaryKey,
|
|
60
|
-
isAutoIncrement: col.autoIncrement,
|
|
61
|
-
isUnique: col.unique,
|
|
62
|
-
comment: col.comment,
|
|
63
|
-
table: { name: tableName },
|
|
64
|
-
referencedBy: [],
|
|
65
|
-
references: col.foreignKey
|
|
66
|
-
? {
|
|
67
|
-
name: derivedForeignKeyName(tableName, [col.name]),
|
|
68
|
-
type: 'ManyToOne',
|
|
69
|
-
from: { table: { name: tableName }, columns: [] },
|
|
70
|
-
to: {
|
|
71
|
-
table: { name: col.foreignKey.table },
|
|
72
|
-
columns: col.foreignKey.columns.map((name) => ({ name })),
|
|
73
|
-
},
|
|
74
|
-
onDelete: col.foreignKey.onDelete,
|
|
75
|
-
onUpdate: col.foreignKey.onUpdate,
|
|
76
|
-
}
|
|
77
|
-
: undefined,
|
|
90
|
+
name: typeof col.index === 'string' ? col.index : derivedIndexName(tableName, [col.name]),
|
|
91
|
+
entries: [{ column: col.name }],
|
|
92
|
+
unique: col.isUnique,
|
|
78
93
|
};
|
|
79
94
|
}
|
|
95
|
+
/** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
|
|
96
|
+
export function columnForeignKey(col) {
|
|
97
|
+
if (!col.foreignKey) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return { ...col.foreignKey, columns: [col.name] };
|
|
101
|
+
}
|
|
@@ -18,9 +18,11 @@ export declare abstract class BaseSqlIntrospector {
|
|
|
18
18
|
constructor(dialect: AbstractSqlDialect, schema?: string | undefined);
|
|
19
19
|
protected escapeId(identifier: string): string;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* The database as a {@link SchemaAST}, or just the tables named. A name nothing matches is left out
|
|
22
|
+
* rather than raised: the point of naming them is to read a database other things are still
|
|
23
|
+
* changing, where scanning every table is both wasted work and a relation that can vanish mid-scan.
|
|
22
24
|
*/
|
|
23
|
-
introspect(): Promise<SchemaAST>;
|
|
25
|
+
introspect(tables?: readonly string[]): Promise<SchemaAST>;
|
|
24
26
|
abstract getTableNames(): Promise<string[]>;
|
|
25
27
|
abstract getTableSchema(tableName: string): Promise<TableSchema | undefined>;
|
|
26
28
|
/**
|
|
@@ -23,10 +23,12 @@ export class BaseSqlIntrospector {
|
|
|
23
23
|
return escapeSqlId(identifier, this.dialect.escapeIdChar);
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* The database as a {@link SchemaAST}, or just the tables named. A name nothing matches is left out
|
|
27
|
+
* rather than raised: the point of naming them is to read a database other things are still
|
|
28
|
+
* changing, where scanning every table is both wasted work and a relation that can vanish mid-scan.
|
|
27
29
|
*/
|
|
28
|
-
async introspect() {
|
|
29
|
-
const tableNames = await this.getTableNames();
|
|
30
|
+
async introspect(tables) {
|
|
31
|
+
const tableNames = tables ?? (await this.getTableNames());
|
|
30
32
|
const tableSchemas = [];
|
|
31
33
|
for (const tableName of tableNames) {
|
|
32
34
|
const schema = await this.getTableSchema(tableName);
|
|
@@ -52,15 +54,13 @@ export class BaseSqlIntrospector {
|
|
|
52
54
|
const table = createTableNode(schema.name, this.schema);
|
|
53
55
|
const { columns } = table;
|
|
54
56
|
for (const col of schema.columns) {
|
|
57
|
+
// Spread, not field by field: a `ColumnSchema` is a `ColumnNode` minus the graph links, so
|
|
58
|
+
// everything but the type crosses unchanged and a field either shape gains cannot be dropped
|
|
59
|
+
// here. Listed by hand this had already lost `enum` and `generatedAs`.
|
|
60
|
+
const { type, length: _length, precision: _precision, scale: _scale, ...rest } = col;
|
|
55
61
|
const column = {
|
|
56
|
-
|
|
57
|
-
type: canonicalColumnType(
|
|
58
|
-
nullable: col.nullable,
|
|
59
|
-
defaultValue: col.defaultValue,
|
|
60
|
-
isPrimaryKey: col.isPrimaryKey,
|
|
61
|
-
isAutoIncrement: col.isAutoIncrement,
|
|
62
|
-
isUnique: col.isUnique,
|
|
63
|
-
comment: col.comment,
|
|
62
|
+
...rest,
|
|
63
|
+
type: canonicalColumnType(type, col),
|
|
64
64
|
table,
|
|
65
65
|
referencedBy: [],
|
|
66
66
|
};
|
|
@@ -10,7 +10,7 @@ export declare class MongoSchemaIntrospector implements SchemaIntrospector {
|
|
|
10
10
|
/** `listIndexes` reports keys and uniqueness; a `partialFilterExpression` is no SQL predicate. */
|
|
11
11
|
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
12
12
|
constructor(pool: QuerierPool);
|
|
13
|
-
introspect(): Promise<SchemaAST>;
|
|
13
|
+
introspect(tables?: readonly string[]): Promise<SchemaAST>;
|
|
14
14
|
getTableSchema(tableName: string): Promise<TableSchema | undefined>;
|
|
15
15
|
getTableNames(): Promise<string[]>;
|
|
16
16
|
tableExists(tableName: string): Promise<boolean>;
|
|
@@ -10,8 +10,8 @@ export class MongoSchemaIntrospector {
|
|
|
10
10
|
constructor(pool) {
|
|
11
11
|
this.pool = pool;
|
|
12
12
|
}
|
|
13
|
-
async introspect() {
|
|
14
|
-
const tableNames = await this.getTableNames();
|
|
13
|
+
async introspect(tables) {
|
|
14
|
+
const tableNames = tables ?? (await this.getTableNames());
|
|
15
15
|
const ast = new SchemaAST();
|
|
16
16
|
for (const name of tableNames) {
|
|
17
17
|
const schema = await this.getTableSchema(name);
|
|
@@ -7,6 +7,11 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
|
|
|
7
7
|
protected getTableNamesQuery(): string;
|
|
8
8
|
protected tableExistsQuery(): string;
|
|
9
9
|
protected parseTableExistsResult(results: SqliteCountRow[]): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* `table_xinfo`, not `table_info`: the latter omits generated columns entirely, so a table carrying
|
|
12
|
+
* one read back without it and every sync offered to add a column that was already there - which
|
|
13
|
+
* SQLite cannot do to an existing table anyway. PRAGMA takes no bound parameters, hence the splice.
|
|
14
|
+
*/
|
|
10
15
|
protected getColumnsQuery(tableName: string): string;
|
|
11
16
|
protected getIndexesQuery(tableName: string): string;
|
|
12
17
|
protected getForeignKeysQuery(tableName: string): string;
|
|
@@ -28,9 +28,13 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
28
28
|
}
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `table_xinfo`, not `table_info`: the latter omits generated columns entirely, so a table carrying
|
|
33
|
+
* one read back without it and every sync offered to add a column that was already there - which
|
|
34
|
+
* SQLite cannot do to an existing table anyway. PRAGMA takes no bound parameters, hence the splice.
|
|
35
|
+
*/
|
|
32
36
|
getColumnsQuery(tableName) {
|
|
33
|
-
return /*sql*/ `PRAGMA
|
|
37
|
+
return /*sql*/ `PRAGMA table_xinfo(${this.escapeId(tableName)})`;
|
|
34
38
|
}
|
|
35
39
|
getIndexesQuery(tableName) {
|
|
36
40
|
return /*sql*/ `PRAGMA index_list(${this.escapeId(tableName)})`;
|
|
@@ -36,6 +36,15 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
36
36
|
* pointing at it emitted `INT`, and every engine refuses that constraint.
|
|
37
37
|
*/
|
|
38
38
|
protected serialType(type: CanonicalType): string;
|
|
39
|
+
/**
|
|
40
|
+
* The SQL type a column is spelled with: the engine's generated-key form for an auto-increment key,
|
|
41
|
+
* the canonical type otherwise.
|
|
42
|
+
*
|
|
43
|
+
* One method because both paths that spell a column need the same answer - written twice, with a
|
|
44
|
+
* comment asking the two to stay in sync, is how the generated key and the column referencing it
|
|
45
|
+
* came to disagree in the first place.
|
|
46
|
+
*/
|
|
47
|
+
protected columnSqlType(col: ColumnNode): string;
|
|
39
48
|
protected canonicalTypeToSql(type: CanonicalType): string;
|
|
40
49
|
/** The entity side as an AST, carrying this generator's default referential action. */
|
|
41
50
|
buildAST(entities: readonly Type<unknown>[]): SchemaAST;
|
|
@@ -101,10 +110,26 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
101
110
|
* Generate ALTER COLUMN statements (database-specific)
|
|
102
111
|
*/
|
|
103
112
|
generateAlterColumnStatements(tableName: string, column: ColumnSchema, newDefinition: string): string[];
|
|
113
|
+
/** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
|
|
114
|
+
generateColumnComment(comment: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* The `COMMENT ON` statements a table and its columns need, on an engine that carries a comment
|
|
117
|
+
* that way. Empty on the others: MySQL writes them inline, SQLite has no comments at all.
|
|
118
|
+
*
|
|
119
|
+
* Emitted after the `CREATE TABLE` rather than folded into it, which is what `COMMENT ON` requires -
|
|
120
|
+
* and what makes a comment reach Postgres, where it was previously read as unsupported and dropped.
|
|
121
|
+
*/
|
|
122
|
+
protected generateCommentStatements(table: TableNode): string[];
|
|
104
123
|
/**
|
|
105
|
-
*
|
|
124
|
+
* The `COMMENT ON COLUMN` one column needs, on an engine that carries a comment that way.
|
|
125
|
+
*
|
|
126
|
+
* Shared by `CREATE TABLE` and every path that adds a column: written only for the former, a column
|
|
127
|
+
* added later reached the database undocumented, the way its enum `CHECK` used to.
|
|
106
128
|
*/
|
|
107
|
-
|
|
129
|
+
protected generateColumnCommentStatement(tableName: string, column: {
|
|
130
|
+
name: string;
|
|
131
|
+
comment?: string;
|
|
132
|
+
}, schema?: string): string[];
|
|
108
133
|
/**
|
|
109
134
|
* Compare an entity with a database table node and return the differences.
|
|
110
135
|
*/
|
|
@@ -131,6 +156,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
131
156
|
*/
|
|
132
157
|
private missingIndexes;
|
|
133
158
|
protected diffOptions(): DiffOptions;
|
|
159
|
+
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
134
160
|
private columnNodeToSchema;
|
|
135
161
|
/**
|
|
136
162
|
* Compare two default values for equality
|
|
@@ -155,10 +181,25 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
155
181
|
ifNotExists?: boolean;
|
|
156
182
|
}): string[];
|
|
157
183
|
generateRenameTableSql(oldName: string, newName: string): string;
|
|
184
|
+
/**
|
|
185
|
+
* `ADD COLUMN`, plus the constraint and index the column declares.
|
|
186
|
+
*
|
|
187
|
+
* `CREATE TABLE` lifts a column's `references` and `index` onto the table it is building; this had
|
|
188
|
+
* no lift, so a hand-written `addColumn(...).references(...).index()` emitted the column alone and
|
|
189
|
+
* dropped both without a word. Several statements in one string is what `generateAlterColumnSql`
|
|
190
|
+
* already returns, and `execute` splits them.
|
|
191
|
+
*/
|
|
158
192
|
generateAddColumnSql(tableName: string, column: FullColumnDefinition): string;
|
|
159
193
|
generateAlterColumnSql(tableName: string, columnName: string, column: FullColumnDefinition): string;
|
|
160
194
|
generateDropColumnSql(tableName: string, columnName: string): string;
|
|
161
195
|
generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
|
|
196
|
+
/**
|
|
197
|
+
* `CONSTRAINT <name> FOREIGN KEY (...) REFERENCES ... ON DELETE ... ON UPDATE ...`.
|
|
198
|
+
*
|
|
199
|
+
* One spelling for the two places that need it - inline in a `CREATE TABLE`, and after `ADD` in an
|
|
200
|
+
* `ALTER`. Written twice, the two drifted over which end they qualified with a schema.
|
|
201
|
+
*/
|
|
202
|
+
protected foreignKeyConstraint(tableName: string, foreignKey: ForeignKeySchema, refTableSql: string): string;
|
|
162
203
|
generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
|
|
163
204
|
generateDropForeignKeySql(tableName: string, constraintName: string): string;
|
|
164
205
|
/**
|
|
@@ -176,6 +217,11 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
176
217
|
* no name at all - a table's key is always `PRIMARY` there.
|
|
177
218
|
*/
|
|
178
219
|
generateDropPrimaryKeySql(tableName: string, constraintName?: string): string;
|
|
220
|
+
/**
|
|
221
|
+
* A column an `ALTER` can carry. Only a generated one is ever refused, and only where the engine
|
|
222
|
+
* takes it in a `CREATE TABLE` but not afterwards.
|
|
223
|
+
*/
|
|
224
|
+
private assertColumnAddable;
|
|
179
225
|
private assertPrimaryKeyAlterable;
|
|
180
226
|
}
|
|
181
227
|
/**
|
|
@@ -8,7 +8,7 @@ import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/in
|
|
|
8
8
|
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
|
|
9
9
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
10
10
|
import { indexDdlFor } from './ddl/index.js';
|
|
11
|
-
import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
|
|
11
|
+
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
12
12
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
13
13
|
/**
|
|
14
14
|
* Unified SQL schema generator.
|
|
@@ -61,6 +61,17 @@ export class SqlSchemaGenerator {
|
|
|
61
61
|
serialType(type) {
|
|
62
62
|
return `${this.canonicalTypeToSql(type)} ${this.dialect.autoIncrementSuffix}`;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The SQL type a column is spelled with: the engine's generated-key form for an auto-increment key,
|
|
66
|
+
* the canonical type otherwise.
|
|
67
|
+
*
|
|
68
|
+
* One method because both paths that spell a column need the same answer - written twice, with a
|
|
69
|
+
* comment asking the two to stay in sync, is how the generated key and the column referencing it
|
|
70
|
+
* came to disagree in the first place.
|
|
71
|
+
*/
|
|
72
|
+
columnSqlType(col) {
|
|
73
|
+
return col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type);
|
|
74
|
+
}
|
|
64
75
|
canonicalTypeToSql(type) {
|
|
65
76
|
return canonicalToSql(type, this.dialect);
|
|
66
77
|
}
|
|
@@ -142,8 +153,9 @@ export class SqlSchemaGenerator {
|
|
|
142
153
|
// Add new columns
|
|
143
154
|
if (diff.columnsToAdd?.length) {
|
|
144
155
|
for (const column of diff.columnsToAdd) {
|
|
145
|
-
|
|
146
|
-
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${
|
|
156
|
+
this.assertColumnAddable(diff.tableName, column);
|
|
157
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.generateColumnDefinitionFromSchema(column)};`);
|
|
158
|
+
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
// Alter existing columns
|
|
@@ -279,6 +291,12 @@ export class SqlSchemaGenerator {
|
|
|
279
291
|
*/
|
|
280
292
|
renderColumn(column) {
|
|
281
293
|
let def = `${this.escapeId(column.name)} ${column.type}`;
|
|
294
|
+
// Before the constraints, which every engine here accepts and is where each documents it. The
|
|
295
|
+
// clause takes the place of a `DEFAULT`, which is mutually exclusive with it; everything else -
|
|
296
|
+
// `NOT NULL`, `UNIQUE`, an enum `CHECK`, a comment - a generated column carries like any other.
|
|
297
|
+
if (column.generatedAs) {
|
|
298
|
+
def += ` GENERATED ALWAYS AS (${column.generatedAs}) STORED`;
|
|
299
|
+
}
|
|
282
300
|
if (!column.nullable && !column.isPrimaryKey) {
|
|
283
301
|
def += ' NOT NULL';
|
|
284
302
|
}
|
|
@@ -291,7 +309,7 @@ export class SqlSchemaGenerator {
|
|
|
291
309
|
}
|
|
292
310
|
def += this.defaultClause(column);
|
|
293
311
|
if (column.comment) {
|
|
294
|
-
def += this.generateColumnComment(column.
|
|
312
|
+
def += this.generateColumnComment(column.comment);
|
|
295
313
|
}
|
|
296
314
|
return def;
|
|
297
315
|
}
|
|
@@ -350,15 +368,40 @@ export class SqlSchemaGenerator {
|
|
|
350
368
|
}
|
|
351
369
|
return [`ALTER TABLE ${table} ${this.dialect.alterColumnSyntax} ${newDefinition};`];
|
|
352
370
|
}
|
|
371
|
+
/** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
|
|
372
|
+
generateColumnComment(comment) {
|
|
373
|
+
return this.features.commentSyntax === 'inline' ? ` COMMENT ${this.dialect.escape(comment)}` : '';
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The `COMMENT ON` statements a table and its columns need, on an engine that carries a comment
|
|
377
|
+
* that way. Empty on the others: MySQL writes them inline, SQLite has no comments at all.
|
|
378
|
+
*
|
|
379
|
+
* Emitted after the `CREATE TABLE` rather than folded into it, which is what `COMMENT ON` requires -
|
|
380
|
+
* and what makes a comment reach Postgres, where it was previously read as unsupported and dropped.
|
|
381
|
+
*/
|
|
382
|
+
generateCommentStatements(table) {
|
|
383
|
+
if (this.features.commentSyntax !== 'statement') {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
const tableRef = this.dialect.escapeQualifiedId(table.name, table.schema);
|
|
387
|
+
const statements = table.comment ? [`COMMENT ON TABLE ${tableRef} IS ${this.dialect.escape(table.comment)};`] : [];
|
|
388
|
+
for (const col of table.columns.values()) {
|
|
389
|
+
statements.push(...this.generateColumnCommentStatement(table.name, col, table.schema));
|
|
390
|
+
}
|
|
391
|
+
return statements;
|
|
392
|
+
}
|
|
353
393
|
/**
|
|
354
|
-
*
|
|
394
|
+
* The `COMMENT ON COLUMN` one column needs, on an engine that carries a comment that way.
|
|
395
|
+
*
|
|
396
|
+
* Shared by `CREATE TABLE` and every path that adds a column: written only for the former, a column
|
|
397
|
+
* added later reached the database undocumented, the way its enum `CHECK` used to.
|
|
355
398
|
*/
|
|
356
|
-
|
|
357
|
-
if (this.features.
|
|
358
|
-
|
|
359
|
-
return ` COMMENT '${escapedComment}'`;
|
|
399
|
+
generateColumnCommentStatement(tableName, column, schema) {
|
|
400
|
+
if (!column.comment || this.features.commentSyntax !== 'statement') {
|
|
401
|
+
return [];
|
|
360
402
|
}
|
|
361
|
-
|
|
403
|
+
const tableRef = this.dialect.escapeQualifiedId(tableName, schema);
|
|
404
|
+
return [`COMMENT ON COLUMN ${tableRef}.${this.escapeId(column.name)} IS ${this.dialect.escape(column.comment)};`];
|
|
362
405
|
}
|
|
363
406
|
/**
|
|
364
407
|
* Compare an entity with a database table node and return the differences.
|
|
@@ -392,8 +435,17 @@ export class SqlSchemaGenerator {
|
|
|
392
435
|
const columnDiffs = tableDiff?.columnDiffs ?? [];
|
|
393
436
|
const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
|
|
394
437
|
const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
|
|
438
|
+
// Without its values: an alter restates the whole column, and MySQL answers a restated `CHECK` by
|
|
439
|
+
// adding a *second* constraint rather than replacing the first, so the column would accumulate one
|
|
440
|
+
// per alter. An enum's values reach the database with the column and are never restated - which is
|
|
441
|
+
// also why changing them is a hand-written migration. See architecture/roadmap.md.
|
|
395
442
|
const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
|
|
396
|
-
? [
|
|
443
|
+
? [
|
|
444
|
+
{
|
|
445
|
+
from: this.columnNodeToSchema(it.actual),
|
|
446
|
+
to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
|
|
447
|
+
},
|
|
448
|
+
]
|
|
397
449
|
: []);
|
|
398
450
|
const primaryKey = tableDiff?.primaryKeyDiff && {
|
|
399
451
|
from: tableDiff.primaryKeyDiff.actual,
|
|
@@ -460,19 +512,10 @@ export class SqlSchemaGenerator {
|
|
|
460
512
|
defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
|
|
461
513
|
};
|
|
462
514
|
}
|
|
515
|
+
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
463
516
|
columnNodeToSchema(col) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
467
|
-
// gets the type it would have had if the table were created from scratch.
|
|
468
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
469
|
-
nullable: col.nullable,
|
|
470
|
-
defaultValue: col.defaultValue,
|
|
471
|
-
isPrimaryKey: col.isPrimaryKey,
|
|
472
|
-
isAutoIncrement: col.isAutoIncrement,
|
|
473
|
-
isUnique: col.isUnique,
|
|
474
|
-
comment: col.comment,
|
|
475
|
-
};
|
|
517
|
+
const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
|
|
518
|
+
return { ...column, type: this.columnSqlType(col) };
|
|
476
519
|
}
|
|
477
520
|
/**
|
|
478
521
|
* Compare two default values for equality
|
|
@@ -531,11 +574,8 @@ export class SqlSchemaGenerator {
|
|
|
531
574
|
});
|
|
532
575
|
for (const rel of table.outgoingRelations) {
|
|
533
576
|
if (rel.from.columns.length > 0) {
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
const constraintName = rel.name ? `CONSTRAINT ${this.escapeId(rel.name)} ` : '';
|
|
537
|
-
constraints.push(`${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema)} (${toCols})` +
|
|
538
|
-
` ON DELETE ${rel.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${rel.onUpdate ?? this.defaultForeignKeyAction}`);
|
|
577
|
+
const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
|
|
578
|
+
constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
|
|
539
579
|
}
|
|
540
580
|
}
|
|
541
581
|
const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
@@ -549,6 +589,9 @@ export class SqlSchemaGenerator {
|
|
|
549
589
|
if (this.dialect.tableOptions) {
|
|
550
590
|
createSql += ` ${this.dialect.tableOptions}`;
|
|
551
591
|
}
|
|
592
|
+
if (table.comment && this.features.commentSyntax === 'inline') {
|
|
593
|
+
createSql += ` COMMENT=${this.dialect.escape(table.comment)}`;
|
|
594
|
+
}
|
|
552
595
|
createSql += ';';
|
|
553
596
|
const statements = [];
|
|
554
597
|
if (this.dialect.vectorExtension) {
|
|
@@ -558,6 +601,7 @@ export class SqlSchemaGenerator {
|
|
|
558
601
|
}
|
|
559
602
|
}
|
|
560
603
|
statements.push(createSql);
|
|
604
|
+
statements.push(...this.generateCommentStatements(table));
|
|
561
605
|
for (const idx of table.indexes) {
|
|
562
606
|
statements.push(this.generateCreateIndexFromNode(idx));
|
|
563
607
|
}
|
|
@@ -568,10 +612,7 @@ export class SqlSchemaGenerator {
|
|
|
568
612
|
* so only a lone primary key column carries `PRIMARY KEY` inline.
|
|
569
613
|
*/
|
|
570
614
|
generateColumnFromNode(col) {
|
|
571
|
-
return this.renderColumn({
|
|
572
|
-
...col,
|
|
573
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
574
|
-
});
|
|
615
|
+
return this.renderColumn({ ...col, type: this.columnSqlType(col) });
|
|
575
616
|
}
|
|
576
617
|
/**
|
|
577
618
|
* Generate CREATE INDEX SQL from an IndexNode.
|
|
@@ -591,9 +632,28 @@ export class SqlSchemaGenerator {
|
|
|
591
632
|
}
|
|
592
633
|
return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
|
|
593
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* `ADD COLUMN`, plus the constraint and index the column declares.
|
|
637
|
+
*
|
|
638
|
+
* `CREATE TABLE` lifts a column's `references` and `index` onto the table it is building; this had
|
|
639
|
+
* no lift, so a hand-written `addColumn(...).references(...).index()` emitted the column alone and
|
|
640
|
+
* dropped both without a word. Several statements in one string is what `generateAlterColumnSql`
|
|
641
|
+
* already returns, and `execute` splits them.
|
|
642
|
+
*/
|
|
594
643
|
generateAddColumnSql(tableName, column) {
|
|
644
|
+
this.assertColumnAddable(tableName, column);
|
|
595
645
|
const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
|
|
596
|
-
|
|
646
|
+
const statements = [`ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`];
|
|
647
|
+
const foreignKey = columnForeignKey(column);
|
|
648
|
+
if (foreignKey) {
|
|
649
|
+
statements.push(...this.addForeignKeyStatements(tableName, [foreignKey]));
|
|
650
|
+
}
|
|
651
|
+
const index = columnIndex(tableName, column);
|
|
652
|
+
if (index) {
|
|
653
|
+
statements.push(this.generateCreateIndex(tableName, index));
|
|
654
|
+
}
|
|
655
|
+
statements.push(...this.generateColumnCommentStatement(tableName, column));
|
|
656
|
+
return statements.join('\n');
|
|
597
657
|
}
|
|
598
658
|
generateAlterColumnSql(tableName, columnName, column) {
|
|
599
659
|
const node = fullColumnDefinitionToNode(column, tableName);
|
|
@@ -605,16 +665,26 @@ export class SqlSchemaGenerator {
|
|
|
605
665
|
generateRenameColumnSql(tableName, oldName, newName) {
|
|
606
666
|
return `ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
|
|
607
667
|
}
|
|
608
|
-
|
|
668
|
+
/**
|
|
669
|
+
* `CONSTRAINT <name> FOREIGN KEY (...) REFERENCES ... ON DELETE ... ON UPDATE ...`.
|
|
670
|
+
*
|
|
671
|
+
* One spelling for the two places that need it - inline in a `CREATE TABLE`, and after `ADD` in an
|
|
672
|
+
* `ALTER`. Written twice, the two drifted over which end they qualified with a schema.
|
|
673
|
+
*/
|
|
674
|
+
foreignKeyConstraint(tableName, foreignKey, refTableSql) {
|
|
609
675
|
const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
|
|
610
676
|
const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
|
|
611
|
-
|
|
677
|
+
return (`CONSTRAINT ${this.escapeId(constraintNameOf(tableName, foreignKey))} ` +
|
|
678
|
+
`FOREIGN KEY (${fkCols}) REFERENCES ${refTableSql} (${refCols}) ` +
|
|
679
|
+
`ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ` +
|
|
680
|
+
`ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction}`);
|
|
681
|
+
}
|
|
682
|
+
generateAddForeignKeySql(tableName, foreignKey) {
|
|
612
683
|
if (!this.features.foreignKeyAlter) {
|
|
613
684
|
throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
614
685
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
`ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`);
|
|
686
|
+
const constraint = this.foreignKeyConstraint(tableName, foreignKey, this.escapeId(foreignKey.references.table));
|
|
687
|
+
return `ALTER TABLE ${this.escapeId(tableName)} ADD ${constraint};`;
|
|
618
688
|
}
|
|
619
689
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
620
690
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
@@ -650,6 +720,18 @@ export class SqlSchemaGenerator {
|
|
|
650
720
|
}
|
|
651
721
|
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
652
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* A column an `ALTER` can carry. Only a generated one is ever refused, and only where the engine
|
|
725
|
+
* takes it in a `CREATE TABLE` but not afterwards.
|
|
726
|
+
*/
|
|
727
|
+
assertColumnAddable(tableName, column) {
|
|
728
|
+
if (!column.generatedAs || this.features.generatedColumnAdd) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
throw new TypeError(`${this.dialect}: Cannot add the computed column "${column.name}" to the existing table ` +
|
|
732
|
+
`"${tableName}" - this database only accepts one in a CREATE TABLE. Drop \`stored\` to have the ` +
|
|
733
|
+
'expression spliced into each statement instead, or recreate the table in a written migration.');
|
|
734
|
+
}
|
|
653
735
|
assertPrimaryKeyAlterable(tableName) {
|
|
654
736
|
if (this.features.primaryKeyAlter) {
|
|
655
737
|
return;
|
|
@@ -17,7 +17,8 @@ export const mongoDialectFeatures = {
|
|
|
17
17
|
renameColumn: false,
|
|
18
18
|
foreignKeyAlter: false,
|
|
19
19
|
primaryKeyAlter: false,
|
|
20
|
-
|
|
20
|
+
generatedColumnAdd: false,
|
|
21
|
+
commentSyntax: 'none',
|
|
21
22
|
vectorIndexRequiresNotNull: false,
|
|
22
23
|
vectorSupportsLength: false,
|
|
23
24
|
supportsTimestamptz: false,
|
|
@@ -150,7 +150,38 @@ export declare class SchemaAST implements ISchemaAST {
|
|
|
150
150
|
indexCount: number;
|
|
151
151
|
};
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
* The schema as a plain object, for serialization and debugging. The graph links are what is left
|
|
154
|
+
* out - they are cycles, and nothing else is: listing the fields to keep instead dropped every
|
|
155
|
+
* option a column had gained since, `defaultValue` and `enum` included.
|
|
156
|
+
*/
|
|
157
|
+
toJSON(): {
|
|
158
|
+
tables: {
|
|
159
|
+
name: string;
|
|
160
|
+
columns: {
|
|
161
|
+
name: string;
|
|
162
|
+
type: import("./types.js").CanonicalType;
|
|
163
|
+
nullable: boolean;
|
|
164
|
+
defaultValue?: unknown;
|
|
165
|
+
isPrimaryKey: boolean;
|
|
166
|
+
isAutoIncrement: boolean;
|
|
167
|
+
isUnique: boolean;
|
|
168
|
+
enum?: import("./types.js").EnumValues;
|
|
169
|
+
generatedAs?: string;
|
|
170
|
+
comment?: string;
|
|
171
|
+
}[];
|
|
172
|
+
indexes: {
|
|
173
|
+
name: string;
|
|
174
|
+
columns: string[];
|
|
175
|
+
unique: boolean;
|
|
176
|
+
}[];
|
|
177
|
+
}[];
|
|
178
|
+
relationships: {
|
|
179
|
+
name: string;
|
|
180
|
+
type: RelationshipType;
|
|
181
|
+
from: string;
|
|
182
|
+
to: string;
|
|
183
|
+
onDelete: import("./types.js").ForeignKeyAction | undefined;
|
|
184
|
+
onUpdate: import("./types.js").ForeignKeyAction | undefined;
|
|
185
|
+
}[];
|
|
186
|
+
};
|
|
156
187
|
}
|
package/dist/schema/schemaAST.js
CHANGED
|
@@ -385,20 +385,15 @@ export class SchemaAST {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
/**
|
|
388
|
-
*
|
|
388
|
+
* The schema as a plain object, for serialization and debugging. The graph links are what is left
|
|
389
|
+
* out - they are cycles, and nothing else is: listing the fields to keep instead dropped every
|
|
390
|
+
* option a column had gained since, `defaultValue` and `enum` included.
|
|
389
391
|
*/
|
|
390
392
|
toJSON() {
|
|
391
393
|
return {
|
|
392
394
|
tables: Array.from(this.tables.values()).map((t) => ({
|
|
393
395
|
name: t.name,
|
|
394
|
-
columns: Array.from(t.columns.values()).map((
|
|
395
|
-
name: c.name,
|
|
396
|
-
type: c.type,
|
|
397
|
-
nullable: c.nullable,
|
|
398
|
-
isPrimaryKey: c.isPrimaryKey,
|
|
399
|
-
isAutoIncrement: c.isAutoIncrement,
|
|
400
|
-
isUnique: c.isUnique,
|
|
401
|
-
})),
|
|
396
|
+
columns: Array.from(t.columns.values()).map(({ table: _table, referencedBy: _referencedBy, references: _references, ...column }) => column),
|
|
402
397
|
indexes: t.indexes.map((i) => ({
|
|
403
398
|
name: i.name,
|
|
404
399
|
columns: i.entries.map((entry) => entry.column),
|