@mikro-orm/core 7.1.0-dev.14 → 7.1.0-dev.16
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/entity/defineEntity.d.ts +2 -0
- package/metadata/MetadataDiscovery.js +6 -0
- package/metadata/MetadataValidator.d.ts +24 -0
- package/metadata/MetadataValidator.js +192 -0
- package/metadata/types.d.ts +61 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +2 -0
- package/platforms/Platform.js +8 -1
- package/typings.d.ts +7 -1
- package/typings.js +2 -2
- package/utils/Utils.js +1 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/partition-utils.d.ts +17 -0
- package/utils/partition-utils.js +79 -0
package/entity/defineEntity.d.ts
CHANGED
|
@@ -644,6 +644,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
644
644
|
type?: string;
|
|
645
645
|
options?: Dictionary;
|
|
646
646
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
647
|
+
where?: string | FilterQuery<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
647
648
|
columns?: IndexColumnOptions[];
|
|
648
649
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
649
650
|
fillFactor?: number;
|
|
@@ -656,6 +657,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
656
657
|
name?: string;
|
|
657
658
|
options?: Dictionary;
|
|
658
659
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
660
|
+
where?: string | FilterQuery<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
659
661
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
660
662
|
columns?: IndexColumnOptions[];
|
|
661
663
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
@@ -568,6 +568,12 @@ export class MetadataDiscovery {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
|
|
571
|
+
// Fail fast when the platform rejects the metadata, so users see the platform-specific
|
|
572
|
+
// error (e.g. "SqlitePlatform does not support partitioned tables") instead of a
|
|
573
|
+
// downstream core validator message that assumes partitioning is supported.
|
|
574
|
+
if (meta.partitionBy && !this.#platform.supportsPartitionedTables()) {
|
|
575
|
+
this.#platform.validateMetadata(meta);
|
|
576
|
+
}
|
|
571
577
|
this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
|
|
572
578
|
for (const prop of Object.values(meta.properties)) {
|
|
573
579
|
this.initNullability(prop);
|
|
@@ -7,6 +7,30 @@ import type { MetadataStorage } from './MetadataStorage.js';
|
|
|
7
7
|
export declare class MetadataValidator {
|
|
8
8
|
validateEntityDefinition<T>(metadata: MetadataStorage, name: EntityName<T>, options: MetadataDiscoveryOptions): void;
|
|
9
9
|
validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
|
|
10
|
+
private validatePartitioning;
|
|
11
|
+
/**
|
|
12
|
+
* Find the first partition name whose normalized form (case-folded for unquoted segments,
|
|
13
|
+
* quoted segments preserved) has already been seen. Returns the offending name in its
|
|
14
|
+
* original form for the error message.
|
|
15
|
+
*/
|
|
16
|
+
private findDuplicatePartitionName;
|
|
17
|
+
/**
|
|
18
|
+
* Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
|
|
19
|
+
* identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
|
|
20
|
+
*/
|
|
21
|
+
private hasValidPartitionName;
|
|
22
|
+
private hasPartitionExpression;
|
|
23
|
+
private validatePartitionKeyConstraints;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the list of physical field names that a partition expression references, or
|
|
26
|
+
* `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
|
|
27
|
+
* that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
|
|
28
|
+
* unique-constraint coverage checks — users are trusted to ensure the referenced columns are
|
|
29
|
+
* part of the partition key, since PostgreSQL will surface the violation at DDL execution.
|
|
30
|
+
*/
|
|
31
|
+
private getPartitionKeyFields;
|
|
32
|
+
private resolvePartitionKeyField;
|
|
33
|
+
private getConstraintFields;
|
|
10
34
|
private validateReference;
|
|
11
35
|
private validateTargetKey;
|
|
12
36
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { normalizePartitionNameForComparison, splitCommaSeparatedIdentifiers } from '../utils/partition-utils.js';
|
|
2
3
|
import { MetadataError } from '../errors.js';
|
|
3
4
|
import { ReferenceKind } from '../enums.js';
|
|
4
5
|
/**
|
|
@@ -28,6 +29,9 @@ export class MetadataValidator {
|
|
|
28
29
|
// Virtual entities (expression without view flag) have restrictions - no PKs, limited relation types
|
|
29
30
|
// Note: meta.virtual is set later in sync(), so we check for expression && !view here
|
|
30
31
|
if (meta.virtual || (meta.expression && !meta.view)) {
|
|
32
|
+
if (meta.partitionBy) {
|
|
33
|
+
throw new MetadataError(`Virtual entity ${meta.className} cannot define partitionBy`);
|
|
34
|
+
}
|
|
31
35
|
for (const prop of Utils.values(meta.properties)) {
|
|
32
36
|
if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED, ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
33
37
|
throw new MetadataError(`Only scalars, embedded properties and to-many relations are allowed inside virtual entity. Found '${prop.kind}' in ${meta.className}.${prop.name}`);
|
|
@@ -46,6 +50,7 @@ export class MetadataValidator {
|
|
|
46
50
|
this.validateDuplicateFieldNames(meta, options);
|
|
47
51
|
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
48
52
|
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
|
53
|
+
this.validatePartitioning(meta);
|
|
49
54
|
this.validatePropertyNames(meta);
|
|
50
55
|
for (const prop of Utils.values(meta.properties)) {
|
|
51
56
|
if (prop.kind !== ReferenceKind.SCALAR) {
|
|
@@ -111,6 +116,189 @@ export class MetadataValidator {
|
|
|
111
116
|
}
|
|
112
117
|
});
|
|
113
118
|
}
|
|
119
|
+
validatePartitioning(meta) {
|
|
120
|
+
if (!meta.partitionBy) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!this.hasPartitionExpression(meta.partitionBy.expression)) {
|
|
124
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: missing expression`);
|
|
125
|
+
}
|
|
126
|
+
// Inheritance (STI/TPT) and partitioning both drive table layout, so combining them would
|
|
127
|
+
// require non-trivial DDL coordination that the schema generator does not produce today.
|
|
128
|
+
const hasInheritance = !!meta.root.discriminatorColumn || meta.root.inheritanceType === 'tpt' || meta.root.inheritance === 'tpt';
|
|
129
|
+
if (hasInheritance) {
|
|
130
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: combining partitioning with inheritance is not supported`);
|
|
131
|
+
}
|
|
132
|
+
this.validatePartitionKeyConstraints(meta);
|
|
133
|
+
if (meta.partitionBy.type === 'hash') {
|
|
134
|
+
const { partitions } = meta.partitionBy;
|
|
135
|
+
if (Array.isArray(partitions)) {
|
|
136
|
+
if (partitions.length === 0) {
|
|
137
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition name list must not be empty`);
|
|
138
|
+
}
|
|
139
|
+
const blank = partitions.find(name => typeof name !== 'string' || !name.trim());
|
|
140
|
+
if (blank !== undefined) {
|
|
141
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition names must be non-empty strings`);
|
|
142
|
+
}
|
|
143
|
+
const ambiguous = partitions.find(name => !this.hasValidPartitionName(name));
|
|
144
|
+
if (ambiguous) {
|
|
145
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguous}' contains more than one '.' — use at most one '.' to separate schema from table`);
|
|
146
|
+
}
|
|
147
|
+
const duplicate = this.findDuplicatePartitionName(partitions);
|
|
148
|
+
if (duplicate !== undefined) {
|
|
149
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate hash partition name '${duplicate}'`);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (typeof partitions !== 'number' || !Number.isInteger(partitions) || partitions < 1) {
|
|
154
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition count must be a positive integer`);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(meta.partitionBy.partitions) || meta.partitionBy.partitions.length === 0) {
|
|
159
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: list/range partitions must be a non-empty array`);
|
|
160
|
+
}
|
|
161
|
+
if (meta.partitionBy.partitions.some(partition => !partition.values?.trim())) {
|
|
162
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: every partition must define values`);
|
|
163
|
+
}
|
|
164
|
+
const ambiguousName = meta.partitionBy.partitions.find(partition => partition.name != null && !this.hasValidPartitionName(partition.name));
|
|
165
|
+
if (ambiguousName) {
|
|
166
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguousName.name}' contains more than one '.' — use at most one '.' to separate schema from table`);
|
|
167
|
+
}
|
|
168
|
+
// Include auto-generated default names (`${tableName}_${index}`, matching
|
|
169
|
+
// `createExplicitPartitions` in the sql package) so an explicit name that collides with
|
|
170
|
+
// an unnamed peer's default is caught here rather than at DDL execution time.
|
|
171
|
+
const resolvedNames = meta.partitionBy.partitions.map((partition, index) => partition.name ?? `${meta.tableName}_${index}`);
|
|
172
|
+
const duplicate = this.findDuplicatePartitionName(resolvedNames);
|
|
173
|
+
if (duplicate !== undefined) {
|
|
174
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate partition name '${duplicate}'`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Find the first partition name whose normalized form (case-folded for unquoted segments,
|
|
179
|
+
* quoted segments preserved) has already been seen. Returns the offending name in its
|
|
180
|
+
* original form for the error message.
|
|
181
|
+
*/
|
|
182
|
+
findDuplicatePartitionName(names) {
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const normalized = normalizePartitionNameForComparison(name);
|
|
186
|
+
if (seen.has(normalized)) {
|
|
187
|
+
return name;
|
|
188
|
+
}
|
|
189
|
+
seen.add(normalized);
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
|
|
195
|
+
* identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
|
|
196
|
+
*/
|
|
197
|
+
hasValidPartitionName(name) {
|
|
198
|
+
let depth = 0;
|
|
199
|
+
let dots = 0;
|
|
200
|
+
for (let i = 0; i < name.length; i++) {
|
|
201
|
+
const ch = name[i];
|
|
202
|
+
if (ch === '"') {
|
|
203
|
+
if (name[i + 1] === '"') {
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
depth = depth === 0 ? 1 : 0;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ch === '.' && depth === 0) {
|
|
211
|
+
dots++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return dots <= 1;
|
|
215
|
+
}
|
|
216
|
+
hasPartitionExpression(expression) {
|
|
217
|
+
if (expression == null) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (typeof expression === 'function') {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(expression)) {
|
|
224
|
+
return expression.length > 0 && expression.every(key => typeof key === 'string' && key.trim().length > 0);
|
|
225
|
+
}
|
|
226
|
+
return String(expression).trim().length > 0;
|
|
227
|
+
}
|
|
228
|
+
validatePartitionKeyConstraints(meta) {
|
|
229
|
+
const partitionFields = this.getPartitionKeyFields(meta);
|
|
230
|
+
if (!partitionFields?.length) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const primaryKeyFields = meta.root.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
234
|
+
if (partitionFields.some(field => !primaryKeyFields.includes(field))) {
|
|
235
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: primary key must include partition key columns '${partitionFields.join("', '")}'`);
|
|
236
|
+
}
|
|
237
|
+
for (const prop of Object.values(meta.root.properties)) {
|
|
238
|
+
if (!prop.unique || !prop.fieldNames?.length) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (partitionFields.some(field => !prop.fieldNames.includes(field))) {
|
|
242
|
+
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: unique property ${meta.root.className}.${prop.name} must include partition key columns '${partitionFields.join("', '")}'`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const unique of meta.root.uniques ?? []) {
|
|
246
|
+
const fields = this.getConstraintFields(meta.root, unique.properties);
|
|
247
|
+
if (!fields?.length) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (partitionFields.some(field => !fields.includes(field))) {
|
|
251
|
+
const constraint = unique.name ? `unique constraint '${unique.name}'` : 'unique constraint';
|
|
252
|
+
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: ${constraint} must include partition key columns '${partitionFields.join("', '")}'`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Returns the list of physical field names that a partition expression references, or
|
|
258
|
+
* `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
|
|
259
|
+
* that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
|
|
260
|
+
* unique-constraint coverage checks — users are trusted to ensure the referenced columns are
|
|
261
|
+
* part of the partition key, since PostgreSQL will surface the violation at DDL execution.
|
|
262
|
+
*/
|
|
263
|
+
getPartitionKeyFields(meta) {
|
|
264
|
+
const expression = meta.partitionBy?.expression;
|
|
265
|
+
if (!expression || typeof expression === 'function') {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (Array.isArray(expression)) {
|
|
269
|
+
return expression.map(key => this.resolvePartitionKeyField(meta, key));
|
|
270
|
+
}
|
|
271
|
+
const keys = splitCommaSeparatedIdentifiers(String(expression).trim());
|
|
272
|
+
if (!keys) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return keys.map(key => this.resolvePartitionKeyField(meta, key));
|
|
276
|
+
}
|
|
277
|
+
resolvePartitionKeyField(meta, key) {
|
|
278
|
+
const trimmed = key.trim().replaceAll('"', '');
|
|
279
|
+
if (!trimmed) {
|
|
280
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: empty partition key`);
|
|
281
|
+
}
|
|
282
|
+
const prop = meta.root.properties[trimmed] ??
|
|
283
|
+
Object.values(meta.root.properties).find(candidate => candidate.fieldNames?.length === 1 && candidate.fieldNames[0] === trimmed);
|
|
284
|
+
if (!prop) {
|
|
285
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: unknown partition key '${key.trim()}'`);
|
|
286
|
+
}
|
|
287
|
+
if (prop.fieldNames?.length !== 1) {
|
|
288
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition key '${key.trim()}' maps to multiple columns ('${prop.fieldNames?.join("', '")}'); list them explicitly as partition keys`);
|
|
289
|
+
}
|
|
290
|
+
return prop.fieldNames[0];
|
|
291
|
+
}
|
|
292
|
+
getConstraintFields(meta, properties) {
|
|
293
|
+
if (!properties) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const fields = Utils.asArray(properties).flatMap(propName => {
|
|
297
|
+
const prop = meta.root.properties[propName];
|
|
298
|
+
return prop?.fieldNames ?? [];
|
|
299
|
+
});
|
|
300
|
+
return fields.length > 0 ? fields : undefined;
|
|
301
|
+
}
|
|
114
302
|
validateReference(meta, prop, options) {
|
|
115
303
|
// references do have types
|
|
116
304
|
if (!prop.type) {
|
|
@@ -358,6 +546,10 @@ export class MetadataValidator {
|
|
|
358
546
|
if (!meta.expression) {
|
|
359
547
|
throw MetadataError.viewEntityWithoutExpression(meta);
|
|
360
548
|
}
|
|
549
|
+
// Views are not partitionable - reject explicitly instead of silently ignoring
|
|
550
|
+
if (meta.partitionBy) {
|
|
551
|
+
throw new MetadataError(`View entity ${meta.className} cannot define partitionBy`);
|
|
552
|
+
}
|
|
361
553
|
// Validate indexes if present
|
|
362
554
|
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
363
555
|
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
package/metadata/types.d.ts
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, TriggerDef } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef } from '../typings.js';
|
|
2
2
|
import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
5
5
|
import type { FilterOptions, FindOptions } from '../drivers/IDatabaseDriver.js';
|
|
6
6
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
7
|
+
/** Supported PostgreSQL partitioning strategies. */
|
|
8
|
+
export type EntityPartitionType = 'hash' | 'list' | 'range';
|
|
9
|
+
/**
|
|
10
|
+
* Partition key expression for PostgreSQL partitioned tables.
|
|
11
|
+
*
|
|
12
|
+
* You can use:
|
|
13
|
+
* - a property name, e.g. `'type'`
|
|
14
|
+
* - a list of property names for composite expressions, e.g. `['tenant', 'createdAt']`
|
|
15
|
+
* - a raw SQL expression string, e.g. `"date_trunc('month', created_at)"`
|
|
16
|
+
* - a callback that receives schema column mappings and returns SQL
|
|
17
|
+
*/
|
|
18
|
+
export type EntityPartitionExpression<E = AnyEntity> = (keyof E & string) | readonly (keyof E & string)[] | AnyString | ((columns: SchemaColumns<E>) => string);
|
|
19
|
+
/** Explicit child partition definition for PostgreSQL `list` and `range` partitioning. */
|
|
20
|
+
export interface EntityPartition<E = AnyEntity> {
|
|
21
|
+
/**
|
|
22
|
+
* Optional child table name. Defaults to `<tableName>_<index>`. A single `.` is treated as a
|
|
23
|
+
* `schema.table` separator; names with more than one `.` are rejected to avoid ambiguity.
|
|
24
|
+
*/
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Partition bound clause, either as `for values ...` or the trailing part such as `in (...)`, `from ... to ...`, or `default`. */
|
|
27
|
+
values: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* PostgreSQL table partitioning definition.
|
|
31
|
+
*
|
|
32
|
+
* - `hash` partitions generate child tables automatically from the partition count
|
|
33
|
+
* - `list` and `range` partitions require explicit child partition definitions
|
|
34
|
+
*/
|
|
35
|
+
export type EntityPartitionBy<E = AnyEntity> = {
|
|
36
|
+
type: Extract<EntityPartitionType, 'hash'>;
|
|
37
|
+
expression: EntityPartitionExpression<E>;
|
|
38
|
+
/**
|
|
39
|
+
* Hash partition fan-out: either a number (auto-named as `${tableName}_N`) or an array of
|
|
40
|
+
* explicit partition names in remainder order. Names may be `schema.name` to create the
|
|
41
|
+
* child in a non-default schema.
|
|
42
|
+
*/
|
|
43
|
+
partitions: number | readonly string[];
|
|
44
|
+
} | {
|
|
45
|
+
type: Exclude<EntityPartitionType, 'hash'>;
|
|
46
|
+
expression: EntityPartitionExpression<E>;
|
|
47
|
+
/** Explicit child partitions created in the order provided. */
|
|
48
|
+
partitions: EntityPartition<E>[];
|
|
49
|
+
};
|
|
7
50
|
export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
8
51
|
/** Override default collection/table name. Alias for `collection`. */
|
|
9
52
|
tableName?: string;
|
|
@@ -59,6 +102,13 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
59
102
|
hasTriggers?: boolean;
|
|
60
103
|
/** Database triggers to create for this entity's table. (SQL drivers only) */
|
|
61
104
|
triggers?: TriggerDef<E>[];
|
|
105
|
+
/**
|
|
106
|
+
* PostgreSQL partitioning definition for this table.
|
|
107
|
+
*
|
|
108
|
+
* Partitioned tables are tracked by the schema generator and introspected back from PostgreSQL.
|
|
109
|
+
* Changing an existing partition layout still requires a manual migration.
|
|
110
|
+
*/
|
|
111
|
+
partitionBy?: EntityPartitionBy<E>;
|
|
62
112
|
/** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
|
|
63
113
|
expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
|
|
64
114
|
/** Set {@doclink repositories#custom-repository | custom repository class}. */
|
|
@@ -551,6 +601,16 @@ interface BaseOptions<T, H extends string> {
|
|
|
551
601
|
include?: T extends EntityClass<infer P> ? Properties<P, H> : Properties<T, H>;
|
|
552
602
|
/** Fill factor for the index as a percentage 0-100 (PostgreSQL, MSSQL). */
|
|
553
603
|
fillFactor?: number;
|
|
604
|
+
/**
|
|
605
|
+
* Predicate for partial indexes. Object form is a `FilterQuery` and is portable across
|
|
606
|
+
* SQL drivers and MongoDB; string form is a raw SQL fragment (SQL drivers only).
|
|
607
|
+
*
|
|
608
|
+
* Native support: PostgreSQL, SQLite, MSSQL (`WHERE`), MongoDB (`partialFilterExpression`).
|
|
609
|
+
* MySQL 8.0.13+ / Oracle: emulated via `CASE WHEN` functional index (uniqueness only enforced
|
|
610
|
+
* where the predicate holds). MariaDB is not supported — it has no inline expression indexes;
|
|
611
|
+
* define a virtual generated column and index that instead.
|
|
612
|
+
*/
|
|
613
|
+
where?: string | (T extends EntityClass<infer P> ? FilterQuery<P> : FilterQuery<T>);
|
|
554
614
|
}
|
|
555
615
|
export interface UniqueOptions<T, H extends string = string> extends BaseOptions<T, H> {
|
|
556
616
|
deferMode?: DeferMode | `${DeferMode}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.16",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export declare abstract class Platform {
|
|
|
43
43
|
getEnumArrayCheckConstraintExpression(column: string, items: string[]): string | null;
|
|
44
44
|
/** Whether this platform supports materialized views. */
|
|
45
45
|
supportsMaterializedViews(): boolean;
|
|
46
|
+
/** Whether this platform supports declarative table partitioning in schema generation. */
|
|
47
|
+
supportsPartitionedTables(): boolean;
|
|
46
48
|
/** Returns the schema helper instance for this platform, or undefined if not supported. */
|
|
47
49
|
getSchemaHelper(): unknown;
|
|
48
50
|
/** Whether the platform automatically creates indexes on foreign key columns. */
|
package/platforms/Platform.js
CHANGED
|
@@ -2,6 +2,7 @@ import { clone } from '../utils/clone.js';
|
|
|
2
2
|
import { EntityRepository } from '../entity/EntityRepository.js';
|
|
3
3
|
import { UnderscoreNamingStrategy } from '../naming-strategy/UnderscoreNamingStrategy.js';
|
|
4
4
|
import { ExceptionConverter } from './ExceptionConverter.js';
|
|
5
|
+
import { MetadataError } from '../errors.js';
|
|
5
6
|
import { ArrayType, BigIntType, BlobType, BooleanType, CharacterType, DateTimeType, DateType, DecimalType, DoubleType, EnumType, FloatType, IntegerType, IntervalType, JsonType, MediumIntType, SmallIntType, StringType, TextType, TimeType, TinyIntType, Type, Uint8ArrayType, UnknownType, UuidType, } from '../types/index.js';
|
|
6
7
|
import { parseJsonSafe, Utils } from '../utils/Utils.js';
|
|
7
8
|
import { ReferenceKind } from '../enums.js';
|
|
@@ -62,6 +63,10 @@ export class Platform {
|
|
|
62
63
|
supportsMaterializedViews() {
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
66
|
+
/** Whether this platform supports declarative table partitioning in schema generation. */
|
|
67
|
+
supportsPartitionedTables() {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
65
70
|
/** Returns the schema helper instance for this platform, or undefined if not supported. */
|
|
66
71
|
getSchemaHelper() {
|
|
67
72
|
return undefined;
|
|
@@ -658,7 +663,9 @@ export class Platform {
|
|
|
658
663
|
}
|
|
659
664
|
/** Platform-specific validation of entity metadata. */
|
|
660
665
|
validateMetadata(meta) {
|
|
661
|
-
|
|
666
|
+
if (meta.partitionBy && !this.supportsPartitionedTables()) {
|
|
667
|
+
throw new MetadataError(`Entity ${meta.className} uses partitionBy, but ${this.constructor.name} does not support partitioned tables`);
|
|
668
|
+
}
|
|
662
669
|
}
|
|
663
670
|
/**
|
|
664
671
|
* Generates a custom order by statement given a set of in order values, eg.
|
package/typings.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { SerializationContext } from './serialization/SerializationContext.
|
|
|
11
11
|
import type { SerializeOptions } from './serialization/EntitySerializer.js';
|
|
12
12
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
13
13
|
import type { EntitySchema } from './metadata/EntitySchema.js';
|
|
14
|
-
import type { IndexColumnOptions } from './metadata/types.js';
|
|
14
|
+
import type { EntityPartitionBy, IndexColumnOptions } from './metadata/types.js';
|
|
15
15
|
import type { Type, types } from './types/index.js';
|
|
16
16
|
import type { Platform } from './platforms/Platform.js';
|
|
17
17
|
import type { Configuration } from './utils/Configuration.js';
|
|
@@ -874,6 +874,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
874
874
|
materialized?: boolean;
|
|
875
875
|
/** For materialized views, whether data is populated on creation. Defaults to true. */
|
|
876
876
|
withData?: boolean;
|
|
877
|
+
/** PostgreSQL partitioning definition for this table. */
|
|
878
|
+
partitionBy?: EntityPartitionBy<Entity>;
|
|
877
879
|
expression?: string | ((em: any, where: ObjectQuery<Entity>, options: FindOptions<Entity, any, any, any>, stream?: boolean) => MaybePromise<Raw | object | string>);
|
|
878
880
|
discriminatorColumn?: EntityKey<Entity> | AnyString;
|
|
879
881
|
discriminatorValue?: number | string;
|
|
@@ -912,6 +914,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
912
914
|
type?: string;
|
|
913
915
|
options?: Dictionary;
|
|
914
916
|
expression?: string | IndexCallback<Entity>;
|
|
917
|
+
where?: string | FilterQuery<Entity>;
|
|
915
918
|
columns?: IndexColumnOptions[];
|
|
916
919
|
include?: EntityKey<Entity> | EntityKey<Entity>[];
|
|
917
920
|
fillFactor?: number;
|
|
@@ -924,6 +927,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
924
927
|
name?: string;
|
|
925
928
|
options?: Dictionary;
|
|
926
929
|
expression?: string | IndexCallback<Entity>;
|
|
930
|
+
where?: string | FilterQuery<Entity>;
|
|
927
931
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
928
932
|
columns?: IndexColumnOptions[];
|
|
929
933
|
include?: EntityKey<Entity> | EntityKey<Entity>[];
|
|
@@ -952,6 +956,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
952
956
|
polymorphicDiscriminatorMap?: Dictionary<EntityClass>;
|
|
953
957
|
/** Inheritance type: 'sti' (Single Table Inheritance) or 'tpt' (Table-Per-Type). Only set on root entities. */
|
|
954
958
|
inheritanceType?: 'sti' | 'tpt';
|
|
959
|
+
/** Legacy alias populated from `EntityOptions.inheritance`; mirrored to `inheritanceType` during discovery. */
|
|
960
|
+
inheritance?: 'tpt';
|
|
955
961
|
/** For TPT: direct parent entity metadata (the entity this one extends). */
|
|
956
962
|
tptParent?: EntityMetadata;
|
|
957
963
|
/** For TPT: direct child entities (entities that extend this one). */
|
package/typings.js
CHANGED
|
@@ -261,8 +261,8 @@ export class EntityMetadata {
|
|
|
261
261
|
}, { __gettersDefined: { value: true, enumerable: false } });
|
|
262
262
|
}
|
|
263
263
|
initIndexes(prop) {
|
|
264
|
-
const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression);
|
|
265
|
-
const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options);
|
|
264
|
+
const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression && index.where == null);
|
|
265
|
+
const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options && !index.expression && index.where == null);
|
|
266
266
|
const owner = prop.kind === ReferenceKind.MANY_TO_ONE;
|
|
267
267
|
if (!prop.index && simpleIndex) {
|
|
268
268
|
Utils.defaultValue(simpleIndex, 'name', true);
|
package/utils/Utils.js
CHANGED
|
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
|
|
|
141
141
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
142
142
|
export class Utils {
|
|
143
143
|
static PK_SEPARATOR = '~~~';
|
|
144
|
-
static #ORM_VERSION = '7.1.0-dev.
|
|
144
|
+
static #ORM_VERSION = '7.1.0-dev.16';
|
|
145
145
|
/**
|
|
146
146
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
147
147
|
*/
|
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
3
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
4
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
5
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
6
|
+
*
|
|
7
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
8
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePartitionNameForComparison(name: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
13
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
14
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
15
|
+
* as an opaque expression.
|
|
16
|
+
*/
|
|
17
|
+
export declare function splitCommaSeparatedIdentifiers(value: string): string[] | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function normalizeIdentifierSegment(segment) {
|
|
2
|
+
const trimmed = segment.trim();
|
|
3
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
4
|
+
return trimmed.slice(1, -1).replaceAll('""', '"');
|
|
5
|
+
}
|
|
6
|
+
return trimmed.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
10
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
11
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
12
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
13
|
+
*
|
|
14
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
15
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizePartitionNameForComparison(name) {
|
|
18
|
+
const trimmed = name.trim();
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
21
|
+
const ch = trimmed[i];
|
|
22
|
+
if (ch === '"') {
|
|
23
|
+
if (trimmed[i + 1] === '"') {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
depth = depth === 0 ? 1 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '.' && depth === 0) {
|
|
31
|
+
return `${normalizeIdentifierSegment(trimmed.slice(0, i))}.${normalizeIdentifierSegment(trimmed.slice(i + 1))}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `.${normalizeIdentifierSegment(trimmed)}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
38
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
39
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
40
|
+
* as an opaque expression.
|
|
41
|
+
*/
|
|
42
|
+
export function splitCommaSeparatedIdentifiers(value) {
|
|
43
|
+
const segments = [];
|
|
44
|
+
let buffer = '';
|
|
45
|
+
let inQuote = false;
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
const ch = value[i];
|
|
48
|
+
if (ch === '"') {
|
|
49
|
+
if (inQuote && value[i + 1] === '"') {
|
|
50
|
+
buffer += '""';
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
inQuote = !inQuote;
|
|
55
|
+
buffer += ch;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!inQuote) {
|
|
59
|
+
if (ch === ',') {
|
|
60
|
+
segments.push(buffer);
|
|
61
|
+
buffer = '';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!/[\w.\s]/.test(ch)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
buffer += ch;
|
|
69
|
+
}
|
|
70
|
+
if (inQuote) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
segments.push(buffer);
|
|
74
|
+
const trimmed = segments.map(s => s.trim());
|
|
75
|
+
if (trimmed.some(s => s === '')) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|