uql-orm 0.45.1 → 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 +109 -36
- 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 -18
- 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,7 +153,9 @@ export class SqlSchemaGenerator {
|
|
|
142
153
|
// Add new columns
|
|
143
154
|
if (diff.columnsToAdd?.length) {
|
|
144
155
|
for (const column of diff.columnsToAdd) {
|
|
156
|
+
this.assertColumnAddable(diff.tableName, column);
|
|
145
157
|
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.generateColumnDefinitionFromSchema(column)};`);
|
|
158
|
+
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
161
|
// Alter existing columns
|
|
@@ -278,6 +291,12 @@ export class SqlSchemaGenerator {
|
|
|
278
291
|
*/
|
|
279
292
|
renderColumn(column) {
|
|
280
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
|
+
}
|
|
281
300
|
if (!column.nullable && !column.isPrimaryKey) {
|
|
282
301
|
def += ' NOT NULL';
|
|
283
302
|
}
|
|
@@ -290,7 +309,7 @@ export class SqlSchemaGenerator {
|
|
|
290
309
|
}
|
|
291
310
|
def += this.defaultClause(column);
|
|
292
311
|
if (column.comment) {
|
|
293
|
-
def += this.generateColumnComment(column.
|
|
312
|
+
def += this.generateColumnComment(column.comment);
|
|
294
313
|
}
|
|
295
314
|
return def;
|
|
296
315
|
}
|
|
@@ -349,15 +368,40 @@ export class SqlSchemaGenerator {
|
|
|
349
368
|
}
|
|
350
369
|
return [`ALTER TABLE ${table} ${this.dialect.alterColumnSyntax} ${newDefinition};`];
|
|
351
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
|
+
}
|
|
352
393
|
/**
|
|
353
|
-
*
|
|
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.
|
|
354
398
|
*/
|
|
355
|
-
|
|
356
|
-
if (this.features.
|
|
357
|
-
|
|
358
|
-
return ` COMMENT '${escapedComment}'`;
|
|
399
|
+
generateColumnCommentStatement(tableName, column, schema) {
|
|
400
|
+
if (!column.comment || this.features.commentSyntax !== 'statement') {
|
|
401
|
+
return [];
|
|
359
402
|
}
|
|
360
|
-
|
|
403
|
+
const tableRef = this.dialect.escapeQualifiedId(tableName, schema);
|
|
404
|
+
return [`COMMENT ON COLUMN ${tableRef}.${this.escapeId(column.name)} IS ${this.dialect.escape(column.comment)};`];
|
|
361
405
|
}
|
|
362
406
|
/**
|
|
363
407
|
* Compare an entity with a database table node and return the differences.
|
|
@@ -468,20 +512,10 @@ export class SqlSchemaGenerator {
|
|
|
468
512
|
defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
|
|
469
513
|
};
|
|
470
514
|
}
|
|
515
|
+
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
471
516
|
columnNodeToSchema(col) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
475
|
-
// gets the type it would have had if the table were created from scratch.
|
|
476
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
477
|
-
nullable: col.nullable,
|
|
478
|
-
defaultValue: col.defaultValue,
|
|
479
|
-
isPrimaryKey: col.isPrimaryKey,
|
|
480
|
-
isAutoIncrement: col.isAutoIncrement,
|
|
481
|
-
isUnique: col.isUnique,
|
|
482
|
-
comment: col.comment,
|
|
483
|
-
enum: col.enum,
|
|
484
|
-
};
|
|
517
|
+
const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
|
|
518
|
+
return { ...column, type: this.columnSqlType(col) };
|
|
485
519
|
}
|
|
486
520
|
/**
|
|
487
521
|
* Compare two default values for equality
|
|
@@ -540,11 +574,8 @@ export class SqlSchemaGenerator {
|
|
|
540
574
|
});
|
|
541
575
|
for (const rel of table.outgoingRelations) {
|
|
542
576
|
if (rel.from.columns.length > 0) {
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
const constraintName = rel.name ? `CONSTRAINT ${this.escapeId(rel.name)} ` : '';
|
|
546
|
-
constraints.push(`${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema)} (${toCols})` +
|
|
547
|
-
` 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));
|
|
548
579
|
}
|
|
549
580
|
}
|
|
550
581
|
const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
@@ -558,6 +589,9 @@ export class SqlSchemaGenerator {
|
|
|
558
589
|
if (this.dialect.tableOptions) {
|
|
559
590
|
createSql += ` ${this.dialect.tableOptions}`;
|
|
560
591
|
}
|
|
592
|
+
if (table.comment && this.features.commentSyntax === 'inline') {
|
|
593
|
+
createSql += ` COMMENT=${this.dialect.escape(table.comment)}`;
|
|
594
|
+
}
|
|
561
595
|
createSql += ';';
|
|
562
596
|
const statements = [];
|
|
563
597
|
if (this.dialect.vectorExtension) {
|
|
@@ -567,6 +601,7 @@ export class SqlSchemaGenerator {
|
|
|
567
601
|
}
|
|
568
602
|
}
|
|
569
603
|
statements.push(createSql);
|
|
604
|
+
statements.push(...this.generateCommentStatements(table));
|
|
570
605
|
for (const idx of table.indexes) {
|
|
571
606
|
statements.push(this.generateCreateIndexFromNode(idx));
|
|
572
607
|
}
|
|
@@ -577,10 +612,7 @@ export class SqlSchemaGenerator {
|
|
|
577
612
|
* so only a lone primary key column carries `PRIMARY KEY` inline.
|
|
578
613
|
*/
|
|
579
614
|
generateColumnFromNode(col) {
|
|
580
|
-
return this.renderColumn({
|
|
581
|
-
...col,
|
|
582
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
583
|
-
});
|
|
615
|
+
return this.renderColumn({ ...col, type: this.columnSqlType(col) });
|
|
584
616
|
}
|
|
585
617
|
/**
|
|
586
618
|
* Generate CREATE INDEX SQL from an IndexNode.
|
|
@@ -600,9 +632,28 @@ export class SqlSchemaGenerator {
|
|
|
600
632
|
}
|
|
601
633
|
return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
|
|
602
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
|
+
*/
|
|
603
643
|
generateAddColumnSql(tableName, column) {
|
|
644
|
+
this.assertColumnAddable(tableName, column);
|
|
604
645
|
const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
|
|
605
|
-
|
|
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');
|
|
606
657
|
}
|
|
607
658
|
generateAlterColumnSql(tableName, columnName, column) {
|
|
608
659
|
const node = fullColumnDefinitionToNode(column, tableName);
|
|
@@ -614,16 +665,26 @@ export class SqlSchemaGenerator {
|
|
|
614
665
|
generateRenameColumnSql(tableName, oldName, newName) {
|
|
615
666
|
return `ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
|
|
616
667
|
}
|
|
617
|
-
|
|
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) {
|
|
618
675
|
const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
|
|
619
676
|
const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
|
|
620
|
-
|
|
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) {
|
|
621
683
|
if (!this.features.foreignKeyAlter) {
|
|
622
684
|
throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
623
685
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`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};`;
|
|
627
688
|
}
|
|
628
689
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
629
690
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
@@ -659,6 +720,18 @@ export class SqlSchemaGenerator {
|
|
|
659
720
|
}
|
|
660
721
|
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
661
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
|
+
}
|
|
662
735
|
assertPrimaryKeyAlterable(tableName) {
|
|
663
736
|
if (this.features.primaryKeyAlter) {
|
|
664
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),
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
8
|
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
|
+
import { ddlText } from '../util/ddlExpression.util.js';
|
|
10
|
+
import { computedExpression, isInlinedExpression } from '../util/field.util.js';
|
|
9
11
|
import { isSoleIdField } from '../util/field.util.js';
|
|
10
12
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
11
13
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
@@ -82,8 +84,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
82
84
|
const field = fields[key];
|
|
83
85
|
if (!field)
|
|
84
86
|
continue;
|
|
85
|
-
//
|
|
86
|
-
if (field
|
|
87
|
+
// An inlined expression has no column; a stored one is a column like any other.
|
|
88
|
+
if (isInlinedExpression(field))
|
|
87
89
|
continue;
|
|
88
90
|
const columnName = ctx.resolveColumnName(key, field);
|
|
89
91
|
const type = resolveColumnCanonicalType(field);
|
|
@@ -99,6 +101,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
99
101
|
isPrimaryKey,
|
|
100
102
|
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
101
103
|
isUnique: field.unique ?? false,
|
|
104
|
+
generatedAs: ddlText(computedExpression(field), `the computed column '${columnName}'`),
|
|
102
105
|
comment: field.comment,
|
|
103
106
|
enum: field.enum,
|
|
104
107
|
table,
|