@mikro-orm/sql 7.0.15-dev.8 → 7.0.15

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 (89) hide show
  1. package/AbstractSqlConnection.d.ts +94 -58
  2. package/AbstractSqlConnection.js +235 -238
  3. package/AbstractSqlDriver.d.ts +410 -155
  4. package/AbstractSqlDriver.js +2100 -1972
  5. package/AbstractSqlPlatform.d.ts +86 -76
  6. package/AbstractSqlPlatform.js +169 -167
  7. package/PivotCollectionPersister.d.ts +33 -15
  8. package/PivotCollectionPersister.js +158 -160
  9. package/README.md +1 -1
  10. package/SqlEntityManager.d.ts +67 -22
  11. package/SqlEntityManager.js +54 -38
  12. package/SqlEntityRepository.d.ts +14 -14
  13. package/SqlEntityRepository.js +23 -23
  14. package/SqlMikroORM.d.ts +49 -8
  15. package/SqlMikroORM.js +8 -8
  16. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +12 -12
  17. package/dialects/mssql/MsSqlNativeQueryBuilder.js +199 -201
  18. package/dialects/mysql/BaseMySqlPlatform.d.ts +65 -46
  19. package/dialects/mysql/BaseMySqlPlatform.js +137 -134
  20. package/dialects/mysql/MySqlExceptionConverter.d.ts +6 -6
  21. package/dialects/mysql/MySqlExceptionConverter.js +91 -77
  22. package/dialects/mysql/MySqlNativeQueryBuilder.d.ts +3 -3
  23. package/dialects/mysql/MySqlNativeQueryBuilder.js +66 -69
  24. package/dialects/mysql/MySqlSchemaHelper.d.ts +58 -39
  25. package/dialects/mysql/MySqlSchemaHelper.js +327 -319
  26. package/dialects/oracledb/OracleDialect.d.ts +81 -52
  27. package/dialects/oracledb/OracleDialect.js +155 -149
  28. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +12 -12
  29. package/dialects/oracledb/OracleNativeQueryBuilder.js +239 -243
  30. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +110 -107
  31. package/dialects/postgresql/BasePostgreSqlPlatform.js +370 -369
  32. package/dialects/postgresql/FullTextType.d.ts +10 -6
  33. package/dialects/postgresql/FullTextType.js +51 -51
  34. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +5 -5
  35. package/dialects/postgresql/PostgreSqlExceptionConverter.js +55 -43
  36. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.d.ts +1 -1
  37. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.js +4 -4
  38. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +117 -82
  39. package/dialects/postgresql/PostgreSqlSchemaHelper.js +748 -712
  40. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -5
  41. package/dialects/sqlite/BaseSqliteConnection.js +21 -19
  42. package/dialects/sqlite/NodeSqliteDialect.d.ts +1 -1
  43. package/dialects/sqlite/NodeSqliteDialect.js +23 -23
  44. package/dialects/sqlite/SqliteDriver.d.ts +1 -1
  45. package/dialects/sqlite/SqliteDriver.js +3 -3
  46. package/dialects/sqlite/SqliteExceptionConverter.d.ts +6 -6
  47. package/dialects/sqlite/SqliteExceptionConverter.js +67 -51
  48. package/dialects/sqlite/SqliteNativeQueryBuilder.d.ts +2 -2
  49. package/dialects/sqlite/SqliteNativeQueryBuilder.js +7 -7
  50. package/dialects/sqlite/SqlitePlatform.d.ts +64 -73
  51. package/dialects/sqlite/SqlitePlatform.js +143 -143
  52. package/dialects/sqlite/SqliteSchemaHelper.d.ts +78 -61
  53. package/dialects/sqlite/SqliteSchemaHelper.js +541 -522
  54. package/package.json +3 -3
  55. package/plugin/index.d.ts +42 -35
  56. package/plugin/index.js +43 -36
  57. package/plugin/transformer.d.ts +137 -95
  58. package/plugin/transformer.js +1012 -881
  59. package/query/ArrayCriteriaNode.d.ts +4 -4
  60. package/query/ArrayCriteriaNode.js +18 -18
  61. package/query/CriteriaNode.d.ts +35 -25
  62. package/query/CriteriaNode.js +142 -132
  63. package/query/CriteriaNodeFactory.d.ts +49 -6
  64. package/query/CriteriaNodeFactory.js +97 -94
  65. package/query/NativeQueryBuilder.d.ts +120 -120
  66. package/query/NativeQueryBuilder.js +507 -501
  67. package/query/ObjectCriteriaNode.d.ts +12 -12
  68. package/query/ObjectCriteriaNode.js +298 -282
  69. package/query/QueryBuilder.d.ts +1558 -906
  70. package/query/QueryBuilder.js +2346 -2202
  71. package/query/QueryBuilderHelper.d.ts +153 -72
  72. package/query/QueryBuilderHelper.js +1084 -1032
  73. package/query/ScalarCriteriaNode.d.ts +3 -3
  74. package/query/ScalarCriteriaNode.js +53 -46
  75. package/query/enums.d.ts +14 -14
  76. package/query/enums.js +14 -14
  77. package/query/raw.d.ts +16 -6
  78. package/query/raw.js +10 -10
  79. package/schema/DatabaseSchema.d.ts +74 -50
  80. package/schema/DatabaseSchema.js +359 -331
  81. package/schema/DatabaseTable.d.ts +96 -73
  82. package/schema/DatabaseTable.js +1046 -974
  83. package/schema/SchemaComparator.d.ts +70 -66
  84. package/schema/SchemaComparator.js +790 -765
  85. package/schema/SchemaHelper.d.ts +128 -97
  86. package/schema/SchemaHelper.js +683 -668
  87. package/schema/SqlSchemaGenerator.d.ts +79 -59
  88. package/schema/SqlSchemaGenerator.js +525 -495
  89. package/typings.d.ts +405 -275
