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); } }