@mikro-orm/sql 7.0.0-dev.246 → 7.0.0-dev.248

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.
@@ -19,6 +19,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
19
19
  private getTableProps;
20
20
  /** Creates a FormulaTable object for use in formula callbacks. */
21
21
  private createFormulaTable;
22
+ private validateSqlOptions;
22
23
  createEntityManager(useContext?: boolean): this[typeof EntityManagerType];
23
24
  private createQueryBuilderFromOptions;
24
25
  find<T extends object, P extends string = never, F extends string = PopulatePath.ALL, E extends string = never>(entityName: EntityName<T>, where: ObjectQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
@@ -32,6 +32,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
32
32
  const qualifiedName = effectiveSchema ? `${effectiveSchema}.${meta.tableName}` : meta.tableName;
33
33
  return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
34
34
  }
35
+ validateSqlOptions(options) {
36
+ if (options.collation != null && typeof options.collation !== 'string') {
37
+ throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
38
+ }
39
+ if (options.indexHint != null && typeof options.indexHint !== 'string') {
40
+ throw new Error('indexHint for SQL drivers must be a string (e.g. \'force index(my_index)\'). Use an object only with MongoDB.');
41
+ }
42
+ }
35
43
  createEntityManager(useContext) {
36
44
  const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
37
45
  return new EntityManagerClass(this.config, this, this.metadata, useContext);
@@ -49,6 +57,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
49
57
  if (Utils.isPrimaryKey(where, meta.compositePK)) {
50
58
  where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
51
59
  }
60
+ this.validateSqlOptions(options);
52
61
  const { first, last, before, after } = options;
53
62
  const isCursorPagination = [first, last, before, after].some(v => v != null);
54
63
  qb.__populateWhere = options._populateWhere;
@@ -59,6 +68,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
59
68
  .groupBy(options.groupBy)
60
69
  .having(options.having)
61
70
  .indexHint(options.indexHint)
71
+ .collation(options.collation)
62
72
  .comment(options.comments)
63
73
  .hintComment(options.hintComments);
64
74
  if (isCursorPagination) {
@@ -474,8 +484,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
474
484
  if (meta && !Utils.isEmpty(populate)) {
475
485
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
476
486
  }
487
+ this.validateSqlOptions(options);
477
488
  qb.__populateWhere = options._populateWhere;
478
489
  qb.indexHint(options.indexHint)
490
+ .collation(options.collation)
479
491
  .comment(options.comments)
480
492
  .hintComment(options.hintComments)
481
493
  .groupBy(options.groupBy)
@@ -1486,7 +1498,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1486
1498
  return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
1487
1499
  }
1488
1500
  return prop.fieldNames.map(fieldName => {
1489
- return `${tableAlias}.${fieldName} as ${tableAlias}__${fieldName}`;
1501
+ return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
1490
1502
  });
1491
1503
  }
1492
1504
  /** @internal */
@@ -33,5 +33,12 @@ export declare abstract class AbstractSqlPlatform extends Platform {
33
33
  /**
34
34
  * @internal
35
35
  */
36
- getOrderByExpression(column: string, direction: string): string[];
36
+ getOrderByExpression(column: string, direction: string, collation?: string): string[];
37
+ /**
38
+ * Quotes a collation name for use in COLLATE clauses.
39
+ * @internal
40
+ */
41
+ quoteCollation(collation: string): string;
42
+ /** @internal */
43
+ protected validateCollationName(collation: string): void;
37
44
  }
@@ -94,7 +94,24 @@ export class AbstractSqlPlatform extends Platform {
94
94
  /**
95
95
  * @internal
96
96
  */
97
- getOrderByExpression(column, direction) {
97
+ getOrderByExpression(column, direction, collation) {
98
+ if (collation) {
99
+ return [`${column} collate ${this.quoteCollation(collation)} ${direction.toLowerCase()}`];
100
+ }
98
101
  return [`${column} ${direction.toLowerCase()}`];
99
102
  }
103
+ /**
104
+ * Quotes a collation name for use in COLLATE clauses.
105
+ * @internal
106
+ */
107
+ quoteCollation(collation) {
108
+ this.validateCollationName(collation);
109
+ return this.quoteIdentifier(collation);
110
+ }
111
+ /** @internal */
112
+ validateCollationName(collation) {
113
+ if (!/^[\w]+$/.test(collation)) {
114
+ throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
115
+ }
116
+ }
100
117
  }
@@ -41,6 +41,6 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
41
41
  supportsCreatingFullTextIndex(): boolean;
42
42
  getFullTextWhereClause(): string;
43
43
  getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string;
44
- getOrderByExpression(column: string, direction: string): string[];
44
+ getOrderByExpression(column: string, direction: string, collation?: string): string[];
45
45
  getDefaultClientUrl(): string;
46
46
  }