@@ -1,1029 +1,1101 @@
1
- import { DecimalType, EntitySchema, isRaw, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core';
1
+ import { DecimalType, EntitySchema, isRaw, ReferenceKind, t, Type, UnknownType, Utils } from '@mikro-orm/core';
2
2
  /**
3
3
  * @internal
4
4
  */
5
5
  export class DatabaseTable {
6
- name;
7
- schema;
8
- #columns = {};
9
- #indexes = [];
10
- #checks = [];
11
- #foreignKeys = {};
12
- #platform;
13
- nativeEnums = {}; // for postgres
14
- comment;
15
- constructor(platform, name, schema) {
16
- this.name = name;
17
- this.schema = schema;
18
- this.#platform = platform;
19
- }
20
- getQuotedName() {
21
- return this.#platform.quoteIdentifier(this.getShortestName());
22
- }
23
- getColumns() {
24
- return Object.values(this.#columns);
25
- }
26
- getColumn(name) {
27
- return this.#columns[name];
28
- }
29
- removeColumn(name) {
30
- delete this.#columns[name];
31
- }
32
- getIndexes() {
33
- return Utils.removeDuplicates(this.#indexes);
34
- }
35
- getChecks() {
36
- return this.#checks;
37
- }
38
- /** @internal */
39
- setIndexes(indexes) {
40
- this.#indexes = indexes;
41
- }
42
- /** @internal */
43
- setChecks(checks) {
44
- this.#checks = checks;
6
+ name;
7
+ schema;
8
+ #columns = {};
9
+ #indexes = [];
10
+ #checks = [];
11
+ #foreignKeys = {};
12
+ #platform;
13
+ nativeEnums = {}; // for postgres
14
+ comment;
15
+ constructor(platform, name, schema) {
16
+ this.name = name;
17
+ this.schema = schema;
18
+ this.#platform = platform;
19
+ }
20
+ getQuotedName() {
21
+ return this.#platform.quoteIdentifier(this.getShortestName());
22
+ }
23
+ getColumns() {
24
+ return Object.values(this.#columns);
25
+ }
26
+ getColumn(name) {
27
+ return this.#columns[name];
28
+ }
29
+ removeColumn(name) {
30
+ delete this.#columns[name];
31
+ }
32
+ getIndexes() {
33
+ return Utils.removeDuplicates(this.#indexes);
34
+ }
35
+ getChecks() {
36
+ return this.#checks;
37
+ }
38
+ /** @internal */
39
+ setIndexes(indexes) {
40
+ this.#indexes = indexes;
41
+ }
42
+ /** @internal */
43
+ setChecks(checks) {
44
+ this.#checks = checks;
45
+ }
46
+ /** @internal */
47
+ setForeignKeys(fks) {
48
+ this.#foreignKeys = fks;
49
+ }
50
+ init(cols, indexes = [], checks = [], pks, fks = {}, enums = {}) {
51
+ this.#indexes = indexes;
52
+ this.#checks = checks;
53
+ this.#foreignKeys = fks;
54
+ const helper = this.#platform.getSchemaHelper();
55
+ this.#columns = cols.reduce((o, v) => {
56
+ const index = indexes.filter(i => i.columnNames[0] === v.name);
57
+ v.primary = v.primary || pks.includes(v.name);
58
+ v.unique = index.some(i => i.unique && !i.primary);
59
+ const type = v.name in enums ? 'enum' : v.type;
60
+ v.mappedType = this.#platform.getMappedType(type);
61
+ v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
62
+ v.enumItems ??= enums[v.name] || [];
63
+ // recover length from the declared type so introspection matches `addColumnFromProperty`;
64
+ // scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
65
+ if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
66
+ v.length = helper.inferLengthFromColumnType(v.type);
67
+ }
68
+ o[v.name] = v;
69
+ return o;
70
+ }, {});
71
+ }
72
+ addColumn(column) {
73
+ this.#columns[column.name] = column;
74
+ }
75
+ addColumnFromProperty(prop, meta, config) {
76
+ prop.fieldNames?.forEach((field, idx) => {
77
+ // numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
78
+ const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
79
+ const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
80
+ const mappedType = this.#platform.getMappedType(type);
81
+ if (mappedType instanceof DecimalType) {
82
+ const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
83
+ /* v8 ignore next */
84
+ if (match) {
85
+ prop.precision ??= +match[1];
86
+ prop.scale ??= +match[2];
87
+ prop.length = undefined;
88
+ }
89
+ }
90
+ if (prop.length == null && prop.columnTypes[idx]) {
91
+ prop.length = this.#platform.getSchemaHelper().inferLengthFromColumnType(prop.columnTypes[idx]);
92
+ if (typeof mappedType.getDefaultLength !== 'undefined') {
93
+ prop.length ??= mappedType.getDefaultLength(this.#platform);
94
+ }
95
+ }
96
+ const primary = !meta.compositePK && prop.fieldNames.length === 1 && !!prop.primary;
97
+ this.#columns[field] = {
98
+ name: prop.fieldNames[idx],
99
+ type: prop.columnTypes[idx],
100
+ generated: isRaw(prop.generated)
101
+ ? this.#platform.formatQuery(prop.generated.sql, prop.generated.params)
102
+ : prop.generated,
103
+ mappedType,
104
+ unsigned: prop.unsigned && this.#platform.isNumericColumn(mappedType),
105
+ autoincrement:
106
+ prop.autoincrement ??
107
+ (primary && prop.kind === ReferenceKind.SCALAR && this.#platform.isNumericColumn(mappedType)),
108
+ primary,
109
+ nullable: this.#columns[field]?.nullable ?? !!prop.nullable,
110
+ nativeEnumName: prop.nativeEnumName,
111
+ length: prop.length,
112
+ precision: prop.precision,
113
+ scale: prop.scale,
114
+ default: prop.defaultRaw,
115
+ enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
116
+ comment: prop.comment,
117
+ extra: prop.extra,
118
+ ignoreSchemaChanges: prop.ignoreSchemaChanges,
119
+ };
120
+ this.#columns[field].unsigned ??= this.#columns[field].autoincrement;
121
+ if (this.nativeEnums[type]) {
122
+ this.#columns[field].enumItems ??= this.nativeEnums[type].items;
123
+ }
124
+ const defaultValue = this.#platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length);
125
+ this.#columns[field].default = defaultValue;
126
+ });
127
+ if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
128
+ const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign');
129
+ let schema =
130
+ prop.targetMeta.root.schema === '*'
131
+ ? this.schema
132
+ : (prop.targetMeta.root.schema ?? config.get('schema', this.#platform.getDefaultSchemaName()));
133
+ if (prop.referencedTableName.includes('.')) {
134
+ schema = undefined;
135
+ }
136
+ // For cross-schema FKs on MySQL/MariaDB (where schema = database), when the referenced
137
+ // table has no explicit schema but the current table does, qualify with dbName so the
138
+ // FK can resolve the referenced table in the correct database
139
+ if (!schema && this.schema && !this.#platform.getDefaultSchemaName()) {
140
+ schema = config.get('dbName');
141
+ }
142
+ if (prop.createForeignKeyConstraint) {
143
+ this.#foreignKeys[constraintName] = {
144
+ constraintName,
145
+ columnNames: prop.fieldNames,
146
+ localTableName: this.getShortestName(false),
147
+ referencedColumnNames: prop.referencedColumnNames,
148
+ referencedTableName: schema ? `${schema}.${prop.referencedTableName}` : prop.referencedTableName,
149
+ };
150
+ const schemaConfig = config.get('schemaGenerator');
151
+ this.#foreignKeys[constraintName].deleteRule = prop.deleteRule ?? schemaConfig.defaultDeleteRule;
152
+ this.#foreignKeys[constraintName].updateRule = prop.updateRule ?? schemaConfig.defaultUpdateRule;
153
+ if (prop.deferMode) {
154
+ this.#foreignKeys[constraintName].deferMode = prop.deferMode;
155
+ }
156
+ }
45
157
  }
46
- /** @internal */
47
- setForeignKeys(fks) {
48
- this.#foreignKeys = fks;
158
+ if (prop.index) {
159
+ this.#indexes.push({
160
+ columnNames: prop.fieldNames,
161
+ composite: prop.fieldNames.length > 1,
162
+ keyName: this.getIndexName(prop.index, prop.fieldNames, 'index'),
163
+ constraint: false,
164
+ primary: false,
165
+ unique: false,
166
+ });
49
167
  }
50
- init(cols, indexes = [], checks = [], pks, fks = {}, enums = {}) {
51
- this.#indexes = indexes;
52
- this.#checks = checks;
53
- this.#foreignKeys = fks;
54
- const helper = this.#platform.getSchemaHelper();
55
- this.#columns = cols.reduce((o, v) => {
56
- const index = indexes.filter(i => i.columnNames[0] === v.name);
57
- v.primary = v.primary || pks.includes(v.name);
58
- v.unique = index.some(i => i.unique && !i.primary);
59
- const type = v.name in enums ? 'enum' : v.type;
60
- v.mappedType = this.#platform.getMappedType(type);
61
- v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
62
- v.enumItems ??= enums[v.name] || [];
63
- // recover length from the declared type so introspection matches `addColumnFromProperty`;
64
- // scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
65
- if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
66
- v.length = helper.inferLengthFromColumnType(v.type);
67
- }
68
- o[v.name] = v;
69
- return o;
70
- }, {});
168
+ if (prop.unique && !(prop.primary && !meta.compositePK)) {
169
+ this.#indexes.push({
170
+ columnNames: prop.fieldNames,
171
+ composite: prop.fieldNames.length > 1,
172
+ keyName: this.getIndexName(prop.unique, prop.fieldNames, 'unique'),
173
+ constraint: !prop.fieldNames.some(d => d.includes('.')),
174
+ primary: false,
175
+ unique: true,
176
+ deferMode: prop.deferMode,
177
+ });
71
178
  }
72
- addColumn(column) {
73
- this.#columns[column.name] = column;
179
+ }
180
+ getIndexName(value, columnNames, type) {
181
+ if (typeof value === 'string') {
182
+ return value;
74
183
  }
75
- addColumnFromProperty(prop, meta, config) {
76
- prop.fieldNames?.forEach((field, idx) => {
77
- // numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
78
- const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
79
- const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
80
- const mappedType = this.#platform.getMappedType(type);
81
- if (mappedType instanceof DecimalType) {
82
- const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
83
- /* v8 ignore next */
84
- if (match) {
85
- prop.precision ??= +match[1];
86
- prop.scale ??= +match[2];
87
- prop.length = undefined;
88
- }
89
- }
90
- if (prop.length == null && prop.columnTypes[idx]) {
91
- prop.length = this.#platform.getSchemaHelper().inferLengthFromColumnType(prop.columnTypes[idx]);
92
- if (typeof mappedType.getDefaultLength !== 'undefined') {
93
- prop.length ??= mappedType.getDefaultLength(this.#platform);
94
- }
95
- }
96
- const primary = !meta.compositePK && prop.fieldNames.length === 1 && !!prop.primary;
97
- this.#columns[field] = {
98
- name: prop.fieldNames[idx],
99
- type: prop.columnTypes[idx],
100
- generated: isRaw(prop.generated)
101
- ? this.#platform.formatQuery(prop.generated.sql, prop.generated.params)
102
- : prop.generated,
103
- mappedType,
104
- unsigned: prop.unsigned && this.#platform.isNumericColumn(mappedType),
105
- autoincrement: prop.autoincrement ??
106
- (primary && prop.kind === ReferenceKind.SCALAR && this.#platform.isNumericColumn(mappedType)),
107
- primary,
108
- nullable: this.#columns[field]?.nullable ?? !!prop.nullable,
109
- nativeEnumName: prop.nativeEnumName,
110
- length: prop.length,
111
- precision: prop.precision,
112
- scale: prop.scale,
113
- default: prop.defaultRaw,
114
- enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
115
- comment: prop.comment,
116
- extra: prop.extra,
117
- ignoreSchemaChanges: prop.ignoreSchemaChanges,
118
- };
119
- this.#columns[field].unsigned ??= this.#columns[field].autoincrement;
120
- if (this.nativeEnums[type]) {
121
- this.#columns[field].enumItems ??= this.nativeEnums[type].items;
122
- }
123
- const defaultValue = this.#platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length);
124
- this.#columns[field].default = defaultValue;
125
- });
126
- if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
127
- const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign');
128
- let schema = prop.targetMeta.root.schema === '*'
129
- ? this.schema
130
- : (prop.targetMeta.root.schema ?? config.get('schema', this.#platform.getDefaultSchemaName()));
131
- if (prop.referencedTableName.includes('.')) {
132
- schema = undefined;
133
- }
134
- // For cross-schema FKs on MySQL/MariaDB (where schema = database), when the referenced
135
- // table has no explicit schema but the current table does, qualify with dbName so the
136
- // FK can resolve the referenced table in the correct database
137
- if (!schema && this.schema && !this.#platform.getDefaultSchemaName()) {
138
- schema = config.get('dbName');
139
- }
140
- if (prop.createForeignKeyConstraint) {
141
- this.#foreignKeys[constraintName] = {
142
- constraintName,
143
- columnNames: prop.fieldNames,
144
- localTableName: this.getShortestName(false),
145
- referencedColumnNames: prop.referencedColumnNames,
146
- referencedTableName: schema ? `${schema}.${prop.referencedTableName}` : prop.referencedTableName,
147
- };
148
- const schemaConfig = config.get('schemaGenerator');
149
- this.#foreignKeys[constraintName].deleteRule = prop.deleteRule ?? schemaConfig.defaultDeleteRule;
150
- this.#foreignKeys[constraintName].updateRule = prop.updateRule ?? schemaConfig.defaultUpdateRule;
151
- if (prop.deferMode) {
152
- this.#foreignKeys[constraintName].deferMode = prop.deferMode;
153
- }
154
- }
184
+ return this.#platform.getIndexName(this.name, columnNames, type);
185
+ }
186
+ getEntityDeclaration(namingStrategy, schemaHelper, scalarPropertiesForRelations) {
187
+ const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } =
188
+ this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
189
+ const name = namingStrategy.getEntityName(this.name, this.schema);
190
+ const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
191
+ const compositeFkIndexes = {};
192
+ const compositeFkUniques = {};
193
+ const potentiallyUnmappedIndexes = this.#indexes.filter(
194
+ index =>
195
+ !index.primary && // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped.
196
+ // Non-trivial non-composite indexes will be declared at the entity's metadata, though later outputted in the property
197
+ (index.columnNames.length > 1 || // All composite indexes are to be mapped to entity decorators or FK props.
198
+ skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
199
+ index.deferMode ||
200
+ index.expression ||
201
+ !(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
202
+ // ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
203
+ !(index.columnNames.some(col => !col) && !index.expression),
204
+ );
205
+ // Helper to map column name to property name
206
+ const columnToPropertyName = colName => this.getPropertyName(namingStrategy, colName);
207
+ for (const index of potentiallyUnmappedIndexes) {
208
+ // Build the index/unique options object with advanced options
209
+ const ret = {
210
+ name: index.keyName,
211
+ deferMode: index.deferMode,
212
+ expression: index.expression,
213
+ // Advanced index options - convert column names to property names
214
+ columns: index.columns?.map(col => ({
215
+ ...col,
216
+ name: columnToPropertyName(col.name),
217
+ })),
218
+ include: index.include?.map(colName => columnToPropertyName(colName)),
219
+ fillFactor: index.fillFactor,
220
+ disabled: index.disabled,
221
+ };
222
+ // Index-only options (not valid for Unique)
223
+ if (!index.unique) {
224
+ if (index.type) {
225
+ // Convert index type - IndexDef.type can be string or object, IndexOptions.type is just string
226
+ ret.type = typeof index.type === 'string' ? index.type : index.type.indexType;
155
227
  }
156
- if (prop.index) {
157
- this.#indexes.push({
158
- columnNames: prop.fieldNames,
159
- composite: prop.fieldNames.length > 1,
160
- keyName: this.getIndexName(prop.index, prop.fieldNames, 'index'),
161
- constraint: false,
162
- primary: false,
163
- unique: false,
164
- });
228
+ if (index.invisible) {
229
+ ret.invisible = index.invisible;
165
230
  }
166
- if (prop.unique && !(prop.primary && !meta.compositePK)) {
167
- this.#indexes.push({
168
- columnNames: prop.fieldNames,
169
- composite: prop.fieldNames.length > 1,
170
- keyName: this.getIndexName(prop.unique, prop.fieldNames, 'unique'),
171
- constraint: !prop.fieldNames.some((d) => d.includes('.')),
172
- primary: false,
173
- unique: true,
174
- deferMode: prop.deferMode,
175
- });
231
+ if (index.clustered) {
232
+ ret.clustered = index.clustered;
176
233
  }
177
- }
178
- getIndexName(value, columnNames, type) {
179
- if (typeof value === 'string') {
180
- return value;
234
+ }
235
+ // An index is trivial if it has no special options that require entity-level declaration
236
+ const hasAdvancedOptions =
237
+ index.columns?.length ||
238
+ index.include?.length ||
239
+ index.fillFactor ||
240
+ index.type ||
241
+ index.invisible ||
242
+ index.disabled ||
243
+ index.clustered;
244
+ const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
245
+ if (isTrivial) {
246
+ // Index is for FK. Map to the FK prop and move on.
247
+ const fkForIndex = fkIndexes.get(index);
248
+ if (fkForIndex && !fkForIndex.fk.columnNames.some(col => !index.columnNames.includes(col))) {
249
+ ret.properties = [this.getPropertyName(namingStrategy, fkForIndex.baseName, fkForIndex.fk)];
250
+ const map = index.unique ? compositeFkUniques : compositeFkIndexes;
251
+ if (typeof map[ret.properties[0]] === 'undefined') {
252
+ map[ret.properties[0]] = index;
253
+ continue;
254
+ }
255
+ }
256
+ }
257
+ const properties =
258
+ ret.properties ??
259
+ this.getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy);
260
+ // If there is a column that cannot be unambiguously mapped to a prop, render an expression.
261
+ if (typeof properties === 'undefined') {
262
+ ret.expression ??= schemaHelper.getCreateIndexSQL(this.name, index);
263
+ } else {
264
+ ret.properties ??= properties;
265
+ // If the index is for one property that is not a FK prop, map to the column prop and move on.
266
+ if (properties.length === 1 && isTrivial && !fksOnStandaloneProps.has(properties[0])) {
267
+ const map = index.unique ? compositeFkUniques : compositeFkIndexes;
268
+ // Only map one trivial index. If the same column is indexed many times over, output
269
+ if (typeof map[properties[0]] === 'undefined') {
270
+ map[properties[0]] = index;
271
+ continue;
272
+ }
181
273
  }
182
- return this.#platform.getIndexName(this.name, columnNames, type);
274
+ }
275
+ // Composite indexes that aren't exclusively mapped to FK props get an entity decorator.
276
+ if (index.unique) {
277
+ schema.addUnique(ret);
278
+ continue;
279
+ }
280
+ schema.addIndex(ret);
183
281
  }
184
- getEntityDeclaration(namingStrategy, schemaHelper, scalarPropertiesForRelations) {
185
- const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
186
- const name = namingStrategy.getEntityName(this.name, this.schema);
187
- const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
188
- const compositeFkIndexes = {};
189
- const compositeFkUniques = {};
190
- const potentiallyUnmappedIndexes = this.#indexes.filter(index => !index.primary && // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped.
191
- // Non-trivial non-composite indexes will be declared at the entity's metadata, though later outputted in the property
192
- (index.columnNames.length > 1 || // All composite indexes are to be mapped to entity decorators or FK props.
193
- skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
194
- index.deferMode ||
195
- index.expression ||
196
- !(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
197
- // ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
198
- !(index.columnNames.some(col => !col) && !index.expression));
199
- // Helper to map column name to property name
200
- const columnToPropertyName = (colName) => this.getPropertyName(namingStrategy, colName);
201
- for (const index of potentiallyUnmappedIndexes) {
202
- // Build the index/unique options object with advanced options
203
- const ret = {
204
- name: index.keyName,
205
- deferMode: index.deferMode,
206
- expression: index.expression,
207
- // Advanced index options - convert column names to property names
208
- columns: index.columns?.map(col => ({
209
- ...col,
210
- name: columnToPropertyName(col.name),
211
- })),
212
- include: index.include?.map(colName => columnToPropertyName(colName)),
213
- fillFactor: index.fillFactor,
214
- disabled: index.disabled,
215
- };
216
- // Index-only options (not valid for Unique)
217
- if (!index.unique) {
218
- if (index.type) {
219
- // Convert index type - IndexDef.type can be string or object, IndexOptions.type is just string
220
- ret.type = typeof index.type === 'string' ? index.type : index.type.indexType;
221
- }
222
- if (index.invisible) {
223
- ret.invisible = index.invisible;
224
- }
225
- if (index.clustered) {
226
- ret.clustered = index.clustered;
227
- }
228
- }
229
- // An index is trivial if it has no special options that require entity-level declaration
230
- const hasAdvancedOptions = index.columns?.length ||
231
- index.include?.length ||
232
- index.fillFactor ||
233
- index.type ||
234
- index.invisible ||
235
- index.disabled ||
236
- index.clustered;
237
- const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
238
- if (isTrivial) {
239
- // Index is for FK. Map to the FK prop and move on.
240
- const fkForIndex = fkIndexes.get(index);
241
- if (fkForIndex && !fkForIndex.fk.columnNames.some(col => !index.columnNames.includes(col))) {
242
- ret.properties = [this.getPropertyName(namingStrategy, fkForIndex.baseName, fkForIndex.fk)];
243
- const map = index.unique ? compositeFkUniques : compositeFkIndexes;
244
- if (typeof map[ret.properties[0]] === 'undefined') {
245
- map[ret.properties[0]] = index;
246
- continue;
247
- }
248
- }
249
- }
250
- const properties = ret.properties ??
251
- this.getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy);
252
- // If there is a column that cannot be unambiguously mapped to a prop, render an expression.
253
- if (typeof properties === 'undefined') {
254
- ret.expression ??= schemaHelper.getCreateIndexSQL(this.name, index);
255
- }
256
- else {
257
- ret.properties ??= properties;
258
- // If the index is for one property that is not a FK prop, map to the column prop and move on.
259
- if (properties.length === 1 && isTrivial && !fksOnStandaloneProps.has(properties[0])) {
260
- const map = index.unique ? compositeFkUniques : compositeFkIndexes;
261
- // Only map one trivial index. If the same column is indexed many times over, output
262
- if (typeof map[properties[0]] === 'undefined') {
263
- map[properties[0]] = index;
264
- continue;
265
- }
266
- }
267
- }
268
- // Composite indexes that aren't exclusively mapped to FK props get an entity decorator.
269
- if (index.unique) {
270
- schema.addUnique(ret);
271
- continue;
272
- }
273
- schema.addIndex(ret);
282
+ const addedStandaloneFkPropsBasedOnColumn = new Set();
283
+ const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
284
+ for (const column of nonSkippedColumns) {
285
+ const columnName = column.name;
286
+ const standaloneFkPropBasedOnColumn = fksOnStandaloneProps.get(columnName);
287
+ if (standaloneFkPropBasedOnColumn && !fksOnColumnProps.get(columnName)) {
288
+ addedStandaloneFkPropsBasedOnColumn.add(columnName);
289
+ const { fkIndex, currentFk } = standaloneFkPropBasedOnColumn;
290
+ const prop = this.getForeignKeyDeclaration(
291
+ currentFk,
292
+ namingStrategy,
293
+ schemaHelper,
294
+ fkIndex,
295
+ nullableForeignKeys.has(currentFk),
296
+ columnName,
297
+ fksOnColumnProps,
298
+ );
299
+ schema.addProperty(prop.name, prop.type, prop);
300
+ }
301
+ const prop = this.getPropertyDeclaration(
302
+ column,
303
+ namingStrategy,
304
+ schemaHelper,
305
+ compositeFkIndexes,
306
+ compositeFkUniques,
307
+ columnFks,
308
+ fksOnColumnProps.get(columnName),
309
+ );
310
+ schema.addProperty(prop.name, prop.type, prop);
311
+ }
312
+ for (const [propBaseName, { fkIndex, currentFk }] of fksOnStandaloneProps.entries()) {
313
+ if (addedStandaloneFkPropsBasedOnColumn.has(propBaseName)) {
314
+ continue;
315
+ }
316
+ const prop = this.getForeignKeyDeclaration(
317
+ currentFk,
318
+ namingStrategy,
319
+ schemaHelper,
320
+ fkIndex,
321
+ nullableForeignKeys.has(currentFk),
322
+ propBaseName,
323
+ fksOnColumnProps,
324
+ );
325
+ schema.addProperty(prop.name, prop.type, prop);
326
+ }
327
+ const meta = schema.init().meta;
328
+ const oneToOneCandidateProperties = meta.relations.filter(
329
+ prop => prop.primary && prop.kind === ReferenceKind.MANY_TO_ONE,
330
+ );
331
+ if (
332
+ oneToOneCandidateProperties.length === 1 &&
333
+ oneToOneCandidateProperties[0].fieldNames.length ===
334
+ new Set(meta.getPrimaryProps().flatMap(prop => prop.fieldNames)).size
335
+ ) {
336
+ oneToOneCandidateProperties[0].kind = ReferenceKind.ONE_TO_ONE;
337
+ }
338
+ return meta;
339
+ }
340
+ foreignKeysToProps(namingStrategy, scalarPropertiesForRelations) {
341
+ const fks = Object.values(this.getForeignKeys());
342
+ const fksOnColumnProps = new Map();
343
+ const fksOnStandaloneProps = new Map();
344
+ const columnFks = {};
345
+ const fkIndexes = new Map();
346
+ const nullableForeignKeys = new Set();
347
+ const standaloneFksBasedOnColumnNames = new Map();
348
+ for (const currentFk of fks) {
349
+ const fkIndex = this.findFkIndex(currentFk);
350
+ if (
351
+ currentFk.columnNames.length === 1 &&
352
+ !fks.some(
353
+ fk => fk !== currentFk && fk.columnNames.length === 1 && currentFk.columnNames[0] === fk.columnNames[0],
354
+ )
355
+ ) {
356
+ // Non-composite FK is the only possible one for a column. Render the column with it.
357
+ const columnName = currentFk.columnNames[0];
358
+ columnFks[columnName] ??= [];
359
+ columnFks[columnName].push(currentFk);
360
+ if (this.getColumn(columnName)?.nullable) {
361
+ nullableForeignKeys.add(currentFk);
274
362
  }
275
- const addedStandaloneFkPropsBasedOnColumn = new Set();
276
- const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
277
- for (const column of nonSkippedColumns) {
278
- const columnName = column.name;
279
- const standaloneFkPropBasedOnColumn = fksOnStandaloneProps.get(columnName);
280
- if (standaloneFkPropBasedOnColumn && !fksOnColumnProps.get(columnName)) {
281
- addedStandaloneFkPropsBasedOnColumn.add(columnName);
282
- const { fkIndex, currentFk } = standaloneFkPropBasedOnColumn;
283
- const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), columnName, fksOnColumnProps);
284
- schema.addProperty(prop.name, prop.type, prop);
285
- }
286
- const prop = this.getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fksOnColumnProps.get(columnName));
287
- schema.addProperty(prop.name, prop.type, prop);
363
+ if (scalarPropertiesForRelations === 'always') {
364
+ const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
365
+ standaloneFksBasedOnColumnNames.set(baseName, currentFk);
366
+ fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
367
+ if (fkIndex) {
368
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName });
369
+ }
370
+ } else {
371
+ fksOnColumnProps.set(columnName, currentFk);
372
+ if (fkIndex) {
373
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
374
+ }
288
375
  }
289
- for (const [propBaseName, { fkIndex, currentFk }] of fksOnStandaloneProps.entries()) {
290
- if (addedStandaloneFkPropsBasedOnColumn.has(propBaseName)) {
291
- continue;
292
- }
293
- const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), propBaseName, fksOnColumnProps);
294
- schema.addProperty(prop.name, prop.type, prop);
376
+ continue;
377
+ }
378
+ const specificColumnNames = [];
379
+ const nullableColumnsInFk = [];
380
+ for (const columnName of currentFk.columnNames) {
381
+ columnFks[columnName] ??= [];
382
+ columnFks[columnName].push(currentFk);
383
+ if (!fks.some(fk => fk !== currentFk && fk.columnNames.includes(columnName))) {
384
+ specificColumnNames.push(columnName);
295
385
  }
296
- const meta = schema.init().meta;
297
- const oneToOneCandidateProperties = meta.relations.filter(prop => prop.primary && prop.kind === ReferenceKind.MANY_TO_ONE);
298
- if (oneToOneCandidateProperties.length === 1 &&
299
- oneToOneCandidateProperties[0].fieldNames.length ===
300
- new Set(meta.getPrimaryProps().flatMap(prop => prop.fieldNames)).size) {
301
- oneToOneCandidateProperties[0].kind = ReferenceKind.ONE_TO_ONE;
386
+ if (this.getColumn(columnName)?.nullable) {
387
+ nullableColumnsInFk.push(columnName);
302
388
  }
303
- return meta;
304
- }
305
- foreignKeysToProps(namingStrategy, scalarPropertiesForRelations) {
306
- const fks = Object.values(this.getForeignKeys());
307
- const fksOnColumnProps = new Map();
308
- const fksOnStandaloneProps = new Map();
309
- const columnFks = {};
310
- const fkIndexes = new Map();
311
- const nullableForeignKeys = new Set();
312
- const standaloneFksBasedOnColumnNames = new Map();
313
- for (const currentFk of fks) {
314
- const fkIndex = this.findFkIndex(currentFk);
315
- if (currentFk.columnNames.length === 1 &&
316
- !fks.some(fk => fk !== currentFk && fk.columnNames.length === 1 && currentFk.columnNames[0] === fk.columnNames[0])) {
317
- // Non-composite FK is the only possible one for a column. Render the column with it.
318
- const columnName = currentFk.columnNames[0];
319
- columnFks[columnName] ??= [];
320
- columnFks[columnName].push(currentFk);
321
- if (this.getColumn(columnName)?.nullable) {
322
- nullableForeignKeys.add(currentFk);
323
- }
324
- if (scalarPropertiesForRelations === 'always') {
325
- const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
326
- standaloneFksBasedOnColumnNames.set(baseName, currentFk);
327
- fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
328
- if (fkIndex) {
329
- fkIndexes.set(fkIndex, { fk: currentFk, baseName });
330
- }
331
- }
332
- else {
333
- fksOnColumnProps.set(columnName, currentFk);
334
- if (fkIndex) {
335
- fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
336
- }
337
- }
338
- continue;
339
- }
340
- const specificColumnNames = [];
341
- const nullableColumnsInFk = [];
342
- for (const columnName of currentFk.columnNames) {
343
- columnFks[columnName] ??= [];
344
- columnFks[columnName].push(currentFk);
345
- if (!fks.some(fk => fk !== currentFk && fk.columnNames.includes(columnName))) {
346
- specificColumnNames.push(columnName);
347
- }
348
- if (this.getColumn(columnName)?.nullable) {
349
- nullableColumnsInFk.push(columnName);
350
- }
351
- }
352
- if (nullableColumnsInFk.length > 0) {
353
- nullableForeignKeys.add(currentFk);
354
- }
355
- if (specificColumnNames.length === 1 &&
356
- (nullableColumnsInFk.length === currentFk.columnNames.length ||
357
- nullableColumnsInFk.length === 0 ||
358
- (nullableColumnsInFk.length === 1 && nullableColumnsInFk[0] === specificColumnNames[0]))) {
359
- // Composite FK has exactly one column which is not used in any other FK.
360
- // The FK also doesn't have a mix of nullable and non-nullable columns,
361
- // or its only nullable column is this very one.
362
- // It is safe to just render this FK attached to the specific column.
363
- const columnName = specificColumnNames[0];
364
- if (scalarPropertiesForRelations === 'always') {
365
- const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
366
- standaloneFksBasedOnColumnNames.set(baseName, currentFk);
367
- fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
368
- if (fkIndex) {
369
- fkIndexes.set(fkIndex, { fk: currentFk, baseName });
370
- }
371
- }
372
- else {
373
- fksOnColumnProps.set(columnName, currentFk);
374
- if (fkIndex) {
375
- fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
376
- }
377
- }
378
- continue;
379
- }
380
- if (specificColumnNames.length === currentFk.columnNames.length) {
381
- // All columns involved with this FK are only covered by this one FK.
382
- if (nullableColumnsInFk.length <= 1) {
383
- // Also, this FK is either not nullable, or has only one nullable column.
384
- // It is safe to name the FK after the nullable column, or any non-nullable one (the first one is picked).
385
- const columnName = nullableColumnsInFk.at(0) ?? currentFk.columnNames[0];
386
- if (scalarPropertiesForRelations === 'always') {
387
- const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
388
- standaloneFksBasedOnColumnNames.set(baseName, currentFk);
389
- fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
390
- if (fkIndex) {
391
- fkIndexes.set(fkIndex, { fk: currentFk, baseName });
392
- }
393
- }
394
- else {
395
- fksOnColumnProps.set(columnName, currentFk);
396
- if (fkIndex) {
397
- fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
398
- }
399
- }
400
- continue;
401
- }
402
- // If the first nullable column's name with FK is different from the name without FK,
403
- // name a standalone prop after the column, but treat the column prop itself as not having FK.
404
- const columnName = nullableColumnsInFk[0];
405
- const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
406
- standaloneFksBasedOnColumnNames.set(baseName, currentFk);
407
- fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
408
- if (fkIndex) {
409
- fkIndexes.set(fkIndex, { fk: currentFk, baseName });
410
- }
411
- continue;
412
- }
413
- // FK is not unambiguously mappable to a column. Pick another name for a standalone FK prop.
414
- const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
389
+ }
390
+ if (nullableColumnsInFk.length > 0) {
391
+ nullableForeignKeys.add(currentFk);
392
+ }
393
+ if (
394
+ specificColumnNames.length === 1 &&
395
+ (nullableColumnsInFk.length === currentFk.columnNames.length ||
396
+ nullableColumnsInFk.length === 0 ||
397
+ (nullableColumnsInFk.length === 1 && nullableColumnsInFk[0] === specificColumnNames[0]))
398
+ ) {
399
+ // Composite FK has exactly one column which is not used in any other FK.
400
+ // The FK also doesn't have a mix of nullable and non-nullable columns,
401
+ // or its only nullable column is this very one.
402
+ // It is safe to just render this FK attached to the specific column.
403
+ const columnName = specificColumnNames[0];
404
+ if (scalarPropertiesForRelations === 'always') {
405
+ const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
406
+ standaloneFksBasedOnColumnNames.set(baseName, currentFk);
407
+ fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
408
+ if (fkIndex) {
409
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName });
410
+ }
411
+ } else {
412
+ fksOnColumnProps.set(columnName, currentFk);
413
+ if (fkIndex) {
414
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
415
+ }
416
+ }
417
+ continue;
418
+ }
419
+ if (specificColumnNames.length === currentFk.columnNames.length) {
420
+ // All columns involved with this FK are only covered by this one FK.
421
+ if (nullableColumnsInFk.length <= 1) {
422
+ // Also, this FK is either not nullable, or has only one nullable column.
423
+ // It is safe to name the FK after the nullable column, or any non-nullable one (the first one is picked).
424
+ const columnName = nullableColumnsInFk.at(0) ?? currentFk.columnNames[0];
425
+ if (scalarPropertiesForRelations === 'always') {
426
+ const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
427
+ standaloneFksBasedOnColumnNames.set(baseName, currentFk);
415
428
  fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
416
429
  if (fkIndex) {
417
- fkIndexes.set(fkIndex, { fk: currentFk, baseName });
430
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName });
418
431
  }
419
- }
420
- const columnsInFks = Object.keys(columnFks);
421
- const skippingHandlers = {
422
- // Never generate scalar props for composite keys,
423
- // i.e. always skip columns if they are covered by foreign keys.
424
- never: (column) => columnsInFks.includes(column.name) && !fksOnColumnProps.has(column.name),
425
- // Always generate scalar props for composite keys,
426
- // i.e. do not skip columns, even if they are covered by foreign keys.
427
- always: (column) => false,
428
- // Smart scalar props generation.
429
- // Skips columns if they are covered by foreign keys.
430
- // But also does not skip if the column is not nullable, and yet all involved FKs are nullable,
431
- // or if one or more FKs involved has multiple nullable columns.
432
- smart: (column) => {
433
- return (columnsInFks.includes(column.name) &&
434
- !fksOnColumnProps.has(column.name) &&
435
- (column.nullable
436
- ? columnFks[column.name].some(fk => !fk.columnNames.some(fkColumnName => fkColumnName !== column.name && this.getColumn(fkColumnName)?.nullable))
437
- : columnFks[column.name].some(fk => !nullableForeignKeys.has(fk))));
438
- },
439
- };
440
- const skippedColumnNames = this.getColumns()
441
- .filter(skippingHandlers[scalarPropertiesForRelations])
442
- .map(column => column.name);
443
- // Check standalone FKs named after columns for potential conflicts among themselves.
444
- // This typically happens when two standalone FKs named after a column resolve to the same prop name
445
- // because the respective columns include the referenced table in the name.
446
- // Depending on naming strategy and actual names, it may also originate from other scenarios.
447
- // We do our best to de-duplicate them here.
448
- const safePropNames = new Set();
449
- const unsafePropNames = new Map();
450
- for (const [unsafeBaseName, currentFk] of standaloneFksBasedOnColumnNames) {
451
- const propName = this.getPropertyName(namingStrategy, unsafeBaseName, currentFk);
452
- if (safePropNames.has(propName)) {
453
- if (!unsafePropNames.has(propName)) {
454
- unsafePropNames.set(propName, []);
455
- }
456
- unsafePropNames.get(propName).push({ unsafeBaseName, currentFk });
457
- continue;
432
+ } else {
433
+ fksOnColumnProps.set(columnName, currentFk);
434
+ if (fkIndex) {
435
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
458
436
  }
459
- safePropNames.add(propName);
437
+ }
438
+ continue;
460
439
  }
461
- for (const [unsafePropName, affectedBaseNames] of unsafePropNames) {
462
- safePropNames.delete(unsafePropName);
463
- for (const { unsafeBaseName, currentFk } of affectedBaseNames) {
464
- const newBaseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
465
- fksOnStandaloneProps.delete(unsafeBaseName);
466
- let fkIndex;
467
- for (const [indexDef, fkIndexDesc] of fkIndexes) {
468
- if (fkIndexDesc.fk !== currentFk) {
469
- continue;
470
- }
471
- fkIndexDesc.baseName = newBaseName;
472
- fkIndex = indexDef;
473
- break;
474
- }
475
- fksOnStandaloneProps.set(newBaseName, { fkIndex, currentFk });
476
- }
440
+ // If the first nullable column's name with FK is different from the name without FK,
441
+ // name a standalone prop after the column, but treat the column prop itself as not having FK.
442
+ const columnName = nullableColumnsInFk[0];
443
+ const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
444
+ standaloneFksBasedOnColumnNames.set(baseName, currentFk);
445
+ fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
446
+ if (fkIndex) {
447
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName });
477
448
  }
