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

2124 lines
87 KiB
JavaScript

import {
ALIAS_REPLACEMENT_RE,
DatabaseDriver,
EntityManagerType,
getLoadingStrategy,
getOnConflictFields,
getOnConflictReturningFields,
helper,
isRaw,
LoadStrategy,
parseJsonSafe,
PolymorphicRef,
QueryFlag,
QueryHelper,
QueryOrder,
raw,
RawQueryFragment,
ReferenceKind,
Utils,
} from '@mikro-orm/core';
import { QueryBuilder } from './query/QueryBuilder.js';
import { JoinType, QueryType } from './query/enums.js';
import { SqlEntityManager } from './SqlEntityManager.js';
import { PivotCollectionPersister } from './PivotCollectionPersister.js';
/** Base class for SQL database drivers, implementing find/insert/update/delete using QueryBuilder. */
export class AbstractSqlDriver extends DatabaseDriver {
[EntityManagerType];
connection;
replicas = [];
platform;
constructor(config, platform, connection, connector) {
super(config, connector);
this.connection = new connection(this.config);
this.replicas = this.createReplicas(conf => new connection(this.config, conf, 'read'));
this.platform = platform;
}
getPlatform() {
return this.platform;
}
/** Evaluates a formula callback, handling both string and Raw return values. */
evaluateFormula(formula, columns, table) {
const result = formula(columns, table);
return isRaw(result) ? this.platform.formatQuery(result.sql, result.params) : result;
}
/** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
getTableProps(meta) {
return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
}
/** Creates a FormulaTable object for use in formula callbacks. */
createFormulaTable(alias, meta, schema) {
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
const qualifiedName = effectiveSchema ? `${effectiveSchema}.${meta.tableName}` : meta.tableName;
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
}
validateSqlOptions(options) {
if (options.collation != null && typeof options.collation !== 'string') {
throw new Error(
'Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.',
);
}
if (options.indexHint != null && typeof options.indexHint !== 'string') {
throw new Error(
"indexHint for SQL drivers must be a string (e.g. 'force index(my_index)'). Use an object only with MongoDB.",
);
}
}
createEntityManager(useContext) {
const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
return new EntityManagerClass(this.config, this, this.metadata, useContext);
}
async createQueryBuilderFromOptions(meta, where, options = {}) {
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
const joinedProps = this.joinedProps(meta, populate, options);
const schema = this.getSchemaName(meta, options);
const qb = this.createQueryBuilder(
meta.class,
options.ctx,
connectionType,
false,
options.logging,
undefined,
options.em,
).withSchema(schema);
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
const orderBy = this.buildOrderBy(qb, meta, populate, options);
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
if (Utils.isPrimaryKey(where, meta.compositePK)) {
where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
}
this.validateSqlOptions(options);
const { first, last, before, after } = options;
const isCursorPagination = [first, last, before, after].some(v => v != null);
qb.state.resolvedPopulateWhere = options._populateWhere;
qb.select(fields)
// only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
.populate(
populate,
joinedProps.length > 0 ? populateWhere : undefined,
joinedProps.length > 0 ? options.populateFilter : undefined,
)
.where(where)
.groupBy(options.groupBy)
.having(options.having)
.indexHint(options.indexHint)
.collation(options.collation)
.comment(options.comments)
.hintComment(options.hintComments);
if (isCursorPagination) {
const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
qb.andWhere(where).orderBy(newOrderBy);
} else {
qb.orderBy(orderBy);
}
if (options.limit != null || options.offset != null) {
qb.limit(options.limit, options.offset);
}
if (options.lockMode) {
qb.setLockMode(options.lockMode, options.lockTableAliases);
}
if (options.em) {
await qb.applyJoinedFilters(options.em, options.filters);
}
return qb;
}
async find(entityName, where, options = {}) {
options = { populate: [], orderBy: [], ...options };
const meta = this.metadata.get(entityName);
if (meta.virtual) {
return this.findVirtual(entityName, where, options);
}
if (options.unionWhere?.length) {
where = await this.applyUnionWhere(meta, where, options);
}
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
const result = await this.rethrow(qb.execute('all'));
if (options.last && !options.first) {
result.reverse();
}
return result;
}
async findOne(entityName, where, options) {
const opts = { populate: [], ...options };
const meta = this.metadata.find(entityName);
const populate = this.autoJoinOneToOneOwner(meta, opts.populate, opts.fields);
const joinedProps = this.joinedProps(meta, populate, options);
const hasToManyJoins = joinedProps.some(hint => this.hasToManyJoins(hint, meta));
if (joinedProps.length === 0 || !hasToManyJoins) {
opts.limit = 1;
}
if (opts.limit > 0 && !opts.flags?.includes(QueryFlag.DISABLE_PAGINATE)) {
opts.flags ??= [];
opts.flags.push(QueryFlag.DISABLE_PAGINATE);
}
const res = await this.find(entityName, where, opts);
return res[0] || null;
}
hasToManyJoins(hint, meta) {
const [propName] = hint.field.split(':', 2);
const prop = meta.properties[propName];
if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
return true;
}
if (hint.children && prop.targetMeta) {
return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
}
return false;
}
async findVirtual(entityName, where, options) {
return this.findFromVirtual(entityName, where, options, QueryType.SELECT);
}
async countVirtual(entityName, where, options) {
return this.findFromVirtual(entityName, where, options, QueryType.COUNT);
}
async findFromVirtual(entityName, where, options, type) {
const meta = this.metadata.get(entityName);
/* v8 ignore next */
if (!meta.expression) {
return type === QueryType.SELECT ? [] : 0;
}
if (typeof meta.expression === 'string') {
return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
}
const em = this.createEntityManager();
em.setTransactionContext(options.ctx);
const res = meta.expression(em, where, options);
if (typeof res === 'string') {
return this.wrapVirtualExpressionInSubquery(meta, res, where, options, type);
}
if (res instanceof QueryBuilder) {
return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options, type);
}
if (res instanceof RawQueryFragment) {
const expr = this.platform.formatQuery(res.sql, res.params);
return this.wrapVirtualExpressionInSubquery(meta, expr, where, options, type);
}
/* v8 ignore next */
return res;
}
async *streamFromVirtual(entityName, where, options) {
const meta = this.metadata.get(entityName);
/* v8 ignore next */
if (!meta.expression) {
return;
}
if (typeof meta.expression === 'string') {
yield* this.wrapVirtualExpressionInSubqueryStream(meta, meta.expression, where, options, QueryType.SELECT);
return;
}
const em = this.createEntityManager();
em.setTransactionContext(options.ctx);
const res = meta.expression(em, where, options, true);
if (typeof res === 'string') {
yield* this.wrapVirtualExpressionInSubqueryStream(meta, res, where, options, QueryType.SELECT);
return;
}
if (res instanceof QueryBuilder) {
yield* this.wrapVirtualExpressionInSubqueryStream(
meta,
res.getFormattedQuery(),
where,
options,
QueryType.SELECT,
);
return;
}
if (res instanceof RawQueryFragment) {
const expr = this.platform.formatQuery(res.sql, res.params);
yield* this.wrapVirtualExpressionInSubqueryStream(meta, expr, where, options, QueryType.SELECT);
return;
}
/* v8 ignore next */
yield* res;
}
async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
qb.setFlag(QueryFlag.DISABLE_PAGINATE);
const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
const native = qb.getNativeQuery(false);
if (type === QueryType.COUNT) {
native.clear('select').clear('limit').clear('offset').count();
}
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
const query = native.compile();
const res = await this.execute(query.sql, query.params, 'all', options.ctx);
if (type === QueryType.COUNT) {
return res[0].count;
}
if (isCursorPagination && !options.first && !!options.last) {
res.reverse();
}
return res.map(row => this.mapResult(row, meta));
}
async *wrapVirtualExpressionInSubqueryStream(meta, expression, where, options, type) {
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
qb.unsetFlag(QueryFlag.DISABLE_PAGINATE);
const native = qb.getNativeQuery(false);
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
const query = native.compile();
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
for await (const row of res) {
yield this.mapResult(row, meta);
}
}
/**
* Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
* Force balanced strategy to load to-many relations via separate queries.
*/
forceBalancedStrategy(options) {
const clearStrategy = hints => {
return hints.map(hint => ({
...hint,
strategy: undefined,
children: hint.children ? clearStrategy(hint.children) : undefined,
}));
};
const opts = { ...options, strategy: 'balanced' };
if (Array.isArray(opts.populate)) {
opts.populate = clearStrategy(opts.populate);
}
return opts;
}
mapResult(result, meta, populate = [], qb, map = {}) {
// For TPT inheritance, map aliased parent table columns back to their field names
if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
this.mapTPTColumns(result, meta, qb);
}
// For TPT polymorphic queries (querying a base class), map child table fields
if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
this.mapTPTChildFields(result, meta, mainAlias, qb, result);
}
const ret = super.mapResult(result, meta);
/* v8 ignore next */
if (!ret) {
return null;
}
if (qb) {
// here we map the aliased results (cartesian product) to an object graph
this.mapJoinedProps(ret, meta, populate, qb, ret, map);
}
return ret;
}
/**
* Maps aliased columns from TPT parent tables back to their original field names.
* TPT parent columns are selected with aliases like `parent_alias__column_name`,
* and need to be renamed back to `column_name` for the result mapper to work.
*/
mapTPTColumns(result, meta, qb) {
const tptAliases = qb.state.tptAlias;
// Walk up the TPT hierarchy
let parentMeta = meta.tptParent;
while (parentMeta) {
const parentAlias = tptAliases[parentMeta.className];
if (parentAlias) {
// Rename columns from this parent table
for (const prop of parentMeta.ownProps) {
for (const fieldName of prop.fieldNames) {
const aliasedKey = `${parentAlias}__${fieldName}`;
if (aliasedKey in result) {
// Copy the value to the unaliased field name and remove the aliased key
result[fieldName] = result[aliasedKey];
delete result[aliasedKey];
}
}
}
}
parentMeta = parentMeta.tptParent;
}
}
mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
const joinedProps = this.joinedProps(meta, populate);
joinedProps.forEach(hint => {
const [propName, ref] = hint.field.split(':', 2);
const prop = meta.properties[propName];
/* v8 ignore next */
if (!prop) {
return;
}
// Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
// Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
if (
prop.polymorphic &&
prop.polymorphTargets?.length &&
!ref &&
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
) {
const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
const pathPrefix = !parentJoinPath ? '[populate]' : '';
let matched = false;
for (const targetMeta of prop.polymorphTargets) {
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
const meta2 = targetMeta;
const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
const hasPK = meta2
.getPrimaryProps()
.every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
if (hasPK && !matched) {
matched = true;
const relationPojo = {};
const tz = this.platform.getTimezone();
for (const p of targetProps) {
this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
}
// Inject the entity class constructor so that the factory creates the correct type
Object.defineProperty(relationPojo, 'constructor', {
value: meta2.class,
enumerable: false,
configurable: true,
});
result[prop.name] = relationPojo;
const populateChildren = hint.children || [];
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
}
// Clean up aliased columns for ALL targets (even non-matching ones)
for (const p of targetProps) {
for (const name of p.fieldNames) {
delete root[`${relationAlias}__${name}`];
}
}
}
if (!matched) {
result[prop.name] = null;
}
return;
}
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
const meta2 = prop.targetMeta;
let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
if (!parentJoinPath) {
path = '[populate]' + path;
}
if (pivotRefJoin) {
path += '[pivot]';
}
const relationAlias = qb.getAliasForJoinPath(path, { matchPopulateJoins: true });
/* v8 ignore next */
if (!relationAlias) {
return;
}
// pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
if (pivotRefJoin) {
let item;
if (prop.inverseJoinColumns.length > 1) {
// composite keys
item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
} else {
const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}`;
item = root[alias];
}
prop.joinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
prop.inverseJoinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
result[prop.name] ??= [];
if (item) {
result[prop.name].push(item);
}
return;
}
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
const targetProps = mapToPk
? meta2.getPrimaryProps()
: meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
// If the primary key value for the relation is null, we know we haven't joined to anything
// and therefore we don't return any record (since all values would be null)
const hasPK = meta2.getPrimaryProps().every(pk =>
pk.fieldNames.every(name => {
return root[`${relationAlias}__${name}`] != null;
}),
);
if (!hasPK) {
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
result[prop.name] = [];
}
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
result[prop.name] = null;
}
for (const prop of targetProps) {
for (const name of prop.fieldNames) {
delete root[`${relationAlias}__${name}`];
}
}
return;
}
let relationPojo = {};
meta2.props
.filter(prop => !ref && prop.persist === false && prop.fieldNames)
.forEach(prop => {
/* v8 ignore next */
if (prop.fieldNames.length > 1) {
// composite keys
relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
} else {
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
relationPojo[prop.name] = root[alias];
}
});
const tz = this.platform.getTimezone();
for (const prop of targetProps) {
this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
}
// Handle TPT polymorphic child fields - map fields from child table aliases
this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
// properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
// so we need to delete them after everything is mapped from given level
for (const prop of targetProps) {
for (const name of prop.fieldNames) {
delete root[`${relationAlias}__${name}`];
}
}
if (mapToPk) {
const tmp = Object.values(relationPojo);
/* v8 ignore next */
relationPojo = meta2.compositePK ? tmp : tmp[0];
}
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
result[prop.name] ??= [];
result[prop.name].push(relationPojo);
} else {
result[prop.name] = relationPojo;
}
const populateChildren = hint.children || [];
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
});
}
/**
* Maps a single property from a joined result row into the relation pojo.
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
*/
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
return;
}
if (prop.polymorphic) {
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
const discriminatorValue = root[discriminatorAlias];
const pkFieldNames = prop.fieldNames.slice(1);
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
if (discriminatorValue != null && pkValue != null) {
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
} else {
relationPojo[prop.name] = null;
}
} else if (prop.fieldNames.length > 1) {
// composite keys
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
} else if (prop.runtimeType === 'Date') {
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
const value = root[alias];
if (
tz &&
tz !== 'local' &&
typeof value === 'string' &&
!value.includes('+') &&
value.lastIndexOf('-') < 11 &&
!value.endsWith('Z')
) {
relationPojo[prop.name] = this.platform.parseDate(value + tz);
} else if (['string', 'number'].includes(typeof value)) {
relationPojo[prop.name] = this.platform.parseDate(value);
} else {
relationPojo[prop.name] = value;
}
} else {
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
relationPojo[prop.name] = root[alias];
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
const item = parseJsonSafe(relationPojo[prop.name]);
if (Array.isArray(item)) {
relationPojo[prop.name] = item.map(row =>
row == null ? row : this.comparator.mapResult(prop.targetMeta, row),
);
} else {
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
}
}
}
if (options?.deleteFromRoot) {
for (const name of prop.fieldNames) {
delete root[`${relationAlias}__${name}`];
}
}
}
async count(entityName, where, options = {}) {
const meta = this.metadata.get(entityName);
if (meta.virtual) {
return this.countVirtual(entityName, where, options);
}
if (options.unionWhere?.length) {
where = await this.applyUnionWhere(meta, where, options);
}
options = { populate: [], ...options };
const populate = options.populate;
const joinedProps = this.joinedProps(meta, populate, options);
const schema = this.getSchemaName(meta, options);
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
if (meta && !Utils.isEmpty(populate)) {
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
}
this.validateSqlOptions(options);
qb.state.resolvedPopulateWhere = options._populateWhere;
qb.indexHint(options.indexHint)
.collation(options.collation)
.comment(options.comments)
.hintComment(options.hintComments)
.groupBy(options.groupBy)
.having(options.having)
.populate(
populate,
joinedProps.length > 0 ? populateWhere : undefined,
joinedProps.length > 0 ? options.populateFilter : undefined,
)
.withSchema(schema)
.where(where);
if (options.em) {
await qb.applyJoinedFilters(options.em, options.filters);
}
return this.rethrow(qb.getCount());
}
async nativeInsert(entityName, data, options = {}) {
options.convertCustomTypes ??= true;
const meta = this.metadata.get(entityName);
const collections = this.extractManyToMany(meta, data);
const qb = this.createQueryBuilder(
entityName,
options.ctx,
'write',
options.convertCustomTypes,
options.loggerContext,
).withSchema(this.getSchemaName(meta, options));
const res = await this.rethrow(qb.insert(data).execute('run', false));
res.row = res.row || {};
let pk;
if (meta.primaryKeys.length > 1) {
// owner has composite pk
pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
} else {
/* v8 ignore next */
res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
if (options.convertCustomTypes && meta?.getPrimaryProp().customType) {
pk = [meta.getPrimaryProp().customType.convertToDatabaseValue(res.insertId, this.platform)];
} else {
pk = [res.insertId];
}
}
await this.processManyToMany(meta, pk, collections, false, options);
return res;
}
async nativeInsertMany(entityName, data, options = {}, transform) {
options.processCollections ??= true;
options.convertCustomTypes ??= true;
const entityMeta = this.metadata.get(entityName);
const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
const pks = this.getPrimaryKeyFields(meta);
const set = new Set();
data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
// For STI with conflicting fieldNames, include all alternative columns
let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
const duplicates = Utils.findDuplicates(fields);
const params = [];
if (duplicates.length) {
fields = Utils.unique(fields);
}
const tableName = this.getTableName(meta, options);
let sql = `insert into ${tableName} `;
sql +=
fields.length > 0
? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
: `(${this.platform.quoteIdentifier(pks[0])})`;
if (this.platform.usesOutputStatement()) {
const returningProps = this.getTableProps(meta)
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
sql +=
returningFields.length > 0
? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
: '';
}
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
sql += ' values ';
} else {
sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
}
const addParams = (prop, row) => {
const rowValue = row[prop.name];
if (prop.nullable && rowValue === null) {
params.push(null);
return;
}
let value = rowValue ?? prop.default;
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
if (prop.array && value) {
value = this.platform.cloneEmbeddable(value);
for (let i = 0; i < value.length; i++) {
const item = value[i];
value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
}
} else {
value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
}
}
if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
params.push(raw('default'));
return;
}
if (options.convertCustomTypes && prop.customType) {
params.push(
prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }),
);
return;
}
params.push(value);
};
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
sql += data
.map(row => {
const keys = [];
const usedDups = [];
props.forEach(prop => {
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
for (const field of prop.stiFieldNames) {
params.push(field === activeField ? row[prop.name] : null);
keys.push('?');
}
return;
}
if (prop.fieldNames.length > 1) {
const newFields = [];
let rawParam;
const target = row[prop.name];
if (prop.polymorphic && target instanceof PolymorphicRef) {
rawParam = target.toTuple();
} else {
rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
}
// Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
// TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
// but that means changing the compiled PK getters, which might be seen as breaking
const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
const param = [];
columns.forEach((field, idx) => {
if (usedDups.includes(field)) {
return;
}
newFields.push(field);
param.push(allParam[idx]);
});
newFields.forEach((field, idx) => {
if (!duplicates.includes(field) || !usedDups.includes(field)) {
params.push(param[idx]);
keys.push('?');
usedDups.push(field);
}
});
} else {
const field = prop.fieldNames[0];
if (!duplicates.includes(field) || !usedDups.includes(field)) {
if (
prop.customType &&
!prop.object &&
'convertToDatabaseValueSQL' in prop.customType &&
row[prop.name] != null &&
!isRaw(row[prop.name])
) {
keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
} else {
keys.push('?');
}
addParams(prop, row);
usedDups.push(field);
}
}
});
return '(' + (keys.join(', ') || 'default') + ')';
})
.join(', ');
}
if (meta && this.platform.usesReturningStatement()) {
const returningProps = this.getTableProps(meta)
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
/* v8 ignore next */
sql +=
returningFields.length > 0
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
: '';
}
if (transform) {
sql = transform(sql);
}
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
let pk;
/* v8 ignore next */
if (pks.length > 1) {
// owner has composite pk
pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
} else {
res.row ??= {};
res.rows ??= [];
pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
res.insertId = res.insertId || res.row[pks[0]];
}
for (let i = 0; i < collections.length; i++) {
await this.processManyToMany(meta, pk[i], collections[i], false, options);
}
return res;
}
async nativeUpdate(entityName, where, data, options = {}) {
options.convertCustomTypes ??= true;
const meta = this.metadata.get(entityName);
const pks = this.getPrimaryKeyFields(meta);
const collections = this.extractManyToMany(meta, data);
let res = { affectedRows: 0, insertId: 0, row: {} };
if (Utils.isPrimaryKey(where) && pks.length === 1) {
/* v8 ignore next */
where = { [meta.primaryKeys[0] ?? pks[0]]: where };
}
if (!options.upsert && options.unionWhere?.length) {
where = await this.applyUnionWhere(meta, where, options, true);
}
if (Utils.hasObjectKeys(data)) {
const qb = this.createQueryBuilder(
entityName,
options.ctx,
'write',
options.convertCustomTypes,
options.loggerContext,
).withSchema(this.getSchemaName(meta, options));
if (options.upsert) {
/* v8 ignore next */
const uniqueFields =
options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
qb.insert(data).onConflict(uniqueFields).returning(returning);
if (!options.onConflictAction || options.onConflictAction === 'merge') {
const fields = getOnConflictFields(meta, data, uniqueFields, options);
qb.merge(fields);
}
if (options.onConflictAction === 'ignore') {
qb.ignore();
}
if (options.onConflictWhere) {
qb.where(options.onConflictWhere);
}
} else {
qb.update(data).where(where);
// reload generated columns and version fields
const returning = [];
meta.props
.filter(prop => (prop.generated && !prop.primary) || prop.version)
.forEach(prop => returning.push(prop.name));
qb.returning(returning);
}
res = await this.rethrow(qb.execute('run', false));
}
/* v8 ignore next */
const pk = pks.map(pk => Utils.extractPK(data[pk] || where, meta));
await this.processManyToMany(meta, pk, collections, true, options);
return res;
}
async nativeUpdateMany(entityName, where, data, options = {}, transform) {
options.processCollections ??= true;
options.convertCustomTypes ??= true;
const meta = this.metadata.get(entityName);
if (options.upsert) {
const uniqueFields =
options.onConflictFields ??
(Utils.isPlainObject(where[0])
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
: meta.primaryKeys);
const qb = this.createQueryBuilder(
entityName,
options.ctx,
'write',
options.convertCustomTypes,
options.loggerContext,
).withSchema(this.getSchemaName(meta, options));
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
qb.insert(data).onConflict(uniqueFields).returning(returning);
if (!options.onConflictAction || options.onConflictAction === 'merge') {
const fields = getOnConflictFields(meta, data[0], uniqueFields, options);
qb.merge(fields);
}
if (options.onConflictAction === 'ignore') {
qb.ignore();
}
if (options.onConflictWhere) {
qb.where(options.onConflictWhere);
}
return this.rethrow(qb.execute('run', false));
}
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
const keys = new Set();
const fields = new Set();
const returning = new Set();
for (const row of data) {
for (const k of Utils.keys(row)) {
keys.add(k);
if (isRaw(row[k])) {
returning.add(k);
}
}
}
// reload generated columns and version fields
meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
.join(' and ');
const params = [];
let sql = `update ${this.getTableName(meta, options)} set `;
const addParams = (prop, value) => {
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
if (prop.array && value) {
for (let i = 0; i < value.length; i++) {
const item = value[i];
value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
}
} else {
value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
}
}
params.push(value);
};
for (const key of keys) {
const prop = meta.properties[key] ?? meta.root.properties[key];
if (prop.polymorphic && prop.fieldNames.length > 1) {
for (let idx = 0; idx < data.length; idx++) {
const rowValue = data[idx][key];
if (rowValue instanceof PolymorphicRef) {
data[idx][key] = rowValue.toTuple();
}
}
}
prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
return;
}
fields.add(fieldName);
sql += `${this.platform.quoteIdentifier(fieldName)} = case`;
where.forEach((cond, idx) => {
if (key in data[idx]) {
const pks = Utils.getOrderedPrimaryKeys(cond, meta);
sql += ` when (${pkCond}) then `;
if (
prop.customType &&
!prop.object &&
'convertToDatabaseValueSQL' in prop.customType &&
data[idx][prop.name] != null &&
!isRaw(data[idx][key])
) {
sql += prop.customType.convertToDatabaseValueSQL('?', this.platform);
} else {
sql += '?';
}
params.push(...pks);
addParams(prop, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
}
});
sql += ` else ${this.platform.quoteIdentifier(fieldName)} end, `;
return sql;
});
}
if (meta.versionProperty) {
const versionProperty = meta.properties[meta.versionProperty];
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
sql += `${quotedFieldName} = `;
if (versionProperty.runtimeType === 'Date') {
sql += this.platform.getCurrentTimestampSQL(versionProperty.length);
} else {
sql += `${quotedFieldName} + 1`;
}
sql += `, `;
}
sql = sql.substring(0, sql.length - 2) + ' where ';
const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
sql +=
pks.length > 1
? `(${pks.map(pk => this.platform.quoteIdentifier(pk)).join(', ')})`
: this.platform.quoteIdentifier(pks[0]);
const conds = where.map(cond => {
if (Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
cond = Object.values(cond)[0];
}
if (pks.length > 1) {
pkProps.forEach(pk => {
if (Array.isArray(cond[pk])) {
params.push(...Utils.flatten(cond[pk]));
} else {
params.push(cond[pk]);
}
});
return `(${Array.from({ length: pks.length }).fill('?').join(', ')})`;
}
params.push(cond);
return '?';
});
sql += ` in (${conds.join(', ')})`;
if (this.platform.usesReturningStatement() && returning.size > 0) {
const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
/* v8 ignore next */
sql +=
returningFields.length > 0
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
: '';
}
if (transform) {
sql = transform(sql, params);
}
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
for (let i = 0; i < collections.length; i++) {
await this.processManyToMany(meta, where[i], collections[i], false, options);
}
return res;
}
async nativeDelete(entityName, where, options = {}) {
const meta = this.metadata.get(entityName);
const pks = this.getPrimaryKeyFields(meta);
if (Utils.isPrimaryKey(where) && pks.length === 1) {
where = { [pks[0]]: where };
}
if (options.unionWhere?.length) {
where = await this.applyUnionWhere(meta, where, options, true);
}
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
.delete(where)
.withSchema(this.getSchemaName(meta, options));
return this.rethrow(qb.execute('run', false));
}
/**
* Fast comparison for collection snapshots that are represented by PK arrays.
* Compares scalars via `===` and fallbacks to Utils.equals()` for more complex types like Buffer.
* Always expects the same length of the arrays, since we only compare PKs of the same entity type.
*/
comparePrimaryKeyArrays(a, b) {
for (let i = a.length; i-- !== 0; ) {
if (['number', 'string', 'bigint', 'boolean'].includes(typeof a[i])) {
if (a[i] !== b[i]) {
return false;
}
} else {
if (!Utils.equals(a[i], b[i])) {
return false;
}
}
}
return true;
}
async syncCollections(collections, options) {
const groups = {};
for (const coll of collections) {
const wrapped = helper(coll.owner);
const meta = wrapped.__meta;
const pks = wrapped.getPrimaryKeys(true);
const snap = coll.getSnapshot();
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
const insertDiff = current.filter(item => !includes(snapshot, item));
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
const equals = Utils.equals(current, target);
// wrong order if we just delete and insert to the end (only owning sides can have fixed order)
if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
deleteDiff.length = insertDiff.length = 0;
for (const item of snapshot) {
deleteDiff.push(item);
}
for (const item of current) {
insertDiff.push(item);
}
}
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
const cols = coll.property.referencedColumnNames;
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(
this.getSchemaName(meta, options),
);
if (coll.getSnapshot() === undefined) {
if (coll.property.orphanRemoval) {
const query = qb
.delete({ [coll.property.mappedBy]: pks })
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
await this.rethrow(query.execute());
continue;
}
const query = qb
.update({ [coll.property.mappedBy]: null })
.where({ [coll.property.mappedBy]: pks })
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
await this.rethrow(query.execute());
continue;
}
/* v8 ignore next */
const query = qb
.update({ [coll.property.mappedBy]: pks })
.where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
await this.rethrow(query.execute());
continue;
}
const pivotMeta = this.metadata.find(coll.property.pivotEntity);
let schema = pivotMeta.schema;
if (schema === '*') {
if (coll.property.owner) {
schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
} else {
const targetMeta = coll.property.targetMeta;
const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
schema =
targetMeta.schema === '*'
? (options?.schema ?? targetSchema ?? this.config.get('schema'))
: targetMeta.schema;
}
} else if (schema == null) {
schema = this.config.get('schema');
}
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
const persister = (groups[tableName] ??= new PivotCollectionPersister(
pivotMeta,
this,
options?.ctx,
schema,
options?.loggerContext,
));
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
}
for (const persister of Utils.values(groups)) {
await this.rethrow(persister.execute());
}
}
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
/* v8 ignore next */
if (owners.length === 0) {
return {};
}
const pivotMeta = this.metadata.get(prop.pivotEntity);
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
}
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
const ownerMeta = pivotProp2.targetMeta;
// The pivot query builder doesn't convert custom types, so we need to manually
// convert owner PKs to DB format for the query and convert result FKs back to
// JS format for consistent key hashing in buildPivotResultMap.
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
if (needsConversion) {
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
}
const cond = {
[pivotProp2.name]: { $in: ownerPks },
};
if (!Utils.isEmpty(where)) {
cond[pivotProp1.name] = { ...where };
}
where = cond;
const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
const res = await this.find(pivotMeta.class, where, {
ctx,
...options,
fields,
exclude: childExclude,
orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
populate: [
{
field: populateField,
strategy: LoadStrategy.JOINED,
joinType: JoinType.innerJoin,
children: populate,
dataOnly: pivotProp1.mapToPk && !pivotJoin,
},
],
populateWhere: undefined,
// @ts-ignore
_populateWhere: 'infer',
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
});
// Convert result FK values back to JS format so key hashing
// in buildPivotResultMap is consistent with the owner keys.
if (needsConversion) {
for (const item of res) {
const fk = item[pivotProp2.name];
if (fk != null) {
item[pivotProp2.name] = pkProp.customType.convertToJSValue(fk, this.platform);
}
}
}
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
}
/**
* Load from a polymorphic M:N pivot table.
*/
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
const pivotMeta = this.metadata.get(prop.pivotEntity);
// Find the M:1 relation on the pivot pointing to the target entity.
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
const inverseProp = pivotMeta.relations.find(
r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta,
);
if (inverseProp) {
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
}
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
}
/**
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
*/
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
const pivotMeta = this.metadata.get(prop.pivotEntity);
const targetMeta = prop.targetMeta;
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
const cond = {
[prop.discriminatorColumn]: prop.discriminatorValue,
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
};
if (!Utils.isEmpty(where)) {
cond[inverseProp.name] = { ...where };
}
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
const fields = pivotJoin
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
const res = await this.find(pivotMeta.class, cond, {
ctx,
...options,
fields,
exclude: childExclude,
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
populate: [
{
field: populateField,
strategy: LoadStrategy.JOINED,
joinType: JoinType.innerJoin,
children: populate,
dataOnly: inverseProp.mapToPk && !pivotJoin,
},
],
populateWhere: undefined,
// @ts-ignore
_populateWhere: 'infer',
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
});
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
}
/**
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
* Uses single query with join via virtual relation on pivot.
*/
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
const pivotMeta = this.metadata.get(prop.pivotEntity);
const targetMeta = prop.targetMeta;
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
const ownerProp = pivotMeta.properties[ownerRelationName];
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
const cond = {
[prop.discriminatorColumn]: prop.discriminatorValue,
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
};
if (!Utils.isEmpty(where)) {
cond[ownerRelationName] = { ...where };
}
const populateField = ownerRelationName;
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
const res = await this.find(pivotMeta.class, cond, {
ctx,
...options,
fields,
exclude: childExclude,
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
populate: [
{
field: populateField,
strategy: LoadStrategy.JOINED,
joinType: JoinType.innerJoin,
children: populate,
},
],
populateWhere: undefined,
// @ts-ignore
_populateWhere: 'infer',
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
});
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
}
/**
* Build a map from owner PKs to their related entities from pivot table results.
*/
buildPivotResultMap(owners, results, keyProp, valueProp) {
const map = {};
for (const owner of owners) {
const key = Utils.getPrimaryKeyHash(owner);
map[key] = [];
}
for (const item of results) {
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
const entity = item[valueProp];
if (map[key]) {
map[key].push(entity);
}
}
return map;
}
wrapPopulateFilter(options, propName) {
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
return { [propName]: options?.populateFilter };
}
return undefined;
}
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
}
if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
return Utils.asArray(parentOrderBy)
.filter(o => o[prop.name])
.map(o => ({ [pivotProp.name]: o[prop.name] }));
}
if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
}
if (prop.fixedOrder) {
return [{ [prop.fixedOrderColumn]: QueryOrder.ASC }];
}
return [];
}
async execute(query, params = [], method = 'all', ctx, loggerContext) {
return this.rethrow(this.connection.execute(query, params, method, ctx, loggerContext));
}
async *stream(entityName, where, options) {
options = { populate: [], orderBy: [], ...options };
const meta = this.metadata.get(entityName);
if (meta.virtual) {
yield* this.streamFromVirtual(entityName, where, options);
return;
}
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
try {
const result = qb.stream(options);
for await (const item of result) {
yield item;
}
} catch (e) {
throw this.convertException(e);
}
}
/**
* 1:1 owner side needs to be marked for population so QB auto-joins the owner id
*/
autoJoinOneToOneOwner(meta, populate, fields = []) {
if (!this.config.get('autoJoinOneToOneOwner')) {
return populate;
}
const relationsToPopulate = populate.map(({ field }) => field.split(':')[0]);
const toPopulate = meta.relations
.filter(
prop =>
prop.kind === ReferenceKind.ONE_TO_ONE &&
!prop.owner &&
!prop.lazy &&
!relationsToPopulate.includes(prop.name),
)
.filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
.map(prop => ({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED }));
return [...populate, ...toPopulate];
}
/**
* @internal
*/
joinedProps(meta, populate, options) {
return populate.filter(hint => {
const [propName, ref] = hint.field.split(':', 2);
const prop = meta.properties[propName] || {};
const strategy = getLoadingStrategy(
hint.strategy || prop.strategy || options?.strategy || this.config.get('loadStrategy'),
prop.kind,
);
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
return true;
}
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
return false;
}
if (strategy !== LoadStrategy.JOINED) {
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
}
return ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind);
});
}
/**
* @internal
*/
mergeJoinedResult(rawResults, meta, joinedProps) {
if (rawResults.length <= 1) {
return rawResults;
}
const res = [];
const map = {};
const collectionsToMerge = {};
const hints = joinedProps.map(hint => {
const [propName, ref] = hint.field.split(':', 2);
return { propName, ref, children: hint.children };
});
for (const item of rawResults) {
const pk = Utils.getCompositeKeyHash(item, meta);
if (map[pk]) {
for (const { propName } of hints) {
if (!item[propName]) {
continue;
}
collectionsToMerge[pk] ??= {};
collectionsToMerge[pk][propName] ??= [map[pk][propName]];
collectionsToMerge[pk][propName].push(item[propName]);
}
} else {
map[pk] = item;
res.push(item);
}
}
for (const pk in collectionsToMerge) {
const entity = map[pk];
const collections = collectionsToMerge[pk];
for (const { propName, ref, children } of hints) {
if (!collections[propName]) {
continue;
}
const prop = meta.properties[propName];
const items = collections[propName].flat();
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
entity[propName] = items;
continue;
}
switch (prop.kind) {
case ReferenceKind.ONE_TO_MANY:
case ReferenceKind.MANY_TO_MANY:
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
break;
case ReferenceKind.MANY_TO_ONE:
case ReferenceKind.ONE_TO_ONE:
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
break;
}
}
}
return res;
}
shouldHaveColumn(meta, prop, populate, fields, exclude) {
if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
return false;
}
if (!fields || fields.includes('*') || prop.primary || meta.root.discriminatorColumn === prop.name) {
return true;
}
return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
}
getFieldsForJoinedLoad(qb, meta, options) {
const fields = [];
const populate = options.populate ?? [];
const joinedProps = this.joinedProps(meta, populate, options);
const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
// Ensure TPT joins are applied early so that _tptAlias is available for join resolution
// This is needed when populating relations that are inherited from TPT parent entities
if (!options.parentJoinPath) {
qb.ensureTPTJoins();
}
// root entity is already handled, skip that
if (options.parentJoinPath) {
// alias all fields in the primary table
meta.props
.filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
.forEach(prop =>
fields.push(
...this.mapPropToFieldNames(
qb,
prop,
options.parentTableAlias,
meta,
options.schema,
options.explicitFields,
),
),
);
}
for (const hint of joinedProps) {
const [propName, ref] = hint.field.split(':', 2);
const prop = meta.properties[propName];
// Polymorphic to-one: create a LEFT JOIN per target type
// Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
if (
prop.polymorphic &&
prop.polymorphTargets?.length &&
!ref &&
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
) {
const basePath = options.parentJoinPath
? `${options.parentJoinPath}.${prop.name}`
: `${meta.name}.${prop.name}`;
const pathPrefix =
!options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
for (const targetMeta of prop.polymorphTargets) {
const tableAlias = qb.getNextAlias(targetMeta.className);
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
qb.addPolymorphicJoin(
prop,
targetMeta,
options.parentTableAlias,
tableAlias,
JoinType.leftJoin,
targetPath,
schema,
);
// Select fields from each target table
fields.push(
...this.getFieldsForJoinedLoad(qb, targetMeta, {
...options,
populate: hint.children,
parentTableAlias: tableAlias,
parentJoinPath: targetPath,
}),
);
}
continue;
}
// ignore ref joins of known FKs unless it's a filter hint
if (
ref &&
!hint.filter &&
(prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
) {
continue;
}
const meta2 = prop.targetMeta;
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
const tableAlias = qb.getNextAlias(prop.name);
const field = `${options.parentTableAlias}.${prop.name}`;
let path = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
if (!options.parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
path = '[populate]' + path;
}
const mandatoryToOneProperty =
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
const joinType = pivotRefJoin
? JoinType.pivotJoin
: hint.joinType
? hint.joinType
: (hint.filter && !prop.nullable) || mandatoryToOneProperty
? JoinType.innerJoin
: JoinType.leftJoin;
const schema =
prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
qb.join(field, tableAlias, {}, joinType, path, schema);
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
// Use the registry metadata to ensure allTPTDescendants is available
const tptMeta = this.metadata.get(meta2.class);
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
}
if (pivotRefJoin) {
fields.push(
...prop.joinColumns.map(col =>
qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`),
),
...prop.inverseJoinColumns.map(col =>
qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`),
),
);
}
if (prop.kind === ReferenceKind.ONE_TO_MANY && ref) {
fields.push(
...this.getFieldsForJoinedLoad(qb, meta2, {
...options,
explicitFields: prop.referencedColumnNames,
exclude: undefined,
populate: hint.children,
parentTableAlias: tableAlias,
parentJoinPath: path,
}),
);
}
const childExplicitFields =
options.explicitFields?.filter(f => Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
options.explicitFields?.forEach(f => {
if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
childExplicitFields.push(f.substring(prop.name.length + 1));
}
});
const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
if (!ref && (!prop.mapToPk || hint.dataOnly)) {
fields.push(
...this.getFieldsForJoinedLoad(qb, meta2, {
...options,
explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
exclude: childExclude,
populate: hint.children,
parentTableAlias: tableAlias,
parentJoinPath: path,
}),
);
} else if (
hint.filter ||
(prop.mapToPk && !hint.dataOnly) ||
(ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))
) {
fields.push(
...prop.referencedColumnNames.map(col =>
qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`),
),
);
}
}
return fields;
}
/**
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
* @internal
*/
addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
// allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
const descendants = meta.allTPTDescendants;
const childAliases = {};
// LEFT JOIN each descendant table
for (const childMeta of descendants) {
const childAlias = qb.getNextAlias(childMeta.className);
qb.createAlias(childMeta.class, childAlias);
childAliases[childMeta.className] = childAlias;
qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
// Add fields from this child (only ownProps, skip PKs)
for (const prop of childMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
for (const fieldName of prop.fieldNames) {
const field = `${childAlias}.${fieldName}`;
const fieldAlias = `${childAlias}__${fieldName}`;
fields.push(raw(`${this.platform.quoteIdentifier(field)} as ${this.platform.quoteIdentifier(fieldAlias)}`));
}
}
}
// Add computed discriminator (descendants already sorted by depth)
if (meta.root.tptDiscriminatorColumn) {
fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
}
}
/**
* Find the alias for a TPT child table in the query builder.
* @internal
*/
findTPTChildAlias(qb, childMeta) {
const joins = qb.state.joins;
for (const key of Object.keys(joins)) {
if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
return joins[key].alias;
}
}
return undefined;
}
/**
* Builds a CASE WHEN expression for TPT discriminator.
* Determines concrete entity type based on which child table has a non-null PK.
* @internal
*/
buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
const cases = descendants.map(child => {
const childAlias = aliasMap[child.className];
const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
});
const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
return raw(`${caseExpr} as ${aliased}`);
}
/**
* Maps TPT child-specific fields during hydration.
* When a relation points to a TPT base class, the actual entity might be a child class.
* This method reads the discriminator to determine the concrete type and maps child-specific fields.
* @internal
*/
mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
// Check if this is a TPT base with polymorphic children
if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
return;
}
// Read the discriminator value
const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
const discriminatorValue = root[discriminatorAlias];
if (!discriminatorValue) {
return;
}
// Set the discriminator in the pojo for EntityFactory
relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
// Find the concrete metadata from discriminator map
const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
/* v8 ignore next 3 - defensive check for invalid discriminator values */
if (!concreteClass) {
return;
}
const concreteMeta = this.metadata.get(concreteClass);
if (concreteMeta === meta) {
// Already the concrete type, no child fields to map
delete root[discriminatorAlias];
return;
}
// Traverse up from concrete type and map fields from each level's table
const tz = this.platform.getTimezone();
let currentMeta = concreteMeta;
while (currentMeta && currentMeta !== meta) {
const childAlias = this.findTPTChildAlias(qb, currentMeta);
if (childAlias) {
// Map fields using same filtering as joined loading, plus skip PKs
for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, {
deleteFromRoot: true,
});
}
}
currentMeta = currentMeta.tptParent;
}
// Clean up the discriminator alias
delete root[discriminatorAlias];
}
/**
* @internal
*/
mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
if (
!this.shouldHaveColumn(
prop.targetMeta,
{ ...childProp, name },
[],
childFields.length > 0 ? childFields : undefined,
)
) {
return [];
}
return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
});
}
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
return prop.fieldNames.map((col, idx) => {
if (!prop.customTypes[idx]?.convertToJSValueSQL) {
return col;
}
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${col}`);
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
});
}
if (prop.customType?.convertToJSValueSQL) {
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${prop.fieldNames[0]}`);
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
}
if (prop.formula) {
const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
const table = this.createFormulaTable(quotedAlias, meta, schema);
const columns = meta.createColumnMappingObject(tableAlias);
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
}
return prop.fieldNames.map(fieldName => {
return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
});
}
/** @internal */
createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext, alias, em) {
// do not compute the connectionType if EM is provided as it will be computed from it in the QB later on
const connectionType = em
? preferredConnectionType
: this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
const qb = new QueryBuilder(entityName, this.metadata, this, ctx, alias, connectionType, em, loggerContext);
if (!convertCustomTypes) {
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
}
return qb;
}
resolveConnectionType(args) {
if (args.ctx) {
return 'write';
}
if (args.connectionType) {
return args.connectionType;
}
if (this.config.get('preferReadReplicas')) {
return 'read';
}
return 'write';
}
extractManyToMany(meta, data) {
const ret = {};
for (const prop of meta.relations) {
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
ret[prop.name] = data[prop.name].map(item => Utils.asArray(item));
delete data[prop.name];
}
}
return ret;
}
async processManyToMany(meta, pks, collections, clear, options) {
for (const prop of meta.relations) {
if (collections[prop.name]) {
const pivotMeta = this.metadata.get(prop.pivotEntity);
const persister = new PivotCollectionPersister(
pivotMeta,
this,
options?.ctx,
options?.schema,
options?.loggerContext,
);
persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
await this.rethrow(persister.execute());
}
}
}
async lockPessimistic(entity, options) {
const meta = helper(entity).__meta;
const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(
options.schema ?? meta.schema,
);
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
qb.select(raw('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
await this.rethrow(qb.execute());
}
buildPopulateWhere(meta, joinedProps, options) {
const where = {};
for (const hint of joinedProps) {
const [propName] = hint.field.split(':', 2);
const prop = meta.properties[propName];
if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
where[prop.name] = Utils.copy(prop.where);
}
if (hint.children) {
const targetMeta = prop.targetMeta;
if (targetMeta) {
const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
where[prop.name] ??= {};
Object.assign(where[prop.name], inner);
}
}
}
}
if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
return where;
}
if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
return options.populateWhere;
}
/* v8 ignore next */
return { $and: [options.populateWhere, where] };
}
/**
* Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
* into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
* Each branch is planned independently by the database, enabling per-table index usage.
*/
async applyUnionWhere(meta, where, options, forDml = false) {
const unionWhere = options.unionWhere;
const strategy = options.unionWhereStrategy ?? 'union-all';
const schema = this.getSchemaName(meta, options);
const connectionType = this.resolveConnectionType({
ctx: options.ctx,
connectionType: options.connectionType,
});
const branchQbs = [];
for (const branch of unionWhere) {
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging).withSchema(
schema,
);
const pkFields = meta.primaryKeys.map(pk => {
const prop = meta.properties[pk];
return `${qb.alias}.${prop.fieldNames[0]}`;
});
qb.select(pkFields).where(branch);
if (options.em) {
await qb.applyJoinedFilters(options.em, options.filters);
}
branchQbs.push(qb);
}
const [first, ...rest] = branchQbs;
const unionQb = strategy === 'union' ? first.union(...rest) : first.unionAll(...rest);
const pkHash = Utils.getPrimaryKeyHash(meta.primaryKeys);
// MySQL does not allow referencing the target table in a subquery
// for UPDATE/DELETE, so we wrap the union in a derived table.
if (forDml) {
const { sql, params } = unionQb.toQuery();
return {
$and: [where, { [pkHash]: { $in: raw(`select * from (${sql}) as __u`, params) } }],
};
}
return {
$and: [where, { [pkHash]: { $in: unionQb.toRaw() } }],
};
}
buildOrderBy(qb, meta, populate, options) {
const joinedProps = this.joinedProps(meta, populate, options);
// `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
// as `options.populateWhere` will be always recomputed to respect filters
const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
const path = (populateWhereAll ? '[populate]' : '') + meta.className;
const optionsOrderBy = Utils.asArray(options.orderBy);
const populateOrderBy = this.buildPopulateOrderBy(
qb,
meta,
Utils.asArray(options.populateOrderBy ?? options.orderBy),
path,
!!options.populateOrderBy,
);
const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
}
buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
const orderBy = [];
for (let i = 0; i < populateOrderBy.length; i++) {
const orderHint = populateOrderBy[i];
for (const field of Utils.getObjectQueryKeys(orderHint)) {
const childOrder = orderHint[field];
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
const { sql, params } = RawQueryFragment.getKnownFragment(field);
const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
orderBy.push({ [key]: childOrder });
continue;
}
const prop = meta.properties[field];
if (!prop) {
throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
}
let path = parentPath;
const meta2 = prop.targetMeta;
if (
prop.kind !== ReferenceKind.SCALAR &&
(![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
!prop.owner ||
Utils.isPlainObject(childOrder))
) {
path += `.${field}`;
}
if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
path += '[pivot]';
}
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
if (!join) {
continue;
}
if (
join &&
![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) &&
typeof childOrder === 'object'
) {
const children = this.buildPopulateOrderBy(qb, meta2, Utils.asArray(childOrder), path, explicit, propAlias);
orderBy.push(...children);
continue;
}
if (prop.kind === ReferenceKind.MANY_TO_MANY && join) {
if (prop.fixedOrderColumn) {
orderBy.push({ [`${join.alias}.${prop.fixedOrderColumn}`]: childOrder });
} else {
for (const col of prop.inverseJoinColumns) {
orderBy.push({ [`${join.ownerAlias}.${col}`]: childOrder });
}
}
continue;
}
const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
if (order) {
orderBy.push({ [`${propAlias}.${field}`]: order });
}
}
}
return orderBy;
}
buildJoinedPropsOrderBy(qb, meta, populate, options, parentPath) {
const orderBy = [];
const joinedProps = this.joinedProps(meta, populate, options);
for (const hint of joinedProps) {
const [propName, ref] = hint.field.split(':', 2);
const prop = meta.properties[propName];
let path = `${parentPath}.${propName}`;
if (prop.kind === ReferenceKind.MANY_TO_MANY && ref) {
path += '[pivot]';
}
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
this.buildToManyOrderBy(qb, prop, path, ref, orderBy);
}
if (hint.children) {
orderBy.push(...this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path));
}
}
return orderBy;
}
buildToManyOrderBy(qb, prop, path, ref, orderBy) {
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
const alias = ref ? propAlias : join.ownerAlias;
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
}
const effectiveOrderBy = QueryHelper.mergeOrderBy(prop.orderBy, prop.targetMeta?.orderBy);
for (const item of effectiveOrderBy) {
for (const field of Utils.getObjectQueryKeys(item)) {
const order = item[field];
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
const { sql, params } = RawQueryFragment.getKnownFragment(field);
const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
const key = raw(sql2, params);
orderBy.push({ [key]: order });
continue;
}
orderBy.push({ [`${propAlias}.${field}`]: order });
}
}
}
normalizeFields(fields, prefix = '') {
const ret = [];
for (const field of fields) {
if (typeof field === 'string') {
ret.push(prefix + field);
continue;
}
if (Utils.isPlainObject(field)) {
for (const key of Object.keys(field)) {
ret.push(...this.normalizeFields(field[key], key + '.'));
}
}
}
return ret;
}
processField(meta, prop, field, ret) {
if (!prop || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) {
return;
}
if (prop.kind === ReferenceKind.EMBEDDED) {
if (prop.object) {
ret.push(prop.name);
return;
}
const parts = field.split('.');
const top = parts.shift();
for (const key of Object.keys(prop.embeddedProps)) {
if (!top || key === top) {
this.processField(meta, prop.embeddedProps[key], parts.join('.'), ret);
}
}
return;
}
if (prop.persist === false && !prop.embedded && !prop.formula) {
return;
}
ret.push(prop.name);
}
buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
const hasExplicitFields = !!options.fields;
const ret = [];
let addFormulas = false;
// handle root entity properties first, this is used for both strategies in the same way
if (options.fields) {
for (const field of this.normalizeFields(options.fields)) {
if (field === '*') {
ret.push('*');
continue;
}
const parts = field.split('.');
const rootPropName = parts.shift(); // first one is the `prop`
const prop = QueryHelper.findProperty(rootPropName, {
metadata: this.metadata,
platform: this.platform,
entityName: meta.class,
where: {},
aliasMap: qb.getAliasMap(),
});
this.processField(meta, prop, parts.join('.'), ret);
}
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
}
if (
meta.root.inheritanceType === 'sti' &&
!options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)
) {
ret.push(meta.root.discriminatorColumn);
}
} else if (!Utils.isEmpty(options.exclude) || lazyProps.some(p => !p.formula && (p.kind !== '1:1' || p.owner))) {
const props = meta.props.filter(prop =>
this.platform.shouldHaveColumn(prop, populate, options.exclude, false, false),
);
ret.push(...props.filter(p => !lazyProps.includes(p)).map(p => p.name));
addFormulas = true;
} else if (hasLazyFormulas || requiresSQLConversion) {
ret.push('*');
addFormulas = true;
} else {
ret.push('*');
}
if (ret.length > 0 && !hasExplicitFields && addFormulas) {
// Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
const quotedAlias = this.platform.quoteIdentifier(alias);
const columns = meta.createColumnMappingObject(alias);
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
for (const prop of meta.props) {
if (lazyProps.includes(prop)) {
continue;
}
if (prop.formula) {
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
}
if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
ret.push(prop.name);
}
}
}
// add joined relations after the root entity fields
if (joinedProps.length > 0) {
ret.push(
...this.getFieldsForJoinedLoad(qb, meta, {
explicitFields: options.fields,
exclude: options.exclude,
populate,
parentTableAlias: alias,
...options,
}),
);
}
return Utils.unique(ret);
}
}