@mikro-orm/sql 7.2.0-dev.0 → 7.2.0-dev.10
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 +18 -2
- package/AbstractSqlConnection.js +43 -17
- package/AbstractSqlDriver.d.ts +2 -2
- package/AbstractSqlDriver.js +64 -21
- package/AbstractSqlPlatform.d.ts +7 -0
- package/AbstractSqlPlatform.js +23 -2
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +1 -2
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.js +1 -1
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
- package/dialects/mysql/MySqlSchemaHelper.js +7 -3
- package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +4 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +32 -5
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
- package/package.json +4 -4
- package/query/NativeQueryBuilder.js +1 -1
- package/query/QueryBuilder.d.ts +37 -19
- package/query/QueryBuilder.js +47 -17
- package/query/QueryBuilderHelper.js +6 -1
- package/schema/DatabaseTable.d.ts +2 -0
- package/schema/DatabaseTable.js +63 -18
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +29 -6
- package/schema/SchemaHelper.d.ts +20 -1
- package/schema/SchemaHelper.js +52 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +51 -22
- package/typings.d.ts +2 -2
|
@@ -3,6 +3,8 @@ import { SchemaHelper, stripStatementNewlines } from '../../schema/SchemaHelper.
|
|
|
3
3
|
import { normalizePartitionBound, normalizePartitionDefinition } from '../../schema/partitioning.js';
|
|
4
4
|
/** PostGIS system views that should be automatically ignored */
|
|
5
5
|
const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
|
|
6
|
+
/** Dollar-quote delimiter, e.g. `$$` or `$body$`, captured so `split` keeps it. */
|
|
7
|
+
const DOLLAR_QUOTE_TAG = /(\$(?:[A-Za-z_]\w*)?\$)/;
|
|
6
8
|
export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
7
9
|
static DEFAULT_VALUES = {
|
|
8
10
|
'now()': ['now()', 'current_timestamp'],
|
|
@@ -492,7 +494,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
492
494
|
// SchemaHelper.createCheck).
|
|
493
495
|
const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
|
|
494
496
|
const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
|
|
495
|
-
const def = m ? m[1].replace(/\((
|
|
497
|
+
const def = m ? m[1].replace(/\(([^()]*)\)::\w+/g, '$1') : single ? single[1] : check.expression;
|
|
496
498
|
ret[key].push({
|
|
497
499
|
name: check.name,
|
|
498
500
|
columnName: check.column_name,
|
|
@@ -505,7 +507,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
505
507
|
/** Generates SQL to create a PostgreSQL trigger and its associated function. */
|
|
506
508
|
createTrigger(table, trigger) {
|
|
507
509
|
if (trigger.expression) {
|
|
508
|
-
return trigger.expression;
|
|
510
|
+
return this.flattenDollarQuotedBodies(trigger.expression);
|
|
509
511
|
}
|
|
510
512
|
const timing = trigger.timing.toUpperCase();
|
|
511
513
|
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
@@ -513,7 +515,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
513
515
|
const when = trigger.when ? `\n when (${trigger.when})` : '';
|
|
514
516
|
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
515
517
|
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
516
|
-
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}
|
|
518
|
+
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${this.normalizeTriggerBody(trigger.body)} end; $$ language plpgsql`;
|
|
517
519
|
const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
|
|
518
520
|
return `${fnSql};\n${triggerSql}`;
|
|
519
521
|
}
|
|
@@ -523,9 +525,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
523
525
|
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
524
526
|
return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
|
|
525
527
|
}
|
|
528
|
+
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
529
|
+
flattenDollarQuotedBodies(ddl) {
|
|
530
|
+
let openTag = '';
|
|
531
|
+
return ddl
|
|
532
|
+
.split(DOLLAR_QUOTE_TAG)
|
|
533
|
+
.map((part, i) => {
|
|
534
|
+
// the capture group puts the delimiters on the odd indexes
|
|
535
|
+
if (i % 2 === 1) {
|
|
536
|
+
openTag = openTag === part ? '' : openTag || part;
|
|
537
|
+
return part;
|
|
538
|
+
}
|
|
539
|
+
return openTag ? stripStatementNewlines(part) : part;
|
|
540
|
+
})
|
|
541
|
+
.join('');
|
|
542
|
+
}
|
|
526
543
|
createRoutine(routine) {
|
|
527
544
|
if (routine.expression) {
|
|
528
|
-
return routine.expression;
|
|
545
|
+
return this.flattenDollarQuotedBodies(routine.expression);
|
|
529
546
|
}
|
|
530
547
|
const qualifiedName = this.qualifiedRoutineName(routine);
|
|
531
548
|
const params = this.formatRoutineParams(routine);
|
|
@@ -579,6 +596,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
579
596
|
)
|
|
580
597
|
-- exclude trigger-helper functions; they're managed alongside their owning trigger.
|
|
581
598
|
and p.prorettype <> 'trigger'::regtype
|
|
599
|
+
order by n.nspname, p.proname
|
|
582
600
|
`;
|
|
583
601
|
const rows = await connection.execute(sql);
|
|
584
602
|
return rows.map(row => {
|
|
@@ -775,7 +793,8 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
775
793
|
left join pg_enum e on t.oid = e.enumtypid
|
|
776
794
|
join pg_catalog.pg_namespace n on n.oid = t.typnamespace
|
|
777
795
|
where t.typtype = 'e' and n.nspname in (${Array(uniqueSchemas.length).fill('?').join(', ')})
|
|
778
|
-
group by t.typname, n.nspname
|
|
796
|
+
group by t.typname, n.nspname
|
|
797
|
+
order by n.nspname, t.typname`, uniqueSchemas, 'all', ctx);
|
|
779
798
|
return res.reduce((o, row) => {
|
|
780
799
|
let name = row.enum_name;
|
|
781
800
|
if (row.schema_name && row.schema_name !== this.platform.getDefaultSchemaName()) {
|
|
@@ -1116,6 +1135,14 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
1116
1135
|
})
|
|
1117
1136
|
.join(', ');
|
|
1118
1137
|
}
|
|
1138
|
+
/** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
|
|
1139
|
+
getIndexAccessMethod(index) {
|
|
1140
|
+
// `fulltext` is a cross-dialect alias handled via `getFullTextIndexExpression`, not a pg access method
|
|
1141
|
+
if (typeof index.type !== 'string' || ['', 'btree', 'fulltext'].includes(index.type.toLowerCase())) {
|
|
1142
|
+
return '';
|
|
1143
|
+
}
|
|
1144
|
+
return index.type.toLowerCase();
|
|
1145
|
+
}
|
|
1119
1146
|
/**
|
|
1120
1147
|
* PostgreSQL-specific index options like fill factor.
|
|
1121
1148
|
*/
|
|
@@ -7,4 +7,7 @@ export declare class BaseSqliteConnection extends AbstractSqlConnection {
|
|
|
7
7
|
skipOnConnect?: boolean;
|
|
8
8
|
}): Promise<void>;
|
|
9
9
|
protected attachDatabases(): Promise<void>;
|
|
10
|
+
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
|
|
11
|
+
protected getConnectionSetupSql(): Promise<string[]>;
|
|
12
|
+
private getAttachDatabasesSql;
|
|
10
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompiledQuery } from 'kysely';
|
|
2
2
|
import { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
3
|
+
const FOREIGN_KEYS_PRAGMA = 'pragma foreign_keys = on';
|
|
3
4
|
export class BaseSqliteConnection extends AbstractSqlConnection {
|
|
4
5
|
createKyselyDialect(options) {
|
|
5
6
|
throw new Error('No SQLite dialect configured. Pass a Kysely dialect via the `driverOptions` config option, ' +
|
|
@@ -7,19 +8,28 @@ export class BaseSqliteConnection extends AbstractSqlConnection {
|
|
|
7
8
|
}
|
|
8
9
|
async connect(options) {
|
|
9
10
|
await super.connect(options);
|
|
10
|
-
await this.getClient().executeQuery(CompiledQuery.raw(
|
|
11
|
+
await this.getClient().executeQuery(CompiledQuery.raw(FOREIGN_KEYS_PRAGMA));
|
|
11
12
|
await this.attachDatabases();
|
|
12
13
|
}
|
|
13
14
|
async attachDatabases() {
|
|
15
|
+
for (const sql of await this.getAttachDatabasesSql()) {
|
|
16
|
+
await this.execute(sql);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
|
|
20
|
+
async getConnectionSetupSql() {
|
|
21
|
+
return [FOREIGN_KEYS_PRAGMA, ...(await this.getAttachDatabasesSql())];
|
|
22
|
+
}
|
|
23
|
+
async getAttachDatabasesSql() {
|
|
14
24
|
const attachDatabases = this.config.get('attachDatabases');
|
|
15
25
|
if (!attachDatabases?.length) {
|
|
16
|
-
return;
|
|
26
|
+
return [];
|
|
17
27
|
}
|
|
18
28
|
const { fs } = await import('@mikro-orm/core/fs-utils');
|
|
19
29
|
const baseDir = this.config.get('baseDir');
|
|
20
|
-
|
|
30
|
+
return attachDatabases.map(db => {
|
|
21
31
|
const path = fs.absolutePath(db.path, baseDir);
|
|
22
|
-
|
|
23
|
-
}
|
|
32
|
+
return `attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`;
|
|
33
|
+
});
|
|
24
34
|
}
|
|
25
35
|
}
|
|
@@ -52,7 +52,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
52
52
|
for (const dbName of targetSchemas) {
|
|
53
53
|
const prefix = this.getSchemaPrefix(dbName);
|
|
54
54
|
const tables = await connection.execute(`select name from ${prefix}sqlite_master where type = 'table' ` +
|
|
55
|
-
`and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys'`, [], 'all', ctx);
|
|
55
|
+
`and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' order by name`, [], 'all', ctx);
|
|
56
56
|
for (const t of tables) {
|
|
57
57
|
allTables.push({ table_name: t.name, schema_name: dbName });
|
|
58
58
|
}
|
|
@@ -561,7 +561,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
561
561
|
for (const event of trigger.events) {
|
|
562
562
|
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
563
563
|
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
564
|
-
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}
|
|
564
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`);
|
|
565
565
|
}
|
|
566
566
|
return ret.join(';\n');
|
|
567
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.10",
|
|
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",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"copy": "node ../../scripts/copy.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"kysely": "0.29.
|
|
50
|
+
"kysely": "0.29.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.1.
|
|
53
|
+
"@mikro-orm/core": "^7.1.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.0-dev.10"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -308,7 +308,7 @@ export class NativeQueryBuilder {
|
|
|
308
308
|
const fields = this.options.groupBy.map(field => this.quote(field));
|
|
309
309
|
this.parts.push(`group by ${fields.join(', ')}`);
|
|
310
310
|
}
|
|
311
|
-
if (this.options.having) {
|
|
311
|
+
if (this.options.having?.sql.trim()) {
|
|
312
312
|
this.parts.push(`having ${this.options.having.sql}`);
|
|
313
313
|
this.params.push(...this.options.having.params);
|
|
314
314
|
}
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ type IsNever<T, True = true, False = false> = [T] extends [never] ? True : False
|
|
|
52
52
|
type GetAlias<T extends string> = T extends `${infer A}.${string}` ? A : never;
|
|
53
53
|
type GetPropName<T extends string> = T extends `${string}.${infer P}` ? P : T;
|
|
54
54
|
type AppendToHint<Parent extends string, Child extends string> = `${Parent}.${Child}`;
|
|
55
|
+
/**
|
|
56
|
+
* Extracts the entity type from a query builder via its main alias. Matching against
|
|
57
|
+
* `QueryBuilder<infer T>` is unreliable once another generic, such as selected fields,
|
|
58
|
+
* differs from its default.
|
|
59
|
+
*/
|
|
60
|
+
type QueryBuilderEntity<Q extends QueryBuilder<any>> = Q['mainAlias'] extends Alias<infer T extends object> ? T : object;
|
|
55
61
|
/**
|
|
56
62
|
* Context tuple format: [Path, Alias, Type, Select]
|
|
57
63
|
* - Path: The relation path from root entity (e.g., 'books', 'books.author')
|
|
@@ -85,7 +91,7 @@ type ContextRelationKeys<Context> = Context[keyof Context] extends infer Join ?
|
|
|
85
91
|
export type QBField<Entity, RootAlias extends string, Context> = EntityRelations<Entity> | `${RootAlias}.${EntityRelations<Entity>}` | ([Context] extends [never] ? never : ContextRelationKeys<Context>);
|
|
86
92
|
type ContextFieldKeys<Context> = Context[keyof Context] extends infer Join ? Join extends any ? Join extends [string, infer Alias, infer Type, any] ? `${Alias & string}.${keyof Type & string}` : never : never : never;
|
|
87
93
|
type WithAlias<T extends string> = T | `${T} as ${string}`;
|
|
88
|
-
export type Field<Entity, RootAlias extends string = never, Context = never> = WithAlias<EntityKey<Entity>> | (IsNever<RootAlias> extends true ? never : WithAlias<`${RootAlias}.${EntityKey<Entity>}`> | `${RootAlias}.*`) | ([Context] extends [never] ? never : WithAlias<ContextFieldKeys<Context>> | `${AliasNames<Context>}.*`) | '*' |
|
|
94
|
+
export type Field<Entity, RootAlias extends string = never, Context = never> = WithAlias<EntityKey<Entity>> | (IsNever<RootAlias> extends true ? never : WithAlias<`${RootAlias}.${EntityKey<Entity>}`> | `${RootAlias}.*`) | ([Context] extends [never] ? never : WithAlias<ContextFieldKeys<Context>> | `${AliasNames<Context>}.*`) | '*' | AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment<any> | (RawQueryFragment & symbol);
|
|
89
95
|
type RootAliasOrderKeys<RootAlias extends string, Entity> = {
|
|
90
96
|
[K in `${RootAlias}.${EntityKey<Entity>}`]?: QueryOrderKeysFlat;
|
|
91
97
|
};
|
|
@@ -146,11 +152,10 @@ type ContextFilterKeys<Context> = {
|
|
|
146
152
|
type RawFilterKeys<RawAliases extends string> = {
|
|
147
153
|
[K in RawAliases]?: AliasedFilterValue;
|
|
148
154
|
};
|
|
149
|
-
type NestedFilterCondition<Entity, RootAlias extends string, Context, RawAliases extends string> = ObjectQuery<Entity> & (IsNever<RootAlias> extends true ? {} : string extends RootAlias ? {} : RootAliasFilterKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextFilterKeys<Context>) & (IsNever<RawAliases> extends true ? {} : string extends RawAliases ? {} : RawFilterKeys<RawAliases>);
|
|
150
155
|
type GroupOperators<RootAlias extends string, Context, Entity, RawAliases extends string> = {
|
|
151
|
-
$and?:
|
|
152
|
-
$or?:
|
|
153
|
-
$not?:
|
|
156
|
+
$and?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>[];
|
|
157
|
+
$or?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>[];
|
|
158
|
+
$not?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>;
|
|
154
159
|
};
|
|
155
160
|
export type AliasedFilterCondition<RootAlias extends string, Context, Entity, RawAliases extends string = never> = (IsNever<RootAlias> extends true ? {} : string extends RootAlias ? {} : RootAliasFilterKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextFilterKeys<Context>) & (IsNever<RawAliases> extends true ? {} : string extends RawAliases ? {} : RawFilterKeys<RawAliases>) & GroupOperators<RootAlias, Context, Entity, RawAliases>;
|
|
156
161
|
export type QBFilterQuery<Entity, RootAlias extends string = never, Context = never, RawAliases extends string = never> = FilterObject<Entity> & AliasedFilterCondition<RootAlias, Context, Entity, RawAliases>;
|
|
@@ -432,29 +437,29 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
432
437
|
* .where({ 'a.name': 'John' });
|
|
433
438
|
* ```
|
|
434
439
|
*/
|
|
435
|
-
join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases,
|
|
440
|
+
join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
436
441
|
/**
|
|
437
442
|
* Adds a JOIN clause to the query for a subquery.
|
|
438
443
|
*/
|
|
439
|
-
join<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases,
|
|
444
|
+
join<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
440
445
|
/**
|
|
441
446
|
* Adds an INNER JOIN clause to the query for an entity relation.
|
|
442
447
|
*/
|
|
443
|
-
innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases,
|
|
448
|
+
innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
444
449
|
/**
|
|
445
450
|
* Adds an INNER JOIN clause to the query for a subquery.
|
|
446
451
|
*/
|
|
447
|
-
innerJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases,
|
|
448
|
-
innerJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases,
|
|
452
|
+
innerJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
453
|
+
innerJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
449
454
|
/**
|
|
450
455
|
* Adds a LEFT JOIN clause to the query for an entity relation.
|
|
451
456
|
*/
|
|
452
|
-
leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases,
|
|
457
|
+
leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
453
458
|
/**
|
|
454
459
|
* Adds a LEFT JOIN clause to the query for a subquery.
|
|
455
460
|
*/
|
|
456
|
-
leftJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases,
|
|
457
|
-
leftJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases,
|
|
461
|
+
leftJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
462
|
+
leftJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
458
463
|
/**
|
|
459
464
|
* Adds a JOIN clause and automatically selects the joined entity's fields.
|
|
460
465
|
* This is useful for eager loading related entities.
|
|
@@ -500,6 +505,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
500
505
|
* @internal
|
|
501
506
|
*/
|
|
502
507
|
applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>;
|
|
508
|
+
/**
|
|
509
|
+
* The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
|
|
510
|
+
* it — can reference the alias of any join in its subtree, both auto-joins created while
|
|
511
|
+
* processing the condition and pre-existing joined paths, all of which render after `condJoin`
|
|
512
|
+
* and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
|
|
513
|
+
* subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
|
|
514
|
+
* shares the scope of the outer `on` clause.
|
|
515
|
+
*/
|
|
516
|
+
private nestReferencedJoins;
|
|
517
|
+
private getJoinSubtree;
|
|
518
|
+
private condReferencesAlias;
|
|
503
519
|
withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this;
|
|
504
520
|
/**
|
|
505
521
|
* Adds a WHERE clause to the query using an object condition.
|
|
@@ -723,7 +739,9 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
723
739
|
* Specifies FROM which entity's table select/update/delete will be executed, removing all previously set FROM-s.
|
|
724
740
|
* Allows setting a main string alias of the selection data.
|
|
725
741
|
*/
|
|
726
|
-
from<Entity extends object>(target:
|
|
742
|
+
from<Entity extends object>(target: Subquery & {
|
|
743
|
+
readonly mainAlias: Alias<Entity>;
|
|
744
|
+
}, aliasName?: string): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs>;
|
|
727
745
|
/**
|
|
728
746
|
* Specifies FROM which entity's table select/update/delete will be executed, removing all previously set FROM-s.
|
|
729
747
|
* Allows setting a main string alias of the selection data.
|
|
@@ -733,7 +751,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
733
751
|
* Specifies a CTE name as the FROM source, with full type safety.
|
|
734
752
|
* The entity type is inferred from the CTE definition passed to `.with()`.
|
|
735
753
|
*/
|
|
736
|
-
from<Name extends string & keyof CTEs, Alias extends string = Name>(target: Name, aliasName?: Alias): SelectQueryBuilder<CTEs[Name], Alias, never, never, never,
|
|
754
|
+
from<Name extends string & keyof CTEs, Alias extends string = Name>(target: Name, aliasName?: Alias): SelectQueryBuilder<CTEs[Name], Alias, never, never, never, Fields, CTEs>;
|
|
737
755
|
getNativeQuery(processVirtualEntity?: boolean): NativeQueryBuilder;
|
|
738
756
|
protected processReturningStatement(qb: NativeQueryBuilder, meta?: EntityMetadata, data?: Dictionary, returning?: Field<any>[]): void;
|
|
739
757
|
/**
|
|
@@ -849,7 +867,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
849
867
|
* const results = await em.find(Employee, { id: { $in: subquery } });
|
|
850
868
|
* ```
|
|
851
869
|
*/
|
|
852
|
-
unionAll(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity>;
|
|
870
|
+
unionAll(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs>;
|
|
853
871
|
/**
|
|
854
872
|
* Combines the current query with one or more other queries using `UNION` (with deduplication).
|
|
855
873
|
* All queries must select the same columns. Returns a `QueryBuilder` that
|
|
@@ -864,7 +882,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
864
882
|
* const results = await em.find(Employee, { id: { $in: subquery } });
|
|
865
883
|
* ```
|
|
866
884
|
*/
|
|
867
|
-
union(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity>;
|
|
885
|
+
union(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs>;
|
|
868
886
|
private buildUnionQuery;
|
|
869
887
|
/**
|
|
870
888
|
* Adds a Common Table Expression (CTE) to the query.
|
|
@@ -879,7 +897,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
879
897
|
* .from('recent_books', 'rb'); // entity type inferred as Book
|
|
880
898
|
* ```
|
|
881
899
|
*/
|
|
882
|
-
with<Name extends string, Q extends QueryBuilder<any>>(name: Name, query: Q, options?: CteOptions): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs & Record<Name, Q
|
|
900
|
+
with<Name extends string, Q extends QueryBuilder<any>>(name: Name, query: Q, options?: CteOptions): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs & Record<Name, QueryBuilderEntity<Q>>>;
|
|
883
901
|
/**
|
|
884
902
|
* Adds a Common Table Expression (CTE) to the query using a `NativeQueryBuilder` or raw SQL fragment.
|
|
885
903
|
* The CTE name is tracked but without entity type inference — use `from()` to query from it.
|
|
@@ -900,7 +918,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
900
918
|
* .from('category_tree', 'ct'); // entity type inferred as Category
|
|
901
919
|
* ```
|
|
902
920
|
*/
|
|
903
|
-
withRecursive<Name extends string, Q extends QueryBuilder<any>>(name: Name, query: Q, options?: CteOptions): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs & Record<Name, Q
|
|
921
|
+
withRecursive<Name extends string, Q extends QueryBuilder<any>>(name: Name, query: Q, options?: CteOptions): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs & Record<Name, QueryBuilderEntity<Q>>>;
|
|
904
922
|
/**
|
|
905
923
|
* Adds a recursive Common Table Expression (CTE) to the query using a `NativeQueryBuilder` or raw SQL fragment.
|
|
906
924
|
* The CTE name is tracked but without entity type inference — use `from()` to query from it.
|
package/query/QueryBuilder.js
CHANGED
|
@@ -433,6 +433,7 @@ export class QueryBuilder {
|
|
|
433
433
|
else {
|
|
434
434
|
join.cond = { ...cond };
|
|
435
435
|
}
|
|
436
|
+
this.nestReferencedJoins(join);
|
|
436
437
|
// For polymorphic LEFT JOIN filters, add a WHERE condition to enforce the filter
|
|
437
438
|
// only for rows matching this target's discriminator value. This ensures rows pointing
|
|
438
439
|
// to other polymorphic targets are not excluded.
|
|
@@ -456,6 +457,45 @@ export class QueryBuilder {
|
|
|
456
457
|
}
|
|
457
458
|
}
|
|
458
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
|
|
462
|
+
* it — can reference the alias of any join in its subtree, both auto-joins created while
|
|
463
|
+
* processing the condition and pre-existing joined paths, all of which render after `condJoin`
|
|
464
|
+
* and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
|
|
465
|
+
* subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
|
|
466
|
+
* shares the scope of the outer `on` clause.
|
|
467
|
+
*/
|
|
468
|
+
nestReferencedJoins(condJoin) {
|
|
469
|
+
// m:n pivot joins might not have the target join entry created
|
|
470
|
+
if (!condJoin) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const subtree = this.getJoinSubtree(condJoin);
|
|
474
|
+
if (!subtree.some(j => this.condReferencesAlias(condJoin.cond, j.alias))) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
for (const j of subtree) {
|
|
478
|
+
const parent = j.ownerAlias === condJoin.alias ? condJoin : subtree.find(p => p.alias === j.ownerAlias);
|
|
479
|
+
if (!parent.nested?.has(j)) {
|
|
480
|
+
const nested = (parent.nested ??= new Set());
|
|
481
|
+
j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
482
|
+
nested.add(j);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
getJoinSubtree(join) {
|
|
487
|
+
const children = Object.values(this.#state.joins).filter(j => j !== join && j.ownerAlias === join.alias);
|
|
488
|
+
return children.flatMap(j => [j, ...this.getJoinSubtree(j)]);
|
|
489
|
+
}
|
|
490
|
+
condReferencesAlias(cond, alias) {
|
|
491
|
+
if (Array.isArray(cond)) {
|
|
492
|
+
return cond.some(c => this.condReferencesAlias(c, alias));
|
|
493
|
+
}
|
|
494
|
+
if (Utils.isPlainObject(cond)) {
|
|
495
|
+
return Object.entries(cond).some(([key, value]) => key.startsWith(`${alias}.`) || this.condReferencesAlias(value, alias));
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
459
499
|
withSubQuery(subQuery, alias) {
|
|
460
500
|
this.ensureNotFinalized();
|
|
461
501
|
if (isRaw(subQuery)) {
|
|
@@ -782,7 +822,9 @@ export class QueryBuilder {
|
|
|
782
822
|
this.fromRawTable(target, aliasName);
|
|
783
823
|
}
|
|
784
824
|
else {
|
|
785
|
-
if (aliasName &&
|
|
825
|
+
if (aliasName &&
|
|
826
|
+
this.#state.mainAlias &&
|
|
827
|
+
Utils.className(target) !== this.#state.mainAlias.aliasName) {
|
|
786
828
|
throw new Error(`Cannot override the alias to '${aliasName}' since a query already contains references to '${this.#state.mainAlias.aliasName}'`);
|
|
787
829
|
}
|
|
788
830
|
this.fromEntityName(target, aliasName);
|
|
@@ -1102,7 +1144,9 @@ export class QueryBuilder {
|
|
|
1102
1144
|
}
|
|
1103
1145
|
if (stack.length > 0) {
|
|
1104
1146
|
const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
|
|
1105
|
-
|
|
1147
|
+
for (const row of merged) {
|
|
1148
|
+
yield this.mapResult(row, options.mapResults);
|
|
1149
|
+
}
|
|
1106
1150
|
}
|
|
1107
1151
|
}
|
|
1108
1152
|
/**
|
|
@@ -1410,7 +1454,6 @@ export class QueryBuilder {
|
|
|
1410
1454
|
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
|
|
1411
1455
|
});
|
|
1412
1456
|
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
|
|
1413
|
-
const joinCountBefore = Object.keys(this.#state.joins).length;
|
|
1414
1457
|
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
|
|
1415
1458
|
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
|
|
1416
1459
|
path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
|
|
@@ -1440,20 +1483,7 @@ export class QueryBuilder {
|
|
|
1440
1483
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1441
1484
|
this.#state.joins[aliasedName].path ??= path;
|
|
1442
1485
|
}
|
|
1443
|
-
|
|
1444
|
-
// forward reference (the auto-join's ON refers to alias, while alias's ON refers back to it);
|
|
1445
|
-
// fold them into the new join so both aliases share scope in the outer ON clause (issue #7681)
|
|
1446
|
-
const condJoin = this.#state.joins[aliasedName];
|
|
1447
|
-
const joinKeys = Object.keys(this.#state.joins);
|
|
1448
|
-
for (let i = joinCountBefore; i < joinKeys.length; i++) {
|
|
1449
|
-
const j = this.#state.joins[joinKeys[i]];
|
|
1450
|
-
if (j === condJoin || j.ownerAlias !== alias) {
|
|
1451
|
-
continue;
|
|
1452
|
-
}
|
|
1453
|
-
const nested = (condJoin.nested ??= new Set());
|
|
1454
|
-
j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
1455
|
-
nested.add(j);
|
|
1456
|
-
}
|
|
1486
|
+
this.nestReferencedJoins(this.#state.joins[aliasedName]);
|
|
1457
1487
|
return { prop, key: aliasedName };
|
|
1458
1488
|
}
|
|
1459
1489
|
prepareFields(fields, type = 'where', schema) {
|
|
@@ -414,7 +414,8 @@ export class QueryBuilderHelper {
|
|
|
414
414
|
}
|
|
415
415
|
if (k === '$not') {
|
|
416
416
|
const res = this._appendQueryCondition(type, cond[k]);
|
|
417
|
-
|
|
417
|
+
// negating a vacuously true condition (e.g. an empty `$and`) matches nothing
|
|
418
|
+
parts.push(res.sql ? `not (${res.sql})` : '1 = 0');
|
|
418
419
|
res.params.forEach(p => params.push(p));
|
|
419
420
|
continue;
|
|
420
421
|
}
|
|
@@ -785,6 +786,10 @@ export class QueryBuilderHelper {
|
|
|
785
786
|
appendGroupCondition(type, operator, subCondition) {
|
|
786
787
|
const parts = [];
|
|
787
788
|
const params = [];
|
|
789
|
+
// an empty disjunction is false, same as `$in: []`, while an empty conjunction is vacuously true
|
|
790
|
+
if (operator === '$or' && subCondition.length === 0) {
|
|
791
|
+
return { sql: '1 = 0', params };
|
|
792
|
+
}
|
|
788
793
|
// single sub-condition can be ignored to reduce nesting of parens
|
|
789
794
|
if (subCondition.length === 1 || operator === '$and') {
|
|
790
795
|
for (const sub of subCondition) {
|
|
@@ -49,6 +49,8 @@ export declare class DatabaseTable {
|
|
|
49
49
|
getEntityDeclaration(namingStrategy: NamingStrategy, schemaHelper: SchemaHelper, scalarPropertiesForRelations: 'always' | 'never' | 'smart'): EntityMetadata;
|
|
50
50
|
private foreignKeysToProps;
|
|
51
51
|
private findFkIndex;
|
|
52
|
+
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
|
|
53
|
+
private hasAdvancedIndexOptions;
|
|
52
54
|
private getIndexProperties;
|
|
53
55
|
private getSafeBaseNameForFkProp;
|
|
54
56
|
/**
|