478
- return { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames };
479
- }
480
- findFkIndex(currentFk) {
481
- const fkColumnsLength = currentFk.columnNames.length;
482
- const possibleIndexes = this.#indexes.filter(index => {
483
- return (index.columnNames.length === fkColumnsLength &&
484
- !currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName));
485
- });
486
- possibleIndexes.sort((a, b) => {
487
- if (a.primary !== b.primary) {
488
- return a.primary ? -1 : 1;
489
- }
490
- if (a.unique !== b.unique) {
491
- return a.unique ? -1 : 1;
492
- }
493
- return a.keyName.localeCompare(b.keyName);
494
- });
495
- return possibleIndexes.at(0);
449
+ continue;
450
+ }
451
+ // FK is not unambiguously mappable to a column. Pick another name for a standalone FK prop.
452
+ const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
453
+ fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
454
+ if (fkIndex) {
455
+ fkIndexes.set(fkIndex, { fk: currentFk, baseName });
456
+ }
496
457
  }
497
- getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
498
- const propBaseNames = new Set();
499
- const columnNames = index.columnNames;
500
- const l = columnNames.length;
501
- if (columnNames.some(col => !col)) {
502
- return;
458
+ const columnsInFks = Object.keys(columnFks);
459
+ const skippingHandlers = {
460
+ // Never generate scalar props for composite keys,
461
+ // i.e. always skip columns if they are covered by foreign keys.
462
+ never: column => columnsInFks.includes(column.name) && !fksOnColumnProps.has(column.name),
463
+ // Always generate scalar props for composite keys,
464
+ // i.e. do not skip columns, even if they are covered by foreign keys.
465
+ always: column => false,
466
+ // Smart scalar props generation.
467
+ // Skips columns if they are covered by foreign keys.
468
+ // But also does not skip if the column is not nullable, and yet all involved FKs are nullable,
469
+ // or if one or more FKs involved has multiple nullable columns.
470
+ smart: column => {
471
+ return (
472
+ columnsInFks.includes(column.name) &&
473
+ !fksOnColumnProps.has(column.name) &&
474
+ (column.nullable
475
+ ? columnFks[column.name].some(
476
+ fk =>
477
+ !fk.columnNames.some(
478
+ fkColumnName => fkColumnName !== column.name && this.getColumn(fkColumnName)?.nullable,
479
+ ),
480
+ )
481
+ : columnFks[column.name].some(fk => !nullableForeignKeys.has(fk)))
482
+ );
483
+ },
484
+ };
485
+ const skippedColumnNames = this.getColumns()
486
+ .filter(skippingHandlers[scalarPropertiesForRelations])
487
+ .map(column => column.name);
488
+ // Check standalone FKs named after columns for potential conflicts among themselves.
489
+ // This typically happens when two standalone FKs named after a column resolve to the same prop name
490
+ // because the respective columns include the referenced table in the name.
491
+ // Depending on naming strategy and actual names, it may also originate from other scenarios.
492
+ // We do our best to de-duplicate them here.
493
+ const safePropNames = new Set();
494
+ const unsafePropNames = new Map();
495
+ for (const [unsafeBaseName, currentFk] of standaloneFksBasedOnColumnNames) {
496
+ const propName = this.getPropertyName(namingStrategy, unsafeBaseName, currentFk);
497
+ if (safePropNames.has(propName)) {
498
+ if (!unsafePropNames.has(propName)) {
499
+ unsafePropNames.set(propName, []);
503
500
  }
504
- for (let i = 0; i < l; ++i) {
505
- const columnName = columnNames[i];
506
- // The column is not involved with FKs.
507
- if (!(columnName in columnFks)) {
508
- // If there is no such column, the "name" is actually an expression.
509
- if (!this.hasColumn(columnName)) {
510
- return;
511
- }
512
- // It has a prop named after it.
513
- // Add it and move on.
514
- propBaseNames.add(columnName);
515
- continue;
516
- }
517
- // If the prop named after the column has a FK and the FK's columns are a subset of this index,
518
- // include this prop and move on.
519
- const columnPropFk = fksOnColumnProps.get(columnName);
520
- if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
521
- propBaseNames.add(columnName);
522
- continue;
523
- }
524
- // If there is at least one standalone FK featuring this column,
525
- // and all of its columns are a subset of this index,
526
- // include that FK, and consider mapping of this column to a prop a success.
527
- let propAdded = false;
528
- for (const [propName, { currentFk: fk }] of fksOnStandaloneProps) {
529
- if (!columnFks[columnName].includes(fk)) {
530
- continue;
531
- }
532
- if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
533
- propBaseNames.add(propName);
534
- propAdded = true;
535
- }
536
- }
537
- if (propAdded) {
538
- continue;
539
- }
540
- // If we have reached this point, it means the column is not mappable to a prop name.
541
- // Break the whole prop creation.
542
- return;
501
+ unsafePropNames.get(propName).push({ unsafeBaseName, currentFk });
502
+ continue;
503
+ }
504
+ safePropNames.add(propName);
505
+ }
506
+ for (const [unsafePropName, affectedBaseNames] of unsafePropNames) {
507
+ safePropNames.delete(unsafePropName);
508
+ for (const { unsafeBaseName, currentFk } of affectedBaseNames) {
509
+ const newBaseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
510
+ fksOnStandaloneProps.delete(unsafeBaseName);
511
+ let fkIndex;
512
+ for (const [indexDef, fkIndexDesc] of fkIndexes) {
513
+ if (fkIndexDesc.fk !== currentFk) {
514
+ continue;
515
+ }
516
+ fkIndexDesc.baseName = newBaseName;
517
+ fkIndex = indexDef;
518
+ break;
543
519
  }
544
- return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
520
+ fksOnStandaloneProps.set(newBaseName, { fkIndex, currentFk });
521
+ }
522
+ }
523
+ return { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames };
524
+ }
525
+ findFkIndex(currentFk) {
526
+ const fkColumnsLength = currentFk.columnNames.length;
527
+ const possibleIndexes = this.#indexes.filter(index => {
528
+ return (
529
+ index.columnNames.length === fkColumnsLength &&
530
+ !currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName)
531
+ );
532
+ });
533
+ possibleIndexes.sort((a, b) => {
534
+ if (a.primary !== b.primary) {
535
+ return a.primary ? -1 : 1;
536
+ }
537
+ if (a.unique !== b.unique) {
538
+ return a.unique ? -1 : 1;
539
+ }
540
+ return a.keyName.localeCompare(b.keyName);
541
+ });
542
+ return possibleIndexes.at(0);
543
+ }
544
+ getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
545
+ const propBaseNames = new Set();
546
+ const columnNames = index.columnNames;
547
+ const l = columnNames.length;
548
+ if (columnNames.some(col => !col)) {
549
+ return;
545
550
  }
