uql-orm 0.25.1 → 0.26.1

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 (56) hide show
  1. package/README.md +9 -8
  2. package/dist/browser/uql-browser.min.js.map +1 -1
  3. package/dist/cockroachdb/cockroachDialect.js +1 -1
  4. package/dist/dialect/indexSqlDialect.d.ts +3 -2
  5. package/dist/dialect/indexSqlDialect.js +10 -8
  6. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -2
  7. package/dist/dialect/mysqlLikeSqlDialect.js +0 -7
  8. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  9. package/dist/maria/mariaDialect.js +1 -1
  10. package/dist/migrate/builder/migrationBuilder.js +1 -1
  11. package/dist/migrate/builder/tableBuilder.js +2 -2
  12. package/dist/migrate/cli.js +4 -6
  13. package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
  14. package/dist/migrate/codegen/entityCodeGenerator.js +22 -25
  15. package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -1
  16. package/dist/migrate/codegen/fieldOptionsSource.js +6 -1
  17. package/dist/migrate/codegen/indexDecoratorSource.d.ts +14 -0
  18. package/dist/migrate/codegen/indexDecoratorSource.js +105 -0
  19. package/dist/migrate/drift/driftDetector.d.ts +5 -50
  20. package/dist/migrate/drift/driftDetector.js +215 -224
  21. package/dist/migrate/drift/index.d.ts +1 -1
  22. package/dist/migrate/drift/index.js +1 -1
  23. package/dist/migrate/generator/definitionToNode.d.ts +9 -0
  24. package/dist/migrate/generator/definitionToNode.js +79 -0
  25. package/dist/migrate/generator/indexNodeToSchema.js +4 -2
  26. package/dist/migrate/generator/mongoSchemaGenerator.js +3 -3
  27. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +3 -0
  28. package/dist/migrate/introspection/baseSqlIntrospector.js +13 -5
  29. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -0
  30. package/dist/migrate/introspection/mongoIntrospector.js +5 -10
  31. package/dist/migrate/introspection/mysqlIntrospector.js +1 -1
  32. package/dist/migrate/introspection/postgresIntrospector.d.ts +57 -5
  33. package/dist/migrate/introspection/postgresIntrospector.js +93 -10
  34. package/dist/migrate/introspection/sqliteIntrospector.js +1 -1
  35. package/dist/migrate/migrator.js +3 -2
  36. package/dist/migrate/schemaGenerator.d.ts +26 -3
  37. package/dist/migrate/schemaGenerator.js +53 -92
  38. package/dist/schema/index.d.ts +3 -3
  39. package/dist/schema/index.js +2 -2
  40. package/dist/schema/indexColumns.d.ts +10 -0
  41. package/dist/schema/indexColumns.js +11 -0
  42. package/dist/schema/indexDifferences.d.ts +22 -0
  43. package/dist/schema/indexDifferences.js +49 -0
  44. package/dist/schema/schemaAST.js +2 -8
  45. package/dist/schema/schemaASTBuilder.d.ts +6 -59
  46. package/dist/schema/schemaASTBuilder.js +208 -236
  47. package/dist/schema/schemaASTDiffer.d.ts +9 -56
  48. package/dist/schema/schemaASTDiffer.js +229 -393
  49. package/dist/schema/types.d.ts +5 -12
  50. package/dist/schema/types.js +15 -0
  51. package/dist/type/dialect.d.ts +7 -2
  52. package/dist/type/dialect.js +1 -0
  53. package/dist/type/entity.d.ts +1 -1
  54. package/dist/type/migration.d.ts +14 -1
  55. package/dist/util/string.util.js +6 -1
  56. package/package.json +5 -5
@@ -6,6 +6,8 @@ import { escapeSqlId } from '../../util/index.js';
6
6
  */