@@ -105,13 +105,14 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
105
105
  const quotedIndexName = this.quoteIdentifier(indexName);
106
106
  return `alter table ${quotedTableName} add fulltext index ${quotedIndexName}(${quotedColumnNames.join(',')})`;
107
107
  }
108
- getOrderByExpression(column, direction) {
108
+ getOrderByExpression(column, direction, collation) {
109
109
  const ret = [];
110
110
  const dir = direction.toLowerCase();
111
+ const col = collation ? `${column} collate ${this.quoteCollation(collation)}` : column;
111
112
  if (dir in this.ORDER_BY_NULLS_TRANSLATE) {
112
- ret.push(`${column} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`);
113
+ ret.push(`${col} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`);
113
114
  }
114
- ret.push(`${column} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`);
115
+ ret.push(`${col} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`);
115
116
  return ret;
116
117
  }
117
118
  getDefaultClientUrl() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.0-dev.246",
3
+ "version": "7.0.0-dev.248",
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
  "type": "module",
6
6
  "exports": {
@@ -56,6 +56,6 @@
56
56
  "@mikro-orm/core": "^6.6.4"
57
57
  },
58
58
  "peerDependencies": {
59
- "@mikro-orm/core": "7.0.0-dev.246"
59
+ "@mikro-orm/core": "7.0.0-dev.248"
60
60
  }
61
61
  }
@@ -57,7 +57,8 @@ type AddToHint<RootAlias, Context, Field extends string, Select extends boolean
57
57
  export type ModifyHint<RootAlias, Context, Hint extends string, Field extends string, Select extends boolean = false> = Hint | AddToHint<RootAlias, Context, Field, Select>;
58
58
  export type ModifyContext<Entity extends object, Context, Field extends string, Alias extends string, Select extends boolean = false> = IsNever<Context> extends true ? AddToContext<GetType<Entity, object, Field>, object, Field, Alias, Select> : Context & AddToContext<GetType<Entity, Context, Field>, Context, Field, Alias, Select>;
59
59
  type StripRootAlias<F extends string, RootAlias extends string, Context = never> = F extends `${RootAlias}.${infer Field}` ? Field : F extends `${infer Alias}.${string}` ? Alias extends AliasNames<Context> ? never : F : F;
60
- type ExtractRootFields<Fields, RootAlias extends string, Context = never> = [Fields] extends ['*'] ? '*' : Fields extends `${RootAlias}.*` ? '*' : Fields extends string ? StripRootAlias<Fields, RootAlias, Context> : never;
60
+ type StripFieldAlias<F extends string> = F extends `${infer Path} as ${string}` ? Path : F;
61
+ type ExtractRootFields<Fields, RootAlias extends string, Context = never> = [Fields] extends ['*'] ? '*' : Fields extends `${RootAlias}.*` ? '*' : Fields extends string ? StripRootAlias<StripFieldAlias<Fields>, RootAlias, Context> : never;
61
62
  type PrefixWithPath<Path extends string, Field extends string> = `${Path}.${Field}`;
62
63
  type StripJoinAlias<F extends string, Alias extends string> = F extends `${Alias}.${infer Field}` ? Field : F;
63
64
  type AddJoinFields<RootAlias, Context, Field extends string, Alias extends string, JoinFields extends readonly string[]> = JoinFields extends readonly (infer F)[] ? F extends string ? PrefixWithPath<AddToHint<RootAlias, Context, Field, true> & string, StripJoinAlias<F, Alias>> : never : never;
@@ -68,17 +69,21 @@ type AliasNames<Context> = Context[keyof Context] extends infer Join ? Join exte
68
69
  type ContextRelationKeys<Context> = Context[keyof Context] extends infer Join ? Join extends any ? Join extends [string, infer Alias, infer Type, any] ? `${Alias & string}.${EntityRelations<Type & object>}` : never : never : never;