546
- getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
547
- if (columnName &&
548
- this.getPropertyName(namingStrategy, columnName, currentFk) !== this.getPropertyName(namingStrategy, columnName)) {
549
- // The eligible scalar column name is different from the name of the FK prop of the same column.
550
- // Both can be safely rendered.
551
- // Use the column name as a base for the FK prop.
552
- return columnName;
551
+ for (let i = 0; i < l; ++i) {
552
+ const columnName = columnNames[i];
553
+ // The column is not involved with FKs.
554
+ if (!(columnName in columnFks)) {
555
+ // If there is no such column, the "name" is actually an expression.
556
+ if (!this.hasColumn(columnName)) {
557
+ return;
553
558
  }
554
- // Strip schema prefix from referenced table name (e.g., "public.fr_usuario" -> "fr_usuario")
555
- const getTableName = (fullName) => {
556
- const parts = fullName.split('.');
557
- return parts[parts.length - 1];
558
- };
559
- const referencedTableName = getTableName(currentFk.referencedTableName);
560
- // Check for conflicts using stripped table names (handles cross-schema FKs to same-named tables)
561
- const hasConflictingFk = fks.some(fk => fk !== currentFk && getTableName(fk.referencedTableName) === referencedTableName);
562
- if (!hasConflictingFk && !this.getColumn(referencedTableName)) {
563
- // FK is the only one in this table that references a table with this name.
564
- // The name of the referenced table is not shared with a column in this table,
565
- // so it is safe to output prop name based on the referenced entity.
566
- return referencedTableName;
559
+ // It has a prop named after it.
560
+ // Add it and move on.
561
+ propBaseNames.add(columnName);
562
+ continue;
563
+ }
564
+ // If the prop named after the column has a FK and the FK's columns are a subset of this index,
565
+ // include this prop and move on.
566
+ const columnPropFk = fksOnColumnProps.get(columnName);
567
+ if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
568
+ propBaseNames.add(columnName);
569
+ continue;
570
+ }
571
+ // If there is at least one standalone FK featuring this column,
572
+ // and all of its columns are a subset of this index,
573
+ // include that FK, and consider mapping of this column to a prop a success.
574
+ let propAdded = false;
575
+ for (const [propName, { currentFk: fk }] of fksOnStandaloneProps) {
576
+ if (!columnFks[columnName].includes(fk)) {
577
+ continue;
567
578
  }
568
- // Any ambiguous FK is rendered with a name based on the FK constraint name
569
- let finalPropBaseName = currentFk.constraintName;
570
- while (this.getColumn(finalPropBaseName)) {
571
- // In the unlikely event that the FK constraint name is shared by a column name, generate a name by
572
- // continuously prefixing with "fk_", until a non-existent column is hit.
573
- // The worst case scenario is a very long name with several repeated "fk_"
574
- // that is not really a valid DB identifier but a valid JS variable name.
575
- finalPropBaseName = `fk_${finalPropBaseName}`;
579
+ if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
580
+ propBaseNames.add(propName);
581
+ propAdded = true;
576
582
  }
577
- return finalPropBaseName;
583
+ }
584
+ if (propAdded) {
585
+ continue;
586
+ }
587
+ // If we have reached this point, it means the column is not mappable to a prop name.
588
+ // Break the whole prop creation.
589
+ return;
578
590
  }
579
- /**
580
- * The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
581
- */
582
- getShortestName(skipDefaultSchema = true) {
583
- const defaultSchema = this.#platform.getDefaultSchemaName();
584
- if (!this.schema ||
585
- this.name.startsWith(defaultSchema + '.') ||
586
- (this.schema === defaultSchema && skipDefaultSchema)) {
587
- return this.name;
588
- }
589
- return `${this.schema}.${this.name}`;
591
+ return Array.from(propBaseNames).map(baseName =>
592
+ this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)),
593
+ );
594
+ }
595
+ getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
596
+ if (
597
+ columnName &&
598
+ this.getPropertyName(namingStrategy, columnName, currentFk) !== this.getPropertyName(namingStrategy, columnName)
599
+ ) {
600
+ // The eligible scalar column name is different from the name of the FK prop of the same column.
601
+ // Both can be safely rendered.
602
+ // Use the column name as a base for the FK prop.
603
+ return columnName;
590
604
  }
