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.
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 +120 -38
  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 -11
  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,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
- const colDef = this.generateColumnDefinitionFromSchema(column);
146
- statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${colDef};`);
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.name, column.comment);
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
- * 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.
355
398
  */
356
- generateColumnComment(columnName, comment) {
357
- if (this.features.columnComment) {
358
- const escapedComment = comment.replace(/'/g, "''");
359
- return ` COMMENT '${escapedComment}'`;
399
+ generateColumnCommentStatement(tableName, column, schema) {
400
+ if (!column.comment || this.features.commentSyntax !== 'statement') {
401
+ return [];
360
402
  }
361
- 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)};`];
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
- ? [{ from: this.columnNodeToSchema(it.actual), to: this.columnNodeToSchema(it.expected) }]
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
- return {
465
- name: col.name,
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 fromCols = rel.from.columns.map((c) => this.escapeId(c.name)).join(', ');
535
- const toCols = rel.to.columns.map((c) => this.escapeId(c.name)).join(', ');
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
- 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');
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
- 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) {
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
- 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) {
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
- return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
616
- `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.references.table)} (${refCols}) ` +
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
- 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),