@mikro-orm/sql 7.1.0-dev.3 → 7.1.0-dev.31
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.
- package/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +2 -2
- package/AbstractSqlDriver.d.ts +19 -1
- package/AbstractSqlDriver.js +215 -16
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.js +13 -2
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
- package/dialects/mysql/MySqlSchemaHelper.js +145 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +9 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +72 -6
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +4 -4
- package/plugin/transformer.d.ts +11 -3
- package/plugin/transformer.js +138 -29
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +36 -0
- package/query/QueryBuilder.js +63 -1
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +182 -31
- package/schema/SchemaComparator.d.ts +10 -0
- package/schema/SchemaComparator.js +104 -1
- package/schema/SchemaHelper.d.ts +63 -1
- package/schema/SchemaHelper.js +235 -6
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +16 -9
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +34 -2
package/query/QueryBuilder.js
CHANGED
|
@@ -721,6 +721,12 @@ export class QueryBuilder {
|
|
|
721
721
|
hasFlag(flag) {
|
|
722
722
|
return this.#state.flags.has(flag);
|
|
723
723
|
}
|
|
724
|
+
/** @internal */
|
|
725
|
+
setPartitionLimit(opts) {
|
|
726
|
+
this.ensureNotFinalized();
|
|
727
|
+
this.#state.partitionLimit = opts;
|
|
728
|
+
return this;
|
|
729
|
+
}
|
|
724
730
|
cache(config = true) {
|
|
725
731
|
this.ensureNotFinalized();
|
|
726
732
|
this.#state.cache = config;
|
|
@@ -825,6 +831,9 @@ export class QueryBuilder {
|
|
|
825
831
|
this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
|
|
826
832
|
}
|
|
827
833
|
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.insertSubQuery ? undefined : this.#state.data, this.#state.returning);
|
|
834
|
+
if (this.#state.partitionLimit) {
|
|
835
|
+
return (this.#query.qb = this.wrapPartitionLimitSubQuery(qb));
|
|
836
|
+
}
|
|
828
837
|
return (this.#query.qb = qb);
|
|
829
838
|
}
|
|
830
839
|
processReturningStatement(qb, meta, data, returning) {
|
|
@@ -1055,9 +1064,10 @@ export class QueryBuilder {
|
|
|
1055
1064
|
options ??= {};
|
|
1056
1065
|
options.mergeResults ??= true;
|
|
1057
1066
|
options.mapResults ??= true;
|
|
1067
|
+
const chunkSize = options.chunkSize ?? 100;
|
|
1058
1068
|
const query = this.toQuery();
|
|
1059
1069
|
const loggerContext = { id: this.em?.id, ...this.loggerContext };
|
|
1060
|
-
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
|
|
1070
|
+
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext, chunkSize);
|
|
1061
1071
|
const meta = this.mainAlias.meta;
|
|
1062
1072
|
if (options.rawResults || !meta) {
|
|
1063
1073
|
yield* res;
|
|
@@ -1385,6 +1395,7 @@ export class QueryBuilder {
|
|
|
1385
1395
|
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
|
|
1386
1396
|
});
|
|
1387
1397
|
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
|
|
1398
|
+
const joinCountBefore = Object.keys(this.#state.joins).length;
|
|
1388
1399
|
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
|
|
1389
1400
|
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
|
|
1390
1401
|
path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
|
|
@@ -1414,6 +1425,20 @@ export class QueryBuilder {
|
|
|
1414
1425
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1415
1426
|
this.#state.joins[aliasedName].path ??= path;
|
|
1416
1427
|
}
|
|
1428
|
+
// auto-joins added by cond processing that depend on the new alias would otherwise produce a
|
|
1429
|
+
// forward reference (the auto-join's ON refers to alias, while alias's ON refers back to it);
|
|
1430
|
+
// fold them into the new join so both aliases share scope in the outer ON clause (issue #7681)
|
|
1431
|
+
const condJoin = this.#state.joins[aliasedName];
|
|
1432
|
+
const joinKeys = Object.keys(this.#state.joins);
|
|
1433
|
+
for (let i = joinCountBefore; i < joinKeys.length; i++) {
|
|
1434
|
+
const j = this.#state.joins[joinKeys[i]];
|
|
1435
|
+
if (j === condJoin || j.ownerAlias !== alias) {
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const nested = (condJoin.nested ??= new Set());
|
|
1439
|
+
j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
1440
|
+
nested.add(j);
|
|
1441
|
+
}
|
|
1417
1442
|
return { prop, key: aliasedName };
|
|
1418
1443
|
}
|
|
1419
1444
|
prepareFields(fields, type = 'where', schema) {
|
|
@@ -1899,6 +1924,9 @@ export class QueryBuilder {
|
|
|
1899
1924
|
(this.#state.limit > 0 || this.#state.offset > 0)) {
|
|
1900
1925
|
this.wrapPaginateSubQuery(meta);
|
|
1901
1926
|
}
|
|
1927
|
+
if (this.#state.partitionLimit) {
|
|
1928
|
+
this.preparePartitionLimit();
|
|
1929
|
+
}
|
|
1902
1930
|
if (meta &&
|
|
1903
1931
|
(this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.#state.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
|
|
1904
1932
|
this.wrapModifySubQuery(meta);
|
|
@@ -2118,6 +2146,40 @@ export class QueryBuilder {
|
|
|
2118
2146
|
[Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
|
|
2119
2147
|
});
|
|
2120
2148
|
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
|
|
2151
|
+
* that filters by the __rn column to apply per-parent limiting.
|
|
2152
|
+
*/
|
|
2153
|
+
wrapPartitionLimitSubQuery(innerQb) {
|
|
2154
|
+
const { limit, offset = 0 } = this.#state.partitionLimit;
|
|
2155
|
+
const rnCol = this.platform.quoteIdentifier('__rn');
|
|
2156
|
+
innerQb.as(this.mainAlias.aliasName);
|
|
2157
|
+
const outerQb = this.platform.createNativeQueryBuilder();
|
|
2158
|
+
outerQb.select('*').from(innerQb);
|
|
2159
|
+
outerQb.where(`${rnCol} > ? and ${rnCol} <= ?`, [offset, offset + limit]);
|
|
2160
|
+
outerQb.orderBy(rnCol);
|
|
2161
|
+
return outerQb;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Adds ROW_NUMBER() OVER (PARTITION BY ...) to the SELECT list and prepares
|
|
2165
|
+
* the query state for per-parent limiting. The actual wrapping into a subquery
|
|
2166
|
+
* with __rn filtering happens in getNativeQuery().
|
|
2167
|
+
*/
|
|
2168
|
+
preparePartitionLimit() {
|
|
2169
|
+
const { partitionBy } = this.#state.partitionLimit;
|
|
2170
|
+
// `partitionBy` is always a declared property name, so mapper returns a string here.
|
|
2171
|
+
const partitionCol = this.helper.mapper(partitionBy, this.type, undefined, null);
|
|
2172
|
+
const quotedPartition = partitionCol
|
|
2173
|
+
.split('.')
|
|
2174
|
+
.map(e => this.platform.quoteIdentifier(e))
|
|
2175
|
+
.join('.');
|
|
2176
|
+
const queryOrder = this.helper.getQueryOrder(this.type, this.#state.orderBy, this.#state.populateMap, this.#state.collation);
|
|
2177
|
+
const orderBySql = queryOrder.length > 0 ? Utils.unique(queryOrder).join(', ') : quotedPartition;
|
|
2178
|
+
const rnAlias = this.platform.quoteIdentifier('__rn');
|
|
2179
|
+
this.#state.fields.push(raw(`row_number() over (partition by ${quotedPartition} order by ${orderBySql}) as ${rnAlias}`));
|
|
2180
|
+
// Moved into the OVER clause; outer query re-applies via wrapPartitionLimitSubQuery
|
|
2181
|
+
this.#state.orderBy = [];
|
|
2182
|
+
}
|
|
2121
2183
|
/**
|
|
2122
2184
|
* Computes the set of populate paths from the _populate hints.
|
|
2123
2185
|
*/
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
2
2
|
import { DatabaseTable } from './DatabaseTable.js';
|
|
3
|
+
import { getTablePartitioning } from './partitioning.js';
|
|
3
4
|
/**
|
|
4
5
|
* @internal
|
|
5
6
|
*/
|
|
@@ -173,6 +174,9 @@ export class DatabaseSchema {
|
|
|
173
174
|
}
|
|
174
175
|
const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
|
|
175
176
|
table.comment = meta.comment;
|
|
177
|
+
if (meta.partitionBy) {
|
|
178
|
+
table.setPartitioning(getTablePartitioning(meta, this.getSchemaName(meta, config, schemaName), id => platform.quoteIdentifier(id)));
|
|
179
|
+
}
|
|
176
180
|
// For TPT child entities, only use ownProps (properties defined in this entity only)
|
|
177
181
|
// For all other entities (including TPT root), use all props
|
|
178
182
|
const propsToProcess = meta.inheritanceType === 'tpt' && meta.tptParent && meta.ownProps ? meta.ownProps : meta.props;
|
|
@@ -220,6 +224,20 @@ export class DatabaseSchema {
|
|
|
220
224
|
columnName,
|
|
221
225
|
});
|
|
222
226
|
}
|
|
227
|
+
for (const trigger of meta.triggers) {
|
|
228
|
+
const body = isRaw(trigger.body)
|
|
229
|
+
? platform.formatQuery(trigger.body.sql, trigger.body.params)
|
|
230
|
+
: trigger.body;
|
|
231
|
+
table.addTrigger({
|
|
232
|
+
name: trigger.name,
|
|
233
|
+
timing: trigger.timing,
|
|
234
|
+
events: trigger.events,
|
|
235
|
+
forEach: trigger.forEach ?? 'row',
|
|
236
|
+
body: body ?? '',
|
|
237
|
+
when: trigger.when,
|
|
238
|
+
expression: trigger.expression,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
223
241
|
}
|
|
224
242
|
return schema;
|
|
225
243
|
}
|
|
@@ -323,12 +341,16 @@ export class DatabaseSchema {
|
|
|
323
341
|
(prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner));
|
|
324
342
|
}
|
|
325
343
|
toJSON() {
|
|
344
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
345
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
346
|
+
const tableKey = (t) => `${t.schema ?? ''}.${t.name}`;
|
|
347
|
+
const byTable = (a, b) => byString(tableKey(a), tableKey(b));
|
|
326
348
|
return {
|
|
327
349
|
name: this.name,
|
|
328
|
-
namespaces: [...this.#namespaces],
|
|
329
|
-
tables: this.#tables,
|
|
330
|
-
views: this.#views,
|
|
331
|
-
nativeEnums: this.#nativeEnums,
|
|
350
|
+
namespaces: [...this.#namespaces].sort(),
|
|
351
|
+
tables: [...this.#tables].sort(byTable),
|
|
352
|
+
views: [...this.#views].sort(byTable),
|
|
353
|
+
nativeEnums: Object.fromEntries(Object.entries(this.#nativeEnums).sort(([a], [b]) => byString(a, b))),
|
|
332
354
|
};
|
|
333
355
|
}
|
|
334
356
|
prune(schema, wildcardSchemaTables) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core';
|
|
2
2
|
import type { SchemaHelper } from './SchemaHelper.js';
|
|
3
|
-
import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js';
|
|
3
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, TablePartitioning, SqlTriggerDef } from '../typings.js';
|
|
4
4
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
@@ -15,6 +15,14 @@ export declare class DatabaseTable {
|
|
|
15
15
|
items: string[];
|
|
16
16
|
}>;
|
|
17
17
|
comment?: string;
|
|
18
|
+
partitioning?: TablePartitioning;
|
|
19
|
+
/**
|
|
20
|
+
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
21
|
+
* For MySQL/MariaDB this is the table collation; for PostgreSQL and MSSQL this is the database default;
|
|
22
|
+
* SQLite has no configurable default. Used by `SchemaComparator.diffCollation` to avoid flapping
|
|
23
|
+
* when a property explicitly names the default collation.
|
|
24
|
+
*/
|
|
25
|
+
collation?: string;
|
|
18
26
|
constructor(platform: AbstractSqlPlatform, name: string, schema?: string | undefined);
|
|
19
27
|
getQuotedName(): string;
|
|
20
28
|
getColumns(): Column[];
|
|
@@ -22,11 +30,17 @@ export declare class DatabaseTable {
|
|
|
22
30
|
removeColumn(name: string): void;
|
|
23
31
|
getIndexes(): IndexDef[];
|
|
24
32
|
getChecks(): CheckDef[];
|
|
33
|
+
getPartitioning(): TablePartitioning | undefined;
|
|
34
|
+
/** @internal */
|
|
35
|
+
setPartitioning(partitioning?: TablePartitioning): void;
|
|
36
|
+
getTriggers(): SqlTriggerDef[];
|
|
25
37
|
/** @internal */
|
|
26
38
|
setIndexes(indexes: IndexDef[]): void;
|
|
27
39
|
/** @internal */
|
|
28
40
|
setChecks(checks: CheckDef[]): void;
|
|
29
41
|
/** @internal */
|
|
42
|
+
setTriggers(triggers: SqlTriggerDef[]): void;
|
|
43
|
+
/** @internal */
|
|
30
44
|
setForeignKeys(fks: Dictionary<ForeignKey>): void;
|
|
31
45
|
init(cols: Column[], indexes: IndexDef[] | undefined, checks: CheckDef[] | undefined, pks: string[], fks?: Dictionary<ForeignKey>, enums?: Dictionary<string[]>): void;
|
|
32
46
|
addColumn(column: Column): void;
|
|
@@ -47,6 +61,8 @@ export declare class DatabaseTable {
|
|
|
47
61
|
hasIndex(indexName: string): boolean;
|
|
48
62
|
getCheck(checkName: string): CheckDef | undefined;
|
|
49
63
|
hasCheck(checkName: string): boolean;
|
|
64
|
+
getTrigger(triggerName: string): SqlTriggerDef | undefined;
|
|
65
|
+
hasTrigger(triggerName: string): boolean;
|
|
50
66
|
getPrimaryKey(): IndexDef | undefined;
|
|
51
67
|
hasPrimaryKey(): boolean;
|
|
52
68
|
private getForeignKeyDeclaration;
|
|
@@ -62,6 +78,7 @@ export declare class DatabaseTable {
|
|
|
62
78
|
name?: string;
|
|
63
79
|
type?: string;
|
|
64
80
|
expression?: string | IndexCallback<any>;
|
|
81
|
+
where?: string | Dictionary;
|
|
65
82
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
66
83
|
options?: Dictionary;
|
|
67
84
|
columns?: {
|
|
@@ -77,6 +94,8 @@ export declare class DatabaseTable {
|
|
|
77
94
|
disabled?: boolean;
|
|
78
95
|
clustered?: boolean;
|
|
79
96
|
}, type: 'index' | 'unique' | 'primary'): void;
|
|
97
|
+
private processIndexWhere;
|
|
80
98
|
addCheck(check: CheckDef): void;
|
|
99
|
+
addTrigger(trigger: SqlTriggerDef): void;
|
|
81
100
|
toJSON(): Dictionary;
|
|
82
101
|
}
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DecimalType, EntitySchema, isRaw, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core';
|
|
2
|
+
import { toEntityPartitionBy } from './partitioning.js';
|
|
2
3
|
/**
|
|
3
4
|
* @internal
|
|
4
5
|
*/
|
|
@@ -8,10 +9,19 @@ export class DatabaseTable {
|
|
|
8
9
|
#columns = {};
|
|
9
10
|
#indexes = [];
|
|
10
11
|
#checks = [];
|
|
12
|
+
#triggers = [];
|
|
11
13
|
#foreignKeys = {};
|
|
12
14
|
#platform;
|
|
13
15
|
nativeEnums = {}; // for postgres
|
|
14
16
|
comment;
|
|
17
|
+
partitioning;
|
|
18
|
+
/**
|
|
19
|
+
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
20
|
+
* For MySQL/MariaDB this is the table collation; for PostgreSQL and MSSQL this is the database default;
|
|
21
|
+
* SQLite has no configurable default. Used by `SchemaComparator.diffCollation` to avoid flapping
|
|
22
|
+
* when a property explicitly names the default collation.
|
|
23
|
+
*/
|
|
24
|
+
collation;
|
|
15
25
|
constructor(platform, name, schema) {
|
|
16
26
|
this.name = name;
|
|
17
27
|
this.schema = schema;
|
|
@@ -35,6 +45,16 @@ export class DatabaseTable {
|
|
|
35
45
|
getChecks() {
|
|
36
46
|
return this.#checks;
|
|
37
47
|
}
|
|
48
|
+
getPartitioning() {
|
|
49
|
+
return this.partitioning;
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
setPartitioning(partitioning) {
|
|
53
|
+
this.partitioning = partitioning;
|
|
54
|
+
}
|
|
55
|
+
getTriggers() {
|
|
56
|
+
return this.#triggers;
|
|
57
|
+
}
|
|
38
58
|
/** @internal */
|
|
39
59
|
setIndexes(indexes) {
|
|
40
60
|
this.#indexes = indexes;
|
|
@@ -44,6 +64,10 @@ export class DatabaseTable {
|
|
|
44
64
|
this.#checks = checks;
|
|
45
65
|
}
|
|
46
66
|
/** @internal */
|
|
67
|
+
setTriggers(triggers) {
|
|
68
|
+
this.#triggers = triggers;
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
47
71
|
setForeignKeys(fks) {
|
|
48
72
|
this.#foreignKeys = fks;
|
|
49
73
|
}
|
|
@@ -51,6 +75,7 @@ export class DatabaseTable {
|
|
|
51
75
|
this.#indexes = indexes;
|
|
52
76
|
this.#checks = checks;
|
|
53
77
|
this.#foreignKeys = fks;
|
|
78
|
+
const helper = this.#platform.getSchemaHelper();
|
|
54
79
|
this.#columns = cols.reduce((o, v) => {
|
|
55
80
|
const index = indexes.filter(i => i.columnNames[0] === v.name);
|
|
56
81
|
v.primary = v.primary || pks.includes(v.name);
|
|
@@ -59,6 +84,11 @@ export class DatabaseTable {
|
|
|
59
84
|
v.mappedType = this.#platform.getMappedType(type);
|
|
60
85
|
v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
|
|
61
86
|
v.enumItems ??= enums[v.name] || [];
|
|
87
|
+
// recover length from the declared type so introspection matches `addColumnFromProperty`;
|
|
88
|
+
// scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
|
|
89
|
+
if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
|
|
90
|
+
v.length = helper.inferLengthFromColumnType(v.type);
|
|
91
|
+
}
|
|
62
92
|
o[v.name] = v;
|
|
63
93
|
return o;
|
|
64
94
|
}, {});
|
|
@@ -68,7 +98,9 @@ export class DatabaseTable {
|
|
|
68
98
|
}
|
|
69
99
|
addColumnFromProperty(prop, meta, config) {
|
|
70
100
|
prop.fieldNames?.forEach((field, idx) => {
|
|
71
|
-
|
|
101
|
+
// numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
|
|
102
|
+
const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
|
|
103
|
+
const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
|
|
72
104
|
const mappedType = this.#platform.getMappedType(type);
|
|
73
105
|
if (mappedType instanceof DecimalType) {
|
|
74
106
|
const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
|
|
@@ -105,6 +137,7 @@ export class DatabaseTable {
|
|
|
105
137
|
default: prop.defaultRaw,
|
|
106
138
|
enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
|
|
107
139
|
comment: prop.comment,
|
|
140
|
+
collation: prop.collation,
|
|
108
141
|
extra: prop.extra,
|
|
109
142
|
ignoreSchemaChanges: prop.ignoreSchemaChanges,
|
|
110
143
|
};
|
|
@@ -177,6 +210,7 @@ export class DatabaseTable {
|
|
|
177
210
|
const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
|
|
178
211
|
const name = namingStrategy.getEntityName(this.name, this.schema);
|
|
179
212
|
const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
|
|
213
|
+
schema.meta.partitionBy = toEntityPartitionBy(this.partitioning, this.name, this.schema);
|
|
180
214
|
const compositeFkIndexes = {};
|
|
181
215
|
const compositeFkUniques = {};
|
|
182
216
|
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.
|
|
@@ -196,6 +230,7 @@ export class DatabaseTable {
|
|
|
196
230
|
name: index.keyName,
|
|
197
231
|
deferMode: index.deferMode,
|
|
198
232
|
expression: index.expression,
|
|
233
|
+
where: index.where,
|
|
199
234
|
// Advanced index options - convert column names to property names
|
|
200
235
|
columns: index.columns?.map(col => ({
|
|
201
236
|
...col,
|
|
@@ -226,7 +261,7 @@ export class DatabaseTable {
|
|
|
226
261
|
index.invisible ||
|
|
227
262
|
index.disabled ||
|
|
228
263
|
index.clustered;
|
|
229
|
-
const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
|
|
264
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
|
|
230
265
|
if (isTrivial) {
|
|
231
266
|
// Index is for FK. Map to the FK prop and move on.
|
|
232
267
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -598,6 +633,12 @@ export class DatabaseTable {
|
|
|
598
633
|
hasCheck(checkName) {
|
|
599
634
|
return !!this.getCheck(checkName);
|
|
600
635
|
}
|
|
636
|
+
getTrigger(triggerName) {
|
|
637
|
+
return this.#triggers.find(t => t.name === triggerName);
|
|
638
|
+
}
|
|
639
|
+
hasTrigger(triggerName) {
|
|
640
|
+
return !!this.getTrigger(triggerName);
|
|
641
|
+
}
|
|
601
642
|
getPrimaryKey() {
|
|
602
643
|
return this.#indexes.find(i => i.primary);
|
|
603
644
|
}
|
|
@@ -632,6 +673,7 @@ export class DatabaseTable {
|
|
|
632
673
|
columnOptions.scale = column.scale;
|
|
633
674
|
columnOptions.extra = column.extra;
|
|
634
675
|
columnOptions.comment = column.comment;
|
|
676
|
+
columnOptions.collation = column.collation;
|
|
635
677
|
columnOptions.enum = !!column.enumItems?.length;
|
|
636
678
|
columnOptions.items = column.enumItems;
|
|
637
679
|
}
|
|
@@ -698,6 +740,7 @@ export class DatabaseTable {
|
|
|
698
740
|
scale: column.scale,
|
|
699
741
|
extra: column.extra,
|
|
700
742
|
comment: column.comment,
|
|
743
|
+
collation: column.collation,
|
|
701
744
|
index: index ? index.keyName : undefined,
|
|
702
745
|
unique: unique ? unique.keyName : undefined,
|
|
703
746
|
enum: !!column.enumItems?.length,
|
|
@@ -862,16 +905,25 @@ export class DatabaseTable {
|
|
|
862
905
|
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
863
906
|
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
|
|
864
907
|
}
|
|
908
|
+
// The `expression` escape hatch takes the full index definition as raw SQL; combining it
|
|
909
|
+
// with `where` is dialect-dependent (PG/Oracle/MySQL drop `where`, MSSQL appends it) so we
|
|
910
|
+
// reject the combination up-front and ask users to inline the predicate into `expression`.
|
|
911
|
+
if (index.expression && index.where != null) {
|
|
912
|
+
throw new Error(`Index '${name}' on entity '${meta.className}': cannot combine \`expression\` with \`where\` — inline the WHERE clause into the \`expression\` escape hatch, or drop \`expression\` and use structured \`properties\` + \`where\`.`);
|
|
913
|
+
}
|
|
914
|
+
const where = this.processIndexWhere(index.where, meta);
|
|
865
915
|
this.#indexes.push({
|
|
866
916
|
keyName: name,
|
|
867
917
|
columnNames: properties,
|
|
868
918
|
composite: properties.length > 1,
|
|
869
|
-
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
870
|
-
|
|
919
|
+
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them.
|
|
920
|
+
// Partial indexes (`where`) must use CREATE [UNIQUE] INDEX form — constraints can't carry predicates.
|
|
921
|
+
constraint: type !== 'index' && !properties.some((d) => d.includes('.')) && !where,
|
|
871
922
|
primary: type === 'primary',
|
|
872
923
|
unique: type !== 'index',
|
|
873
924
|
type: index.type,
|
|
874
925
|
expression: this.processIndexExpression(name, index.expression, meta),
|
|
926
|
+
where,
|
|
875
927
|
options: index.options,
|
|
876
928
|
deferMode: index.deferMode,
|
|
877
929
|
columns,
|
|
@@ -882,56 +934,155 @@ export class DatabaseTable {
|
|
|
882
934
|
clustered: index.clustered,
|
|
883
935
|
});
|
|
884
936
|
}
|
|
937
|
+
processIndexWhere(where, meta) {
|
|
938
|
+
if (where == null) {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
// The driver is always an `AbstractSqlDriver` here — `DatabaseTable` is only instantiated
|
|
942
|
+
// by SQL-side schema code, so the platform's config-bound driver is guaranteed to be one.
|
|
943
|
+
const driver = this.#platform.getConfig().getDriver();
|
|
944
|
+
return driver.renderPartialIndexWhere(meta.class, where);
|
|
945
|
+
}
|
|
885
946
|
addCheck(check) {
|
|
886
947
|
this.#checks.push(check);
|
|
887
948
|
}
|
|
949
|
+
addTrigger(trigger) {
|
|
950
|
+
this.#triggers.push(trigger);
|
|
951
|
+
}
|
|
888
952
|
toJSON() {
|
|
889
953
|
const columns = this.#columns;
|
|
890
|
-
|
|
954
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
955
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
956
|
+
const sortedColumnKeys = Utils.keys(columns).sort(byString);
|
|
957
|
+
// mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
|
|
958
|
+
// so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
|
|
959
|
+
const primaryColumns = new Set();
|
|
960
|
+
const uniqueColumns = new Set();
|
|
961
|
+
for (const idx of this.#indexes) {
|
|
962
|
+
if (idx.primary) {
|
|
963
|
+
idx.columnNames.forEach(c => primaryColumns.add(c));
|
|
964
|
+
}
|
|
965
|
+
if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
|
|
966
|
+
uniqueColumns.add(idx.columnNames[0]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
|
|
970
|
+
const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
|
|
971
|
+
mappedType instanceof t.smallint ||
|
|
972
|
+
mappedType instanceof t.tinyint ||
|
|
973
|
+
mappedType instanceof t.mediumint ||
|
|
974
|
+
mappedType instanceof t.bigint ||
|
|
975
|
+
mappedType instanceof t.float ||
|
|
976
|
+
mappedType instanceof t.double;
|
|
977
|
+
const supportsUnsigned = this.#platform.supportsUnsigned();
|
|
978
|
+
const columnsMapped = sortedColumnKeys.reduce((o, col) => {
|
|
891
979
|
const c = columns[col];
|
|
980
|
+
// omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
|
|
981
|
+
const rawType = c.type?.toLowerCase();
|
|
982
|
+
const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
|
|
983
|
+
const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
|
|
984
|
+
const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
|
|
892
985
|
const normalized = {
|
|
893
986
|
name: c.name,
|
|
894
|
-
type
|
|
895
|
-
unsigned: !!c.unsigned,
|
|
987
|
+
type,
|
|
988
|
+
unsigned: supportsUnsigned && !!c.unsigned,
|
|
896
989
|
autoincrement: !!c.autoincrement,
|
|
897
|
-
primary: !!c.primary,
|
|
990
|
+
primary: primaryColumns.has(c.name) || !!c.primary,
|
|
898
991
|
nullable: !!c.nullable,
|
|
899
|
-
unique: !!c.unique,
|
|
900
|
-
length: c.length
|
|
901
|
-
precision: c.precision ?? null,
|
|
902
|
-
scale: c.scale ?? null,
|
|
992
|
+
unique: uniqueColumns.has(c.name) || !!c.unique,
|
|
993
|
+
length: c.length || null,
|
|
994
|
+
precision: fixedPrecision ? null : (c.precision ?? null),
|
|
995
|
+
scale: fixedPrecision ? null : (c.scale ?? null),
|
|
903
996
|
default: c.default ?? null,
|
|
904
997
|
comment: c.comment ?? null,
|
|
998
|
+
collation: c.collation ?? null,
|
|
905
999
|
enumItems: c.enumItems ?? [],
|
|
906
1000
|
mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
|
|
907
1001
|
};
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
|
|
919
|
-
}
|
|
920
|
-
if (c.defaultConstraint) {
|
|
921
|
-
normalized.defaultConstraint = c.defaultConstraint;
|
|
1002
|
+
for (const field of [
|
|
1003
|
+
'generated',
|
|
1004
|
+
'nativeEnumName',
|
|
1005
|
+
'extra',
|
|
1006
|
+
'ignoreSchemaChanges',
|
|
1007
|
+
'defaultConstraint',
|
|
1008
|
+
]) {
|
|
1009
|
+
if (c[field]) {
|
|
1010
|
+
normalized[field] = c[field];
|
|
1011
|
+
}
|
|
922
1012
|
}
|
|
923
1013
|
o[col] = normalized;
|
|
924
1014
|
return o;
|
|
925
1015
|
}, {});
|
|
1016
|
+
const normalizeIndex = (idx) => {
|
|
1017
|
+
const out = {
|
|
1018
|
+
columnNames: idx.columnNames,
|
|
1019
|
+
composite: !!idx.composite,
|
|
1020
|
+
// PK indexes are always backed by a constraint — force it so postgres introspection matches
|
|
1021
|
+
constraint: !!idx.constraint || !!idx.primary,
|
|
1022
|
+
keyName: idx.keyName,
|
|
1023
|
+
primary: !!idx.primary,
|
|
1024
|
+
unique: !!idx.unique,
|
|
1025
|
+
};
|
|
1026
|
+
const optional = [
|
|
1027
|
+
'expression',
|
|
1028
|
+
'type',
|
|
1029
|
+
'deferMode',
|
|
1030
|
+
'columns',
|
|
1031
|
+
'include',
|
|
1032
|
+
'fillFactor',
|
|
1033
|
+
'invisible',
|
|
1034
|
+
'disabled',
|
|
1035
|
+
'clustered',
|
|
1036
|
+
];
|
|
1037
|
+
for (const field of optional) {
|
|
1038
|
+
if (idx[field] != null && idx[field] !== false) {
|
|
1039
|
+
out[field] = idx[field];
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
};
|
|
1044
|
+
const normalizeFk = (fk) => {
|
|
1045
|
+
const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
|
|
1046
|
+
// JSON.stringify drops undefined properties — let them through instead of guarding
|
|
1047
|
+
return {
|
|
1048
|
+
columnNames: fk.columnNames,
|
|
1049
|
+
constraintName: fk.constraintName,
|
|
1050
|
+
localTableName: fk.localTableName,
|
|
1051
|
+
referencedColumnNames: fk.referencedColumnNames,
|
|
1052
|
+
referencedTableName: fk.referencedTableName,
|
|
1053
|
+
updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
|
|
1054
|
+
deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
|
|
1055
|
+
deferMode: fk.deferMode,
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
const normalizeCheck = (check) => {
|
|
1059
|
+
const out = { name: check.name };
|
|
1060
|
+
if (typeof check.expression === 'string') {
|
|
1061
|
+
out.expression = check.expression;
|
|
1062
|
+
}
|
|
1063
|
+
for (const field of ['definition', 'columnName']) {
|
|
1064
|
+
if (check[field]) {
|
|
1065
|
+
out[field] = check[field];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
};
|
|
1070
|
+
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1071
|
+
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1072
|
+
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1073
|
+
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1074
|
+
.sort(([a], [b]) => byString(a, b))
|
|
1075
|
+
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
926
1076
|
return {
|
|
927
1077
|
name: this.name,
|
|
928
1078
|
schema: this.schema,
|
|
929
1079
|
columns: columnsMapped,
|
|
930
|
-
indexes:
|
|
931
|
-
checks:
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
comment
|
|
1080
|
+
indexes: sortedIndexes,
|
|
1081
|
+
checks: sortedChecks,
|
|
1082
|
+
triggers: sortedTriggers,
|
|
1083
|
+
foreignKeys: sortedForeignKeys,
|
|
1084
|
+
// emit `comment` even when unset so introspection (which always reads it) matches metadata
|
|
1085
|
+
comment: this.comment ?? null,
|
|
935
1086
|
};
|
|
936
1087
|
}
|
|
937
1088
|
}
|
|
@@ -39,6 +39,15 @@ export declare class SchemaComparator {
|
|
|
39
39
|
diffColumn(fromColumn: Column, toColumn: Column, fromTable: DatabaseTable, logging?: boolean): Set<string>;
|
|
40
40
|
diffEnumItems(items1?: string[], items2?: string[]): boolean;
|
|
41
41
|
diffComment(comment1?: string, comment2?: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
|
|
44
|
+
* clause naming the table/database default is just verbose syntax for inheriting that default,
|
|
45
|
+
* so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
|
|
46
|
+
* compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
|
|
47
|
+
* treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
|
|
48
|
+
* `pg_collation.collname` is case-sensitive and is compared verbatim.
|
|
49
|
+
*/
|
|
50
|
+
diffCollation(fromCollation?: string, toCollation?: string, tableDefault?: string): boolean;
|
|
42
51
|
/**
|
|
43
52
|
* Finds the difference between the indexes index1 and index2.
|
|
44
53
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
@@ -69,6 +78,7 @@ export declare class SchemaComparator {
|
|
|
69
78
|
* @see https://github.com/mikro-orm/mikro-orm/issues/7308
|
|
70
79
|
*/
|
|
71
80
|
private diffViewExpression;
|
|
81
|
+
private diffTrigger;
|
|
72
82
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
73
83
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
74
84
|
private mapColumnToProperty;
|