Files
evento/node_modules/@mikro-orm/sql/query/QueryBuilder.js
2026-03-18 14:55:56 -03:00

2313 lines
82 KiB
JavaScript

var _a;
import {
EntityMetadata,
helper,
inspect,
isRaw,
LoadStrategy,
LockMode,
PopulateHint,
QueryFlag,
QueryHelper,
raw,
RawQueryFragment,
Reference,
ReferenceKind,
serialize,
Utils,
ValidationError,
} from '@mikro-orm/core';
import { JoinType, QueryType } from './enums.js';
import { QueryBuilderHelper } from './QueryBuilderHelper.js';
import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
import { NativeQueryBuilder } from './NativeQueryBuilder.js';
/** Matches 'path as alias' — safe because ORM property names are JS identifiers (no spaces). */
const FIELD_ALIAS_RE = /^(.+?)\s+as\s+(\w+)$/i;
/**
* SQL query builder with fluent interface.
*
* ```ts
* const qb = orm.em.createQueryBuilder(Publisher);
* qb.select('*')
* .where({
* name: 'test 123',
* type: PublisherType.GLOBAL,
* })
* .orderBy({
* name: QueryOrder.DESC,
* type: QueryOrder.ASC,
* })
* .limit(2, 1);
*
* const publisher = await qb.getSingleResult();
* ```
*/
export class QueryBuilder {
metadata;
driver;
context;
connectionType;
em;
loggerContext;
#state = _a.createDefaultState();
#helper;
#query;
/** @internal */
static createDefaultState() {
return {
aliasCounter: 0,
explicitAlias: false,
populateHintFinalized: false,
joins: {},
cond: {},
orderBy: [],
groupBy: [],
having: {},
comments: [],
hintComments: [],
subQueries: {},
aliases: {},
tptAlias: {},
ctes: [],
tptJoinsApplied: false,
autoJoinedPaths: [],
populate: [],
populateMap: {},
flags: new Set([QueryFlag.CONVERT_CUSTOM_TYPES]),
finalized: false,
joinedProps: new Map(),
};
}
get mainAlias() {
this.ensureFromClause();
return this.#state.mainAlias;
}
get alias() {
return this.mainAlias.aliasName;
}
get helper() {
this.ensureFromClause();
return this.#helper;
}
get type() {
return this.#state.type ?? QueryType.SELECT;
}
/** @internal */
get state() {
return this.#state;
}
platform;
/**
* @internal
*/
constructor(entityName, metadata, driver, context, alias, connectionType, em, loggerContext) {
this.metadata = metadata;
this.driver = driver;
this.context = context;
this.connectionType = connectionType;
this.em = em;
this.loggerContext = loggerContext;
this.platform = this.driver.getPlatform();
if (alias) {
this.#state.aliasCounter++;
this.#state.explicitAlias = true;
}
// @ts-expect-error union type does not match the overloaded method signature
this.from(entityName, alias);
}
select(fields, distinct = false) {
this.ensureNotFinalized();
this.#state.fields = Utils.asArray(fields).flatMap(f => {
if (typeof f !== 'string') {
// Normalize sql.ref('prop') and sql.ref('prop').as('alias') to string form
if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
return this.resolveNestedPath(String(f.params[0]));
}
if (isRaw(f) && f.sql === '?? as ??' && f.params.length === 2) {
return `${this.resolveNestedPath(String(f.params[0]))} as ${String(f.params[1])}`;
}
return f;
}
const asMatch = FIELD_ALIAS_RE.exec(f);
if (asMatch) {
return `${this.resolveNestedPath(asMatch[1].trim())} as ${asMatch[2]}`;
}
return this.resolveNestedPath(f);
});
if (distinct) {
this.#state.flags.add(QueryFlag.DISTINCT);
}
return this.init(QueryType.SELECT);
}
/**
* Adds fields to an existing SELECT query.
*/
addSelect(fields) {
this.ensureNotFinalized();
if (this.#state.type && this.#state.type !== QueryType.SELECT) {
return this;
}
return this.select([...Utils.asArray(this.#state.fields), ...Utils.asArray(fields)]);
}
distinct() {
this.ensureNotFinalized();
return this.setFlag(QueryFlag.DISTINCT);
}
distinctOn(fields) {
this.ensureNotFinalized();
this.#state.distinctOn = Utils.asArray(fields);
return this;
}
/**
* Creates an INSERT query with the given data.
*
* @example
* ```ts
* await em.createQueryBuilder(User)
* .insert({ name: 'John', email: 'john@example.com' })
* .execute();
*
* // Bulk insert
* await em.createQueryBuilder(User)
* .insert([{ name: 'John' }, { name: 'Jane' }])
* .execute();
* ```
*/
insert(data) {
return this.init(QueryType.INSERT, data);
}
/**
* Creates an UPDATE query with the given data.
* Use `where()` to specify which rows to update.
*
* @example
* ```ts
* await em.createQueryBuilder(User)
* .update({ name: 'John Doe' })
* .where({ id: 1 })
* .execute();
* ```
*/
update(data) {
return this.init(QueryType.UPDATE, data);
}
/**
* Creates a DELETE query.
* Use `where()` to specify which rows to delete.
*
* @example
* ```ts
* await em.createQueryBuilder(User)
* .delete()
* .where({ id: 1 })
* .execute();
*
* // Or pass the condition directly
* await em.createQueryBuilder(User)
* .delete({ isActive: false })
* .execute();
* ```
*/
delete(cond) {
return this.init(QueryType.DELETE, undefined, cond);
}
/**
* Creates a TRUNCATE query to remove all rows from the table.
*/
truncate() {
return this.init(QueryType.TRUNCATE);
}
/**
* Creates a COUNT query to count matching rows.
*
* @example
* ```ts
* const count = await em.createQueryBuilder(User)
* .count()
* .where({ isActive: true })
* .execute('get');
* ```
*/
count(field, distinct = false) {
if (field) {
this.#state.fields = Utils.asArray(field);
} else if (distinct || this.hasToManyJoins()) {
this.#state.fields = this.mainAlias.meta.primaryKeys;
} else {
this.#state.fields = [raw('*')];
}
if (distinct) {
this.#state.flags.add(QueryFlag.DISTINCT);
}
return this.init(QueryType.COUNT);
}
join(field, alias, cond = {}, type = JoinType.innerJoin, path, schema) {
this.joinReference(field, alias, cond, type, path, schema);
return this;
}
innerJoin(field, alias, cond = {}, schema) {
this.join(field, alias, cond, JoinType.innerJoin, undefined, schema);
return this;
}
innerJoinLateral(field, alias, cond = {}, schema) {
return this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
}
leftJoin(field, alias, cond = {}, schema) {
return this.join(field, alias, cond, JoinType.leftJoin, undefined, schema);
}
leftJoinLateral(field, alias, cond = {}, schema) {
return this.join(field, alias, cond, JoinType.leftJoinLateral, undefined, schema);
}
/**
* Adds a JOIN clause and automatically selects the joined entity's fields.
* This is useful for eager loading related entities.
*
* @example
* ```ts
* const qb = em.createQueryBuilder(Book, 'b');
* qb.select('*')
* .joinAndSelect('b.author', 'a')
* .where({ 'a.name': 'John' });
* ```
*/
joinAndSelect(field, alias, cond = {}, type = JoinType.innerJoin, path, fields, schema) {
if (!this.#state.type) {
this.select('*');
}
let subquery;
if (Array.isArray(field)) {
const rawFragment = field[1] instanceof _a ? field[1].toRaw() : field[1];
subquery = this.platform.formatQuery(rawFragment.sql, rawFragment.params);
field = field[0];
}
const { prop, key } = this.joinReference(field, alias, cond, type, path, schema, subquery);
const [fromAlias] = this.helper.splitField(field);
if (subquery) {
this.#state.joins[key].subquery = subquery;
}
const populate = this.#state.joinedProps.get(fromAlias);
const item = { field: prop.name, strategy: LoadStrategy.JOINED, children: [] };
if (populate) {
populate.children.push(item);
} else {
// root entity
this.#state.populate.push(item);
}
this.#state.joinedProps.set(alias, item);
this.addSelect(this.getFieldsForJoinedLoad(prop, alias, fields));
return this;
}
leftJoinAndSelect(field, alias, cond = {}, fields, schema) {
return this.joinAndSelect(field, alias, cond, JoinType.leftJoin, undefined, fields, schema);
}
leftJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
return this;
}
innerJoinAndSelect(field, alias, cond = {}, fields, schema) {
return this.joinAndSelect(field, alias, cond, JoinType.innerJoin, undefined, fields, schema);
}
innerJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
return this;
}
getFieldsForJoinedLoad(prop, alias, explicitFields) {
const fields = [];
const populate = [];
const joinKey = Object.keys(this.#state.joins).find(join => join.endsWith(`#${alias}`));
const targetMeta = prop.targetMeta;
const schema = this.#state.schema ?? (targetMeta.schema !== '*' ? targetMeta.schema : undefined);
if (joinKey) {
const path = this.#state.joins[joinKey].path.split('.').slice(1);
let children = this.#state.populate;
for (let i = 0; i < path.length; i++) {
const child = children.filter(hint => {
const [propName] = hint.field.split(':', 2);
return propName === path[i];
});
children = child.flatMap(c => c.children);
}
populate.push(...children);
}
for (const p of targetMeta.getPrimaryProps()) {
fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
}
if (explicitFields && explicitFields.length > 0) {
for (const field of explicitFields) {
const [a, f] = this.helper.splitField(field);
const p = targetMeta.properties[f];
if (p) {
fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
} else {
fields.push(`${a}.${f} as ${a}__${f}`);
}
}
}
targetMeta.props
.filter(prop => {
if (!explicitFields || explicitFields.length === 0) {
return this.platform.shouldHaveColumn(prop, populate);
}
return prop.primary && !explicitFields.includes(prop.name) && !explicitFields.includes(`${alias}.${prop.name}`);
})
.forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias, targetMeta, schema)));
return fields;
}
/**
* Apply filters to the QB where condition.
*/
async applyFilters(filterOptions = {}) {
/* v8 ignore next */
if (!this.em) {
throw new Error('Cannot apply filters, this QueryBuilder is not attached to an EntityManager');
}
const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
this.andWhere(cond);
}
/**
* @internal
*/
scheduleFilterCheck(path) {
this.#state.autoJoinedPaths.push(path);
}
/**
* @internal
*/
async applyJoinedFilters(em, filterOptions) {
for (const path of this.#state.autoJoinedPaths) {
const join = this.getJoinForPath(path);
if (join.type === JoinType.pivotJoin) {
continue;
}
filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
let cond = await em.applyFilters(join.prop.targetMeta.class, join.cond, filterOptions, 'read');
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, join.prop.targetMeta.class, cond);
cond = criteriaNode.process(this, {
matchPopulateJoins: true,
filter: true,
alias: join.alias,
ignoreBranching: true,
parentPath: join.path,
});
if (Utils.hasObjectKeys(cond) || RawQueryFragment.hasObjectFragments(cond)) {
// remove nested filters, we only care about scalars here, nesting would require another join branch
for (const key of Object.keys(cond)) {
if (
Utils.isPlainObject(cond[key]) &&
Object.keys(cond[key]).every(
k => !(Utils.isOperator(k) && !['$some', '$none', '$every', '$size'].includes(k)),
)
) {
delete cond[key];
}
}
if (Utils.hasObjectKeys(join.cond) || RawQueryFragment.hasObjectFragments(join.cond)) {
/* v8 ignore next */
join.cond = { $and: [join.cond, cond] };
} else {
join.cond = { ...cond };
}
}
}
}
withSubQuery(subQuery, alias) {
this.ensureNotFinalized();
if (isRaw(subQuery)) {
this.#state.subQueries[alias] = this.platform.formatQuery(subQuery.sql, subQuery.params);
} else {
this.#state.subQueries[alias] = subQuery.toString();
}
return this;
}
where(cond, params, operator) {
this.ensureNotFinalized();
let processedCond;
if (isRaw(cond)) {
const sql = this.platform.formatQuery(cond.sql, cond.params);
processedCond = { [raw(`(${sql})`)]: Utils.asArray(params) };
operator ??= '$and';
} else if (typeof cond === 'string') {
processedCond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
operator ??= '$and';
} else {
processedCond = QueryHelper.processWhere({
where: cond,
entityName: this.mainAlias.entityName,
metadata: this.metadata,
platform: this.platform,
aliasMap: this.getAliasMap(),
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
convertCustomTypes: this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES),
});
}
const op = operator || params;
const topLevel =
!op || !(Utils.hasObjectKeys(this.#state.cond) || RawQueryFragment.hasObjectFragments(this.#state.cond));
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processedCond);
const ignoreBranching = this.#state.resolvedPopulateWhere === 'infer';
if (
[QueryType.UPDATE, QueryType.DELETE].includes(this.type) &&
criteriaNode.willAutoJoin(this, undefined, { ignoreBranching })
) {
// use sub-query to support joining
this.setFlag(this.type === QueryType.UPDATE ? QueryFlag.UPDATE_SUB_QUERY : QueryFlag.DELETE_SUB_QUERY);
this.select(this.mainAlias.meta.primaryKeys, true);
}
if (topLevel) {
this.#state.cond = criteriaNode.process(this, { ignoreBranching });
} else if (Array.isArray(this.#state.cond[op])) {
this.#state.cond[op].push(criteriaNode.process(this, { ignoreBranching }));
} else {
const cond1 = [this.#state.cond, criteriaNode.process(this, { ignoreBranching })];
this.#state.cond = { [op]: cond1 };
}
if (this.#state.onConflict) {
this.#state.onConflict[this.#state.onConflict.length - 1].where = this.helper.processOnConflictCondition(
this.#state.cond,
this.#state.schema,
);
this.#state.cond = {};
}
return this;
}
andWhere(cond, params) {
return this.where(cond, params, '$and');
}
orWhere(cond, params) {
return this.where(cond, params, '$or');
}
orderBy(orderBy) {
return this.processOrderBy(orderBy, true);
}
andOrderBy(orderBy) {
return this.processOrderBy(orderBy, false);
}
processOrderBy(orderBy, reset = true) {
this.ensureNotFinalized();
if (reset) {
this.#state.orderBy = [];
}
const selectAliases = this.getSelectAliases();
Utils.asArray(orderBy).forEach(orig => {
// Shallow clone to avoid mutating the caller's object — safe because the clone
// is only used within this loop iteration and `orig` is not referenced afterward.
const o = { ...orig };
// Wrap known select aliases in raw() so they bypass property validation and alias prefixing
for (const key of Object.keys(o)) {
if (selectAliases.has(key)) {
o[raw('??', [key])] = o[key];
delete o[key];
}
}
this.helper.validateQueryOrder(o);
const processed = QueryHelper.processWhere({
where: o,
entityName: this.mainAlias.entityName,
metadata: this.metadata,
platform: this.platform,
aliasMap: this.getAliasMap(),
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
convertCustomTypes: false,
type: 'orderBy',
});
this.#state.orderBy.push(
CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, {
matchPopulateJoins: true,
type: 'orderBy',
}),
);
});
return this;
}
/** Collect custom aliases from select fields (stored as 'resolved as alias' strings by select()). */
getSelectAliases() {
const aliases = new Set();
for (const field of this.#state.fields ?? []) {
if (typeof field === 'string') {
const m = FIELD_ALIAS_RE.exec(field);
if (m) {
aliases.add(m[2]);
}
}
}
return aliases;
}
groupBy(fields) {
this.ensureNotFinalized();
this.#state.groupBy = Utils.asArray(fields).flatMap(f => {
if (typeof f !== 'string') {
// Normalize sql.ref('prop') to string for proper formula resolution
if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
return this.resolveNestedPath(String(f.params[0]));
}
return f;
}
return this.resolveNestedPath(f);
});
return this;
}
/**
* Adds a HAVING clause to the query, typically used with GROUP BY.
*
* @example
* ```ts
* qb.select([raw('count(*) as count'), 'status'])
* .groupBy('status')
* .having({ count: { $gt: 5 } });
* ```
*/
having(cond = {}, params, operator) {
this.ensureNotFinalized();
if (typeof cond === 'string') {
cond = { [raw(`(${cond})`, params)]: [] };
}
const processed = CriteriaNodeFactory.createNode(
this.metadata,
this.mainAlias.entityName,
cond,
undefined,
undefined,
false,
).process(this, { type: 'having' });
if (!this.#state.having || !operator) {
this.#state.having = processed;
} else {
const cond1 = [this.#state.having, processed];
this.#state.having = { [operator]: cond1 };
}
return this;
}
andHaving(cond, params) {
return this.having(cond, params, '$and');
}
orHaving(cond, params) {
return this.having(cond, params, '$or');
}
onConflict(fields = []) {
const meta = this.mainAlias.meta;
this.ensureNotFinalized();
this.#state.onConflict ??= [];
this.#state.onConflict.push({
fields: isRaw(fields)
? fields
: Utils.asArray(fields).flatMap(f => {
const key = f.toString();
/* v8 ignore next */
return meta.properties[key]?.fieldNames ?? [key];
}),
});
return this;
}
ignore() {
if (!this.#state.onConflict) {
throw new Error('You need to call `qb.onConflict()` first to use `qb.ignore()`');
}
this.#state.onConflict[this.#state.onConflict.length - 1].ignore = true;
return this;
}
merge(data) {
if (!this.#state.onConflict) {
throw new Error('You need to call `qb.onConflict()` first to use `qb.merge()`');
}
if (Array.isArray(data) && data.length === 0) {
return this.ignore();
}
this.#state.onConflict[this.#state.onConflict.length - 1].merge = data;
return this;
}
returning(fields) {
this.#state.returning = Utils.asArray(fields);
return this;
}
/**
* @internal
*/
populate(populate, populateWhere, populateFilter) {
this.ensureNotFinalized();
this.#state.populate = populate;
this.#state.populateWhere = populateWhere;
this.#state.populateFilter = populateFilter;
return this;
}
/**
* Sets a LIMIT clause to restrict the number of results.
*
* @example
* ```ts
* qb.select('*').limit(10); // First 10 results
* qb.select('*').limit(10, 20); // 10 results starting from offset 20
* ```
*/
limit(limit, offset = 0) {
this.ensureNotFinalized();
this.#state.limit = limit;
if (offset) {
this.offset(offset);
}
return this;
}
/**
* Sets an OFFSET clause to skip a number of results.
*
* @example
* ```ts
* qb.select('*').limit(10).offset(20); // Results 21-30
* ```
*/
offset(offset) {
this.ensureNotFinalized();
this.#state.offset = offset;
return this;
}
withSchema(schema) {
this.ensureNotFinalized();
this.#state.schema = schema;
return this;
}
setLockMode(mode, tables) {
this.ensureNotFinalized();
if (mode != null && ![LockMode.OPTIMISTIC, LockMode.NONE].includes(mode) && !this.context) {
throw ValidationError.transactionRequired();
}
this.#state.lockMode = mode;
this.#state.lockTables = tables;
return this;
}
setFlushMode(flushMode) {
this.ensureNotFinalized();
this.#state.flushMode = flushMode;
return this;
}
setFlag(flag) {
this.ensureNotFinalized();
this.#state.flags.add(flag);
return this;
}
unsetFlag(flag) {
this.ensureNotFinalized();
this.#state.flags.delete(flag);
return this;
}
hasFlag(flag) {
return this.#state.flags.has(flag);
}
cache(config = true) {
this.ensureNotFinalized();
this.#state.cache = config;
return this;
}
/**
* Adds index hint to the FROM clause.
*/
indexHint(sql) {
this.ensureNotFinalized();
this.#state.indexHint = sql;
return this;
}
/**
* Adds COLLATE clause to ORDER BY expressions.
*/
collation(collation) {
this.ensureNotFinalized();
this.#state.collation = collation;
return this;
}
/**
* Prepend comment to the sql query using the syntax `/* ... *&#8205;/`. Some characters are forbidden such as `/*, *&#8205;/` and `?`.
*/
comment(comment) {
this.ensureNotFinalized();
this.#state.comments.push(...Utils.asArray(comment));
return this;
}
/**
* Add hints to the query using comment-like syntax `/*+ ... *&#8205;/`. MySQL and Oracle use this syntax for optimizer hints.
* Also various DB proxies and routers use this syntax to pass hints to alter their behavior. In other dialects the hints
* are ignored as simple comments.
*/
hintComment(comment) {
this.ensureNotFinalized();
this.#state.hintComments.push(...Utils.asArray(comment));
return this;
}
from(target, aliasName) {
this.ensureNotFinalized();
if (target instanceof _a) {
this.fromSubQuery(target, aliasName);
} else if (typeof target === 'string' && !this.metadata.find(target)) {
this.fromRawTable(target, aliasName);
} else {
if (aliasName && this.#state.mainAlias && Utils.className(target) !== this.#state.mainAlias.aliasName) {
throw new Error(
`Cannot override the alias to '${aliasName}' since a query already contains references to '${this.#state.mainAlias.aliasName}'`,
);
}
this.fromEntityName(target, aliasName);
}
return this;
}
getNativeQuery(processVirtualEntity = true) {
if (this.#state.unionQuery) {
if (!this.#query?.qb) {
this.#query = {};
const nqb = this.platform.createNativeQueryBuilder();
nqb.select('*');
nqb.from(raw(`(${this.#state.unionQuery.sql})`, this.#state.unionQuery.params));
this.#query.qb = nqb;
}
return this.#query.qb;
}
if (this.#query?.qb) {
return this.#query.qb;
}
this.#query = {};
this.finalize();
const qb = this.getQueryBase(processVirtualEntity);
for (const cte of this.#state.ctes) {
const query = cte.query;
const opts = { columns: cte.columns, materialized: cte.materialized };
if (cte.recursive) {
qb.withRecursive(cte.name, query, opts);
} else {
qb.with(cte.name, query, opts);
}
}
const schema = this.getSchema(this.mainAlias);
const isNotEmptyObject = obj => Utils.hasObjectKeys(obj) || RawQueryFragment.hasObjectFragments(obj);
Utils.runIfNotEmpty(
() => this.helper.appendQueryCondition(this.type, this.#state.cond, qb),
this.#state.cond && !this.#state.onConflict,
);
Utils.runIfNotEmpty(
() => qb.groupBy(this.prepareFields(this.#state.groupBy, 'groupBy', schema)),
isNotEmptyObject(this.#state.groupBy),
);
Utils.runIfNotEmpty(
() => this.helper.appendQueryCondition(this.type, this.#state.having, qb, undefined, 'having'),
isNotEmptyObject(this.#state.having),
);
Utils.runIfNotEmpty(() => {
const queryOrder = this.helper.getQueryOrder(
this.type,
this.#state.orderBy,
this.#state.populateMap,
this.#state.collation,
);
if (queryOrder.length > 0) {
const sql = Utils.unique(queryOrder).join(', ');
qb.orderBy(sql);
return;
}
}, isNotEmptyObject(this.#state.orderBy));
Utils.runIfNotEmpty(() => qb.limit(this.#state.limit), this.#state.limit != null);
Utils.runIfNotEmpty(() => qb.offset(this.#state.offset), this.#state.offset);
Utils.runIfNotEmpty(() => qb.comment(this.#state.comments), this.#state.comments);
Utils.runIfNotEmpty(() => qb.hintComment(this.#state.hintComments), this.#state.hintComments);
Utils.runIfNotEmpty(
() => this.helper.appendOnConflictClause(QueryType.UPSERT, this.#state.onConflict, qb),
this.#state.onConflict,
);
if (this.#state.lockMode) {
this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
}
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.data, this.#state.returning);
return (this.#query.qb = qb);
}
processReturningStatement(qb, meta, data, returning) {
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
if (!meta || !data || !usesReturningStatement) {
return;
}
// always respect explicit returning hint
if (returning && returning.length > 0) {
qb.returning(returning.map(field => this.helper.mapper(field, this.type)));
return;
}
if (this.type === QueryType.INSERT) {
const returningProps = meta.hydrateProps
.filter(
prop =>
prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)),
)
.filter(prop => !(prop.name in data));
if (returningProps.length > 0) {
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
}
return;
}
if (this.type === QueryType.UPDATE) {
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
if (returningProps.length > 0) {
qb.returning(
returningProps.flatMap(prop => {
if (prop.hasConvertToJSValueSQL) {
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
const sql =
prop.customType.convertToJSValueSQL(aliased, this.platform) +
' as ' +
this.platform.quoteIdentifier(prop.fieldNames[0]);
return [raw(sql)];
}
return prop.fieldNames;
}),
);
}
}
}
/**
* Returns the query with parameters as wildcards.
*/
getQuery() {
return this.toQuery().sql;
}
/**
* Returns raw fragment representation of this QueryBuilder.
*/
toRaw() {
const { sql, params } = this.toQuery();
return raw(sql, params);
}
toQuery() {
if (this.#state.unionQuery) {
return this.#state.unionQuery;
}
if (this.#query?.sql) {
return { sql: this.#query.sql, params: this.#query.params };
}
const query = this.getNativeQuery().compile();
this.#query.sql = query.sql;
this.#query.params = query.params;
return { sql: this.#query.sql, params: this.#query.params };
}
/**
* Returns the list of all parameters for this query.
*/
getParams() {
return this.toQuery().params;
}
/**
* Returns raw interpolated query string with all the parameters inlined.
*/
getFormattedQuery() {
const query = this.toQuery();
return this.platform.formatQuery(query.sql, query.params);
}
/**
* @internal
*/
getAliasForJoinPath(path, options) {
if (!path || path === Utils.className(this.mainAlias.entityName)) {
return this.mainAlias.aliasName;
}
const join = typeof path === 'string' ? this.getJoinForPath(path, options) : path;
if (join?.path?.endsWith('[pivot]')) {
return join.alias;
}
return join?.inverseAlias || join?.alias;
}
/**
* @internal
*/
getJoinForPath(path, options) {
const joins = Object.values(this.#state.joins);
if (joins.length === 0) {
return undefined;
}
let join = joins.find(j => j.path === path);
if (options?.preferNoBranch) {
join = joins.find(j => {
return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '');
});
}
if (!join && options?.ignoreBranching) {
join = joins.find(j => {
return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '');
});
}
if (!join && options?.matchPopulateJoins && options?.ignoreBranching) {
join = joins.find(j => {
return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '');
});
}
if (!join && options?.matchPopulateJoins) {
join = joins.find(j => {
return j.path?.replace(/\[populate]/g, '') === path.replace(/\[populate]/g, '');
});
}
return join;
}
/**
* @internal
*/
getNextAlias(entityName = 'e') {
entityName = Utils.className(entityName);
return this.driver.config.getNamingStrategy().aliasName(entityName, this.#state.aliasCounter++);
}
/**
* Registers a join for a specific polymorphic target type.
* Used by the driver to create per-target LEFT JOINs for JOINED loading.
* @internal
*/
addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
// Override referencedColumnNames to use the specific target's PK columns
// (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
const targetProp = { ...prop, targetMeta, referencedColumnNames };
const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(
targetProp,
ownerAlias,
alias,
type,
{},
schema,
);
this.#state.joins[aliasedName].path = path;
this.createAlias(targetMeta.class, alias);
}
/**
* @internal
*/
getAliasMap() {
return Object.fromEntries(Object.entries(this.#state.aliases).map(([key, value]) => [key, value.entityName]));
}
/**
* Executes this QB and returns the raw results, mapped to the property names (unless disabled via last parameter).
* Use `method` to specify what kind of result you want to get (array/single/meta).
*/
async execute(method, options) {
options = typeof options === 'boolean' ? { mapResults: options } : (options ?? {});
options.mergeResults ??= true;
options.mapResults ??= true;
const isRunType = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE, QueryType.TRUNCATE].includes(this.type);
method ??= isRunType ? 'run' : 'all';
if (!this.connectionType && (isRunType || this.context)) {
this.connectionType = 'write';
}
if (!this.#state.finalized && method === 'get' && this.type === QueryType.SELECT) {
this.limit(1);
}
const query = this.toQuery();
const cached = await this.em?.tryCache(this.mainAlias.entityName, this.#state.cache, [
'qb.execute',
query.sql,
query.params,
method,
]);
if (cached?.data !== undefined) {
return cached.data;
}
const loggerContext = { id: this.em?.id, ...this.loggerContext };
const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
const meta = this.mainAlias.meta;
if (!options.mapResults || !meta) {
await this.em?.storeCache(this.#state.cache, cached, res);
return res;
}
if (method === 'run') {
return res;
}
const joinedProps = this.driver.joinedProps(meta, this.#state.populate);
let mapped;
if (Array.isArray(res)) {
const map = {};
mapped = res.map(r => this.driver.mapResult(r, meta, this.#state.populate, this, map));
if (options.mergeResults && joinedProps.length > 0) {
mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.meta, joinedProps);
}
} else {
mapped = [this.driver.mapResult(res, meta, joinedProps, this)];
}
if (method === 'get') {
await this.em?.storeCache(this.#state.cache, cached, mapped[0]);
return mapped[0];
}
await this.em?.storeCache(this.#state.cache, cached, mapped);
return mapped;
}
getConnection() {
const write = !this.platform.getConfig().get('preferReadReplicas');
const type = this.connectionType || (write ? 'write' : 'read');
return this.driver.getConnection(type);
}
/**
* Executes the query and returns an async iterable (async generator) that yields results one by one.
* By default, the results are merged and mapped to entity instances, without adding them to the identity map.
* You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`.
* This is useful for processing large datasets without loading everything into memory at once.
*
* ```ts
* const qb = em.createQueryBuilder(Book, 'b');
* qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a');
*
* for await (const book of qb.stream()) {
* // book is an instance of Book entity
* console.log(book.title, book.author.name);
* }
* ```
*/
async *stream(options) {
options ??= {};
options.mergeResults ??= true;
options.mapResults ??= true;
const query = this.toQuery();
const loggerContext = { id: this.em?.id, ...this.loggerContext };
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
const meta = this.mainAlias.meta;
if (options.rawResults || !meta) {
yield* res;
return;
}
const joinedProps = this.driver.joinedProps(meta, this.#state.populate);
const stack = [];
const hash = data => {
return Utils.getPrimaryKeyHash(meta.primaryKeys.map(pk => data[pk]));
};
for await (const row of res) {
const mapped = this.driver.mapResult(row, meta, this.#state.populate, this);
if (!options.mergeResults || joinedProps.length === 0) {
yield this.mapResult(mapped, options.mapResults);
continue;
}
if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
const res = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
for (const row of res) {
yield this.mapResult(row, options.mapResults);
}
stack.length = 0;
}
stack.push(mapped);
}
if (stack.length > 0) {
const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
yield this.mapResult(merged[0], options.mapResults);
}
}
/**
* Alias for `qb.getResultList()`
*/
async getResult() {
return this.getResultList();
}
/**
* Executes the query, returning array of results mapped to entity instances.
*/
async getResultList(limit) {
await this.em.tryFlush(this.mainAlias.entityName, { flushMode: this.#state.flushMode });
const res = await this.execute('all', true);
return this.mapResults(res, limit);
}
propagatePopulateHint(entity, hint) {
helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
hint.forEach(hint => {
const [propName] = hint.field.split(':', 2);
const value = Reference.unwrapReference(entity[propName]);
if (Utils.isEntity(value)) {
this.propagatePopulateHint(value, hint.children ?? []);
} else if (Utils.isCollection(value)) {
value.populated();
value.getItems(false).forEach(item => this.propagatePopulateHint(item, hint.children ?? []));
}
});
}
mapResult(row, map = true) {
if (!map) {
return row;
}
const entity = this.em.map(this.mainAlias.entityName, row, { schema: this.#state.schema });
this.propagatePopulateHint(entity, this.#state.populate);
return entity;
}
mapResults(res, limit) {
const entities = [];
for (const row of res) {
const entity = this.mapResult(row);
this.propagatePopulateHint(entity, this.#state.populate);
entities.push(entity);
if (limit != null && --limit === 0) {
break;
}
}
return Utils.unique(entities);
}
/**
* Executes the query, returning the first result or null
*/
async getSingleResult() {
if (!this.#state.finalized) {
this.limit(1);
}
const [res] = await this.getResultList(1);
return res || null;
}
async getCount(field, distinct) {
let res;
if (this.type === QueryType.COUNT) {
res = await this.execute('get', false);
} else {
const qb = this.#state.type === undefined ? this : this.clone();
qb.processPopulateHint(); // needs to happen sooner so `qb.hasToManyJoins()` reports correctly
qb.count(field, distinct ?? qb.hasToManyJoins())
.limit(undefined)
.offset(undefined)
.orderBy([]);
res = await qb.execute('get', false);
}
return res ? +res.count : 0;
}
/**
* Executes the query, returning both array of results and total count query (without offset and limit).
*/
async getResultAndCount() {
return [await this.clone().getResultList(), await this.clone().getCount()];
}
as(aliasOrTargetEntity, alias) {
const qb = this.getNativeQuery();
let finalAlias = aliasOrTargetEntity;
/* v8 ignore next */
if (typeof aliasOrTargetEntity === 'string' && aliasOrTargetEntity.includes('.')) {
throw new Error(
'qb.as(alias) no longer supports target entity name prefix, use qb.as(TargetEntity, key) signature instead',
);
}
if (alias) {
const meta = this.metadata.get(aliasOrTargetEntity);
/* v8 ignore next */
finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
}
qb.as(finalAlias);
// tag the instance, so it is possible to detect it easily
Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
return qb;
}
/**
* Combines the current query with one or more other queries using `UNION ALL`.
* All queries must select the same columns. Returns a `QueryBuilder` that
* can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
* `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
*
* ```ts
* const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
* const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
* const qb3 = em.createQueryBuilder(Employee).select('id').where(condition3);
* const subquery = qb1.unionAll(qb2, qb3);
*
* const results = await em.find(Employee, { id: { $in: subquery } });
* ```
*/
unionAll(...others) {
return this.buildUnionQuery('union all', others);
}
/**
* Combines the current query with one or more other queries using `UNION` (with deduplication).
* All queries must select the same columns. Returns a `QueryBuilder` that
* can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
* `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
*
* ```ts
* const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
* const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
* const subquery = qb1.union(qb2);
*
* const results = await em.find(Employee, { id: { $in: subquery } });
* ```
*/
union(...others) {
return this.buildUnionQuery('union', others);
}
buildUnionQuery(separator, others) {
const all = [this, ...others];
const parts = [];
const params = [];
for (const qb of all) {
const compiled = qb instanceof _a ? qb.toQuery() : qb.compile();
parts.push(`(${compiled.sql})`);
params.push(...compiled.params);
}
const result = this.clone(true);
result.#state.unionQuery = { sql: parts.join(` ${separator} `), params };
return result;
}
with(name, query, options) {
return this.addCte(name, query, options);
}
withRecursive(name, query, options) {
return this.addCte(name, query, options, true);
}
addCte(name, query, options, recursive) {
this.ensureNotFinalized();
if (this.#state.ctes.some(cte => cte.name === name)) {
throw new Error(`CTE with name '${name}' already exists`);
}
// Eagerly compile QueryBuilder to RawQueryFragment — later mutations to the sub-query won't be reflected
const compiled = query instanceof _a ? query.toRaw() : query;
this.#state.ctes.push({
name,
query: compiled,
recursive,
columns: options?.columns,
materialized: options?.materialized,
});
return this;
}
clone(reset, preserve) {
const qb = new _a(
this.#state.mainAlias.entityName,
this.metadata,
this.driver,
this.context,
this.#state.mainAlias.aliasName,
this.connectionType,
this.em,
);
if (reset !== true) {
qb.#state = Utils.copy(this.#state);
// CTEs contain NativeQueryBuilder instances that should not be deep-cloned
qb.#state.ctes = this.#state.ctes.map(cte => ({ ...cte }));
if (Array.isArray(reset)) {
const fresh = _a.createDefaultState();
for (const key of reset) {
qb.#state[key] = fresh[key];
}
}
} else if (preserve) {
for (const key of preserve) {
qb.#state[key] = Utils.copy(this.#state[key]);
}
}
qb.#state.finalized = false;
qb.#query = undefined;
qb.#helper = qb.createQueryBuilderHelper();
return qb;
}
/**
* Sets logger context for this query builder.
*/
setLoggerContext(context) {
this.loggerContext = context;
}
/**
* Gets logger context for this query builder.
*/
getLoggerContext() {
this.loggerContext ??= {};
return this.loggerContext;
}
fromVirtual(meta) {
if (typeof meta.expression === 'string') {
return `(${meta.expression}) as ${this.platform.quoteIdentifier(this.alias)}`;
}
const res = meta.expression(this.em, this.#state.cond, {});
if (typeof res === 'string') {
return `(${res}) as ${this.platform.quoteIdentifier(this.alias)}`;
}
if (res instanceof _a) {
return `(${res.getFormattedQuery()}) as ${this.platform.quoteIdentifier(this.alias)}`;
}
if (isRaw(res)) {
const query = this.platform.formatQuery(res.sql, res.params);
return `(${query}) as ${this.platform.quoteIdentifier(this.alias)}`;
}
/* v8 ignore next */
return res;
}
/**
* Adds a join from a property object. Used internally for TPT joins where the property
* is synthetic (not in entity.properties) but defined on metadata (e.g., tptParentProp).
* The caller must create the alias first via createAlias().
* @internal
*/
addPropertyJoin(prop, ownerAlias, alias, type, path, schema) {
schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
const key = `[tpt]${ownerAlias}#${alias}`;
this.#state.joins[key] =
prop.kind === ReferenceKind.MANY_TO_ONE
? this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, {}, schema)
: this.helper.joinOneToReference(prop, ownerAlias, alias, type, {}, schema);
this.#state.joins[key].path = path;
return key;
}
joinReference(field, alias, cond, type, path, schema, subquery) {
this.ensureNotFinalized();
if (typeof field === 'object') {
const prop = {
name: '__subquery__',
kind: ReferenceKind.MANY_TO_ONE,
};
if (field instanceof _a) {
prop.type = Utils.className(field.mainAlias.entityName);
prop.targetMeta = field.mainAlias.meta;
field = field.getNativeQuery();
}
if (isRaw(field)) {
field = this.platform.formatQuery(field.sql, field.params);
}
const key = `${this.alias}.${prop.name}#${alias}`;
this.#state.joins[key] = {
prop,
alias,
type,
cond,
schema,
subquery: field.toString(),
ownerAlias: this.alias,
};
return { prop, key };
}
if (!subquery && type.includes('lateral')) {
throw new Error(`Lateral join can be used only with a sub-query.`);
}
const [fromAlias, fromField] = this.helper.splitField(field);
const q = str => `'${str}'`;
if (!this.#state.aliases[fromAlias]) {
throw new Error(
`Trying to join ${q(fromField)} with alias ${q(fromAlias)}, but ${q(fromAlias)} is not a known alias. Available aliases are: ${Object.keys(this.#state.aliases).map(q).join(', ')}.`,
);
}
const entityName = this.#state.aliases[fromAlias].entityName;
const meta = this.metadata.get(entityName);
const prop = meta.properties[fromField];
if (!prop) {
throw new Error(
`Trying to join ${q(field)}, but ${q(fromField)} is not a defined relation on ${meta.className}.`,
);
}
// For TPT inheritance, owning relations (M:1 and owning 1:1) may have FK columns in a parent table
// Resolve the correct alias for the table that owns the FK column
const ownerAlias =
prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)
? this.helper.getTPTAliasForProperty(fromField, fromAlias)
: fromAlias;
this.createAlias(prop.targetMeta.class, alias);
cond = QueryHelper.processWhere({
where: cond,
entityName: this.mainAlias.entityName,
metadata: this.metadata,
platform: this.platform,
aliasMap: this.getAliasMap(),
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
});
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
this.#state.joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
this.#state.joins[aliasedName].path ??= path;
} else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
let pivotAlias = alias;
if (type !== JoinType.pivotJoin) {
const oldPivotAlias = this.getAliasForJoinPath(path + '[pivot]');
pivotAlias = oldPivotAlias ?? this.getNextAlias(prop.pivotEntity);
aliasedName = `${fromAlias}.${prop.name}#${pivotAlias}`;
}
const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
Object.assign(this.#state.joins, joins);
this.createAlias(prop.pivotEntity, pivotAlias);
this.#state.joins[aliasedName].path ??= path;
aliasedName = Object.keys(joins)[1];
} else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
this.#state.joins[aliasedName] = this.helper.joinOneToReference(prop, ownerAlias, alias, type, cond, schema);
this.#state.joins[aliasedName].path ??= path;
} else {
// MANY_TO_ONE
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
this.#state.joins[aliasedName].path ??= path;
}
return { prop, key: aliasedName };
}
prepareFields(fields, type = 'where', schema) {
const ret = [];
const getFieldName = (name, customAlias) => {
const alias = customAlias ?? (type === 'groupBy' ? null : undefined);
return this.helper.mapper(name, this.type, undefined, alias, schema);
};
fields.forEach(originalField => {
if (typeof originalField !== 'string') {
ret.push(originalField);
return;
}
// Strip 'as alias' suffix if present — the alias is passed to mapper at the end
let field = originalField;
let customAlias;
const asMatch = FIELD_ALIAS_RE.exec(originalField);
if (asMatch) {
field = asMatch[1].trim();
customAlias = asMatch[2];
}
const join = Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
if (join && type === 'where') {
ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[join]));
return;
}
const [a, f] = this.helper.splitField(field);
const prop = this.helper.getProperty(f, a);
/* v8 ignore next */
if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
return;
}
if (prop?.persist === false && !prop.embedded && !prop.formula && type === 'where') {
return;
}
if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
const aliased = this.#state.aliases[a] ? `${a}.${name}` : name;
ret.push(getFieldName(aliased, customAlias));
return;
}
if (prop?.kind === ReferenceKind.EMBEDDED) {
if (customAlias) {
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}').`,
);
}
const nest = prop => {
for (const childProp of Object.values(prop.embeddedProps)) {
if (
childProp.fieldNames &&
(childProp.kind !== ReferenceKind.EMBEDDED || childProp.object) &&
childProp.persist !== false
) {
ret.push(getFieldName(childProp.fieldNames[0]));
} else {
nest(childProp);
}
}
};
nest(prop);
return;
}
if (prop && prop.fieldNames.length > 1 && !prop.fieldNames.includes(f)) {
if (customAlias) {
throw new Error(
`Cannot use 'as ${customAlias}' alias on '${field}' because it expands to multiple columns (${prop.fieldNames.join(', ')}).`,
);
}
ret.push(...prop.fieldNames.map(f => getFieldName(f)));
return;
}
ret.push(getFieldName(field, customAlias));
});
const requiresSQLConversion = this.mainAlias.meta.props.filter(
p => p.hasConvertToJSValueSQL && p.persist !== false,
);
if (
this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) &&
(fields.includes('*') || fields.includes(`${this.mainAlias.aliasName}.*`)) &&
requiresSQLConversion.length > 0
) {
for (const p of requiresSQLConversion) {
ret.push(this.helper.mapper(p.name, this.type));
}
}
for (const f of Object.keys(this.#state.populateMap)) {
if (type === 'where' && this.#state.joins[f]) {
ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[f]));
}
}
return Utils.unique(ret);
}
/**
* Resolves nested paths like `a.books.title` to their actual field references.
* Auto-joins relations as needed and returns `{alias}.{field}`.
* For embeddeds: navigates into flattened embeddeds to return the correct field name.
*/
resolveNestedPath(field) {
if (typeof field !== 'string' || !field.includes('.')) {
return field;
}
const parts = field.split('.');
// Simple alias.property case - let prepareFields handle it
if (parts.length === 2 && this.#state.aliases[parts[0]]) {
return field;
}
// Start with root alias
let currentAlias = parts[0];
let currentMeta = this.#state.aliases[currentAlias]
? this.metadata.get(this.#state.aliases[currentAlias].entityName)
: this.mainAlias.meta;
// If first part is not an alias, it's a property of the main entity
if (!this.#state.aliases[currentAlias]) {
currentAlias = this.mainAlias.aliasName;
parts.unshift(currentAlias);
}
// Walk through the path parts (skip the alias)
for (let i = 1; i < parts.length; i++) {
const propName = parts[i];
const prop = currentMeta.properties[propName];
if (!prop) {
return field; // Unknown property, return as-is for raw SQL support
}
const isLastPart = i === parts.length - 1;
// Handle embedded properties - navigate into flattened embeddeds
if (prop.kind === ReferenceKind.EMBEDDED) {
if (prop.object) {
return `${currentAlias}.${propName}`;
}
// Navigate through remaining path to find the leaf property
const remainingPath = parts.slice(i + 1);
let embeddedProp = prop;
for (const part of remainingPath) {
embeddedProp = embeddedProp?.embeddedProps?.[part];
if (embeddedProp?.object && embeddedProp.fieldNames?.[0]) {
return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
}
}
return `${currentAlias}.${embeddedProp?.fieldNames?.[0] ?? propName}`;
}
// Handle relations - auto-join if not the last part
if (
prop.kind === ReferenceKind.MANY_TO_ONE ||
prop.kind === ReferenceKind.ONE_TO_ONE ||
prop.kind === ReferenceKind.ONE_TO_MANY ||
prop.kind === ReferenceKind.MANY_TO_MANY
) {
if (isLastPart) {
return `${currentAlias}.${propName}`;
}
// Find existing join or create new one
const joinPath = parts.slice(0, i + 1).join('.');
const existingJoinKey = Object.keys(this.#state.joins).find(k => {
const join = this.#state.joins[k];
// Check by path or by key prefix (key format is `alias.field#joinAlias`)
return join.path === joinPath || k.startsWith(`${currentAlias}.${propName}#`);
});
let joinAlias;
if (existingJoinKey) {
joinAlias = this.#state.joins[existingJoinKey].alias;
} else {
joinAlias = this.getNextAlias(prop.targetMeta?.className ?? propName);
this.join(`${currentAlias}.${propName}`, joinAlias, {}, JoinType.leftJoin);
}
currentAlias = joinAlias;
currentMeta = prop.targetMeta;
continue;
}
// Scalar property - return it (if not last part, it's an invalid path but let SQL handle it)
return `${currentAlias}.${propName}`;
}
return field;
}
init(type, data, cond) {
this.ensureNotFinalized();
this.#state.type = type;
if ([QueryType.UPDATE, QueryType.DELETE].includes(type) && Utils.hasObjectKeys(this.#state.cond)) {
throw new Error(
`You are trying to call \`qb.where().${type.toLowerCase()}()\`. Calling \`qb.${type.toLowerCase()}()\` before \`qb.where()\` is required.`,
);
}
if (!this.helper.isTableNameAliasRequired(type)) {
this.#state.fields = undefined;
}
if (data) {
if (Utils.isEntity(data)) {
data = this.em?.getComparator().prepareEntity(data) ?? serialize(data);
}
this.#state.data = this.helper.processData(data, this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES), false);
}
if (cond) {
this.where(cond);
}
return this;
}
getQueryBase(processVirtualEntity) {
const qb = this.platform.createNativeQueryBuilder().setFlags(this.#state.flags);
const { subQuery, aliasName, entityName, meta, rawTableName } = this.mainAlias;
const requiresAlias =
this.#state.finalized && (this.#state.explicitAlias || this.helper.isTableNameAliasRequired(this.type));
const alias = requiresAlias ? aliasName : undefined;
const schema = this.getSchema(this.mainAlias);
const tableName = rawTableName
? rawTableName
: subQuery instanceof NativeQueryBuilder
? subQuery.as(aliasName)
: subQuery
? raw(`(${subQuery.sql}) as ${this.platform.quoteIdentifier(aliasName)}`, subQuery.params)
: this.helper.getTableName(entityName);
const joinSchema = this.#state.schema ?? this.em?.schema ?? schema;
const schemaOverride = this.#state.schema ?? this.em?.schema;
if (meta.virtual && processVirtualEntity) {
qb.from(raw(this.fromVirtual(meta)), { indexHint: this.#state.indexHint });
} else {
qb.from(tableName, {
schema: rawTableName ? undefined : schema,
alias,
indexHint: this.#state.indexHint,
});
}
switch (this.type) {
case QueryType.SELECT:
qb.select(this.prepareFields(this.#state.fields, 'where', schema));
if (this.#state.distinctOn) {
qb.distinctOn(this.prepareFields(this.#state.distinctOn, 'where', schema));
} else if (this.#state.flags.has(QueryFlag.DISTINCT)) {
qb.distinct();
}
this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
break;
case QueryType.COUNT: {
const fields = this.#state.fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
qb.count(fields, this.#state.flags.has(QueryFlag.DISTINCT));
this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
break;
}
case QueryType.INSERT:
qb.insert(this.#state.data);
break;
case QueryType.UPDATE:
qb.update(this.#state.data);
this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
this.helper.updateVersionProperty(qb, this.#state.data);
break;
case QueryType.DELETE:
qb.delete();
break;
case QueryType.TRUNCATE:
qb.truncate();
break;
}
return qb;
}
applyDiscriminatorCondition() {
const meta = this.mainAlias.meta;
if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
return;
}
const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
const children = [];
const lookUpChildren = (ret, type) => {
const children = types.filter(meta2 => meta2.extends === type);
children.forEach(m => lookUpChildren(ret, m.class));
ret.push(...children.filter(c => c.discriminatorValue));
return children;
};
lookUpChildren(children, meta.class);
this.andWhere({
[meta.root.discriminatorColumn]:
children.length > 0
? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] }
: meta.discriminatorValue,
});
}
/**
* Ensures TPT joins are applied. Can be called early before finalize() to populate
* the _tptAlias map for use in join resolution. Safe to call multiple times.
* @internal
*/
ensureTPTJoins() {
this.applyTPTJoins();
}
/**
* For TPT (Table-Per-Type) inheritance: INNER JOINs parent tables.
* When querying a child entity, we need to join all parent tables.
* Field selection is handled separately in addTPTParentFields().
*/
applyTPTJoins() {
const meta = this.mainAlias.meta;
if (
meta?.inheritanceType !== 'tpt' ||
!meta.tptParent ||
![QueryType.SELECT, QueryType.COUNT].includes(this.type)
) {
return;
}
if (this.#state.tptJoinsApplied) {
return;
}
this.#state.tptJoinsApplied = true;
let childMeta = meta;
let childAlias = this.mainAlias.aliasName;
while (childMeta.tptParent) {
const parentMeta = childMeta.tptParent;
const parentAlias = this.getNextAlias(parentMeta.className);
this.createAlias(parentMeta.class, parentAlias);
this.#state.tptAlias[parentMeta.className] = parentAlias;
this.addPropertyJoin(
childMeta.tptParentProp,
childAlias,
parentAlias,
JoinType.innerJoin,
`[tpt]${childMeta.className}`,
);
childMeta = parentMeta;
childAlias = parentAlias;
}
}
/**
* For TPT inheritance: adds field selections from parent tables.
*/
addTPTParentFields() {
const meta = this.mainAlias.meta;
if (
meta?.inheritanceType !== 'tpt' ||
!meta.tptParent ||
![QueryType.SELECT, QueryType.COUNT].includes(this.type)
) {
return;
}
if (!this.#state.fields?.includes('*') && !this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`)) {
return;
}
let parentMeta = meta.tptParent;
while (parentMeta) {
const parentAlias = this.#state.tptAlias[parentMeta.className];
if (parentAlias) {
const schema = parentMeta.schema === '*' ? '*' : this.driver.getSchemaName(parentMeta);
parentMeta.ownProps
.filter(prop => this.platform.shouldHaveColumn(prop, []))
.forEach(prop =>
this.#state.fields.push(...this.driver.mapPropToFieldNames(this, prop, parentAlias, parentMeta, schema)),
);
}
parentMeta = parentMeta.tptParent;
}
}
/**
* For TPT polymorphic queries: LEFT JOINs all child tables when querying a TPT base class.
* Adds discriminator and child fields to determine and load the concrete type.
*/
applyTPTPolymorphicJoins() {
const meta = this.mainAlias.meta;
const descendants = meta?.allTPTDescendants;
if (!descendants?.length || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
return;
}
if (!this.#state.fields?.includes('*') && !this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`)) {
return;
}
// LEFT JOIN each descendant table and add their fields
for (const childMeta of descendants) {
const childAlias = this.getNextAlias(childMeta.className);
this.createAlias(childMeta.class, childAlias);
this.#state.tptAlias[childMeta.className] = childAlias;
this.addPropertyJoin(
childMeta.tptInverseProp,
this.mainAlias.aliasName,
childAlias,
JoinType.leftJoin,
`[tpt]${meta.className}`,
);
// Add child fields
const schema = childMeta.schema === '*' ? '*' : this.driver.getSchemaName(childMeta);
childMeta.ownProps
.filter(prop => !prop.primary && this.platform.shouldHaveColumn(prop, []))
.forEach(prop =>
this.#state.fields.push(...this.driver.mapPropToFieldNames(this, prop, childAlias, childMeta, schema)),
);
}
// Add computed discriminator (CASE WHEN to determine concrete type)
// descendants is pre-sorted by depth (deepest first) during discovery
if (meta.tptDiscriminatorColumn) {
this.#state.fields.push(
this.driver.buildTPTDiscriminatorExpression(meta, descendants, this.#state.tptAlias, this.mainAlias.aliasName),
);
}
}
finalize() {
if (this.#state.finalized) {
return;
}
if (!this.#state.type) {
this.select('*');
}
const meta = this.mainAlias.meta;
this.applyDiscriminatorCondition();
this.applyTPTJoins();
this.addTPTParentFields();
this.applyTPTPolymorphicJoins();
this.processPopulateHint();
this.processNestedJoins();
if (meta && (this.#state.fields?.includes('*') || this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`))) {
const schema = this.getSchema(this.mainAlias);
// Create a column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
// For TPT, use helper to resolve correct alias per property (inherited props use parent alias)
const quotedMainAlias = this.platform.quoteIdentifier(this.mainAlias.aliasName).toString();
const columns = meta.createColumnMappingObject(
prop => this.helper.getTPTAliasForProperty(prop.name, this.mainAlias.aliasName),
quotedMainAlias,
);
meta.props
.filter(prop => prop.formula && (!prop.lazy || this.#state.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
.map(prop => {
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
const table = this.helper.createFormulaTable(quotedMainAlias, meta, schema);
return `${this.driver.evaluateFormula(prop.formula, columns, table)} as ${aliased}`;
})
.filter(
field =>
!this.#state.fields.some(f => {
if (isRaw(f)) {
return f.sql === field && f.params.length === 0;
}
return f === field;
}),
)
.forEach(field => this.#state.fields.push(raw(field)));
}
QueryHelper.processObjectParams(this.#state.data);
QueryHelper.processObjectParams(this.#state.cond);
QueryHelper.processObjectParams(this.#state.having);
// automatically enable paginate flag when we detect to-many joins, but only if there is no `group by` clause
if (
!this.#state.flags.has(QueryFlag.DISABLE_PAGINATE) &&
this.#state.groupBy.length === 0 &&
this.hasToManyJoins()
) {
this.#state.flags.add(QueryFlag.PAGINATE);
}
if (
meta &&
!meta.virtual &&
this.#state.flags.has(QueryFlag.PAGINATE) &&
!this.#state.flags.has(QueryFlag.DISABLE_PAGINATE) &&
(this.#state.limit > 0 || this.#state.offset > 0)
) {
this.wrapPaginateSubQuery(meta);
}
if (
meta &&
(this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.#state.flags.has(QueryFlag.DELETE_SUB_QUERY))
) {
this.wrapModifySubQuery(meta);
}
this.#state.finalized = true;
}
/** @internal */
processPopulateHint() {
if (this.#state.populateHintFinalized) {
return;
}
const meta = this.mainAlias.meta;
if (meta && this.#state.flags.has(QueryFlag.AUTO_JOIN_ONE_TO_ONE_OWNER)) {
const relationsToPopulate = this.#state.populate.map(({ field }) => field);
meta.relations
.filter(
prop =>
prop.kind === ReferenceKind.ONE_TO_ONE &&
!prop.owner &&
!relationsToPopulate.includes(prop.name) &&
!relationsToPopulate.includes(`${prop.name}:ref`),
)
.map(prop => ({ field: `${prop.name}:ref` }))
.forEach(item => this.#state.populate.push(item));
}
this.#state.populate.forEach(({ field }) => {
const [fromAlias, fromField] = this.helper.splitField(field);
const aliasedField = `${fromAlias}.${fromField}`;
const join = Object.keys(this.#state.joins).find(k => `${aliasedField}#${this.#state.joins[k].alias}` === k);
if (join && this.#state.joins[join] && this.helper.isOneToOneInverse(fromField)) {
this.#state.populateMap[join] = this.#state.joins[join].alias;
return;
}
if (meta && this.helper.isOneToOneInverse(fromField)) {
const prop = meta.properties[fromField];
const alias = this.getNextAlias(prop.pivotEntity ?? prop.targetMeta.class);
const aliasedName = `${fromAlias}.${prop.name}#${alias}`;
this.#state.joins[aliasedName] = this.helper.joinOneToReference(
prop,
this.mainAlias.aliasName,
alias,
JoinType.leftJoin,
);
this.#state.joins[aliasedName].path =
`${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? meta.className}.${prop.name}`;
this.#state.populateMap[aliasedName] = this.#state.joins[aliasedName].alias;
this.createAlias(prop.targetMeta.class, alias);
}
});
this.processPopulateWhere(false);
this.processPopulateWhere(true);
this.#state.populateHintFinalized = true;
}
processPopulateWhere(filter) {
const value = filter ? this.#state.populateFilter : this.#state.populateWhere;
if (value == null || value === PopulateHint.ALL) {
return;
}
let joins = Object.values(this.#state.joins);
for (const join of joins) {
join.cond_ ??= join.cond;
join.cond = { ...join.cond };
}
if (typeof value === 'object') {
const cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, value).process(this, {
matchPopulateJoins: true,
ignoreBranching: true,
preferNoBranch: true,
filter,
});
// there might be new joins created by processing the `populateWhere` object
joins = Object.values(this.#state.joins);
this.mergeOnConditions(joins, cond, filter);
}
}
mergeOnConditions(joins, cond, filter, op) {
for (const k of Object.keys(cond)) {
if (Utils.isOperator(k)) {
if (Array.isArray(cond[k])) {
cond[k].forEach(c => this.mergeOnConditions(joins, c, filter, k));
}
/* v8 ignore next */
this.mergeOnConditions(joins, cond[k], filter, k);
}
const [alias] = this.helper.splitField(k);
const join = joins.find(j => j.alias === alias);
if (join) {
const parentJoin = joins.find(j => j.alias === join.ownerAlias);
// https://stackoverflow.com/a/56815807/3665878
if (parentJoin && !filter) {
const nested = (parentJoin.nested ??= new Set());
join.type =
join.type === JoinType.innerJoin ||
[ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(parentJoin.prop.kind)
? JoinType.nestedInnerJoin
: JoinType.nestedLeftJoin;
nested.add(join);
}
if (join.cond[k]) {
/* v8 ignore next */
join.cond = { [op ?? '$and']: [join.cond, { [k]: cond[k] }] };
} else if (op === '$or') {
join.cond.$or ??= [];
join.cond.$or.push({ [k]: cond[k] });
} else {
join.cond = { ...join.cond, [k]: cond[k] };
}
}
}
}
/**
* When adding an inner join on a left joined relation, we need to nest them,
* otherwise the inner join could discard rows of the root table.
*/
processNestedJoins() {
if (this.#state.flags.has(QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
return;
}
const joins = Object.values(this.#state.joins);
const lookupParentGroup = j => {
return j.nested ?? (j.parent ? lookupParentGroup(j.parent) : undefined);
};
for (const join of joins) {
if (join.type === JoinType.innerJoin) {
join.parent = joins.find(j => j.alias === join.ownerAlias);
// https://stackoverflow.com/a/56815807/3665878
if (join.parent?.type === JoinType.leftJoin || join.parent?.type === JoinType.nestedLeftJoin) {
const nested = (join.parent.nested ??= new Set());
join.type = join.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
nested.add(join);
} else if (join.parent?.type === JoinType.nestedInnerJoin) {
const group = lookupParentGroup(join.parent);
const nested = group ?? (join.parent.nested ??= new Set());
join.type = join.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
nested.add(join);
}
}
}
}
hasToManyJoins() {
return Object.values(this.#state.joins).some(join => {
return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
});
}
wrapPaginateSubQuery(meta) {
const schema = this.getSchema(this.mainAlias);
const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
const subQuery = this.clone(['orderBy', 'fields', 'lockMode', 'lockTables'])
.select(pks)
.groupBy(pks)
.limit(this.#state.limit);
// revert the on conditions added via populateWhere, we want to apply those only once
for (const join of Object.values(subQuery.#state.joins)) {
if (join.cond_) {
join.cond = join.cond_;
}
}
if (this.#state.offset) {
subQuery.offset(this.#state.offset);
}
const addToSelect = [];
if (this.#state.orderBy.length > 0) {
const orderBy = [];
for (const orderMap of this.#state.orderBy) {
for (const field of Utils.getObjectQueryKeys(orderMap)) {
const direction = orderMap[field];
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
orderBy.push({ [field]: direction });
continue;
}
const [a, f] = this.helper.splitField(field);
const prop = this.helper.getProperty(f, a);
const type = this.platform.castColumn(prop);
const fieldName = this.helper.mapper(field, this.type, undefined, null);
if (!prop?.persist && !prop?.formula && !prop?.hasConvertToJSValueSQL && !pks.includes(fieldName)) {
addToSelect.push(fieldName);
}
const quoted = this.platform.quoteIdentifier(fieldName);
const key = raw(`min(${quoted}${type})`);
orderBy.push({ [key]: direction });
}
}
subQuery.orderBy(orderBy);
}
subQuery.#state.finalized = true;
const innerQuery = subQuery.as(this.mainAlias.aliasName).clear('select').select(pks);
if (addToSelect.length > 0) {
addToSelect.forEach(prop => {
const field = this.#state.fields.find(field => {
if (typeof field === 'object' && field && '__as' in field) {
return field.__as === prop;
}
if (isRaw(field)) {
// not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
return field.sql.includes(prop);
}
return false;
});
/* v8 ignore next */
if (isRaw(field)) {
innerQuery.select(field);
} else if (field instanceof NativeQueryBuilder) {
innerQuery.select(field.toRaw());
} else if (field) {
innerQuery.select(field);
}
});
}
// multiple sub-queries are needed to get around mysql limitations with order by + limit + where in + group by (o.O)
// https://stackoverflow.com/questions/17892762/mysql-this-version-of-mysql-doesnt-yet-support-limit-in-all-any-some-subqu
const subSubQuery = this.platform.createNativeQueryBuilder();
subSubQuery.select(pks).from(innerQuery);
this.#state.limit = undefined;
this.#state.offset = undefined;
// Save the original WHERE conditions before pruning joins
const originalCond = this.#state.cond;
const populatePaths = this.getPopulatePaths();
if (!this.#state.fields.some(field => isRaw(field))) {
this.pruneJoinsForPagination(meta, populatePaths);
}
// Transfer WHERE conditions to ORDER BY joins (GH #6160)
this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
const { sql, params } = subSubQuery.compile();
this.select(this.#state.fields).where({
[Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
});
}
/**
* Computes the set of populate paths from the _populate hints.
*/
getPopulatePaths() {
const paths = new Set();
function addPath(hints, prefix = '') {
for (const hint of hints) {
const field = hint.field.split(':')[0];
const fullPath = prefix ? prefix + '.' + field : field;
paths.add(fullPath);
if (hint.children) {
addPath(hint.children, fullPath);
}
}
}
addPath(this.#state.populate);
return paths;
}
normalizeJoinPath(join, meta) {
return join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '') ?? '';
}
/**
* Transfers WHERE conditions to ORDER BY joins that are not used for population.
* This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
* GH #6160
*/
transferConditionsForOrderByJoins(meta, cond, populatePaths) {
if (!cond || this.#state.orderBy.length === 0) {
return;
}
const orderByAliases = new Set(
this.#state.orderBy
.flatMap(hint => Object.keys(hint))
.filter(k => !RawQueryFragment.isKnownFragmentSymbol(k))
.map(k => k.split('.')[0]),
);
for (const join of Object.values(this.#state.joins)) {
const joinPath = this.normalizeJoinPath(join, meta);
const isPopulateJoin = populatePaths.has(joinPath);
// Only transfer conditions for joins used for ORDER BY but not for population
if (orderByAliases.has(join.alias) && !isPopulateJoin) {
this.transferConditionsToJoin(cond, join);
}
}
}
/**
* Removes joins that are not used for population or ordering to improve performance.
*/
pruneJoinsForPagination(meta, populatePaths) {
const orderByAliases = this.#state.orderBy.flatMap(hint => Object.keys(hint)).map(k => k.split('.')[0]);
const joins = Object.entries(this.#state.joins);
const rootAlias = this.alias;
function addParentAlias(alias) {
const join = joins.find(j => j[1].alias === alias);
if (join && join[1].ownerAlias !== rootAlias) {
orderByAliases.push(join[1].ownerAlias);
addParentAlias(join[1].ownerAlias);
}
}
for (const orderByAlias of orderByAliases) {
addParentAlias(orderByAlias);
}
for (const [key, join] of joins) {
const path = this.normalizeJoinPath(join, meta);
if (!populatePaths.has(path) && !orderByAliases.includes(join.alias)) {
delete this.#state.joins[key];
}
}
}
/**
* Transfers WHERE conditions that reference a join alias to the join's ON clause.
* This is needed when a join is kept for ORDER BY after pagination wrapping,
* so the outer query orders by the same filtered rows as the subquery.
* @internal
*/
transferConditionsToJoin(cond, join, path = '') {
const aliasPrefix = join.alias + '.';
for (const key of Object.keys(cond)) {
const value = cond[key];
// Handle $and/$or operators - recurse into nested conditions
if (key === '$and' || key === '$or') {
if (Array.isArray(value)) {
for (const item of value) {
this.transferConditionsToJoin(item, join, path);
}
}
continue;
}
// Check if this condition references the join alias
if (key.startsWith(aliasPrefix)) {
// Add condition to the join's ON clause
join.cond[key] = value;
}
}
}
wrapModifySubQuery(meta) {
const subQuery = this.clone();
subQuery.#state.finalized = true;
// wrap one more time to get around MySQL limitations
// https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause
const subSubQuery = this.platform.createNativeQueryBuilder();
const method = this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) ? 'update' : 'delete';
const schema = this.getSchema(this.mainAlias);
const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
this.#state.cond = {}; // otherwise we would trigger validation error
this.#state.joins = {}; // included in the subquery
subSubQuery.select(pks).from(subQuery.as(this.mainAlias.aliasName));
const { sql, params } = subSubQuery.compile();
this[method](this.#state.data).where({
[Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
});
}
getSchema(alias) {
const { meta } = alias;
const metaSchema = meta.schema && meta.schema !== '*' ? meta.schema : undefined;
const schema = this.#state.schema ?? metaSchema ?? this.em?.schema ?? this.em?.config.getSchema(true);
if (schema === this.platform.getDefaultSchemaName()) {
return undefined;
}
return schema;
}
/** @internal */
createAlias(entityName, aliasName, subQuery) {
const meta = this.metadata.find(entityName);
const alias = { aliasName, entityName, meta, subQuery };
this.#state.aliases[aliasName] = alias;
return alias;
}
createMainAlias(entityName, aliasName, subQuery) {
this.#state.mainAlias = this.createAlias(entityName, aliasName, subQuery);
this.#helper = this.createQueryBuilderHelper();
return this.#state.mainAlias;
}
fromSubQuery(target, aliasName) {
const { entityName } = target.mainAlias;
aliasName ??= this.getNextAlias(entityName);
const subQuery = target.#state.unionQuery ? target.toRaw() : target.getNativeQuery();
this.createMainAlias(entityName, aliasName, subQuery);
}
fromEntityName(entityName, aliasName) {
aliasName ??= this.#state.mainAlias?.aliasName ?? this.getNextAlias(entityName);
this.createMainAlias(entityName, aliasName);
}
fromRawTable(tableName, aliasName) {
aliasName ??= this.#state.mainAlias?.aliasName ?? this.getNextAlias(tableName);
const meta = new EntityMetadata({
className: tableName,
collection: tableName,
});
meta.root = meta;
this.#state.mainAlias = {
aliasName,
entityName: tableName,
meta,
rawTableName: tableName,
};
this.#state.aliases[aliasName] = this.#state.mainAlias;
this.#helper = this.createQueryBuilderHelper();
}
createQueryBuilderHelper() {
return new QueryBuilderHelper(
this.mainAlias.entityName,
this.mainAlias.aliasName,
this.#state.aliases,
this.#state.subQueries,
this.driver,
this.#state.tptAlias,
);
}
ensureFromClause() {
/* v8 ignore next */
if (!this.#state.mainAlias) {
throw new Error(`Cannot proceed to build a query because the main alias is not set.`);
}
}
ensureNotFinalized() {
if (this.#state.finalized) {
throw new Error('This QueryBuilder instance is already finalized, clone it first if you want to modify it.');
}
}
/** @ignore */
/* v8 ignore next */
[Symbol.for('nodejs.util.inspect.custom')](depth = 2) {
const object = { ...this };
const hidden = ['metadata', 'driver', 'context', 'platform', 'type'];
Object.keys(object)
.filter(k => k.startsWith('_'))
.forEach(k => delete object[k]);
Object.keys(object)
.filter(k => object[k] == null)
.forEach(k => delete object[k]);
hidden.forEach(k => delete object[k]);
let prefix = this.type ? this.type.substring(0, 1) + this.type.toLowerCase().substring(1) : '';
if (this.#state.data) {
object.data = this.#state.data;
}
if (this.#state.schema) {
object.schema = this.#state.schema;
}
if (!Utils.isEmpty(this.#state.cond)) {
object.where = this.#state.cond;
}
if (this.#state.onConflict?.[0]) {
prefix = 'Upsert';
object.onConflict = this.#state.onConflict[0];
}
if (!Utils.isEmpty(this.#state.orderBy)) {
object.orderBy = this.#state.orderBy;
}
const name = this.#state.mainAlias
? `${prefix}QueryBuilder<${Utils.className(this.#state.mainAlias?.entityName)}>`
: 'QueryBuilder';
const ret = inspect(object, { depth });
return ret === '[Object]' ? `[${name}]` : name + ' ' + ret;
}
}
_a = QueryBuilder;