69
70
  export type QBField<Entity, RootAlias extends string, Context> = EntityRelations<Entity> | `${RootAlias}.${EntityRelations<Entity>}` | ([Context] extends [never] ? never : ContextRelationKeys<Context>);
70
71
  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;
71
- export type Field<Entity, RootAlias extends string = never, Context = never> = EntityKey<Entity> | (IsNever<RootAlias> extends true ? never : `${RootAlias}.${EntityKey<Entity>}` | `${RootAlias}.*`) | ([Context] extends [never] ? never : ContextFieldKeys<Context> | `${AliasNames<Context>}.*`) | '*' | QueryBuilder<any> | NativeQueryBuilder | RawQueryFragment<any> | (RawQueryFragment & symbol);
72
+ type WithAlias<T extends string> = T | `${T} as ${string}`;
73
+ 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);
72
74
  type RootAliasOrderKeys<RootAlias extends string, Entity> = {
73
75
  [K in `${RootAlias}.${EntityKey<Entity>}`]?: QueryOrderKeysFlat;
74
76
  };
75
77
  type ContextOrderKeys<Context> = {
76
78
  [K in ContextFieldKeys<Context>]?: QueryOrderKeysFlat;
77
79
  };
78
- export type ContextOrderByMap<Entity, RootAlias extends string = never, Context = never> = QueryOrderMap<Entity> | ((IsNever<RootAlias> extends true ? {} : RootAliasOrderKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextOrderKeys<Context>));
80
+ type RawOrderKeys<RawAliases extends string> = {
81
+ [K in RawAliases]?: QueryOrderKeysFlat;
82
+ };
83
+ export type ContextOrderByMap<Entity, RootAlias extends string = never, Context = never, RawAliases extends string = never> = QueryOrderMap<Entity> | ((IsNever<RootAlias> extends true ? {} : RootAliasOrderKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextOrderKeys<Context>) & (IsNever<RawAliases> extends true ? {} : string extends RawAliases ? {} : RawOrderKeys<RawAliases>));
79
84
  type AliasedPath<Alias extends string, Type, P extends string> = P extends `${Alias}.*` ? P : P extends `${Alias}.${infer Rest}` ? `${Alias}.${AutoPath<Type & object, Rest, `${PopulatePath.ALL}`>}` : never;
80
85
  type ContextAliasedPath<Context, P extends string> = Context[keyof Context] extends infer Join ? Join extends any ? Join extends [string, infer Alias, infer Type, any] ? AliasedPath<Alias & string, Type, P> : never : never : never;
81
- type NestedAutoPath<Entity, RootAlias extends string, Context, P extends string> = P extends `${string}:ref` ? never : AliasedPath<RootAlias, Entity, P> | ContextAliasedPath<Context, P> | AutoPath<Entity, P, `${PopulatePath.ALL}`>;
86
+ type NestedAutoPath<Entity, RootAlias extends string, Context, P extends string> = P extends `${string}:ref` ? never : P extends `${infer Path} as ${string}` ? (AliasedPath<RootAlias, Entity, Path> | ContextAliasedPath<Context, Path> | AutoPath<Entity, Path, `${PopulatePath.ALL}`>) extends never ? never : P : AliasedPath<RootAlias, Entity, P> | ContextAliasedPath<Context, P> | AutoPath<Entity, P, `${PopulatePath.ALL}`>;
82
87
  type AliasedObjectQuery<Entity extends object, Alias extends string> = {
83
88
  [K in EntityKey<Entity> as `${Alias}.${K}`]?: ObjectQuery<Entity>[K];
84
89
  };
@@ -86,7 +91,7 @@ type JoinCondition<JoinedEntity extends object, Alias extends string> = ObjectQu
86
91
  type RawJoinCondition = {
87
92
  [key: string]: FilterValue<Scalar> | RawQueryFragment;
88
93
  };
89
- type ExtractRawAliasFromField<F> = F extends RawQueryFragment<infer A> ? (A extends string ? A : never) : never;
94
+ type ExtractRawAliasFromField<F> = F extends RawQueryFragment<infer A> ? (A extends string ? A : never) : F extends `${string} as ${infer A}` ? A : never;
90
95
  type ExtractRawAliasesFromTuple<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Tail] ? ExtractRawAliasFromField<Head> | ExtractRawAliasesFromTuple<Tail> : never;
