uql-orm 0.34.0 → 0.35.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 (32) hide show
  1. package/README.md +1 -1
  2. package/dist/dialect/abstractDialect.js +2 -3
  3. package/dist/dialect/abstractSqlDialect.d.ts +6 -0
  4. package/dist/dialect/abstractSqlDialect.js +10 -3
  5. package/dist/migrate/builder/expressions.d.ts +8 -28
  6. package/dist/migrate/builder/expressions.js +8 -40
  7. package/dist/migrate/builder/migrationBuilder.js +3 -1
  8. package/dist/migrate/builder/tableBuilder.js +5 -4
  9. package/dist/migrate/generator/definitionToNode.js +3 -3
  10. package/dist/migrate/generator/mongoSchemaGenerator.js +3 -2
  11. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
  12. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +16 -2
  13. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +7 -1
  14. package/dist/migrate/introspection/baseSqlIntrospector.js +11 -12
  15. package/dist/migrate/introspection/mongoIntrospector.js +0 -1
  16. package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
  17. package/dist/migrate/introspection/mysqlIntrospector.js +8 -6
  18. package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
  19. package/dist/migrate/introspection/postgresIntrospector.js +6 -5
  20. package/dist/migrate/migrator.d.ts +14 -5
  21. package/dist/migrate/migrator.js +32 -10
  22. package/dist/migrate/schemaGenerator.d.ts +12 -8
  23. package/dist/migrate/schemaGenerator.js +44 -29
  24. package/dist/schema/schemaAST.d.ts +8 -1
  25. package/dist/schema/schemaAST.js +22 -13
  26. package/dist/schema/schemaASTBuilder.d.ts +4 -2
  27. package/dist/schema/schemaASTBuilder.js +17 -25
  28. package/dist/schema/types.d.ts +10 -3
  29. package/dist/type/migration.d.ts +15 -1
  30. package/dist/util/sql.util.d.ts +17 -0
  31. package/dist/util/sql.util.js +23 -0
  32. package/package.json +2 -2
