@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.
@@ -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(/\((.*?)\)::\w+/g, '$1') : single ? single[1] : check.expression;
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}; end; $$ language plpgsql`;
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`, uniqueSchemas, 'all', ctx);
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('pragma foreign_keys = on'));
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
- for (const db of attachDatabases) {
30
+ return attachDatabases.map(db => {
21
31
  const path = fs.absolutePath(db.path, baseDir);
22
- await this.execute(`attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`);
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}; end`);
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.0",
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.3"
50
+ "kysely": "0.29.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.6"
53
+ "@mikro-orm/core": "^7.1.11"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.0-dev.0"
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
  }
@@ -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>}.*`) | '*' | QueryBuilder<any> | NativeQueryBuilder | RawQueryFragment<any> | (RawQueryFragment & symbol);
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?: NestedFilterCondition<Entity, RootAlias, Context, RawAliases>[];
152
- $or?: NestedFilterCondition<Entity, RootAlias, Context, RawAliases>[];
153
- $not?: NestedFilterCondition<Entity, RootAlias, Context, RawAliases>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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, '*', CTEs>;
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: QueryBuilder<Entity>, aliasName?: string): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs>;
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, '*', CTEs>;
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 extends QueryBuilder<infer T> ? T : object>>;
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 extends QueryBuilder<infer T> ? T : object>>;
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.
@@ -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 && this.#state.mainAlias && Utils.className(target) !== this.#state.mainAlias.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
- yield this.mapResult(merged[0], options.mapResults);
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
- // auto-joins added by cond processing that depend on the new alias would otherwise produce a
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
- parts.push(`not (${res.sql})`);
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
  /**