7
7
  export class BaseSqlIntrospector {
8
8
  dialect;
9
+ /** Columns and uniqueness only; each introspector opts in to what its catalogue queries report. */
10
+ indexFacets = new Set();
9
11
  constructor(dialect) {
10
12
  this.dialect = dialect;
11
13
  }
@@ -82,8 +84,8 @@ export class BaseSqlIntrospector {
82
84
  const toTable = tableNodes.get(fk.referencedTable);
83
85
  if (!toTable)
84
86
  continue;
85
- const fromColumns = fk.columns.map((c) => fromTable.columns.get(c)).filter((c) => !!c);
86
- const toColumns = fk.referencedColumns.map((c) => toTable.columns.get(c)).filter((c) => !!c);
87
+ const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
88
+ const toColumns = fk.referencedColumns.flatMap((name) => toTable.columns.get(name) ?? []);
87
89
  if (fromColumns.length > 0 && toColumns.length > 0) {
88
90
  const rel = {
89
91
  name: fk.name,
@@ -106,13 +108,19 @@ export class BaseSqlIntrospector {
106
108
  if (!table)
107
109
  continue;
108
110
  for (const idx of schema.indexes) {
109
- const columns = idx.columns.map((e) => table.columns.get(e.column)).filter((c) => !!c);
110
- if (columns.length > 0) {
111
+ // An expression has no column to resolve. Dropping the entries that name a column this table
112
+ // does not have, and the index if that leaves none, is what the entity side does too.
113
+ const entries = idx.entries.filter((entry) => entry.expression || table.columns.has(entry.column));
114
+ if (entries.length > 0) {
111
115
  const index = {
112
116
  name: idx.name,
113
117
  table,
114
- columns,
118
+ entries,
115
119
  unique: idx.unique,
120
+ type: idx.type,
121
+ where: idx.where,
122
+ include: idx.include,
123
+ source: 'database',
116
124
  };
117
125
  ast.addIndex(index);
118
126
  }
@@ -1,3 +1,4 @@
1
+ import type { IndexFacet } from '../../schema/indexDifferences.js';
1
2
  import { SchemaAST } from '../../schema/schemaAST.js';
2
3
  import type { QuerierPool, SchemaIntrospector, TableSchema } from '../../type/index.js';
3
4
  /**
@@ -6,6 +7,8 @@ import type { QuerierPool, SchemaIntrospector, TableSchema } from '../../type/in
6
7
  */
7
8
  export declare class MongoSchemaIntrospector implements SchemaIntrospector {
8
9
  private readonly pool;
10
+ /** `listIndexes` reports keys and uniqueness; a `partialFilterExpression` is no SQL predicate. */
11
+ readonly indexFacets: ReadonlySet<IndexFacet>;
9
12
  constructor(pool: QuerierPool);
10
13
  introspect(): Promise<SchemaAST>;
11
14
  getTableSchema(tableName: string): Promise<TableSchema | undefined>;
@@ -5,6 +5,8 @@ import { SchemaAST } from '../../schema/schemaAST.js';
5
5
  */
6
6
  export class MongoSchemaIntrospector {
7
7
  pool;
8
+ /** `listIndexes` reports keys and uniqueness; a `partialFilterExpression` is no SQL predicate. */
9
+ indexFacets = new Set();
8
10
  constructor(pool) {
9
11
  this.pool = pool;
10
12
  }
@@ -26,8 +28,7 @@ export class MongoSchemaIntrospector {
26
28
  };
27
29
  if (schema.indexes) {
28
30
  for (const idx of schema.indexes) {
29
- const indexColumns = [];
30
- for (const { column: colName } of idx.columns) {
31
+ for (const { column: colName } of idx.entries) {
31
32
  let column = columns.get(colName);
32
33
  if (!column) {
33
34
  column = {
@@ -42,14 +43,8 @@ export class MongoSchemaIntrospector {
42
43
  };
43
44
  columns.set(colName, column);
44
45
  }
45
- indexColumns.push(column);
46
46
  }
47
- table.indexes.push({
48
- name: idx.name,
49
- table,
50
- columns: indexColumns,
51
- unique: idx.unique,
52
- });
47
+ table.indexes.push({ name: idx.name, table, entries: idx.entries, unique: idx.unique });
53
48
  }
54
49
  }
55
50
  ast.addTable(table);
@@ -73,7 +68,7 @@ export class MongoSchemaIntrospector {
73
68
  columns: [], // We don't have columns in Mongo
74
69
  indexes: indexes.map((idx) => ({
75
70
  name: idx.name ?? Object.keys(idx.key).join('_'),
76
- columns: Object.keys(idx.key).map((column) => ({ column })),
71
+ entries: Object.keys(idx.key).map((column) => ({ column })),
77
72
  unique: !!idx.unique,
78
73
  })),
79
74
  };
@@ -110,7 +110,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
110
110
  async mapIndexesResult(_read, _tableName, results) {
111
111
  return results.map((row) => ({
112
112
  name: row.index_name,
113
- columns: (row.columns || '').split(',').map((column) => ({ column })),
113
+ entries: (row.columns || '').split(',').map((column) => ({ column })),
114
114
  unique: Boolean(row.is_unique),
115
115
  }));
116
116
  }
@@ -1,22 +1,44 @@
1
+ import type { IndexFacet } from '../../schema/indexDifferences.js';
1
2
  import type { ColumnSchema, ForeignKeySchema, IndexSchema, RawRow } from '../../type/index.js';
2
3
  import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
4
  /**
4
5
  * PostgreSQL schema introspector
5
6
  */
6
7
  export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
+ /**
9
+ * Expressions and predicates are read back too, for `generate:from-db`, but they are text the
10
+ * database reprints in its own words, so they are not comparable and are not claimed here.
11
+ */
12
+ readonly indexFacets: ReadonlySet<IndexFacet>;
7
13
  protected getTableNamesQuery(): string;
8
14
  protected tableExistsQuery(): string;
9
15
  protected parseTableExistsResult(results: RawRow[]): boolean;
10
16
  protected getColumnsQuery(_tableName: string): string;
17
+ /**
18
+ * `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
19
+ * expression. Neither alone will do: an expression entry has `attnum = 0`, so joining `pg_attribute`
20
+ * on it silently dropped the entry (a `lower(email)` index read back as having no columns at all),
21
+ * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
22
+ * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
23
+ *
24
+ * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
25
+ * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
26
+ * project it had an index its entities never asked for.
27
+ */
11
28
  protected getIndexesQuery(_tableName: string): string;
29
+ /**
30
+ * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
31
+ * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
32
+ * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
33
+ *
34
+ * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
35
+ * compare. This one decides which indexes are reported at all.
36
+ */
37
+ protected readonly constraintIndexTypes: readonly string[];
12
38
  protected getForeignKeysQuery(_tableName: string): string;
13
39
  protected getPrimaryKeyQuery(_tableName: string): string;
14
40
  protected mapColumnsResult(_read: TableRowReader, _tableName: string, results: PostgresColumnRow[]): Promise<ColumnSchema[]>;
15
- protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: {
16
- index_name: string;
17
- columns: string[];
18
- is_unique: boolean;
19
- }[]): Promise<IndexSchema[]>;
41
+ protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: PostgresIndexRow[]): Promise<IndexSchema[]>;
20
42
  protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
21
43
  constraint_name: string;
22
44
  columns: string[];
@@ -29,6 +51,36 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
29
51
  protected parseDefaultValue(defaultValue: string | null): unknown;
30
52
  protected isAutoIncrement(columnDefault: string | null, isIdentity: string): boolean;
31
53
  }
54
+ /**
55
+ * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
56
+ * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
57
+ * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
58
+ * every ascending index as drifted, against an entity that could not have asked for one. Its access
59
+ * method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
60
+ */
61
+ export declare class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
62
+ readonly indexFacets: ReadonlySet<IndexFacet>;
63
+ /**
64
+ * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
65
+ * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
66
+ * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
67
+ * index a `@Field({ unique })` builds underneath itself stays visible there.
68
+ */
69
+ protected readonly constraintIndexTypes: readonly string[];
70
+ }
71
+ /** One entry of one index; what the index itself is repeats across its rows. */
72
+ type PostgresIndexRow = {
73
+ index_name: string;
74
+ is_unique: boolean;
75
+ method: string;
76
+ predicate: string | null;
77
+ is_key: boolean;
78
+ is_expression: boolean;
79
+ entry: string;
80
+ descending: boolean;
81
+ nulls_first: boolean;
82
+ ops_class: string | null;
83
+ };
32
84
  type PostgresColumnRow = {
33
85
  column_name: string;
34
86
  data_type: string;
@@ -1,8 +1,20 @@
1
+ import { INDEX_TYPES } from '../../schema/types.js';
1
2
  import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
2
3
  /**
3
4
  * PostgreSQL schema introspector
4
5
  */
5
6
  export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
7
+ /**
8
+ * Expressions and predicates are read back too, for `generate:from-db`, but they are text the
9
+ * database reprints in its own words, so they are not comparable and are not claimed here.
10
+ */
11
+ indexFacets = new Set([
12
+ 'order',
13
+ 'nulls',
14
+ 'opsClass',
15
+ 'accessMethod',
16
+ 'include',
17
+ ]);
6
18
  getTableNamesQuery() {
7
19
  return /*sql*/ `
8
20
  SELECT table_name
@@ -67,25 +79,58 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
67
79
  ORDER BY c.ordinal_position
68
80
  `;
69
81
  }
82
+ /**
83
+ * `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
84
+ * expression. Neither alone will do: an expression entry has `attnum = 0`, so joining `pg_attribute`
85
+ * on it silently dropped the entry (a `lower(email)` index read back as having no columns at all),
86
+ * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
87
+ * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
88
+ *
89
+ * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
90
+ * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
91
+ * project it had an index its entities never asked for.
92
+ */
70
93
  getIndexesQuery(_tableName) {
71
94
  return /*sql*/ `
72
95
  SELECT
73
96
  i.relname AS index_name,
74
- array_to_json(array_agg(a.attname ORDER BY k.n)) AS columns,
75
- ix.indisunique AS is_unique
97
+ ix.indisunique AS is_unique,
98
+ am.amname AS method,
99
+ pg_get_expr(ix.indpred, ix.indrelid, true) AS predicate,
100
+ k.n <= ix.indnkeyatts AS is_key,
101
+ k.attnum = 0 AS is_expression,
102
+ COALESCE(a.attname::text, pg_get_indexdef(ix.indexrelid, k.n::int, true)) AS entry,
103
+ (ix.indoption[k.n - 1] & 1) <> 0 AS descending,
104
+ (ix.indoption[k.n - 1] & 2) <> 0 AS nulls_first,
105
+ CASE WHEN op.opcdefault THEN NULL ELSE op.opcname END AS ops_class
76
106
  FROM pg_class t
77
107
  JOIN pg_index ix ON t.oid = ix.indrelid
78
108
  JOIN pg_class i ON i.oid = ix.indexrelid
109
+ JOIN pg_am am ON am.oid = i.relam
79
110
  JOIN pg_namespace n ON n.oid = t.relnamespace
80
111
  CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
81
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
112
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0
113
+ LEFT JOIN pg_opclass op ON op.oid = ix.indclass[k.n - 1]
82
114
  WHERE t.relname = $1
83
115
  AND n.nspname = 'public'
84
116
  AND NOT ix.indisprimary
85
- GROUP BY i.relname, ix.indisunique
86
- ORDER BY i.relname
117
+ AND NOT EXISTS (
118
+ SELECT 1 FROM pg_constraint con
119
+ WHERE con.conindid = ix.indexrelid
120
+ AND con.contype IN (${this.constraintIndexTypes.map((type) => `'${type}'`).join(', ')})
121
+ )
122
+ ORDER BY i.relname, k.n
87
123
  `;
88
124
  }
125
+ /**
126
+ * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
127
+ * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
128
+ * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
129
+ *
130
+ * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
131
+ * compare. This one decides which indexes are reported at all.
132
+ */
133
+ constraintIndexTypes = ['p', 'u', 'x'];
89
134
  getForeignKeysQuery(_tableName) {
90
135
  return /*sql*/ `
91
136
  SELECT
@@ -141,11 +186,18 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
141
186
  }));
142
187
  }
143
188
  async mapIndexesResult(_read, _tableName, results) {
144
- return results.map((row) => ({
145
- name: row.index_name,
146
- columns: row.columns.map((column) => ({ column })),
147
- unique: row.is_unique,
148
- }));
189
+ // One row per index entry, ordered by position, so the rows of an index are its entries in order.
190
+ return [...Map.groupBy(results, (row) => row.index_name)].map(([name, rows]) => {
191
+ const include = rows.filter((row) => !row.is_key).map((row) => row.entry);
192
+ return {
193
+ name,
194
+ entries: rows.filter((row) => row.is_key).map(mapIndexEntry),
195
+ unique: rows[0].is_unique,
196
+ type: INDEX_TYPES.find((type) => type === rows[0].method),
197
+ where: rows[0].predicate ?? undefined,
198
+ include: include.length > 0 ? include : undefined,
199
+ };
200
+ });
149
201
  }
150
202
  async mapForeignKeysResult(_read, _tableName, results) {
151
203
  return results.map((row) => ({
@@ -200,3 +252,34 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
200
252
  return columnDefault?.includes('nextval(') ?? false;
201
253
  }
202
254
  }
255
+ /**
256
+ * Postgres states every entry in full: a plain column still reports `order: 'asc'`, and only a
257
+ * non-default operator class is named. The diff defaults the entity side to match, so an option
258
+ * omitted there and one written out are not read as two different indexes.
259
+ */
260
+ function mapIndexEntry(row) {
261
+ return {
262
+ column: row.entry,
263
+ ...(row.is_expression && { expression: true }),
264
+ order: row.descending ? 'desc' : 'asc',
265
+ nulls: row.nulls_first ? 'first' : 'last',
266
+ ...(row.ops_class && { opsClass: row.ops_class }),
267
+ };
268
+ }
269
+ /**
270
+ * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
271
+ * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
272
+ * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
273
+ * every ascending index as drifted, against an entity that could not have asked for one. Its access
274
+ * method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
275
+ */
276
+ export class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
277
+ indexFacets = new Set(['order', 'include']);
278
+ /**
279
+ * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
280
+ * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
281
+ * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
282
+ * index a `@Field({ unique })` builds underneath itself stays visible there.
283
+ */
284
+ constraintIndexTypes = ['p', 'x'];
285
+ }
@@ -88,7 +88,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
88
88
  if (named.length === columns.length && (isUserCreated || isCompositeUnique)) {
89
89
  indexSchemas.push({
90
90
  name: index.name,
91
- columns: named.map((column) => ({ column: column.name })),
91
+ entries: named.map((column) => ({ column: column.name })),
92
92
  unique: Boolean(index.unique),
93
93
  });
94
94
  }
@@ -8,7 +8,7 @@ import { LoggerWrapper } from '../util/index.js';
8
8
  import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
9
9
  import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCalls, } from './codegen/migrationFile.js';
10
10
  import { runMongoCommand } from './generator/mongoCommand.js';
11
- import { MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
11
+ import { CockroachSchemaIntrospector, MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
12
12
  import { createSchemaGenerator } from './schemaGenerator.js';
13
13
  import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
14
14
  import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
@@ -78,8 +78,9 @@ export class Migrator {
78
78
  }
79
79
  switch (d) {
80
80
  case 'postgres':
81
- case 'cockroachdb':
82
81
  return new PostgresSchemaIntrospector(this.pool);
82
+ case 'cockroachdb':
83
+ return new CockroachSchemaIntrospector(this.pool);
83
84
  case 'mysql':
84
85
  case 'mariadb':
85
86
  return new MysqlSchemaIntrospector(this.pool);
@@ -1,6 +1,7 @@
1
1
  import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
+ import type { SchemaAST } from '../schema/schemaAST.js';
2
3
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
3
- import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
4
+ import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
4
5
  import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
5
6
  /**
6
7
  * Unified SQL schema generator.
@@ -82,6 +83,21 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
82
83
  * Compare an entity with a database table node and return the differences.
83
84
  */
84
85
  diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
86
+ /**
87
+ * Indexes the entity declares that the table does not have, matched by name and built the same way
88
+ * `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
89
+ * up rather than waiting for the table to be created from scratch somewhere else.
90
+ *
91
+ * Only ever additive. An index the entity does not name is left alone: it may well have been
92
+ * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
93
+ */
94
+ private missingIndexes;
95
+ /**
96
+ * A vector index this dialect declares inside `CREATE TABLE` rather than as a statement of its own,
97
+ * which MariaDB is alone in doing. It has no `CREATE INDEX` form, so it can only ever be created
98
+ * with its table, never added to one.
99
+ */
100
+ private isInlineVectorIndex;
85
101
  private columnNodeToSchema;
86
102
  /**
87
103
  * Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
@@ -131,9 +147,16 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
131
147
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
132
148
  generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
133
149
  generateDropForeignKeySql(tableName: string, constraintName: string): string;
134
- private tableDefinitionToNode;
135
- private fullColumnDefinitionToNode;
136
150
  }
151
+ /**
152
+ * The entities as an AST, named the way `generator` names things.
153
+ *
154
+ * Its resolvers rather than a naming strategy, because the two disagree: a strategy renames whatever
155
+ * it is handed, while a generator leaves an explicit `@Entity({ name })` alone. Build the AST the
156
+ * other way and the table is created under one name and compared under another, which reports every
157
+ * table of a project using a naming strategy as both missing and unexpected.
158
+ */
159
+ export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableName' | 'resolveColumnName'>, entities: readonly Type<unknown>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
137
160
  /**
138
161
  * Synchronous factory for SQL schema generators only.
139
162
  * For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
@@ -1,9 +1,10 @@
1
1
  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
- import { SchemaASTBuilder } from '../schema/schemaASTBuilder.js';
4
+ import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
5
5
  import { escapeSqlId, getKeys, isAutoIncrement } from '../util/index.js';
6
6
  import { formatDefaultValue } from './builder/expressions.js';
7
+ import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
7
8
  import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
8
9
  /**
9
10
  * Unified SQL schema generator.
@@ -90,7 +91,7 @@ export class SqlSchemaGenerator {
90
91
  * resolves instead of being silently dropped.
91
92
  */
92
93
  orderedTables(entities, direction, only) {
93
- const ast = new SchemaASTBuilder(this.dialect.namingStrategy).fromEntities(entities);
94
+ const ast = buildEntityAST(this, entities, this.defaultForeignKeyAction);
94
95
  const tables = direction === 'create' ? ast.getCreateOrder() : ast.getDropOrder();
95
96
  if (!only) {
96
97
  return tables;
@@ -331,7 +332,11 @@ export class SqlSchemaGenerator {
331
332
  for (const [name] of currentColumns) {
332
333
  columnsToDrop.push(name);
333
334
  }
334
- if (columnsToAdd.length === 0 && columnsToAlter.length === 0 && columnsToDrop.length === 0) {
335
+ const indexesToAdd = this.missingIndexes(entity, currentTable);
336
+ if (columnsToAdd.length === 0 &&
337
+ columnsToAlter.length === 0 &&
338
+ columnsToDrop.length === 0 &&
339
+ indexesToAdd.length === 0) {
335
340
  return undefined;
336
341
  }
337
342
  return {
@@ -340,8 +345,32 @@ export class SqlSchemaGenerator {
340
345
  columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
341
346
  columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
342
347
  columnsToDrop: columnsToDrop.length > 0 ? columnsToDrop : undefined,
348
+ indexesToAdd: indexesToAdd.length > 0 ? indexesToAdd : undefined,
343
349
  };
344
350
  }
351
+ /**
352
+ * Indexes the entity declares that the table does not have, matched by name and built the same way
353
+ * `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
354
+ * up rather than waiting for the table to be created from scratch somewhere else.
355
+ *
356
+ * Only ever additive. An index the entity does not name is left alone: it may well have been
357
+ * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
358
+ */
359
+ missingIndexes(entity, currentTable) {
360
+ const desired = buildEntityAST(this, [entity]).getTable(currentTable.name)?.indexes ?? [];
361
+ const present = new Set(currentTable.indexes.map((index) => index.name));
362
+ return desired
363
+ .filter((index) => !present.has(index.name) && !this.isInlineVectorIndex(index))
364
+ .map(indexNodeToSchema);
365
+ }
366
+ /**
367
+ * A vector index this dialect declares inside `CREATE TABLE` rather than as a statement of its own,
368
+ * which MariaDB is alone in doing. It has no `CREATE INDEX` form, so it can only ever be created
369
+ * with its table, never added to one.
370
+ */
371
+ isInlineVectorIndex(index) {
372
+ return this.features.inlineVectorIndex && index.type === 'vector';
373
+ }
345
374
  columnNodeToSchema(col) {
346
375
  return {
347
376
  name: col.name,
@@ -430,12 +459,11 @@ export class SqlSchemaGenerator {
430
459
  generateCreateTableFromNode(table, options = {}) {
431
460
  const columns = [];
432
461
  const constraints = [];
433
- const isInlineVectorIdx = this.features.inlineVectorIndex;
434
- const vectorIndexes = isInlineVectorIdx ? table.indexes.filter((idx) => idx.type === 'vector') : [];
435
- const regularIndexes = isInlineVectorIdx ? table.indexes.filter((idx) => idx.type !== 'vector') : table.indexes;
462
+ const vectorIndexes = table.indexes.filter((index) => this.isInlineVectorIndex(index));
463
+ const regularIndexes = table.indexes.filter((index) => !this.isInlineVectorIndex(index));
436
464
  // MariaDB rejects a `VECTOR INDEX` whose column is nullable ("All parts of a VECTOR index must
437
465
  // be NOT NULL"), so being indexed decides it rather than the entity's own nullability.
438
- const indexedVectorColumns = new Set(vectorIndexes.flatMap((idx) => idx.columns.map((col) => col.name)));
466
+ const indexedVectorColumns = new Set(vectorIndexes.flatMap((idx) => idx.entries.map((entry) => entry.column)));
439
467
  for (const col of table.columns.values()) {
440
468
  const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
441
469
  columns.push(colDef);
@@ -503,7 +531,7 @@ export class SqlSchemaGenerator {
503
531
  return this.generateCreateIndex(index.table.name, indexNodeToSchema(index), options);
504
532
  }
505
533
  generateCreateTableFromDefinition(table, options = {}) {
506
- const tableNode = this.tableDefinitionToNode(table);
534
+ const tableNode = tableDefinitionToNode(table);
507
535
  return this.generateCreateTableFromNode(tableNode, options);
508
536
  }
509
537
  generateRenameTableSql(oldName, newName) {
@@ -513,11 +541,11 @@ export class SqlSchemaGenerator {
513
541
  return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
514
542
  }
515
543
  generateAddColumnSql(tableName, column) {
516
- const colSql = this.generateColumnFromNode(this.fullColumnDefinitionToNode(column, tableName));
544
+ const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
517
545
  return `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`;
518
546
  }
519
547
  generateAlterColumnSql(tableName, columnName, column) {
520
- const node = this.fullColumnDefinitionToNode(column, tableName);
548
+ const node = fullColumnDefinitionToNode(column, tableName);
521
549
  return this.generateAlterColumnStatements(tableName, { ...this.columnNodeToSchema(node), name: columnName }, this.generateColumnFromNode(node)).join('\n');
522
550
  }
523
551
  generateDropColumnSql(tableName, columnName) {
@@ -542,88 +570,21 @@ export class SqlSchemaGenerator {
542
570
  generateDropForeignKeySql(tableName, constraintName) {
543
571
  return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
544
572
  }
545
- tableDefinitionToNode(def) {
546
- const columns = new Map();
547
- const pkNodes = [];
548
- const table = {
549
- name: def.name,
550
- columns,
551
- primaryKey: [], // placeholder
552
- indexes: [],
553
- schema: { tables: new Map(), relationships: [], indexes: [] },
554
- incomingRelations: [],
555
- outgoingRelations: [],
556
- comment: def.comment,
557
- };
558
- for (const colDef of def.columns) {
559
- const node = this.fullColumnDefinitionToNode(colDef, def.name);
560
- node.table = table;
561
- columns.set(node.name, node);
562
- if (node.isPrimaryKey) {
563
- pkNodes.push(node);
564
- }
565
- }
566
- const finalPrimaryKey = def.primaryKey
567
- ? def.primaryKey.map((name) => columns.get(name)).filter((c) => c !== undefined)
568
- : pkNodes;
569
- table.primaryKey = finalPrimaryKey;
570
- for (const idxDef of def.indexes) {
571
- const indexNode = {
572
- ...idxDef,
573
- table,
574
- columns: idxDef.columns
575
- .map((entry) => (entry.expression ? undefined : columns.get(entry.column)))
576
- .filter((c) => c !== undefined),
577
- entries: idxDef.columns,
578
- };
579
- table.indexes.push(indexNode);
580
- }
581
- for (const fkDef of def.foreignKeys) {
582
- const relNode = {
583
- name: fkDef.name ?? `fk_${def.name}_${fkDef.columns.join('_')}`,
584
- type: 'ManyToOne', // Builder default
585
- from: {
586
- table,
587
- columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
588
- },
589
- to: {
590
- table: { name: fkDef.referencesTable },
591
- columns: fkDef.referencesColumns.map((name) => ({ name })),
592
- },
593
- onDelete: fkDef.onDelete,
594
- onUpdate: fkDef.onUpdate,
595
- };
596
- table.outgoingRelations.push(relNode);
597
- }
598
- return table;
599
- }
600
- fullColumnDefinitionToNode(col, tableName) {
601
- return {
602
- name: col.name,
603
- type: col.type,
604
- nullable: col.nullable,
605
- defaultValue: col.defaultValue,
606
- isPrimaryKey: col.primaryKey,
607
- isAutoIncrement: col.autoIncrement,
608
- isUnique: col.unique,
609
- comment: col.comment,
610
- table: { name: tableName },
611
- referencedBy: [],
612
- references: col.foreignKey
613
- ? {
614
- name: `fk_${tableName}_${col.name}`,
615
- type: 'ManyToOne',
616
- from: { table: { name: tableName }, columns: [] },
617
- to: {
618
- table: { name: col.foreignKey.table },
619
- columns: col.foreignKey.columns.map((name) => ({ name })),
620
- },
621
- onDelete: col.foreignKey.onDelete,
622
- onUpdate: col.foreignKey.onUpdate,
623
- }
624
- : undefined,
625
- };
626
- }
573
+ }
574
+ /**
575
+ * The entities as an AST, named the way `generator` names things.
576
+ *
577
+ * Its resolvers rather than a naming strategy, because the two disagree: a strategy renames whatever
578
+ * it is handed, while a generator leaves an explicit `@Entity({ name })` alone. Build the AST the
579
+ * other way and the table is created under one name and compared under another, which reports every
580
+ * table of a project using a naming strategy as both missing and unexpected.
581
+ */
582
+ export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
583
+ return buildSchemaAST(entities, {
584
+ resolveTableName: (entity, meta) => generator.resolveTableName(entity, meta),
585
+ resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
586
+ defaultForeignKeyAction,
587
+ });
627
588
  }
628
589
  /**
629
590
  * Synchronous factory for SQL schema generators only.
@@ -18,8 +18,8 @@ export { areTypesEqual, canonicalToColumnType, canonicalToSql, canonicalToTypeSc
18
18
  */
19
19
  export declare function introspectSchema(introspector: SchemaIntrospector): Promise<SchemaAST>;
20
20
  export { SchemaAST } from './schemaAST.js';
21
- export type { BuildFromEntitiesOptions } from './schemaASTBuilder.js';
22
- export { SchemaASTBuilder } from './schemaASTBuilder.js';
21
+ export type { BuildSchemaASTOptions } from './schemaASTBuilder.js';
22
+ export { buildSchemaAST } from './schemaASTBuilder.js';
23
23
  export type { DiffOptions } from './schemaASTDiffer.js';
24
- export { diffSchemas, SchemaASTDiffer } from './schemaASTDiffer.js';
24
+ export { diffSchemas } from './schemaASTDiffer.js';
25
25
  export type { CanonicalType, ColumnDiff, ColumnNode, Drift, DriftReport, DriftSeverity, DriftStatus, DriftType, ForeignKeyAction, IndexDiff, IndexNode, IndexSource, IndexSyncStatus, IndexType, RelationshipDiff, RelationshipNode, RelationshipSource, RelationshipType, SchemaAST as ISchemaAST, SchemaDiffResult, SizeVariant, TableDiff, TableNode, TypeCategory, ValidationError, ValidationErrorType, } from './types.js';