591
- getForeignKeys() {
592
- return this.#foreignKeys;
605
+ // Strip schema prefix from referenced table name (e.g., "public.fr_usuario" -> "fr_usuario")
606
+ const getTableName = fullName => {
607
+ const parts = fullName.split('.');
608
+ return parts[parts.length - 1];
609
+ };
610
+ const referencedTableName = getTableName(currentFk.referencedTableName);
611
+ // Check for conflicts using stripped table names (handles cross-schema FKs to same-named tables)
612
+ const hasConflictingFk = fks.some(
613
+ fk => fk !== currentFk && getTableName(fk.referencedTableName) === referencedTableName,
614
+ );
615
+ if (!hasConflictingFk && !this.getColumn(referencedTableName)) {
616
+ // FK is the only one in this table that references a table with this name.
617
+ // The name of the referenced table is not shared with a column in this table,
618
+ // so it is safe to output prop name based on the referenced entity.
619
+ return referencedTableName;
593
620
  }
594
- hasColumn(columnName) {
595
- return columnName in this.#columns;
621
+ // Any ambiguous FK is rendered with a name based on the FK constraint name
622
+ let finalPropBaseName = currentFk.constraintName;
623
+ while (this.getColumn(finalPropBaseName)) {
624
+ // In the unlikely event that the FK constraint name is shared by a column name, generate a name by
625
+ // continuously prefixing with "fk_", until a non-existent column is hit.
626
+ // The worst case scenario is a very long name with several repeated "fk_"
627
+ // that is not really a valid DB identifier but a valid JS variable name.
628
+ finalPropBaseName = `fk_${finalPropBaseName}`;
596
629
  }
597
- getIndex(indexName) {
598
- return this.#indexes.find(i => i.keyName === indexName);
630
+ return finalPropBaseName;
631
+ }
632
+ /**
633
+ * The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
634
+ */
635
+ getShortestName(skipDefaultSchema = true) {
636
+ const defaultSchema = this.#platform.getDefaultSchemaName();
637
+ if (
638
+ !this.schema ||
639
+ this.name.startsWith(defaultSchema + '.') ||
640
+ (this.schema === defaultSchema && skipDefaultSchema)
641
+ ) {
642
+ return this.name;
599
643
  }
600
- hasIndex(indexName) {
601
- return !!this.getIndex(indexName);
644
+ return `${this.schema}.${this.name}`;
645
+ }
646
+ getForeignKeys() {
647
+ return this.#foreignKeys;
648
+ }
649
+ hasColumn(columnName) {
650
+ return columnName in this.#columns;
651
+ }
652
+ getIndex(indexName) {
653
+ return this.#indexes.find(i => i.keyName === indexName);
654
+ }
655
+ hasIndex(indexName) {
656
+ return !!this.getIndex(indexName);
657
+ }
658
+ getCheck(checkName) {
659
+ return this.#checks.find(i => i.name === checkName);
660
+ }
661
+ hasCheck(checkName) {
662
+ return !!this.getCheck(checkName);
663
+ }
664
+ getPrimaryKey() {
665
+ return this.#indexes.find(i => i.primary);
666
+ }
667
+ hasPrimaryKey() {
668
+ return !!this.getPrimaryKey();
669
+ }
670
+ getForeignKeyDeclaration(fk, namingStrategy, schemaHelper, fkIndex, nullable, propNameBase, fksOnColumnProps) {
671
+ const prop = this.getPropertyName(namingStrategy, propNameBase, fk);
672
+ const kind = fkIndex?.unique && !fkIndex.primary ? this.getReferenceKind(fk, fkIndex) : this.getReferenceKind(fk);
673
+ const runtimeType = this.getPropertyTypeForForeignKey(namingStrategy, fk);
674
+ const fkOptions = {};
675
+ fkOptions.fieldNames = fk.columnNames;
676
+ fkOptions.referencedTableName = fk.referencedTableName;
677
+ fkOptions.referencedColumnNames = fk.referencedColumnNames;
678
+ fkOptions.updateRule = fk.updateRule?.toLowerCase();
679
+ fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
680
+ fkOptions.deferMode = fk.deferMode;
681
+ fkOptions.columnTypes = fk.columnNames.map(c => this.getColumn(c).type);
682
+ const columnOptions = {};
683
+ if (fk.columnNames.length === 1) {
684
+ const column = this.getColumn(fk.columnNames[0]);
685
+ const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, column.type, true);
686
+ const defaultTs = this.getPropertyDefaultValue(schemaHelper, column, column.type);
687
+ columnOptions.default = defaultRaw !== defaultTs || defaultRaw === '' ? defaultTs : undefined;
688
+ columnOptions.defaultRaw = column.nullable && defaultRaw === 'null' ? undefined : defaultRaw;
689
+ columnOptions.optional = typeof column.generated !== 'undefined' || defaultRaw !== 'null';
690
+ columnOptions.generated = column.generated;
691
+ columnOptions.nullable = column.nullable;
692
+ columnOptions.primary = column.primary;
693
+ columnOptions.length = column.length;
694
+ columnOptions.precision = column.precision;
695
+ columnOptions.scale = column.scale;
696
+ columnOptions.extra = column.extra;
697
+ columnOptions.comment = column.comment;
698
+ columnOptions.enum = !!column.enumItems?.length;
699
+ columnOptions.items = column.enumItems;
602
700
  }
603
- getCheck(checkName) {
604
- return this.#checks.find(i => i.name === checkName);
701
+ return {
702
+ name: prop,
703
+ type: runtimeType,
704
+ runtimeType,
705
+ kind,
706
+ ...columnOptions,
707
+ nullable,
708
+ primary:
709
+ fkIndex?.primary || !fk.columnNames.some(columnName => !this.getPrimaryKey()?.columnNames.includes(columnName)),
710
+ index: !fkIndex?.unique ? fkIndex?.keyName : undefined,
711
+ unique: fkIndex?.unique && !fkIndex.primary ? fkIndex.keyName : undefined,
712
+ ...fkOptions,
713
+ };
714
+ }
715
+ getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fk) {
716
+ const prop = this.getPropertyName(namingStrategy, column.name, fk);
717
+ const persist = !(column.name in columnFks && typeof fk === 'undefined');
718
+ const index =
719
+ compositeFkIndexes[prop] ||
720
+ this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary);
721
+ const unique =
722
+ compositeFkUniques[prop] ||
723
+ this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary);
724
+ const kind = this.getReferenceKind(fk, unique);
725
+ const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
726
+ const type = fk
727
+ ? runtimeType
728
+ : (Utils.keys(t).find(k => {
729
+ const typeInCoreMap = this.#platform.getMappedType(k);
730
+ return (
731
+ (typeInCoreMap !== Type.getType(UnknownType) || k === 'unknown') && typeInCoreMap === column.mappedType
732
+ );
733
+ }) ?? runtimeType);
734
+ const ignoreSchemaChanges =
735
+ type === 'unknown' && column.length ? (column.extra ? ['type', 'extra'] : ['type']) : undefined;
736
+ const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, runtimeType, true);
737
+ const defaultParsed = this.getPropertyDefaultValue(schemaHelper, column, runtimeType);
738
+ const defaultTs = defaultRaw !== defaultParsed || defaultParsed === '' ? defaultParsed : undefined;
739
+ const fkOptions = {};
740
+ if (fk) {
741
+ fkOptions.fieldNames = fk.columnNames;
742
+ fkOptions.referencedTableName = fk.referencedTableName;
743
+ fkOptions.referencedColumnNames = fk.referencedColumnNames;
744
+ fkOptions.updateRule = fk.updateRule?.toLowerCase();
745
+ fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
746
+ fkOptions.deferMode = fk.deferMode;
747
+ fkOptions.columnTypes = fk.columnNames.map(col => this.getColumn(col).type);
605
748
  }
