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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/dialect/abstractSqlDialect.d.ts +9 -2
  3. package/dist/dialect/abstractSqlDialect.js +25 -16
  4. package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
  5. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  6. package/dist/entity/metadata/definition.js +8 -1
  7. package/dist/migrate/builder/columnBuilder.d.ts +14 -0
  8. package/dist/migrate/builder/columnBuilder.js +28 -9
  9. package/dist/migrate/builder/tableBuilder.js +8 -19
  10. package/dist/migrate/builder/types.d.ts +16 -33
  11. package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
  12. package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
  13. package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
  14. package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
  15. package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
  16. package/dist/migrate/codegen/sourceLiteral.js +17 -0
  17. package/dist/migrate/generator/definitionToNode.d.ts +21 -0
  18. package/dist/migrate/generator/definitionToNode.js +47 -25
  19. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
  20. package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
  21. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  22. package/dist/migrate/introspection/mongoIntrospector.js +2 -2
  23. package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
  24. package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
  25. package/dist/migrate/schemaGenerator.d.ts +48 -2
  26. package/dist/migrate/schemaGenerator.js +109 -36
  27. package/dist/mongo/mongoDialect.js +2 -1
  28. package/dist/schema/schemaAST.d.ts +34 -3
  29. package/dist/schema/schemaAST.js +4 -9
  30. package/dist/schema/schemaASTBuilder.js +5 -2
  31. package/dist/schema/schemaASTDiffer.js +8 -4
  32. package/dist/schema/types.d.ts +2 -0
  33. package/dist/sqlite/sqliteDialect.js +2 -1
  34. package/dist/type/dialect.d.ts +21 -2
  35. package/dist/type/entity.d.ts +21 -0
  36. package/dist/type/migration.d.ts +11 -18
  37. package/dist/util/dialect.util.js +5 -4
  38. package/dist/util/field.util.d.ts +23 -0
  39. package/dist/util/field.util.js +28 -0
  40. package/dist/util/fieldOption.util.d.ts +16 -3
  41. package/dist/util/fieldOption.util.js +23 -4
  42. 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: { name: fkDef.references.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
- type: col.type,
57
- nullable: col.nullable,
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
- * Introspect entire database schema and return SchemaAST.
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
- * Introspect entire database schema and return SchemaAST.
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
- name: col.name,
57
- type: canonicalColumnType(col.type, col),
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
- // SQLite uses PRAGMA which doesn't use parameterized queries in the same way
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 table_info(${this.escapeId(tableName)})`;
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
- * Generate column comment clause (if supported)
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
- generateColumnComment(columnName: string, comment: string): string;
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.name, column.comment);
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
- * Generate column comment clause (if supported)
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
- generateColumnComment(columnName, comment) {
356
- if (this.features.columnComment) {
357
- const escapedComment = comment.replace(/'/g, "''");
358
- return ` COMMENT '${escapedComment}'`;
399
+ generateColumnCommentStatement(tableName, column, schema) {
400
+ if (!column.comment || this.features.commentSyntax !== 'statement') {
401
+ return [];
359
402
  }
360
- return '';
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
- return {
473
- name: col.name,
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 fromCols = rel.from.columns.map((c) => this.escapeId(c.name)).join(', ');
544
- const toCols = rel.to.columns.map((c) => this.escapeId(c.name)).join(', ');
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
- return `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`;
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
- generateAddForeignKeySql(tableName, foreignKey) {
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
- const constraintName = this.escapeId(constraintNameOf(tableName, foreignKey));
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
- return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
625
- `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.references.table)} (${refCols}) ` +
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
- columnComment: false,
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
- * Convert schema to a plain object for serialization/debugging.
154
- */
155
- toJSON(): object;
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
  }
@@ -385,20 +385,15 @@ export class SchemaAST {
385
385
  };
386
386
  }
387
387
  /**
388
- * Convert schema to a plain object for serialization/debugging.
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((c) => ({
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
- // Skip virtual fields
86
- if (field.virtual)
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,