@@ -2,7 +2,7 @@ import { mkdir, readdir, writeFile } from 'node:fs/promises';
2
2
  import { basename, extname, join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { getEntities, getMeta } from '../entity/index.js';
5
- import { introspectSchema } from '../schema/index.js';
5
+ import { introspectSchema, SchemaAST } from '../schema/index.js';
6
6
  import { isKnownMigratorDialect, isSqlQuerier } from '../type/index.js';
7
7
  import { LoggerWrapper } from '../util/index.js';
8
8
  import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
@@ -71,19 +71,20 @@ export class Migrator {
71
71
  setSchemaGenerator(generator) {
72
72
  this.schemaGenerator = generator;
73
73
  }
74
- createIntrospector() {
74
+ /** `schema` reads one namespace instead of the connection's own; see {@link BaseSqlIntrospector.schema}. */
75
+ createIntrospector(schema) {
75
76
  const d = this.dialectName;
76
77
  if (!isKnownMigratorDialect(d)) {
77
78
  return undefined;
78
79
  }
79
80
  switch (d) {
80
81
  case 'postgres':
81
- return new PostgresSchemaIntrospector(this.pool);
82
+ return new PostgresSchemaIntrospector(this.pool, schema);
82
83
  case 'cockroachdb':
83
- return new CockroachSchemaIntrospector(this.pool);
84
+ return new CockroachSchemaIntrospector(this.pool, schema);
84
85
  case 'mysql':
85
86
  case 'mariadb':
86
- return new MysqlSchemaIntrospector(this.pool);
87
+ return new MysqlSchemaIntrospector(this.pool, schema);
87
88
  case 'sqlite':
88
89
  return new SqliteSchemaIntrospector(this.pool);
89
90
  case 'mongodb':
@@ -279,7 +280,7 @@ export class Migrator {
279
280
  if (!this.schemaGenerator || !this.schemaIntrospector) {
280
281
  throw new TypeError('Schema generator and introspector must be set');
281
282
  }
282
- const ast = await introspectSchema(this.schemaIntrospector);
283
+ const ast = await this.introspectClaimedSchemas();
283
284
  const diffs = [];
284
285
  for (const entity of this.entities) {
285
286
  const meta = getMeta(entity);
@@ -292,6 +293,27 @@ export class Migrator {
292
293
  }
293
294
  return diffs;
294
295
  }
296
+ /**
297
+ * One AST spanning every schema the entities claim, each table stamped with the schema it was read
298
+ * from. Read per schema rather than all at once, so a table comes back keyed exactly as the entity
299
+ * that wants it spells the key: `undefined` on both sides for the connection's default, a name on
300
+ * both sides otherwise. Ordinarily that is one schema and one pass, as before, and that pass keeps
301
+ * {@link schemaIntrospector} so a caller that replaced it still wins.
302
+ */
303
+ async introspectClaimedSchemas() {
304
+ const claimed = new Set(this.entities.map((entity) => this.pool.dialect.resolveSchema(getMeta(entity))));
305
+ const merged = new SchemaAST();
306
+ for (const schema of claimed) {
307
+ const introspector = schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
308
+ if (!introspector) {
309
+ continue;
310
+ }
311
+ for (const table of (await introspectSchema(introspector)).getTables()) {
312
+ merged.addTable(table);
313
+ }
314
+ }
315
+ return merged;
316
+ }
295
317
  async findEntityForTable(tableName) {
296
318
  await this.ensureSchemaGenerator();
297
319
  if (!this.schemaGenerator) {
@@ -549,10 +571,10 @@ export function defineMigration(migration) {
549
571
  * ```ts
550
572
  * export default defineBuilderMigration({
551
573
  * async up(m) {
552
- * await m.createTable('users', (table) => {
553
- * table.id();
554
- * table.string('email', 255).unique();
555
- * table.timestamps();
574
+ * await m.createTable('users', (t) => {
575
+ * t.id();
576
+ * t.string('email', { length: 255 }).unique();
577
+ * t.timestamps();
556
578
  * });
557
579
  * },
558
580
  * async down(m) {
@@ -14,10 +14,10 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
14
14
  get namingStrategy(): NamingStrategy | undefined;
15
15
  get features(): DialectFeatures;
16
16
  resolveTableName<E>(meta: EntityMeta<E>): string;
17
+ resolveTableAlias<E>(meta: EntityMeta<E>): string;
18
+ resolveSchema<E>(meta: EntityMeta<E>): string | undefined;
17
19
  resolveColumnName(key: string, field: FieldOptions): string;
18
- /**
19
- * Escape an identifier (table name, column name, etc.)
20
- */
20
+ /** Escape an identifier (table name, column name, etc.) */
21
21
  protected escapeId(identifier: string): string;
22
22
  /**
23
23
  * Primary key type for auto-increment integer IDs
@@ -39,9 +39,9 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
39
39
  */
40
40
  generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
41
41
  /**
42
- * One statement per distinct schema the entities name, in first-seen order. `resolveSchema` is
43
- * already `undefined` on an engine without schemas, so this is empty there, and empty in the
44
- * ordinary case where nothing named one.
42
+ * One statement per distinct schema the tables being created live in, in first-seen order. Only
43
+ * the tables actually being created, so a narrowed `only` does not declare namespaces it is not
44
+ * about to fill. Empty on an engine without schemas, whose tables are never qualified.
45
45
  */
46
46
  private generateCreateSchemas;
47
47
  generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
@@ -57,7 +57,11 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
57
57
  generateCreateIndex(tableName: string, index: IndexSchema, options?: {
58
58
  ifNotExists?: boolean;
59
59
  }): string;
60
- generateDropIndex(tableName: string, indexName: string): string;
60
+ /**
61
+ * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
62
+ * operand instead, which is already qualified.
63
+ */
64
+ generateDropIndex(tableName: string, indexName: string, schema?: string): string;
61
65
  /**
62
66
  * Generate a column definition from a {@link ColumnSchema}, whose type is the engine's own spelling
63
67
  * and may already carry its precision (or even `PRIMARY KEY`, for a serial).
@@ -162,7 +166,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
162
166
  * other way and the table is created under one name and compared under another, which reports every
163
167
  * table of a project using a naming strategy as both missing and unexpected.
164
168
  */
165
- export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableName' | 'resolveColumnName'>, entities: readonly Type<unknown>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
169
+ export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName'>, entities: readonly Type<unknown>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
166
170
  /**
167
171
  * Synchronous factory for SQL schema generators only.
168
172
  * For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
@@ -2,7 +2,8 @@ import { AbstractSqlDialect } from '../dialect/index.js';
2
2
  import { getMeta } from '../entity/index.js';
3
3
  import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } from '../schema/canonicalType.js';
4
4
  import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
5
- import { escapeSqlId, getKeys, isAutoIncrement } from '../util/index.js';
5
+ import { getKeys, isAutoIncrement, qualifyName } from '../util/index.js';
6
+ import { derivedForeignKeyName } from '../util/sql.util.js';
6
7
  import { formatDefaultValue } from './builder/expressions.js';
7
8
  import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
8
9
  import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
@@ -26,14 +27,18 @@ export class SqlSchemaGenerator {
26
27
  resolveTableName(meta) {
27
28
  return this.dialect.resolveTableName(meta);
28
29
  }
30
+ resolveTableAlias(meta) {
31
+ return this.dialect.resolveTableAlias(meta);
32
+ }
33
+ resolveSchema(meta) {
34
+ return this.dialect.resolveSchema(meta);
35
+ }
29
36
  resolveColumnName(key, field) {
30
37
  return this.dialect.resolveColumnName(key, field);
31
38
  }
32
- /**
33
- * Escape an identifier (table name, column name, etc.)
34
- */
39
+ /** Escape an identifier (table name, column name, etc.) */
35
40
  escapeId(identifier) {
36
- return escapeSqlId(identifier, this.dialect.escapeIdChar);
41
+ return this.dialect.escapeId(identifier);
37
42
  }
38
43
  /**
39
44
  * Primary key type for auto-increment integer IDs
@@ -67,15 +72,15 @@ export class SqlSchemaGenerator {
67
72
  const inline = withForeignKeys && !this.features.foreignKeyAlter;
68
73
  // Namespaces first: a qualified `CREATE TABLE` fails against a schema nobody created, and the
69
74
  // schema is the one part of the layout a migration cannot infer from the table it is making.
70
- const statements = this.generateCreateSchemas(entities);
75
+ const statements = this.generateCreateSchemas(tables);
71
76
  statements.push(...tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options)));
72
77
  if (withForeignKeys && !inline) {
73
78
  for (const table of tables) {
74
79
  for (const rel of table.outgoingRelations) {
75
- statements.push(this.generateAddForeignKeySql(table.name, {
80
+ statements.push(this.generateAddForeignKeySql(qualifyName(table.name, table.schema), {
76
81
  name: rel.name,
77
82
  columns: rel.from.columns.map((c) => c.name),
78
- referencesTable: rel.to.table.name,
83
+ referencesTable: qualifyName(rel.to.table.name, rel.to.table.schema),
79
84
  referencesColumns: rel.to.columns.map((c) => c.name),
80
85
  onDelete: rel.onDelete ?? this.defaultForeignKeyAction,
81
86
  onUpdate: rel.onUpdate ?? this.defaultForeignKeyAction,
@@ -86,18 +91,16 @@ export class SqlSchemaGenerator {
86
91
  return statements;
87
92
  }
88
93
  /**
89
- * One statement per distinct schema the entities name, in first-seen order. `resolveSchema` is
90
- * already `undefined` on an engine without schemas, so this is empty there, and empty in the
91
- * ordinary case where nothing named one.
94
+ * One statement per distinct schema the tables being created live in, in first-seen order. Only
95
+ * the tables actually being created, so a narrowed `only` does not declare namespaces it is not
96
+ * about to fill. Empty on an engine without schemas, whose tables are never qualified.
92
97
  */
93
- generateCreateSchemas(entities) {
94
- const named = entities
95
- .map((entity) => this.dialect.resolveSchema(getMeta(entity)))
96
- .filter((it) => it !== undefined);
98
+ generateCreateSchemas(tables) {
99
+ const named = tables.map((table) => table.schema).filter((it) => it !== undefined);
97
100
  return [...new Set(named)].map((schema) => this.dialect.createSchemaSql(schema));
98
101
  }
99
102
  generateDropSchema(entities, options = {}) {
100
- return this.orderedTables(entities, 'drop').map((table) => this.generateDropTable(table.name, options));
103
+ return this.orderedTables(entities, 'drop').map((table) => this.generateDropTable(qualifyName(table.name, table.schema), options));
101
104
  }
102
105
  /**
103
106
  * The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
@@ -111,7 +114,7 @@ export class SqlSchemaGenerator {
111
114
  return tables;
112
115
  }
113
116
  const wanted = new Set(only);
114
- return tables.filter((table) => wanted.has(table.name));
117
+ return tables.filter((table) => wanted.has(qualifyName(table.name, table.schema)));
115
118
  }
116
119
  generateDropTable(tableName, options = {}) {
117
120
  const ifExists = options.ifExists ? 'IF EXISTS ' : '';
@@ -151,7 +154,7 @@ export class SqlSchemaGenerator {
151
154
  // Drop indexes
152
155
  if (diff.indexesToDrop?.length) {
153
156
  for (const indexName of diff.indexesToDrop) {
154
- statements.push(this.generateDropIndex(diff.tableName, indexName));
157
+ statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
155
158
  }
156
159
  }
157
160
  return statements;
@@ -176,7 +179,7 @@ export class SqlSchemaGenerator {
176
179
  // Reverse index additions by dropping them
177
180
  if (diff.indexesToAdd?.length) {
178
181
  for (const index of diff.indexesToAdd) {
179
- statements.push(this.generateDropIndex(diff.tableName, index.name));
182
+ statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
180
183
  }
181
184
  }
182
185
  if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
@@ -187,11 +190,15 @@ export class SqlSchemaGenerator {
187
190
  generateCreateIndex(tableName, index, options = {}) {
188
191
  return this.dialect.getCreateIndexStatement(tableName, index, options);
189
192
  }
190
- generateDropIndex(tableName, indexName) {
193
+ /**
194
+ * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
195
+ * operand instead, which is already qualified.
196
+ */
197
+ generateDropIndex(tableName, indexName, schema) {
191
198
  if (this.dialect.dropIndexSyntax === 'on-table') {
192
199
  return `DROP INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)};`;
193
200
  }
194
- return `DROP INDEX IF EXISTS ${this.escapeId(indexName)};`;
201
+ return `DROP INDEX IF EXISTS ${this.dialect.escapeQualifiedId(indexName, schema)};`;
195
202
  }
196
203
  /**
197
204
  * Generate a column definition from a {@link ColumnSchema}, whose type is the engine's own spelling
@@ -316,7 +323,8 @@ export class SqlSchemaGenerator {
316
323
  const meta = getMeta(entity);
317
324
  if (!currentTable) {
318
325
  return {
319
- tableName: this.dialect.resolveTableName(meta),
326
+ tableName: this.resolveTableName(meta),
327
+ schema: this.resolveSchema(meta),
320
328
  type: 'create',
321
329
  };
322
330
  }
@@ -354,7 +362,8 @@ export class SqlSchemaGenerator {
354
362
  return undefined;
355
363
  }
356
364
  return {
357
- tableName: this.dialect.resolveTableName(meta),
365
+ tableName: this.resolveTableName(meta),
366
+ schema: this.resolveSchema(meta),
358
367
  type: 'alter',
359
368
  columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
360
369
  columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
@@ -371,7 +380,9 @@ export class SqlSchemaGenerator {
371
380
  * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
372
381
  */
373
382
  missingIndexes(entity, currentTable) {
374
- const desired = buildEntityAST(this, [entity]).getTable(currentTable.name)?.indexes ?? [];
383
+ // Keyed by the qualified name, so a table in a schema finds itself rather than reporting that
384
+ // the entity declares no indexes at all.
385
+ const desired = buildEntityAST(this, [entity]).getTable(qualifyName(currentTable.name, currentTable.schema))?.indexes ?? [];
375
386
  const present = new Set(currentTable.indexes.map((index) => index.name));
376
387
  return desired
377
388
  .filter((index) => !present.has(index.name) && !this.isInlineVectorIndex(index))
@@ -491,12 +502,12 @@ export class SqlSchemaGenerator {
491
502
  const fromCols = rel.from.columns.map((c) => this.escapeId(c.name)).join(', ');
492
503
  const toCols = rel.to.columns.map((c) => this.escapeId(c.name)).join(', ');
493
504
  const constraintName = rel.name ? `CONSTRAINT ${this.escapeId(rel.name)} ` : '';
494
- constraints.push(`${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.escapeId(rel.to.table.name)} (${toCols})` +
505
+ constraints.push(`${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema)} (${toCols})` +
495
506
  ` ON DELETE ${rel.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${rel.onUpdate ?? this.defaultForeignKeyAction}`);
496
507
  }
497
508
  }
498
509
  const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
499
- let createSql = `CREATE TABLE ${ifNotExists}${this.escapeId(table.name)} (\n`;
510
+ let createSql = `CREATE TABLE ${ifNotExists}${this.dialect.escapeQualifiedId(table.name, table.schema)} (\n`;
500
511
  createSql += columns.map((col) => ` ${col}`).join(',\n');
501
512
  if (constraints.length > 0) {
502
513
  createSql += ',\n';
@@ -542,7 +553,8 @@ export class SqlSchemaGenerator {
542
553
  * Delegates to `generateCreateIndex` for unified SQL assembly.
543
554
  */
544
555
  generateCreateIndexFromNode(index, options = { ifNotExists: false }) {
545
- return this.generateCreateIndex(index.table.name, indexNodeToSchema(index), options);
556
+ // The index's own name stays unqualified: it is created in the schema of the table it is on.
557
+ return this.generateCreateIndex(qualifyName(index.table.name, index.table.schema), indexNodeToSchema(index), options);
546
558
  }
547
559
  generateCreateTableFromDefinition(table, options = {}) {
548
560
  const tableNode = tableDefinitionToNode(table);
@@ -573,7 +585,7 @@ export class SqlSchemaGenerator {
573
585
  const refCols = foreignKey.referencesColumns.map((c) => this.escapeId(c)).join(', ');
574
586
  const constraintName = foreignKey.name
575
587
  ? this.escapeId(foreignKey.name)
576
- : this.escapeId(`fk_${tableName}_${foreignKey.columns.join('_')}`);
588
+ : this.escapeId(derivedForeignKeyName(tableName, foreignKey.columns));
577
589
  if (!this.features.foreignKeyAlter) {
578
590
  throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
579
591
  }
@@ -595,7 +607,10 @@ export class SqlSchemaGenerator {
595
607
  */
596
608
  export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
597
609
  return buildSchemaAST(entities, {
598
- resolveTableName: (meta) => generator.resolveTableName(meta),
610
+ // The alias, not `resolveTableName`: a node holds its schema separately, so that a name derived
611
+ // from it stays a single identifier.
612
+ resolveTableName: (meta) => generator.resolveTableAlias(meta),
613
+ resolveSchema: (meta) => generator.resolveSchema(meta),
599
614
  resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
600
615
  defaultForeignKeyAction,
601
616
  });
@@ -5,6 +5,12 @@
5
5
  * Provides graph operations like navigation, validation, and topological sorting.
6
6
  */
7
7
  import type { ColumnNode, IndexNode, SchemaAST as ISchemaAST, RelationshipNode, RelationshipType, TableNode, ValidationError } from './types.js';
8
+ /**
9
+ * A table node with its collections empty, ready to be filled. Six places build one, and the fields
10
+ * that are pure boilerplate are exactly the ones a new field gets forgotten in: {@link TableNode.schema}
11
+ * was added and `SchemaAST.clone` kept rebuilding nodes without it.
12
+ */
13
+ export declare function createTableNode(name: string, schema?: string, comment?: string): TableNode;
8
14
  /**
9
15
  * Schema AST - A graph representation of a database schema.
10
16
  *
@@ -20,7 +26,8 @@ export declare class SchemaAST implements ISchemaAST {
20
26
  readonly relationships: RelationshipNode[];
21
27
  readonly indexes: IndexNode[];
22
28
  /**
23
- * Get a table by name.
29
+ * Get a table by the name it is keyed under: schema-qualified where it has one, so two tables of
30
+ * the same name in different schemas stay distinct. Build the key with `qualifyName`.
24
31
  */
25
32
  getTable(name: string): TableNode | undefined;
26
33
  /**
@@ -4,6 +4,24 @@
4
4
  * The main class for working with schema graphs.
5
5
  * Provides graph operations like navigation, validation, and topological sorting.
6
6
  */
7
+ import { qualifyName } from '../util/sql.util.js';
8
+ /**
9
+ * A table node with its collections empty, ready to be filled. Six places build one, and the fields
10
+ * that are pure boilerplate are exactly the ones a new field gets forgotten in: {@link TableNode.schema}
11
+ * was added and `SchemaAST.clone` kept rebuilding nodes without it.
12
+ */
13
+ export function createTableNode(name, schema, comment) {
14
+ return {
15
+ name,
16
+ schema,
17
+ comment,
18
+ columns: new Map(),
19
+ primaryKey: [],
20
+ indexes: [],
21
+ incomingRelations: [],
22
+ outgoingRelations: [],
23
+ };
24
+ }
7
25
  /**
8
26
  * Schema AST - A graph representation of a database schema.
9
27
  *
@@ -19,7 +37,8 @@ export class SchemaAST {
19
37
  relationships = [];
20
38
  indexes = [];
21
39
  /**
22
- * Get a table by name.
40
+ * Get a table by the name it is keyed under: schema-qualified where it has one, so two tables of
41
+ * the same name in different schemas stay distinct. Build the key with `qualifyName`.
23
42
  */
24
43
  getTable(name) {
25
44
  return this.tables.get(name);
@@ -28,8 +47,7 @@ export class SchemaAST {
28
47
  * Add a table to the schema.
29
48
  */
30
49
  addTable(table) {
31
- table.schema = this;
32
- this.tables.set(table.name, table);
50
+ this.tables.set(qualifyName(table.name, table.schema), table);
33
51
  }
34
52
  /**
35
53
  * Remove a table from the schema.
@@ -353,16 +371,7 @@ export class SchemaAST {
353
371
  // back to it on creation - `columns` and `primaryKey` are readonly properties holding mutable
354
372
  // containers, so they are filled in afterwards without reassigning anything.
355
373
  for (const [name, table] of this.tables) {
356
- const clonedTable = {
357
- name,
358
- columns: new Map(),
359
- primaryKey: [],
360
- indexes: [],
361
- comment: table.comment,
362
- schema: clone,
363
- incomingRelations: [],
364
- outgoingRelations: [],
365
- };
374
+ const clonedTable = createTableNode(table.name, table.schema, table.comment);
366
375
  for (const [colName, col] of table.columns) {
367
376
  clonedTable.columns.set(colName, { ...col, table: clonedTable, referencedBy: [], references: undefined });
368
377
  }
@@ -8,13 +8,15 @@
8
8
  import type { EntityMeta, FieldOptions, Type } from '../type/index.js';
9
9
  import type { NamingStrategy } from '../type/namingStrategy.js';
10
10
  import { SchemaAST } from './schemaAST.js';
11
- import type { ForeignKeyAction } from './types.js';
11
+ import { type ForeignKeyAction } from './types.js';
12
12
  /**
13
13
  * Options for building SchemaAST from entities.
14
14
  */
15
15
  export interface BuildSchemaASTOptions {
16
- /** Custom table name resolver */
16
+ /** Custom resolver for a table's own name, unqualified. */
17
17
  resolveTableName?: (meta: EntityMeta<unknown>) => string;
18
+ /** Custom resolver for the schema a table lives in; `undefined` leaves it unqualified. */
19
+ resolveSchema?: (meta: EntityMeta<unknown>) => string | undefined;
18
20
  /** Custom column name resolver */
19
21
  resolveColumnName?: (key: string, field: FieldOptions) => string;
20
22
  /** Naming strategy to use */
@@ -6,9 +6,10 @@
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
8
  import { getMeta } from '../entity/metadata/definition.js';
9
+ import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
9
10
  import { fieldOptionsToCanonical } from './canonicalType.js';
10
- import { SchemaAST } from './schemaAST.js';
11
- import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
11
+ import { createTableNode, SchemaAST } from './schemaAST.js';
12
+ import { DEFAULT_FOREIGN_KEY_ACTION, } from './types.js';
12
13
  /**
13
14
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
14
15
  *
@@ -21,6 +22,7 @@ export function buildSchemaAST(entities, options = {}) {
21
22
  ast: new SchemaAST(),
22
23
  resolveTableName: options.resolveTableName ??
23
24
  ((m) => namingStrategy?.tableName(m.name ?? m.entity.name) ?? m.name ?? m.entity.name),
25
+ resolveSchema: options.resolveSchema ?? ((m) => m.schema),
24
26
  resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
25
27
  defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
26
28
  };
@@ -67,18 +69,8 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
67
69
  */
68
70
  function addTableFromEntity(ctx, meta) {
69
71
  const tableName = ctx.resolveTableName(meta);
70
- const columns = new Map();
71
- const primaryKey = [];
72
- // Create placeholder table (will be fully initialized below)
73
- const table = {
74
- name: tableName,
75
- columns,
76
- primaryKey,
77
- indexes: [],
78
- schema: ctx.ast,
79
- incomingRelations: [],
80
- outgoingRelations: [],
81
- };
72
+ const table = createTableNode(tableName, ctx.resolveSchema(meta));
73
+ const { columns, primaryKey } = table;
82
74
  // Add columns from fields
83
75
  const fields = meta.fields;
84
76
  for (const key of Object.keys(fields)) {
@@ -113,12 +105,15 @@ function addTableFromEntity(ctx, meta) {
113
105
  }
114
106
  ctx.ast.addTable(table);
115
107
  }
108
+ /** The node an entity maps to, found under the key {@link SchemaAST} stores it by. */
109
+ function tableOf(ctx, meta) {
110
+ return ctx.ast.getTable(qualifyName(ctx.resolveTableName(meta), ctx.resolveSchema(meta)));
111
+ }
116
112
  /**
117
113
  * Add relationships from entity relation decorators.
118
114
  */
119
115
  function addRelationshipsFromEntity(ctx, meta) {
120
- const tableName = ctx.resolveTableName(meta);
121
- const table = ctx.ast.getTable(tableName);
116
+ const table = tableOf(ctx, meta);
122
117
  if (!table)
123
118
  return;
124
119
  const relations = meta.relations;
@@ -126,10 +121,8 @@ function addRelationshipsFromEntity(ctx, meta) {
126
121
  const relation = relations[key];
127
122
  if (!relation)
128
123
  continue;
129
- const relatedEntity = relation.entity();
130
- const relatedMeta = getMeta(relatedEntity);
131
- const relatedTableName = ctx.resolveTableName(relatedMeta);
132
- const relatedTable = ctx.ast.getTable(relatedTableName);
124
+ const relatedMeta = getMeta(relation.entity());
125
+ const relatedTable = tableOf(ctx, relatedMeta);
133
126
  if (!relatedTable)
134
127
  continue;
135
128
  // Only the owning side gets the FK. `mappedBy` marks the inverse side of a one-to-one, whose
@@ -152,7 +145,7 @@ function addRelationshipsFromEntity(ctx, meta) {
152
145
  const foreignColumn = relatedTable.columns.get(foreignColName);
153
146
  if (localColumn && foreignColumn) {
154
147
  const relNode = {
155
- name: `fk_${tableName}_${localColName}`,
148
+ name: derivedForeignKeyName(table.name, [localColName]),
156
149
  type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
157
150
  from: { table, columns: [localColumn] },
158
151
  to: { table: relatedTable, columns: [foreignColumn] },
@@ -173,8 +166,7 @@ function addRelationshipsFromEntity(ctx, meta) {
173
166
  * in common beyond their target table.
174
167
  */
175
168
  function addIndexesFromEntity(ctx, meta) {
176
- const tableName = ctx.resolveTableName(meta);
177
- const table = ctx.ast.getTable(tableName);
169
+ const table = tableOf(ctx, meta);
178
170
  if (!table)
179
171
  return;
180
172
  for (const key of Object.keys(meta.fields)) {
@@ -185,7 +177,7 @@ function addIndexesFromEntity(ctx, meta) {
185
177
  if (!column)
186
178
  continue;
187
179
  ctx.ast.addIndex({
188
- name: typeof field.index === 'string' ? field.index : `idx_${tableName}_${column.name}`,
180
+ name: typeof field.index === 'string' ? field.index : derivedIndexName(table.name, [column.name]),
189
181
  table,
190
182
  entries: [{ column: column.name }],
191
183
  unique: field.unique ?? false,
@@ -224,7 +216,7 @@ function addCompositeIndex(ctx, table, meta, idxMeta) {
224
216
  // An index over expressions alone has no column names to build a default name from.
225
217
  const named = entries.map((entry, at) => (entry.expression ? `expr${at}` : entry.column));
226
218
  ctx.ast.addIndex({
227
- name: idxMeta.name ?? `idx_${table.name}_${named.join('_')}`,
219
+ name: idxMeta.name ?? derivedIndexName(table.name, named),
228
220
  table,
229
221
  entries,
230
222
  include: idxMeta.include?.map((column) => resolveIncludeColumn(ctx, meta, column)),
@@ -98,8 +98,17 @@ export interface ColumnNode {
98
98
  * Represents a database table with all its columns, indexes, and relationships.
99
99
  */
100
100
  export interface TableNode {
101
- /** Table name in the database */
101
+ /**
102
+ * The table's own name, never qualified. Everything derived from a table reads this: an index or
103
+ * constraint name is a single identifier, and `idx_sales.Order_total` is a syntax error.
104
+ */
102
105
  readonly name: string;
106
+ /**
107
+ * The namespace the table lives in, absent where nothing named one and it resolves through the
108
+ * connection's own default. Joined onto {@link name} by `qualifyName`, which is the key a
109
+ * `SchemaAST` stores the table under and the operand a statement names it by.
110
+ */
111
+ readonly schema?: string;
103
112
  /** Map of column name to column node */
104
113
  readonly columns: Map<string, ColumnNode>;
105
114
  /** Primary key columns (supports composite keys) */
@@ -108,8 +117,6 @@ export interface TableNode {
108
117
  readonly indexes: IndexNode[];
109
118
  /** Optional table comment */
110
119
  readonly comment?: string;
111
- /** Reference to the parent schema */
112
- schema: SchemaAST;
113
120
  /** Relationships pointing TO this table (other tables referencing this one) */
114
121
  incomingRelations: RelationshipNode[];
115
122
  /** Relationships pointing FROM this table (this table referencing others) */
@@ -168,7 +168,13 @@ export interface ForeignKeySchema {
168
168
  * Represents a difference between current and desired schema
169
169
  */
170
170
  export interface SchemaDiff {
171
+ /** Qualified where the table has a schema, since it is also the key the table is found under. */
171
172
  readonly tableName: string;
173
+ /**
174
+ * The schema {@link tableName} is in, carried separately for the identifiers that live in it
175
+ * rather than name it: a Postgres index is dropped as `schema.index`, never `schema.table`.
176
+ */
177
+ readonly schema?: string;
172
178
  readonly type: 'create' | 'alter' | 'drop';
173
179
  readonly columnsToAdd?: ColumnSchema[];
174
180
  readonly columnsToAlter?: {
@@ -243,9 +249,17 @@ export interface SchemaGenerator {
243
249
  */
244
250
  diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
245
251
  /**
246
- * Resolve table name using entity and naming strategy
252
+ * The table's key: {@link resolveTableAlias} behind {@link resolveSchema}, which is how a
253
+ * `SchemaAST` stores it and how a diff finds it again.
247
254
  */
248
255
  resolveTableName<E>(meta: EntityMeta<E>): string;
256
+ /**
257
+ * The table's own name, unqualified. What a derived index or constraint name is built from, since
258
+ * those are single identifiers.
259
+ */
260
+ resolveTableAlias<E>(meta: EntityMeta<E>): string;
261
+ /** The schema the table lives in, `undefined` where nothing named one. */
262
+ resolveSchema<E>(meta: EntityMeta<E>): string | undefined;
249
263
  /**
250
264
  * Resolve column name using field options and naming strategy
251
265
  */
@@ -9,6 +9,23 @@ export declare function unflatObject<T extends object>(row: RawRow, attrsPaths:
9
9
  export declare function obtainAttrsPaths<T extends object>(row: T): {
10
10
  [k: string]: string[];
11
11
  };
12
+ /**
13
+ * A name behind its namespace, or bare where there is none: the one place the two are joined, so a
14
+ * table's key, its statement operand and its escaped form cannot spell it differently. Never the
15
+ * seed for a derived identifier - an index or constraint name is a single identifier, and
16
+ * `idx_sales.Order_total` is a syntax error.
17
+ */
18
+ export declare function qualifyName(name: string, schema?: string): string;
19
+ /**
20
+ * The name a derived index or constraint gets when nothing named it: `idx_Order_total`.
21
+ *
22
+ * One owner, because it is a rule two layers apply and a third has to match: the entity AST derives
23
+ * it, the DDL generator falls back to it, and a diff compares what the database reports against it.
24
+ * `table` is the table's own name, never qualified - the result is a single identifier.
25
+ */
26
+ export declare function derivedIndexName(table: string, columns: readonly string[]): string;
27
+ /** The constraint name a foreign key gets when nothing named it: `fk_Order_customerId`. */
28
+ export declare function derivedForeignKeyName(table: string, columns: readonly string[]): string;
12
29
  /**
13
30
  * Escape a SQL identifier (table name, column name, etc.)
14
31
  * @param val the identifier to escape