606
- hasCheck(checkName) {
607
- return !!this.getCheck(checkName);
749
+ const ret = {
750
+ name: prop,
751
+ type,
752
+ runtimeType,
753
+ kind,
754
+ ignoreSchemaChanges,
755
+ generated: column.generated,
756
+ optional: defaultRaw !== 'null' || defaultTs != null || typeof column.generated !== 'undefined',
757
+ columnType: column.type,
758
+ default: defaultTs,
759
+ defaultRaw: column.nullable && defaultRaw === 'null' ? undefined : defaultRaw,
760
+ nullable: column.nullable,
761
+ primary: column.primary && persist,
762
+ autoincrement: column.autoincrement,
763
+ fieldName: column.name,
764
+ unsigned: column.unsigned,
765
+ length: column.length,
766
+ precision: column.precision,
767
+ scale: column.scale,
768
+ extra: column.extra,
769
+ comment: column.comment,
770
+ index: index ? index.keyName : undefined,
771
+ unique: unique ? unique.keyName : undefined,
772
+ enum: !!column.enumItems?.length,
773
+ items: column.enumItems,
774
+ persist,
775
+ ...fkOptions,
776
+ };
777
+ const nativeEnumName = Object.keys(this.nativeEnums).find(name => name === column.type);
778
+ if (nativeEnumName) {
779
+ ret.nativeEnumName = nativeEnumName;
608
780
  }
609
- getPrimaryKey() {
610
- return this.#indexes.find(i => i.primary);
781
+ return ret;
782
+ }
783
+ getReferenceKind(fk, unique) {
784
+ if (fk && unique) {
785
+ return ReferenceKind.ONE_TO_ONE;
611
786
  }
612
- hasPrimaryKey() {
613
- return !!this.getPrimaryKey();
787
+ if (fk) {
788
+ return ReferenceKind.MANY_TO_ONE;
614
789
  }
615
- getForeignKeyDeclaration(fk, namingStrategy, schemaHelper, fkIndex, nullable, propNameBase, fksOnColumnProps) {
616
- const prop = this.getPropertyName(namingStrategy, propNameBase, fk);
617
- const kind = fkIndex?.unique && !fkIndex.primary ? this.getReferenceKind(fk, fkIndex) : this.getReferenceKind(fk);
618
- const runtimeType = this.getPropertyTypeForForeignKey(namingStrategy, fk);
619
- const fkOptions = {};
620
- fkOptions.fieldNames = fk.columnNames;
621
- fkOptions.referencedTableName = fk.referencedTableName;
622
- fkOptions.referencedColumnNames = fk.referencedColumnNames;
623
- fkOptions.updateRule = fk.updateRule?.toLowerCase();
624
- fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
625
- fkOptions.deferMode = fk.deferMode;
626
- fkOptions.columnTypes = fk.columnNames.map(c => this.getColumn(c).type);
627
- const columnOptions = {};
628
- if (fk.columnNames.length === 1) {
629
- const column = this.getColumn(fk.columnNames[0]);
630
- const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, column.type, true);
631
- const defaultTs = this.getPropertyDefaultValue(schemaHelper, column, column.type);
632
- columnOptions.default = defaultRaw !== defaultTs || defaultRaw === '' ? defaultTs : undefined;
633
- columnOptions.defaultRaw = column.nullable && defaultRaw === 'null' ? undefined : defaultRaw;
634
- columnOptions.optional = typeof column.generated !== 'undefined' || defaultRaw !== 'null';
635
- columnOptions.generated = column.generated;
636
- columnOptions.nullable = column.nullable;
637
- columnOptions.primary = column.primary;
638
- columnOptions.length = column.length;
639
- columnOptions.precision = column.precision;
640
- columnOptions.scale = column.scale;
641
- columnOptions.extra = column.extra;
642
- columnOptions.comment = column.comment;
643
- columnOptions.enum = !!column.enumItems?.length;
644
- columnOptions.items = column.enumItems;
645
- }
646
- return {
647
- name: prop,
648
- type: runtimeType,
649
- runtimeType,
650
- kind,
651
- ...columnOptions,
652
- nullable,
653
- primary: fkIndex?.primary || !fk.columnNames.some(columnName => !this.getPrimaryKey()?.columnNames.includes(columnName)),
654
- index: !fkIndex?.unique ? fkIndex?.keyName : undefined,
655
- unique: fkIndex?.unique && !fkIndex.primary ? fkIndex.keyName : undefined,
656
- ...fkOptions,
657
- };
790
+ return ReferenceKind.SCALAR;
791
+ }
792
+ getPropertyName(namingStrategy, baseName, fk) {
793
+ let field = baseName;
794
+ if (fk) {
795
+ const idx = fk.columnNames.indexOf(baseName);
796
+ let replacedFieldName = field.replace(new RegExp(`_${fk.referencedColumnNames[idx]}$`), '');
797
+ if (replacedFieldName === field) {
798
+ replacedFieldName = field.replace(new RegExp(`_${namingStrategy.referenceColumnName()}$`), '');
799
+ }
800
+ field = replacedFieldName;
658
801
  }
659
- getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fk) {
660
- const prop = this.getPropertyName(namingStrategy, column.name, fk);
661
- const persist = !(column.name in columnFks && typeof fk === 'undefined');
662
- const index = compositeFkIndexes[prop] ||
663
- this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary);
664
- const unique = compositeFkUniques[prop] ||
665
- this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary);
666
- const kind = this.getReferenceKind(fk, unique);
667
- const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
668
- const type = fk
669
- ? runtimeType
670
- : (Utils.keys(t).find(k => {
671
- const typeInCoreMap = this.#platform.getMappedType(k);
672
- return ((typeInCoreMap !== Type.getType(UnknownType) || k === 'unknown') && typeInCoreMap === column.mappedType);
673
- }) ?? runtimeType);
674
- const ignoreSchemaChanges = type === 'unknown' && column.length ? (column.extra ? ['type', 'extra'] : ['type']) : undefined;
675
- const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, runtimeType, true);
676
- const defaultParsed = this.getPropertyDefaultValue(schemaHelper, column, runtimeType);
677
- const defaultTs = defaultRaw !== defaultParsed || defaultParsed === '' ? defaultParsed : undefined;
678
- const fkOptions = {};
679
- if (fk) {
680
- fkOptions.fieldNames = fk.columnNames;
681
- fkOptions.referencedTableName = fk.referencedTableName;
682
- fkOptions.referencedColumnNames = fk.referencedColumnNames;
683
- fkOptions.updateRule = fk.updateRule?.toLowerCase();
684
- fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
685
- fkOptions.deferMode = fk.deferMode;
686
- fkOptions.columnTypes = fk.columnNames.map(col => this.getColumn(col).type);
687
- }
688
- const ret = {
689
- name: prop,
690
- type,
691
- runtimeType,
692
- kind,
693
- ignoreSchemaChanges,
694
- generated: column.generated,
695
- optional: defaultRaw !== 'null' || defaultTs != null || typeof column.generated !== 'undefined',
696
- columnType: column.type,
697
- default: defaultTs,
698
- defaultRaw: column.nullable && defaultRaw === 'null' ? undefined : defaultRaw,
699
- nullable: column.nullable,
700
- primary: column.primary && persist,
701
- autoincrement: column.autoincrement,
702
- fieldName: column.name,
703
- unsigned: column.unsigned,
704
- length: column.length,
705
- precision: column.precision,
706
- scale: column.scale,
707
- extra: column.extra,
708
- comment: column.comment,
709
- index: index ? index.keyName : undefined,
710
- unique: unique ? unique.keyName : undefined,
711
- enum: !!column.enumItems?.length,
712
- items: column.enumItems,
713
- persist,
714
- ...fkOptions,
715
- };
716
- const nativeEnumName = Object.keys(this.nativeEnums).find(name => name === column.type);
717
- if (nativeEnumName) {
718
- ret.nativeEnumName = nativeEnumName;
719
- }
720
- return ret;
802
+ if (field.startsWith('_')) {
803
+ return field;
721
804
  }
