2313 lines
82 KiB
JavaScript
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 `/* ... *‍/`. Some characters are forbidden such as `/*, *‍/` and `?`.
|
|
*/
|
|
comment(comment) {
|
|
this.ensureNotFinalized();
|
|
this.#state.comments.push(...Utils.asArray(comment));
|
|
return this;
|
|
}
|
|
/**
|
|
* Add hints to the query using comment-like syntax `/*+ ... *‍/`. 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;
|