@mikro-orm/sql 7.1.6-dev.9 → 7.1.7-dev.0

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.
@@ -494,6 +494,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
494
494
  if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
495
495
  return;
496
496
  }
497
+ // inline embeddables are mapped via their flattened child props; mapping the embedded root prop
498
+ // here would leak a raw column value into the snapshot and produce a phantom changeset
499
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.object && !meta.embeddable) {
500
+ return;
501
+ }
497
502
  if (prop.polymorphic && prop.kind !== ReferenceKind.EMBEDDED) {
498
503
  const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
499
504
  const discriminatorValue = root[discriminatorAlias];
@@ -9,10 +9,10 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
9
9
  protected readonly schemaHelper: MySqlSchemaHelper;
10
10
  protected readonly exceptionConverter: MySqlExceptionConverter;
11
11
  protected readonly ORDER_BY_NULLS_TRANSLATE: {
12
- readonly "asc nulls first": "is not null";
13
- readonly "asc nulls last": "is null";
14
- readonly "desc nulls first": "is not null";
15
- readonly "desc nulls last": "is null";
12
+ readonly "asc nulls first": 'is not null';
13
+ readonly "asc nulls last": 'is null';
14
+ readonly "desc nulls first": 'is not null';
15
+ readonly "desc nulls last": 'is null';
16
16
  };
17
17
  supportsMultiColumnCountDistinct(): boolean;
18
18
  /** @internal */
@@ -9,6 +9,7 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
9
9
  static readonly DEFAULT_VALUES: {
10
10
  'now()': string[];
11
11
  'current_timestamp(?)': string[];
12
+ 'curdate()': string[];
12
13
  '0': string[];
13
14
  };
14
15
  private static readonly PARTIAL_INDEX_RE;
@@ -5,6 +5,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
5
5
  static DEFAULT_VALUES = {
6
6
  'now()': ['now()', 'current_timestamp'],
7
7
  'current_timestamp(?)': ['current_timestamp(?)'],
8
+ 'curdate()': ['(current_date)', 'curdate()'],
8
9
  '0': ['0', 'false'],
9
10
  };
10
11
  // Greedy `(.+)` so nested CASE expressions inside the predicate don't trip the match on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.6-dev.9",
3
+ "version": "7.1.7-dev.0",
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.2"
50
+ "kysely": "0.29.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.5"
53
+ "@mikro-orm/core": "^7.1.6"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.6-dev.9"
56
+ "@mikro-orm/core": "7.1.7-dev.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -9,13 +9,13 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
9
9
  protected readonly metadata: MetadataStorage;
10
10
  readonly entityName: EntityName<T>;
11
11
  readonly parent?: ICriteriaNode<T> | undefined;
12
- readonly key?: (EntityKey<T> | RawQueryFragmentSymbol) | undefined;
12
+ readonly key?: EntityKey<T> | RawQueryFragmentSymbol | undefined;
13
13
  readonly validate: boolean;
14
14
  readonly strict: boolean;
15
15
  payload: any;
16
16
  prop?: EntityProperty<T>;
17
17
  index?: number;
18
- constructor(metadata: MetadataStorage, entityName: EntityName<T>, parent?: ICriteriaNode<T> | undefined, key?: (EntityKey<T> | RawQueryFragmentSymbol) | undefined, validate?: boolean, strict?: boolean);
18
+ constructor(metadata: MetadataStorage, entityName: EntityName<T>, parent?: ICriteriaNode<T> | undefined, key?: EntityKey<T> | RawQueryFragmentSymbol | undefined, validate?: boolean, strict?: boolean);
19
19
  process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
20
20
  unwrap(): any;
21
21
  shouldInline(payload: any): boolean;
@@ -236,7 +236,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
236
236
  #private;
237
237
  protected readonly metadata: MetadataStorage;
238
238
  protected readonly driver: AbstractSqlDriver;
239
- protected readonly context?: Transaction | undefined;
239
+ protected readonly context?: Transaction;
240
240
  protected connectionType?: ConnectionType | undefined;
241
241
  protected em?: SqlEntityManager | undefined;
242
242
  protected loggerContext?: (LoggingOptions & Dictionary) | undefined;
@@ -253,7 +253,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
253
253
  /**
254
254
  * @internal
255
255
  */
256
- constructor(entityName: EntityName<Entity> | QueryBuilder<Entity, any, any, any>, metadata: MetadataStorage, driver: AbstractSqlDriver, context?: Transaction | undefined, alias?: string, connectionType?: ConnectionType | undefined, em?: SqlEntityManager | undefined, loggerContext?: (LoggingOptions & Dictionary) | undefined);
256
+ constructor(entityName: EntityName<Entity> | QueryBuilder<Entity, any, any, any>, metadata: MetadataStorage, driver: AbstractSqlDriver, context?: Transaction, alias?: string, connectionType?: ConnectionType | undefined, em?: SqlEntityManager | undefined, loggerContext?: (LoggingOptions & Dictionary) | undefined);
257
257
  /**
258
258
  * Creates a SELECT query, specifying the fields to retrieve.
259
259
  *
@@ -982,6 +982,22 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
982
982
  */
983
983
  private processNestedJoins;
984
984
  private hasToManyJoins;
985
+ /**
986
+ * Resolves a `persist: false` virtual field referenced in `orderBy` (e.g. `qb.as('reviewCount')` or a
987
+ * `raw()` fragment aliased in the select) to the alias and SQL expression it was selected under. The
988
+ * alias is used to order the outer query; the raw expression is inlined into `min(...)` inside the
989
+ * pagination sub-query, where dialects like PostgreSQL cannot reference a select alias.
990
+ */
991
+ protected resolveVirtualField(propName: string, fieldName: string): {
992
+ alias: string;
993
+ expr: string;
994
+ } | undefined;
995
+ /**
996
+ * Rewrites `orderBy` keys that reference a `persist: false` virtual field selected via `qb.as(alias)` so
997
+ * they point at the actual select alias. The mapper would otherwise resolve such a key to the property's
998
+ * naming-strategy column name, which does not exist when the alias literal differs (e.g. `reviewCount`).
999
+ */
1000
+ private rewriteVirtualFieldOrderBy;
985
1001
  protected wrapPaginateSubQuery(meta: EntityMetadata): void;
986
1002
  /**
987
1003
  * Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
@@ -4,6 +4,12 @@ import { JoinType, QueryType } from './enums.js';
4
4
  import { QueryBuilderHelper } from './QueryBuilderHelper.js';
5
5
  import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
6
6
  import { NativeQueryBuilder } from './NativeQueryBuilder.js';
7
+ /**
8
+ * Tags a {@link NativeQueryBuilder} produced by `qb.as(alias)` with the alias literal, so it can be
9
+ * matched back to its `orderBy`/pagination usage. A symbol is used (rather than a string key) so the
10
+ * tag survives `clone()` — {@link Utils.copy} preserves non-enumerable symbols but drops string keys.
11
+ */
12
+ const VirtualFieldAlias = Symbol('virtualFieldAlias');
7
13
  /** Matches 'path as alias' — safe because ORM property names are JS identifiers (no spaces). */
8
14
  const FIELD_ALIAS_RE = /^(.+?)\s+as\s+(\w+)$/i;
9
15
  /**
@@ -816,7 +822,7 @@ export class QueryBuilder {
816
822
  Utils.runIfNotEmpty(() => qb.groupBy(this.prepareFields(this.#state.groupBy, 'groupBy', schema)), isNotEmptyObject(this.#state.groupBy));
817
823
  Utils.runIfNotEmpty(() => this.helper.appendQueryCondition(this.type, this.#state.having, qb, undefined, 'having'), isNotEmptyObject(this.#state.having));
818
824
  Utils.runIfNotEmpty(() => {
819
- const queryOrder = this.helper.getQueryOrder(this.type, this.#state.orderBy, this.#state.populateMap, this.#state.collation);
825
+ const queryOrder = this.helper.getQueryOrder(this.type, this.rewriteVirtualFieldOrderBy(this.#state.orderBy), this.#state.populateMap, this.#state.collation);
820
826
  if (queryOrder.length > 0) {
821
827
  const sql = Utils.unique(queryOrder).join(', ');
822
828
  qb.orderBy(sql);
@@ -1195,8 +1201,8 @@ export class QueryBuilder {
1195
1201
  finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
1196
1202
  }
1197
1203
  qb.as(finalAlias);
1198
- // tag the instance, so it is possible to detect it easily
1199
- Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
1204
+ // tag the instance, so it is possible to detect it easily (symbol survives clone, see VirtualFieldAlias)
1205
+ Object.defineProperty(qb, VirtualFieldAlias, { value: finalAlias });
1200
1206
  return qb;
1201
1207
  }
1202
1208
  /**
@@ -2070,6 +2076,72 @@ export class QueryBuilder {
2070
2076
  return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
2071
2077
  });
2072
2078
  }
2079
+ /**
2080
+ * Resolves a `persist: false` virtual field referenced in `orderBy` (e.g. `qb.as('reviewCount')` or a
2081
+ * `raw()` fragment aliased in the select) to the alias and SQL expression it was selected under. The
2082
+ * alias is used to order the outer query; the raw expression is inlined into `min(...)` inside the
2083
+ * pagination sub-query, where dialects like PostgreSQL cannot reference a select alias.
2084
+ */
2085
+ resolveVirtualField(propName, fieldName) {
2086
+ const field = this.#state.fields?.find(f => {
2087
+ if (f instanceof NativeQueryBuilder) {
2088
+ const as = f[VirtualFieldAlias];
2089
+ return as === propName || as === fieldName;
2090
+ }
2091
+ // not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
2092
+ return isRaw(f) && (f.sql.includes(propName) || f.sql.includes(fieldName));
2093
+ });
2094
+ if (field instanceof NativeQueryBuilder) {
2095
+ const alias = field[VirtualFieldAlias];
2096
+ const rendered = field.toString();
2097
+ // strip the known trailing alias via the platform quoting, so it works on every dialect (mssql uses `[...]`)
2098
+ const suffix = ` as ${this.platform.quoteIdentifier(alias)}`;
2099
+ return { alias, expr: rendered.endsWith(suffix) ? rendered.slice(0, -suffix.length) : rendered };
2100
+ }
2101
+ if (isRaw(field)) {
2102
+ const compiled = this.platform.formatQuery(field.sql, field.params);
2103
+ const close = compiled[compiled.length - 1];
2104
+ const open = close === ']' ? '[' : close === '"' ? '"' : close === '`' ? '`' : undefined;
2105
+ // scan back for the trailing `as <quoted alias>` (linear, avoids regex backtracking on user SQL)
2106
+ const start = open ? compiled.lastIndexOf(open, compiled.length - 2) : -1;
2107
+ if (start >= 4 && compiled.slice(start - 4, start) === ' as ') {
2108
+ return { alias: compiled.slice(start + 1, -1), expr: compiled.slice(0, start - 4) };
2109
+ }
2110
+ return { alias: fieldName, expr: compiled };
2111
+ }
2112
+ return undefined;
2113
+ }
2114
+ /**
2115
+ * Rewrites `orderBy` keys that reference a `persist: false` virtual field selected via `qb.as(alias)` so
2116
+ * they point at the actual select alias. The mapper would otherwise resolve such a key to the property's
2117
+ * naming-strategy column name, which does not exist when the alias literal differs (e.g. `reviewCount`).
2118
+ */
2119
+ rewriteVirtualFieldOrderBy(orderBy) {
2120
+ // only aliased sub-queries (`qb.as(...)`) or `raw()` fragments can back a virtual field, skip otherwise
2121
+ if (!this.#state.fields?.some(f => f instanceof NativeQueryBuilder || isRaw(f))) {
2122
+ return orderBy;
2123
+ }
2124
+ return orderBy.map(orderMap => {
2125
+ const out = {};
2126
+ for (const key of Utils.getObjectQueryKeys(orderMap)) {
2127
+ const direction = orderMap[key];
2128
+ if (!RawQueryFragment.isKnownFragmentSymbol(key)) {
2129
+ const [a, f] = this.helper.splitField(key);
2130
+ const prop = this.helper.getProperty(f, a);
2131
+ if (prop?.persist === false && !prop.formula && !prop.embedded) {
2132
+ const fieldName = this.helper.mapper(key, this.type, undefined, null);
2133
+ const virtual = this.resolveVirtualField(f, fieldName);
2134
+ if (virtual) {
2135
+ out[raw(this.platform.quoteIdentifier(virtual.alias))] = direction;
2136
+ continue;
2137
+ }
2138
+ }
2139
+ }
2140
+ out[key] = direction;
2141
+ }
2142
+ return out;
2143
+ });
2144
+ }
2073
2145
  wrapPaginateSubQuery(meta) {
2074
2146
  const schema = this.getSchema(this.mainAlias);
2075
2147
  const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
@@ -2086,7 +2158,6 @@ export class QueryBuilder {
2086
2158
  if (this.#state.offset) {
2087
2159
  subQuery.offset(this.#state.offset);
2088
2160
  }
2089
- const addToSelect = [];
2090
2161
  if (this.#state.orderBy.length > 0) {
2091
2162
  const orderBy = [];
2092
2163
  for (const orderMap of this.#state.orderBy) {
@@ -2100,42 +2171,19 @@ export class QueryBuilder {
2100
2171
  const prop = this.helper.getProperty(f, a);
2101
2172
  const type = this.platform.castColumn(prop);
2102
2173
  const fieldName = this.helper.mapper(field, this.type, undefined, null);
2103
- if (!prop?.persist && !prop?.formula && !prop?.hasConvertToJSValueSQL && !pks.includes(fieldName)) {
2104
- addToSelect.push(fieldName);
2105
- }
2106
- const quoted = this.platform.quoteIdentifier(fieldName);
2107
- const key = raw(`min(${quoted}${type})`);
2108
- orderBy.push({ [key]: direction });
2174
+ // virtual fields (e.g. `qb.as(...)`) have no column to reference inside `min()`; inline their
2175
+ // expression instead, as a select alias is not resolvable there on some dialects (e.g. PostgreSQL)
2176
+ const virtual = !prop?.persist && !prop?.formula && !prop?.hasConvertToJSValueSQL && !pks.includes(fieldName)
2177
+ ? this.resolveVirtualField(f, fieldName)
2178
+ : undefined;
2179
+ const expr = virtual ? virtual.expr : this.platform.quoteIdentifier(fieldName);
2180
+ orderBy.push({ [raw(`min(${expr}${type})`)]: direction });
2109
2181
  }
2110
2182
  }
2111
2183
  subQuery.orderBy(orderBy);
2112
2184
  }
2113
2185
  subQuery.#state.finalized = true;
2114
2186
  const innerQuery = subQuery.as(this.mainAlias.aliasName).clear('select').select(pks);
2115
- if (addToSelect.length > 0) {
2116
- addToSelect.forEach(prop => {
2117
- const field = this.#state.fields.find(field => {
2118
- if (typeof field === 'object' && field && '__as' in field) {
2119
- return field.__as === prop;
2120
- }
2121
- if (isRaw(field)) {
2122
- // not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
2123
- return field.sql.includes(prop);
2124
- }
2125
- return false;
2126
- });
2127
- /* v8 ignore next */
2128
- if (isRaw(field)) {
2129
- innerQuery.select(field);
2130
- }
2131
- else if (field instanceof NativeQueryBuilder) {
2132
- innerQuery.select(field.toRaw());
2133
- }
2134
- else if (field) {
2135
- innerQuery.select(field);
2136
- }
2137
- });
2138
- }
2139
2187
  // multiple sub-queries are needed to get around mysql limitations with order by + limit + where in + group by (o.O)
2140
2188
  // https://stackoverflow.com/questions/17892762/mysql-this-version-of-mysql-doesnt-yet-support-limit-in-all-any-some-subqu
2141
2189
  const subSubQuery = this.platform.createNativeQueryBuilder();
@@ -230,6 +230,12 @@ export class DatabaseSchema {
230
230
  table.addIndex(meta, { properties: pkPropsForIndex.map(prop => prop.name) }, 'primary');
231
231
  for (const check of meta.checks) {
232
232
  const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined;
233
+ // In TPT, inherited columns live on the parent table only, so a check
234
+ // that references such a column must not be emitted onto the child
235
+ // table, where that column does not exist.
236
+ if (meta.inheritanceType === 'tpt' && meta.tptParent && columnName && !table.hasColumn(columnName)) {
237
+ continue;
238
+ }
233
239
  const expression = isRaw(check.expression)
234
240
  ? platform.formatQuery(check.expression.sql, check.expression.params)
235
241
  : check.expression;
@@ -894,7 +894,8 @@ export class SchemaComparator {
894
894
  // e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
895
895
  const simplify = (str) => {
896
896
  return (str
897
- ?.replace(/_\w+'(.*?)'/g, '$1')
897
+ // lookbehind: only strip a real charset introducer, never an underscore inside a literal like 'a_b'
898
+ ?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
898
899
  .replace(/!=/g, '<>')
899
900
  .replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
900
901
  // MySQL normalizes count(*) to count(0)
package/typings.d.ts CHANGED
@@ -359,7 +359,10 @@ type InferColumnValue<TBuilder, TProcessOnCreate extends boolean> = TBuilder ext
359
359
  value: infer Value;
360
360
  };
361
361
  '~options': infer TOptions;
362
- } ? MaybeNever<MaybeGenerated<MaybeJoinKey<Value, TOptions>, TOptions, TProcessOnCreate>, TOptions> : never;
362
+ } ? MaybeNever<MaybeGenerated<MaybeJoinKey<MaybeArray<Value, TOptions>, TOptions>, TOptions, TProcessOnCreate>, TOptions> : never;
363
+ type MaybeArray<TValue, TOptions> = TOptions extends {
364
+ array: true;
365
+ } ? TValue[] : TValue;
363
366
  type MaybeGenerated<TValue, TOptions, TProcessOnCreate extends boolean> = TOptions extends {
364
367
  nullable: true;
365
368
  } ? TValue | null : TOptions extends {