@mikro-orm/sql 7.1.6-dev.14 → 7.1.6-dev.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/query/QueryBuilder.d.ts +16 -0
- package/query/QueryBuilder.js +82 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.6-dev.
|
|
3
|
+
"version": "7.1.6-dev.16",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"copy": "node ../../scripts/copy.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"kysely": "0.29.
|
|
50
|
+
"kysely": "0.29.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@mikro-orm/core": "^7.1.5"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.6-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.6-dev.16"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -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
|
package/query/QueryBuilder.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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();
|