722
- getReferenceKind(fk, unique) {
723
- if (fk && unique) {
724
- return ReferenceKind.ONE_TO_ONE;
725
- }
726
- if (fk) {
727
- return ReferenceKind.MANY_TO_ONE;
728
- }
729
- return ReferenceKind.SCALAR;
805
+ return namingStrategy.columnNameToProperty(field);
806
+ }
807
+ getPropertyTypeForForeignKey(namingStrategy, fk) {
808
+ const parts = fk.referencedTableName.split('.', 2);
809
+ return namingStrategy.getEntityName(...parts.reverse());
810
+ }
811
+ getPropertyTypeForColumn(namingStrategy, column, fk) {
812
+ if (fk) {
813
+ return this.getPropertyTypeForForeignKey(namingStrategy, fk);
730
814
  }
731
- getPropertyName(namingStrategy, baseName, fk) {
732
- let field = baseName;
733
- if (fk) {
734
- const idx = fk.columnNames.indexOf(baseName);
735
- let replacedFieldName = field.replace(new RegExp(`_${fk.referencedColumnNames[idx]}$`), '');
736
- if (replacedFieldName === field) {
737
- replacedFieldName = field.replace(new RegExp(`_${namingStrategy.referenceColumnName()}$`), '');
738
- }
739
- field = replacedFieldName;
740
- }
741
- if (field.startsWith('_')) {
742
- return field;
743
- }
744
- return namingStrategy.columnNameToProperty(field);
815
+ const enumMode = this.#platform.getConfig().get('entityGenerator').enumMode;
816
+ // If this column is using an enum.
817
+ if (column.enumItems?.length) {
818
+ const name = column.nativeEnumName ?? column.name;
819
+ const tableName = column.nativeEnumName ? undefined : this.name;
820
+ if (enumMode === 'ts-enum') {
821
+ // We will create a new enum name for this type and set it as the property type as well.
822
+ return namingStrategy.getEnumClassName(name, tableName, this.schema);
823
+ }
824
+ // With other enum strategies, we need to use the type name.
825
+ return namingStrategy.getEnumTypeName(name, tableName, this.schema);
745
826
  }
746
- getPropertyTypeForForeignKey(namingStrategy, fk) {
747
- const parts = fk.referencedTableName.split('.', 2);
748
- return namingStrategy.getEntityName(...parts.reverse());
827
+ return column.mappedType?.runtimeType ?? 'unknown';
828
+ }
829
+ getPropertyDefaultValue(schemaHelper, column, propType, raw = false) {
830
+ const defaultValue = column.default ?? 'null';
831
+ const val = schemaHelper.normalizeDefaultValue(defaultValue, column.length);
832
+ if (val === 'null') {
833
+ return raw ? 'null' : column.nullable ? null : undefined;
749
834
  }
750
- getPropertyTypeForColumn(namingStrategy, column, fk) {
751
- if (fk) {
752
- return this.getPropertyTypeForForeignKey(namingStrategy, fk);
753
- }
754
- const enumMode = this.#platform.getConfig().get('entityGenerator').enumMode;
755
- // If this column is using an enum.
756
- if (column.enumItems?.length) {
757
- const name = column.nativeEnumName ?? column.name;
758
- const tableName = column.nativeEnumName ? undefined : this.name;
759
- if (enumMode === 'ts-enum') {
760
- // We will create a new enum name for this type and set it as the property type as well.
761
- return namingStrategy.getEnumClassName(name, tableName, this.schema);
762
- }
763
- // With other enum strategies, we need to use the type name.
764
- return namingStrategy.getEnumTypeName(name, tableName, this.schema);
765
- }
766
- return column.mappedType?.runtimeType ?? 'unknown';
835
+ if (propType === 'boolean' && !raw) {
836
+ return !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + column.default);
767
837
  }
768
- getPropertyDefaultValue(schemaHelper, column, propType, raw = false) {
769
- const defaultValue = column.default ?? 'null';
770
- const val = schemaHelper.normalizeDefaultValue(defaultValue, column.length);
771
- if (val === 'null') {
772
- return raw ? 'null' : column.nullable ? null : undefined;
773
- }
774
- if (propType === 'boolean' && !raw) {
775
- return !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + column.default);
776
- }
777
- if (propType === 'number' && !raw) {
778
- return +defaultValue;
779
- }
780
- // unquote string defaults if `raw = false`
781
- const match = /^'(.*)'$/.exec('' + val);
782
- if (!raw && match) {
783
- return match[1];
784
- }
785
- return '' + val;
838
+ if (propType === 'number' && !raw) {
839
+ return +defaultValue;
786
840
  }
787
- processIndexExpression(indexName, expression, meta) {
788
- if (expression instanceof Function) {
789
- const qualifiedName = this.schema ? `${this.schema}.${this.name}` : this.name;
790
- const table = {
791
- name: this.name,
792
- schema: this.schema,
793
- qualifiedName,
794
- toString: () => qualifiedName,
795
- };
796
- const columns = meta.createSchemaColumnMappingObject();
797
- const exp = expression(columns, table, indexName);
798
- return isRaw(exp) ? this.#platform.formatQuery(exp.sql, exp.params) : exp;
799
- }
800
- return expression;
841
+ // unquote string defaults if `raw = false`
842
+ const match = /^'(.*)'$/.exec('' + val);
843
+ if (!raw && match) {
844
+ return match[1];
801
845
  }
802
- addIndex(meta, index, type) {
803
- // If columns are specified but properties are not, derive properties from column names
804
- if (index.columns?.length &&
805
- !index.expression &&
806
- (!index.properties || Utils.asArray(index.properties).length === 0)) {
807
- index = { ...index, properties: index.columns.map(c => c.name) };
808
- }
809
- const properties = Utils.unique(Utils.flatten(Utils.asArray(index.properties).map(prop => {
810
- const parts = prop.split('.');
811
- const root = parts[0];
812
- if (meta.properties[prop]) {
813
- if (meta.properties[prop].embeddedPath) {
814
- return [meta.properties[prop].embeddedPath.join('.')];
815
- }
816
- return meta.properties[prop].fieldNames;
817
- }
818
- const rootProp = meta.properties[root];
819
- // inline embedded property index, we need to find the field name of the child property
820
- if (rootProp?.embeddable && !rootProp.object && parts.length > 1) {
821
- const expand = (p, i) => {
822
- if (parts.length === i) {
823
- return p.fieldNames[0];
824
- }
825
- return expand(p.embeddedProps[parts[i]], i + 1);
826
- };
827
- return [expand(rootProp, 1)];
828
- }
829
- // json index, we need to rename the column only
830
- if (rootProp) {
831
- return [prop.replace(root, rootProp.fieldNames[0])];
846
+ return '' + val;
847
+ }
848
+ processIndexExpression(indexName, expression, meta) {
849
+ if (expression instanceof Function) {
850
+ const qualifiedName = this.schema ? `${this.schema}.${this.name}` : this.name;
851
+ const table = {
852
+ name: this.name,
853
+ schema: this.schema,
854
+ qualifiedName,
855
+ toString: () => qualifiedName,
856
+ };
857
+ const columns = meta.createSchemaColumnMappingObject();
858
+ const exp = expression(columns, table, indexName);
859
+ return isRaw(exp) ? this.#platform.formatQuery(exp.sql, exp.params) : exp;
860
+ }
861
+ return expression;
862
+ }
863
+ addIndex(meta, index, type) {
864
+ // If columns are specified but properties are not, derive properties from column names
865
+ if (
866
+ index.columns?.length &&
867
+ !index.expression &&
868
+ (!index.properties || Utils.asArray(index.properties).length === 0)
869
+ ) {
870
+ index = { ...index, properties: index.columns.map(c => c.name) };
871
+ }
872
+ const properties = Utils.unique(
873
+ Utils.flatten(
874
+ Utils.asArray(index.properties).map(prop => {
875
+ const parts = prop.split('.');
876
+ const root = parts[0];
877
+ if (meta.properties[prop]) {
878
+ if (meta.properties[prop].embeddedPath) {
879
+ return [meta.properties[prop].embeddedPath.join('.')];
832
880
  }
833
- /* v8 ignore next */
834
- return [prop];
835
- })));
836
- if (properties.length === 0 && !index.expression) {
837
- return;
838
- }
839
- const name = this.getIndexName(index.name, properties, type);
840
- // Process include columns (map property names to field names)
841
- const includeColumns = index.include
842
- ? Utils.unique(Utils.flatten(Utils.asArray(index.include).map(prop => {
843
- if (meta.properties[prop]) {
844
- return meta.properties[prop].fieldNames;
845
- }
846
- /* v8 ignore next */
847
- return [prop];
848
- })))
849
- : undefined;
850
- // Process columns with advanced options (map property names to field names)
851
- const columns = index.columns?.map(col => {
852
- const fieldName = meta.properties[col.name]?.fieldNames[0] ?? col.name;
853
- return {
854
- name: fieldName,
855
- sort: col.sort?.toUpperCase(),
856
- nulls: col.nulls?.toUpperCase(),
857
- length: col.length,
858
- collation: col.collation,
881
+ return meta.properties[prop].fieldNames;
882
+ }
883
+ const rootProp = meta.properties[root];
884
+ // inline embedded property index, we need to find the field name of the child property
885
+ if (rootProp?.embeddable && !rootProp.object && parts.length > 1) {
886
+ const expand = (p, i) => {
887
+ if (parts.length === i) {
888
+ return p.fieldNames[0];
889
+ }
890
+ return expand(p.embeddedProps[parts[i]], i + 1);
859
891
  };
860
- });
861
- // Validate that column options reference fields in the index properties
862
- if (columns?.length && properties.length > 0) {
863
- for (const col of columns) {
864
- if (!properties.includes(col.name)) {
865
- throw new Error(`Index '${name}' on entity '${meta.className}': column option references field '${col.name}' which is not in the index properties`);
866
- }
867
- }
868
- }
869
- // Validate fillFactor range
870
- if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
871
- throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
892
+ return [expand(rootProp, 1)];
893
+ }
894
+ // json index, we need to rename the column only
895
+ if (rootProp) {
896
+ return [prop.replace(root, rootProp.fieldNames[0])];
897
+ }
898
+ /* v8 ignore next */
899
+ return [prop];
900
+ }),
901
+ ),
902
+ );
903
+ if (properties.length === 0 && !index.expression) {
904
+ return;
905
+ }
906
+ const name = this.getIndexName(index.name, properties, type);
907
+ // Process include columns (map property names to field names)
908
+ const includeColumns = index.include
909
+ ? Utils.unique(
910
+ Utils.flatten(
911
+ Utils.asArray(index.include).map(prop => {
912
+ if (meta.properties[prop]) {
913
+ return meta.properties[prop].fieldNames;
914
+ }
915
+ /* v8 ignore next */
916
+ return [prop];
917
+ }),
918
+ ),
919
+ )
920
+ : undefined;
921
+ // Process columns with advanced options (map property names to field names)
922
+ const columns = index.columns?.map(col => {
923
+ const fieldName = meta.properties[col.name]?.fieldNames[0] ?? col.name;
924
+ return {
925
+ name: fieldName,
926
+ sort: col.sort?.toUpperCase(),
927
+ nulls: col.nulls?.toUpperCase(),
928
+ length: col.length,
929
+ collation: col.collation,
930
+ };
931
+ });
932
+ // Validate that column options reference fields in the index properties
933
+ if (columns?.length && properties.length > 0) {
934
+ for (const col of columns) {
935
+ if (!properties.includes(col.name)) {
936
+ throw new Error(
937
+ `Index '${name}' on entity '${meta.className}': column option references field '${col.name}' which is not in the index properties`,
938
+ );
872
939
  }
873
- this.#indexes.push({
874
- keyName: name,
875
- columnNames: properties,
876
- composite: properties.length > 1,
877
- // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
878
- constraint: type !== 'index' && !properties.some((d) => d.includes('.')),
879
- primary: type === 'primary',
880
- unique: type !== 'index',
881
- type: index.type,
882
- expression: this.processIndexExpression(name, index.expression, meta),
883
- options: index.options,
884
- deferMode: index.deferMode,
885
- columns,
886
- include: includeColumns,
887
- fillFactor: index.fillFactor,
888
- invisible: index.invisible,
889
- disabled: index.disabled,
890
- clustered: index.clustered,
891
- });
940
+ }
892
941
  }
893
- addCheck(check) {
894
- this.#checks.push(check);
942
+ // Validate fillFactor range
943
+ if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
944
+ throw new Error(
945
+ `fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`,
946
+ );
895
947
  }
896
- toJSON() {
897
- const columns = this.#columns;
898
- // locale-independent comparison so the snapshot is stable across machines
899
- const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
900
- const sortedColumnKeys = Utils.keys(columns).sort(byString);
901
- // mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
902
- // so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
903
- const primaryColumns = new Set();
904
- const uniqueColumns = new Set();
905
- for (const idx of this.#indexes) {
906
- if (idx.primary) {
907
- idx.columnNames.forEach(c => primaryColumns.add(c));
908
- }
909
- if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
910
- uniqueColumns.add(idx.columnNames[0]);
911
- }
912
- }
913
- // integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
914
- const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
915
- mappedType instanceof t.smallint ||
916
- mappedType instanceof t.tinyint ||
917
- mappedType instanceof t.mediumint ||
918
- mappedType instanceof t.bigint ||
919
- mappedType instanceof t.float ||
920
- mappedType instanceof t.double;
921
- const supportsUnsigned = this.#platform.supportsUnsigned();
922
- const columnsMapped = sortedColumnKeys.reduce((o, col) => {
923
- const c = columns[col];
924
- // omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
925
- const rawType = c.type?.toLowerCase();
926
- const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
927
- const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
928
- const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
929
- const normalized = {
930
- name: c.name,
931
- type,
932
- unsigned: supportsUnsigned && !!c.unsigned,
933
- autoincrement: !!c.autoincrement,
934
- primary: primaryColumns.has(c.name) || !!c.primary,
935
- nullable: !!c.nullable,
936
- unique: uniqueColumns.has(c.name) || !!c.unique,
937
- length: c.length || null,
938
- precision: fixedPrecision ? null : (c.precision ?? null),
939
- scale: fixedPrecision ? null : (c.scale ?? null),
940
- default: c.default ?? null,
941
- comment: c.comment ?? null,
942
- enumItems: c.enumItems ?? [],
943
- mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
944
- };
945
- for (const field of [
946
- 'generated',
947
- 'nativeEnumName',
948
- 'extra',
949
- 'ignoreSchemaChanges',
950
- 'defaultConstraint',
951
- ]) {
952
- if (c[field]) {
953
- normalized[field] = c[field];
954
- }
955
- }
956
- o[col] = normalized;
957
- return o;
958
- }, {});
959
- const normalizeIndex = (idx) => {
960
- const out = {
961
- columnNames: idx.columnNames,
962
- composite: !!idx.composite,
963
- // PK indexes are always backed by a constraint — force it so postgres introspection matches
964
- constraint: !!idx.constraint || !!idx.primary,
965
- keyName: idx.keyName,
966
- primary: !!idx.primary,
967
- unique: !!idx.unique,
968
- };
969
- const optional = [
970
- 'expression',
971
- 'type',
972
- 'deferMode',
973
- 'columns',
974
- 'include',
975
- 'fillFactor',
976
- 'invisible',
977
- 'disabled',
978
- 'clustered',
979
- ];
980
- for (const field of optional) {
981
- if (idx[field] != null && idx[field] !== false) {
982
- out[field] = idx[field];
983
- }
984
- }
985
- return out;
986
- };
987
- const normalizeFk = (fk) => {
988
- const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
989
- // JSON.stringify drops undefined properties — let them through instead of guarding
990
- return {
991
- columnNames: fk.columnNames,
992
- constraintName: fk.constraintName,
993
- localTableName: fk.localTableName,
994
- referencedColumnNames: fk.referencedColumnNames,
995
- referencedTableName: fk.referencedTableName,
996
- updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
997
- deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
998
- deferMode: fk.deferMode,
999
- };
1000
- };
1001
- const normalizeCheck = (check) => {
1002
- const out = { name: check.name };
1003
- if (typeof check.expression === 'string') {
1004
- out.expression = check.expression;
1005
- }
1006
- for (const field of ['definition', 'columnName']) {
1007
- if (check[field]) {
1008
- out[field] = check[field];
1009
- }
1010
- }
1011
- return out;
1012
- };
1013
- const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
1014
- const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
1015
- const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
1016
- .sort(([a], [b]) => byString(a, b))
1017
- .map(([k, v]) => [k, normalizeFk(v)]));
1018
- return {
1019
- name: this.name,
1020
- schema: this.schema,
1021
- columns: columnsMapped,
1022
- indexes: sortedIndexes,
1023
- checks: sortedChecks,
1024
- foreignKeys: sortedForeignKeys,
1025
- // emit `comment` even when unset so introspection (which always reads it) matches metadata
1026
- comment: this.comment ?? null,
1027
- };
948
+ this.#indexes.push({
949
+ keyName: name,
950
+ columnNames: properties,
951
+ composite: properties.length > 1,
952
+ // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
953
+ constraint: type !== 'index' && !properties.some(d => d.includes('.')),
954
+ primary: type === 'primary',
955
+ unique: type !== 'index',
956
+ type: index.type,
957
+ expression: this.processIndexExpression(name, index.expression, meta),
958
+ options: index.options,
959
+ deferMode: index.deferMode,
960
+ columns,
961
+ include: includeColumns,
962
+ fillFactor: index.fillFactor,
963
+ invisible: index.invisible,
964
+ disabled: index.disabled,
965
+ clustered: index.clustered,
966
+ });
967
+ }
968
+ addCheck(check) {
969
+ this.#checks.push(check);
970
+ }
971
+ toJSON() {
972
+ const columns = this.#columns;
973
+ // locale-independent comparison so the snapshot is stable across machines
974
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
975
+ const sortedColumnKeys = Utils.keys(columns).sort(byString);
976
+ // mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
977
+ // so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
978
+ const primaryColumns = new Set();
979
+ const uniqueColumns = new Set();
980
+ for (const idx of this.#indexes) {
981
+ if (idx.primary) {
982
+ idx.columnNames.forEach(c => primaryColumns.add(c));
983
+ }
984
+ if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
985
+ uniqueColumns.add(idx.columnNames[0]);
986
+ }
1028
987
  }
988
+ // integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
989
+ const isFixedPrecisionFamily = mappedType =>
990
+ mappedType instanceof t.integer ||
991
+ mappedType instanceof t.smallint ||
992
+ mappedType instanceof t.tinyint ||
993
+ mappedType instanceof t.mediumint ||
994
+ mappedType instanceof t.bigint ||
995
+ mappedType instanceof t.float ||
996
+ mappedType instanceof t.double;
997
+ const supportsUnsigned = this.#platform.supportsUnsigned();
998
+ const columnsMapped = sortedColumnKeys.reduce((o, col) => {
999
+ const c = columns[col];
1000
+ // omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
1001
+ const rawType = c.type?.toLowerCase();
1002
+ const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
1003
+ const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
1004
+ const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
1005
+ const normalized = {
1006
+ name: c.name,
1007
+ type,
1008
+ unsigned: supportsUnsigned && !!c.unsigned,
1009
+ autoincrement: !!c.autoincrement,
1010
+ primary: primaryColumns.has(c.name) || !!c.primary,
1011
+ nullable: !!c.nullable,
1012
+ unique: uniqueColumns.has(c.name) || !!c.unique,
1013
+ length: c.length || null,
1014
+ precision: fixedPrecision ? null : (c.precision ?? null),
1015
+ scale: fixedPrecision ? null : (c.scale ?? null),
1016
+ default: c.default ?? null,
1017
+ comment: c.comment ?? null,
1018
+ enumItems: c.enumItems ?? [],
1019
+ mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
1020
+ };
1021
+ for (const field of ['generated', 'nativeEnumName', 'extra', 'ignoreSchemaChanges', 'defaultConstraint']) {
1022
+ if (c[field]) {
1023
+ normalized[field] = c[field];
1024
+ }
1025
+ }
1026
+ o[col] = normalized;
1027
+ return o;
1028
+ }, {});
1029
+ const normalizeIndex = idx => {
1030
+ const out = {
1031
+ columnNames: idx.columnNames,
1032
+ composite: !!idx.composite,
1033
+ // PK indexes are always backed by a constraint — force it so postgres introspection matches
1034
+ constraint: !!idx.constraint || !!idx.primary,
1035
+ keyName: idx.keyName,
1036
+ primary: !!idx.primary,
1037
+ unique: !!idx.unique,
1038
+ };
1039
+ const optional = [
1040
+ 'expression',
1041
+ 'type',
1042
+ 'deferMode',
1043
+ 'columns',
1044
+ 'include',
1045
+ 'fillFactor',
1046
+ 'invisible',
1047
+ 'disabled',
1048
+ 'clustered',
1049
+ ];
1050
+ for (const field of optional) {
1051
+ if (idx[field] != null && idx[field] !== false) {
1052
+ out[field] = idx[field];
1053
+ }
1054
+ }
1055
+ return out;
1056
+ };
1057
+ const normalizeFk = fk => {
1058
+ const isNoAction = rule => !rule || rule.toLowerCase() === 'no action';
1059
+ // JSON.stringify drops undefined properties — let them through instead of guarding
1060
+ return {
1061
+ columnNames: fk.columnNames,
1062
+ constraintName: fk.constraintName,
1063
+ localTableName: fk.localTableName,
1064
+ referencedColumnNames: fk.referencedColumnNames,
1065
+ referencedTableName: fk.referencedTableName,
1066
+ updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
1067
+ deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
1068
+ deferMode: fk.deferMode,
1069
+ };
1070
+ };
1071
+ const normalizeCheck = check => {
1072
+ const out = { name: check.name };
1073
+ if (typeof check.expression === 'string') {
1074
+ out.expression = check.expression;
1075
+ }
1076
+ for (const field of ['definition', 'columnName']) {
1077
+ if (check[field]) {
1078
+ out[field] = check[field];
1079
+ }
1080
+ }
1081
+ return out;
1082
+ };
1083
+ const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
1084
+ const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
1085
+ const sortedForeignKeys = Object.fromEntries(
1086
+ Object.entries(this.#foreignKeys)
1087
+ .sort(([a], [b]) => byString(a, b))
1088
+ .map(([k, v]) => [k, normalizeFk(v)]),
1089
+ );
1090
+ return {
1091
+ name: this.name,
1092
+ schema: this.schema,
1093
+ columns: columnsMapped,
1094
+ indexes: sortedIndexes,
1095
+ checks: sortedChecks,
1096
+ foreignKeys: sortedForeignKeys,
1097
+ // emit `comment` even when unset so introspection (which always reads it) matches metadata
1098
+ comment: this.comment ?? null,
1099
+ };
1100
+ }
1029
1101
  }