91
96
  type ExtractRawAliases<Fields> = Fields extends readonly unknown[] ? ExtractRawAliasesFromTuple<Fields> : ExtractRawAliasFromField<Fields>;
92
97
  type FlatOperatorMap = {
@@ -194,6 +199,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
194
199
  protected _joinedProps: Map<string, PopulateOptions<any>>;
195
200
  protected _cache?: boolean | number | [string, number];
196
201
  protected _indexHint?: string;
202
+ protected _collation?: string;
197
203
  protected _comments: string[];
198
204
  protected _hintComments: string[];
199
205
  protected flushMode?: FlushMode;
@@ -228,6 +234,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
228
234
  * // Select with raw expressions
229
235
  * qb.select([raw('count(*) as total')]);
230
236
  *
237
+ * // Select with aliases (works for regular and formula properties)
238
+ * qb.select(['id', 'fullName as displayName']);
239
+ * qb.select(['id', sql.ref('fullName').as('displayName')]);
240
+ *
231
241
  * // Select with distinct
232
242
  * qb.select('*', true);
233
243
  * ```
@@ -503,7 +513,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
503
513
  * qb.orderBy({ 'profile.bio': 'asc' }); // nested via dot notation
504
514
  * ```
505
515
  */
506
- orderBy(orderBy: ContextOrderByMap<Entity, RootAlias, Context> | ContextOrderByMap<Entity, RootAlias, Context>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
516
+ orderBy(orderBy: ContextOrderByMap<Entity, RootAlias, Context, RawAliases> | ContextOrderByMap<Entity, RootAlias, Context, RawAliases>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
507
517
  /**
508
518
  * Adds an ORDER BY clause to the query, replacing any existing order.
509
519
  *
@@ -516,19 +526,21 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
516
526
  * ```
517
527
  */
518
528
  orderBy<const T extends Record<string, QueryOrderKeysFlat>>(orderBy: T & {
519
- [K in keyof T]: K extends NestedAutoPath<Entity, RootAlias, Context, K & string> ? T[K] : never;
529
+ [K in keyof T]: K extends NestedAutoPath<Entity, RootAlias, Context, K & string> ? T[K] : (K extends RawAliases ? T[K] : never);
520
530
  }): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
521
531
  /**
522
532
  * Adds additional ORDER BY clause without replacing existing order.
523
533
  */
524
- andOrderBy(orderBy: ContextOrderByMap<Entity, RootAlias, Context> | ContextOrderByMap<Entity, RootAlias, Context>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
534
+ andOrderBy(orderBy: ContextOrderByMap<Entity, RootAlias, Context, RawAliases> | ContextOrderByMap<Entity, RootAlias, Context, RawAliases>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
525
535
  /**
526
536
  * Adds additional ORDER BY clause without replacing existing order.
527
537
  */
528
538
  andOrderBy<const T extends Record<string, QueryOrderKeysFlat>>(orderBy: T & {
529
- [K in keyof T]: K extends NestedAutoPath<Entity, RootAlias, Context, K & string> ? T[K] : never;
539
+ [K in keyof T]: K extends NestedAutoPath<Entity, RootAlias, Context, K & string> ? T[K] : (K extends RawAliases ? T[K] : never);
530
540
  }): SelectQueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
531
541
  private processOrderBy;
542
+ /** Collect custom aliases from select fields (stored as 'resolved as alias' strings by select()). */
543
+ private getSelectAliases;
532
544
  /**
533
545
  * Adds a GROUP BY clause to the query.
534
546
  *
@@ -611,6 +623,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
611
623
  * Adds index hint to the FROM clause.
612
624
  */
613
625
  indexHint(sql: string | undefined): this;
626
+ /**
627
+ * Adds COLLATE clause to ORDER BY expressions.
628
+ */
629
+ collation(collation: string | undefined): this;
614
630
  /**
615
631
  * Prepend comment to the sql query using the syntax `/* ... *&#8205;/`. Some characters are forbidden such as `/*, *&#8205;/` and `?`.
616
632
  */
@@ -3,6 +3,8 @@ import { JoinType, QueryType } from './enums.js';
3
3
  import { QueryBuilderHelper } from './QueryBuilderHelper.js';
4
4
  import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
5
5
  import { NativeQueryBuilder } from './NativeQueryBuilder.js';
6
+ /** Matches 'path as alias' — safe because ORM property names are JS identifiers (no spaces). */
7
+ const FIELD_ALIAS_RE = /^(.+?)\s+as\s+(\w+)$/i;
6
8
  /**
7
9
  * SQL query builder with fluent interface.
8
10
  *
@@ -67,6 +69,7 @@ export class QueryBuilder {
67
69
  _joinedProps = new Map();
68
70
  _cache;
69
71
  _indexHint;
72
+ _collation;
70
73
  _comments = [];
71
74
  _hintComments = [];
72
75
  flushMode;
@@ -103,8 +106,19 @@ export class QueryBuilder {
103
106
  this.ensureNotFinalized();
104
107
  this._fields = Utils.asArray(fields).flatMap(f => {
105
108
  if (typeof f !== 'string') {
109
+ // Normalize sql.ref('prop') and sql.ref('prop').as('alias') to string form
110
+ if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
111
+ return this.resolveNestedPath(String(f.params[0]));
112
+ }
113
+ if (isRaw(f) && f.sql === '?? as ??' && f.params.length === 2) {
114
+ return `${this.resolveNestedPath(String(f.params[0]))} as ${String(f.params[1])}`;
115
+ }
106
116
  return f;
107
117
  }
118
+ const asMatch = f.match(FIELD_ALIAS_RE);
119
+ if (asMatch) {
120
+ return `${this.resolveNestedPath(asMatch[1].trim())} as ${asMatch[2]}`;
121
+ }
108
122
  return this.resolveNestedPath(f);
109
123
  });
110
124
  if (distinct) {
@@ -457,7 +471,18 @@ export class QueryBuilder {
457
471
  if (reset) {
458
472
  this._orderBy = [];
459
473
  }
460
- Utils.asArray(orderBy).forEach(o => {
474
+ const selectAliases = this.getSelectAliases();
475
+ Utils.asArray(orderBy).forEach(orig => {
476
+ // Shallow clone to avoid mutating the caller's object — safe because the clone
477
+ // is only used within this loop iteration and `orig` is not referenced afterward.
478
+ const o = { ...orig };
479
+ // Wrap known select aliases in raw() so they bypass property validation and alias prefixing
480
+ for (const key of Object.keys(o)) {
481
+ if (selectAliases.has(key)) {
482
+ o[raw('??', [key])] = o[key];
483
+ delete o[key];
484
+ }
485
+ }
461
486
  this.helper.validateQueryOrder(o);
462
487
  const processed = QueryHelper.processWhere({
463
488
  where: o,
@@ -476,10 +501,27 @@ export class QueryBuilder {
476
501
  });
477
502
  return this;
478
503
  }
504
+ /** Collect custom aliases from select fields (stored as 'resolved as alias' strings by select()). */
505
+ getSelectAliases() {
506
+ const aliases = new Set();
507
+ for (const field of this._fields ?? []) {
508
+ if (typeof field === 'string') {
509
+ const m = field.match(FIELD_ALIAS_RE);
510
+ if (m) {
511
+ aliases.add(m[2]);
512
+ }
513
+ }
514
+ }
515
+ return aliases;
516
+ }
479
517
  groupBy(fields) {
480
518
  this.ensureNotFinalized();
481
519
  this._groupBy = Utils.asArray(fields).flatMap(f => {
482
520
  if (typeof f !== 'string') {
521
+ // Normalize sql.ref('prop') to string for proper formula resolution
522
+ if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
523
+ return this.resolveNestedPath(String(f.params[0]));
524
+ }
483
525
  return f;
484
526
  }
485
527
  return this.resolveNestedPath(f);
@@ -638,6 +680,14 @@ export class QueryBuilder {
638
680
  this._indexHint = sql;
639
681
  return this;
640
682
  }
683
+ /**
684
+ * Adds COLLATE clause to ORDER BY expressions.
685
+ */
686
+ collation(collation) {
687
+ this.ensureNotFinalized();
688
+ this._collation = collation;
689
+ return this;
690
+ }
641
691
  /**
642
692
  * Prepend comment to the sql query using the syntax `/* ... *&#8205;/`. Some characters are forbidden such as `/*, *&#8205;/` and `?`.
643
693
  */
@@ -682,7 +732,7 @@ export class QueryBuilder {
682
732
  Utils.runIfNotEmpty(() => qb.groupBy(this.prepareFields(this._groupBy, 'groupBy', schema)), isNotEmptyObject(this._groupBy));
683
733
  Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this._having, qb, undefined, 'having'), isNotEmptyObject(this._having));
684
734
  Utils.runIfNotEmpty(() => {
685
- const queryOrder = this.helper.getQueryOrder(this.type, this._orderBy, this._populateMap);
735
+ const queryOrder = this.helper.getQueryOrder(this.type, this._orderBy, this._populateMap, this._collation);
686
736
  if (queryOrder.length > 0) {
687
737
  const sql = Utils.unique(queryOrder).join(', ');
688
738
  qb.orderBy(sql);
@@ -1015,7 +1065,7 @@ export class QueryBuilder {
1015
1065
  // clone array/object properties
1016
1066
  const properties = [
1017
1067
  'flags', '_populate', '_populateWhere', '_populateFilter', '__populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy',
1018
- '_schema', '_indexHint', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
1068
+ '_schema', '_indexHint', '_collation', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
1019
1069
  '_comments', '_hintComments', 'aliasCounter',
1020
1070
  ];
1021
1071
  for (const prop of Object.keys(this)) {
@@ -1167,14 +1217,23 @@ export class QueryBuilder {
1167
1217
  }
1168
1218
  prepareFields(fields, type = 'where', schema) {
1169
1219
  const ret = [];
1170
- const getFieldName = (name) => {
1171
- return this.helper.mapper(name, this.type, undefined, type === 'groupBy' ? null : undefined, schema);
1220
+ const getFieldName = (name, customAlias) => {
1221
+ const alias = customAlias ?? (type === 'groupBy' ? null : undefined);
1222
+ return this.helper.mapper(name, this.type, undefined, alias, schema);
1172
1223
  };
1173
- fields.forEach(field => {
1174
- if (typeof field !== 'string') {
1175
- ret.push(field);
1224
+ fields.forEach(originalField => {
1225
+ if (typeof originalField !== 'string') {
1226
+ ret.push(originalField);
1176
1227
  return;
1177
1228
  }
1229
+ // Strip 'as alias' suffix if present — the alias is passed to mapper at the end
1230
+ let field = originalField;
1231
+ let customAlias;
1232
+ const asMatch = originalField.match(FIELD_ALIAS_RE);
1233
+ if (asMatch) {
1234
+ field = asMatch[1].trim();
1235
+ customAlias = asMatch[2];
1236
+ }
1178
1237
  const join = Object.keys(this._joins).find(k => field === k.substring(0, k.indexOf('#')));
1179
1238
  if (join && type === 'where') {
1180
1239
  ret.push(...this.helper.mapJoinColumns(this.type, this._joins[join]));
@@ -1192,10 +1251,13 @@ export class QueryBuilder {
1192
1251
  if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
1193
1252
  const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
1194
1253
  const aliased = this._aliases[a] ? `${a}.${name}` : name;
1195
- ret.push(getFieldName(aliased));
1254
+ ret.push(getFieldName(aliased, customAlias));
1196
1255
  return;
1197
1256
  }
1198
1257
  if (prop?.kind === ReferenceKind.EMBEDDED) {
1258
+ if (customAlias) {
1259
+ throw new Error(`Cannot use 'as ${customAlias}' alias on embedded property '${field}' because it expands to multiple columns. Alias individual fields instead (e.g. '${field}.propertyName as ${customAlias}').`);
1260
+ }
1199
1261
  const nest = (prop) => {
1200
1262
  for (const childProp of Object.values(prop.embeddedProps)) {
1201
1263
  if (childProp.fieldNames && (childProp.kind !== ReferenceKind.EMBEDDED || childProp.object) && childProp.persist !== false) {
@@ -1209,11 +1271,14 @@ export class QueryBuilder {
1209
1271
  nest(prop);
1210
1272
  return;
1211
1273
  }
1212
- if (prop && prop.fieldNames.length > 1) {
1274
+ if (prop && prop.fieldNames.length > 1 && !prop.fieldNames.includes(f)) {
1275
+ if (customAlias) {
1276
+ throw new Error(`Cannot use 'as ${customAlias}' alias on '${field}' because it expands to multiple columns (${prop.fieldNames.join(', ')}).`);
1277
+ }
1213
1278
  ret.push(...prop.fieldNames.map(f => getFieldName(f)));
1214
1279
  return;
1215
1280
  }
1216
- ret.push(getFieldName(field));
1281
+ ret.push(getFieldName(field, customAlias));
1217
1282
  });
1218
1283
  const requiresSQLConversion = this.mainAlias.meta.props.filter(p => p.hasConvertToJSValueSQL && p.persist !== false);
1219
1284
  if (this.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) &&
@@ -52,8 +52,8 @@ export declare class QueryBuilderHelper {
52
52
  private getValueReplacement;
53
53
  private getOperatorReplacement;
54
54
  validateQueryOrder<T>(orderBy: QueryOrderMap<T>): void;
55
- getQueryOrder(type: QueryType, orderBy: FlatQueryOrderMap | FlatQueryOrderMap[], populate: Dictionary<string>): string[];
56
- getQueryOrderFromObject(type: QueryType, orderBy: FlatQueryOrderMap, populate: Dictionary<string>): string[];
55
+ getQueryOrder(type: QueryType, orderBy: FlatQueryOrderMap | FlatQueryOrderMap[], populate: Dictionary<string>, collation?: string): string[];
56
+ getQueryOrderFromObject(type: QueryType, orderBy: FlatQueryOrderMap, populate: Dictionary<string>, collation?: string): string[];
57
57
  finalize(type: QueryType, qb: NativeQueryBuilder, meta?: EntityMetadata, data?: Dictionary, returning?: InternalField<any>[]): void;
58
58
  splitField<T>(field: EntityKey<T>, greedyAlias?: boolean): [string, EntityKey<T>, string | undefined];
59
59
  getLockSQL(qb: NativeQueryBuilder, lockMode: LockMode, lockTables?: string[], joinsMap?: Dictionary<JoinOptions>): void;
@@ -110,8 +110,8 @@ export class QueryBuilderHelper {
110
110
  }
111
111
  if (prop?.formula) {
112
112
  const alias2 = this.platform.quoteIdentifier(a).toString();
113
- const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]).toString();
114
- const as = alias === null ? '' : ` as ${aliased}`;
113
+ const aliasName = alias === undefined ? prop.fieldNames[0] : alias;
114
+ const as = aliasName === null ? '' : ` as ${this.platform.quoteIdentifier(aliasName)}`;
115
115
  const meta = this.aliasMap[a]?.meta ?? this.metadata.get(this.entityName);
116
116
  const table = this.createFormulaTable(alias2, meta, schema);
117
117
  const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2);
@@ -605,20 +605,20 @@ export class QueryBuilderHelper {
605
605
  ].join('\n'));
606
606
  }
607
607
  }
608
- getQueryOrder(type, orderBy, populate) {
608
+ getQueryOrder(type, orderBy, populate, collation) {
609
609
  if (Array.isArray(orderBy)) {
610
- return orderBy.flatMap(o => this.getQueryOrder(type, o, populate));
610
+ return orderBy.flatMap(o => this.getQueryOrder(type, o, populate, collation));
611
611
  }
612
- return this.getQueryOrderFromObject(type, orderBy, populate);
612
+ return this.getQueryOrderFromObject(type, orderBy, populate, collation);
613
613
  }
614
- getQueryOrderFromObject(type, orderBy, populate) {
614
+ getQueryOrderFromObject(type, orderBy, populate, collation) {
615
615
  const ret = [];
616
616
  for (const key of Utils.getObjectQueryKeys(orderBy)) {
617
617
  const direction = orderBy[key];
618
618
  const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction;
619
619
  if (Raw.isKnownFragmentSymbol(key)) {
620
620
  const raw = Raw.getKnownFragment(key);
621
- ret.push(...this.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order));
621
+ ret.push(...this.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order, collation));
622
622
  continue;
623
623
  }
624
624
  for (const f of Utils.splitPrimaryKeys(key)) {
@@ -638,10 +638,10 @@ export class QueryBuilderHelper {
638
638
  colPart = this.platform.formatQuery(colPart.sql, colPart.params);
639
639
  }
640
640
  if (Array.isArray(order)) {
641
- order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate)));
641
+ order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate, collation)));
642
642
  }
643
643
  else {
644
- ret.push(...this.platform.getOrderByExpression(colPart, order));
644
+ ret.push(...this.platform.getOrderByExpression(colPart, order, collation));
645
645
  }
646
646
  }
647
647
  }