import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core'; import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js'; /** * @internal */ export class QueryBuilderHelper { #platform; #metadata; #entityName; #alias; #aliasMap; #subQueries; #driver; #tptAliasMap; /** Monotonically increasing counter for unique JSON array iteration aliases within a single query. */ #jsonAliasCounter = 0; constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) { this.#entityName = entityName; this.#alias = alias; this.#aliasMap = aliasMap; this.#subQueries = subQueries; this.#driver = driver; this.#tptAliasMap = tptAliasMap; this.#platform = this.#driver.getPlatform(); this.#metadata = this.#driver.getMetadata(); } /** * For TPT inheritance, finds the correct alias for a property based on which entity owns it. * Returns the main alias if not a TPT property or if the property belongs to the main entity. */ getTPTAliasForProperty(propName, defaultAlias) { const meta = this.#aliasMap[defaultAlias]?.meta ?? this.#metadata.get(this.#entityName); if (meta?.inheritanceType !== 'tpt' || !meta.tptParent) { return defaultAlias; } // Check if property is in the main entity's ownProps if (meta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) { return defaultAlias; } // Walk up the TPT hierarchy to find which parent owns this property let parentMeta = meta.tptParent; while (parentMeta) { const parentAlias = this.#tptAliasMap[parentMeta.className]; if (parentAlias && parentMeta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) { return parentAlias; } parentMeta = parentMeta.tptParent; } // Property not found in hierarchy, return default alias return defaultAlias; } mapper(field, type = QueryType.SELECT, value, alias, schema) { if (isRaw(field)) { return raw(field.sql, field.params); } if (Raw.isKnownFragmentSymbol(field)) { return Raw.getKnownFragment(field); } /* v8 ignore next */ if (typeof field !== 'string') { return field; } const isTableNameAliasRequired = this.isTableNameAliasRequired(type); const fields = Utils.splitPrimaryKeys(field); if (fields.length > 1) { const parts = []; for (const p of fields) { const [a, f] = this.splitField(p); const prop = this.getProperty(f, a); const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1; if (fkIdx2 !== -1) { parts.push( this.mapper( a !== this.#alias ? `${a}.${prop.fieldNames[fkIdx2]}` : prop.fieldNames[fkIdx2], type, value, alias, ), ); } else if (prop) { parts.push(...prop.fieldNames.map(f => this.mapper(a !== this.#alias ? `${a}.${f}` : f, type, value, alias))); } else { parts.push(this.mapper(a !== this.#alias ? `${a}.${f}` : f, type, value, alias)); } } // flatten the value if we see we are expanding nested composite key // hackish, but cleaner solution would require quite a lot of refactoring if (fields.length !== parts.length && Array.isArray(value)) { value.forEach(row => { if (Array.isArray(row)) { const tmp = Utils.flatten(row); row.length = 0; row.push(...tmp); } }); } return raw('(' + parts.map(part => this.#platform.quoteIdentifier(part)).join(', ') + ')'); } const [a, f] = this.splitField(field); const prop = this.getProperty(f, a); // For TPT inheritance, resolve the correct alias for this property // Only apply TPT resolution when `a` is an actual table alias (in aliasMap), // not when it's an embedded property name like 'profile1.identity.links' const isTableAlias = !!this.#aliasMap[a]; const baseAlias = isTableAlias ? a : this.#alias; const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.#alias; const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : ''; const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1; const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2; if (a === prop?.embedded?.[0]) { return aliasPrefix + prop.fieldNames[fkIdx]; } const noPrefix = prop?.persist === false; if (prop?.fieldNameRaw) { return raw(this.prefix(field, isTableNameAliasRequired)); } if (prop?.formula) { const alias2 = this.#platform.quoteIdentifier(a).toString(); const aliasName = alias === undefined ? prop.fieldNames[0] : alias; const as = aliasName === null ? '' : ` as ${this.#platform.quoteIdentifier(aliasName)}`; const meta = this.#aliasMap[a]?.meta ?? this.#metadata.get(this.#entityName); const table = this.createFormulaTable(alias2, meta, schema); const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2); let value = this.#driver.evaluateFormula(prop.formula, columns, table); if (!this.isTableNameAliasRequired(type)) { value = value.replaceAll(alias2 + '.', ''); } return raw(`${value}${as}`); } if (prop?.hasConvertToJSValueSQL && type !== QueryType.UPSERT) { let valueSQL; if (prop.fieldNames.length > 1 && fkIdx !== -1) { const fk = prop.targetMeta.getPrimaryProps()[fkIdx]; const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx); valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.#platform); } else { const prefixed = this.prefix(field, isTableNameAliasRequired, true); valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.#platform); } if (alias === null) { return raw(valueSQL); } return raw(`${valueSQL} as ${this.#platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`); } let ret = this.prefix(field, false, false, fkIdx); if (alias) { ret += ' as ' + alias; } if (!isTableNameAliasRequired || this.isPrefixed(ret) || noPrefix) { return ret; } return resolvedAlias + '.' + ret; } processData(data, convertCustomTypes, multi = false) { if (Array.isArray(data)) { return data.map(d => this.processData(d, convertCustomTypes, true)); } const meta = this.#metadata.find(this.#entityName); data = this.#driver.mapDataToFieldNames(data, true, meta?.properties, convertCustomTypes); if (!Utils.hasObjectKeys(data) && meta && multi) { /* v8 ignore next */ data[meta.getPrimaryProps()[0].fieldNames[0]] = this.#platform.usesDefaultKeyword() ? raw('default') : undefined; } return data; } joinOneToReference(prop, ownerAlias, alias, type, cond = {}, schema) { const prop2 = prop.targetMeta.properties[prop.mappedBy || prop.inversedBy]; const table = this.getTableName(prop.targetMeta.class); const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns; const inverseJoinColumns = prop.referencedColumnNames; const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames; schema ??= prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta); cond = Utils.merge(cond, prop.where); // For inverse side of polymorphic relations, add discriminator condition if (!prop.owner && prop2.polymorphic && prop2.discriminatorColumn && prop2.discriminatorMap) { const ownerMeta = this.#aliasMap[ownerAlias]?.meta ?? this.#metadata.get(this.#entityName); const discriminatorValue = QueryHelper.findDiscriminatorValue(prop2.discriminatorMap, ownerMeta.class); if (discriminatorValue) { cond[`${alias}.${prop2.discriminatorColumn}`] = discriminatorValue; } } return { prop, type, cond, ownerAlias, alias, table, schema, joinColumns, inverseJoinColumns, primaryKeys, }; } joinManyToOneReference(prop, ownerAlias, alias, type, cond = {}, schema) { return { prop, type, cond, ownerAlias, alias, table: this.getTableName(prop.targetMeta.class), schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta, { schema }), joinColumns: prop.referencedColumnNames, // For polymorphic relations, fieldNames includes the discriminator column which is not // part of the join condition - use joinColumns (the FK columns only) instead primaryKeys: prop.polymorphic ? prop.joinColumns : prop.fieldNames, }; } joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) { const pivotMeta = this.#metadata.find(prop.pivotEntity); const ret = { [`${ownerAlias}.${prop.name}#${pivotAlias}`]: { prop, type, ownerAlias, alias: pivotAlias, inverseAlias: alias, joinColumns: prop.joinColumns, inverseJoinColumns: prop.inverseJoinColumns, primaryKeys: prop.referencedColumnNames, cond: {}, table: pivotMeta.tableName, schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(pivotMeta, { schema }), path: path.endsWith('[pivot]') ? path : `${path}[pivot]`, }, }; if (type === JoinType.pivotJoin) { return ret; } const prop2 = pivotMeta.relations[prop.owner ? 1 : 0]; ret[`${pivotAlias}.${prop2.name}#${alias}`] = this.joinManyToOneReference( prop2, pivotAlias, alias, type, cond, schema, ); ret[`${pivotAlias}.${prop2.name}#${alias}`].path = path; const tmp = prop2.referencedTableName.split('.'); ret[`${pivotAlias}.${prop2.name}#${alias}`].schema ??= tmp.length > 1 ? tmp[0] : undefined; return ret; } processJoins(qb, joins, schema, schemaOverride) { Object.values(joins).forEach(join => { if ([JoinType.nestedInnerJoin, JoinType.nestedLeftJoin].includes(join.type)) { return; } const { sql, params } = this.createJoinExpression(join, joins, schema, schemaOverride); qb.join(sql, params); }); } createJoinExpression(join, joins, schema, schemaOverride) { let table = join.table; const method = { [JoinType.nestedInnerJoin]: 'inner join', [JoinType.nestedLeftJoin]: 'left join', [JoinType.pivotJoin]: 'left join', }[join.type] ?? join.type; const conditions = []; const params = []; schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride); if (schema && schema !== this.#platform.getDefaultSchemaName()) { table = `${schema}.${table}`; } if (join.prop.name !== '__subquery__') { join.primaryKeys.forEach((primaryKey, idx) => { const right = `${join.alias}.${join.joinColumns[idx]}`; if (join.prop.formula) { const quotedAlias = this.#platform.quoteIdentifier(join.ownerAlias).toString(); const ownerMeta = this.#aliasMap[join.ownerAlias]?.meta ?? this.#metadata.get(this.#entityName); const table = this.createFormulaTable(quotedAlias, ownerMeta, schema); const columns = ownerMeta.createColumnMappingObject( p => this.getTPTAliasForProperty(p.name, join.ownerAlias), quotedAlias, ); const left = this.#driver.evaluateFormula(join.prop.formula, columns, table); conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`); return; } const left = join.prop.object && join.prop.fieldNameRaw ? join.prop.fieldNameRaw.replaceAll(ALIAS_REPLACEMENT, join.ownerAlias) : this.#platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`); conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`); }); } if ( join.prop.targetMeta?.root.inheritanceType === 'sti' && join.prop.targetMeta?.discriminatorValue && !join.path?.endsWith('[pivot]') ) { const typeProperty = join.prop.targetMeta.root.discriminatorColumn; const alias = join.inverseAlias ?? join.alias; join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue; } // For polymorphic relations, add discriminator condition to filter by target entity type if (join.prop.polymorphic && join.prop.discriminatorColumn && join.prop.discriminatorMap) { const discriminatorValue = QueryHelper.findDiscriminatorValue( join.prop.discriminatorMap, join.prop.targetMeta.class, ); if (discriminatorValue) { const discriminatorCol = this.#platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`); conditions.push(`${discriminatorCol} = ?`); params.push(discriminatorValue); } } let sql = method + ' '; if (join.nested) { const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' '; sql += `(${this.#platform.quoteIdentifier(table)}${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`; for (const nested of join.nested) { const { sql: nestedSql, params: nestedParams } = this.createJoinExpression( nested, joins, schema, schemaOverride, ); sql += ' ' + nestedSql; params.push(...nestedParams); } sql += `)`; } else if (join.subquery) { const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' '; sql += `(${join.subquery})${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`; } else { sql += this.#platform.quoteIdentifier(table) + (this.#platform.usesAsKeyword() ? ' as ' : ' ') + this.#platform.quoteIdentifier(join.alias); } const oldAlias = this.#alias; this.#alias = join.alias; const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond); this.#alias = oldAlias; if (subquery.sql) { conditions.push(subquery.sql); subquery.params.forEach(p => params.push(p)); } if (conditions.length > 0) { sql += ` on ${conditions.join(' and ')}`; } return { sql, params }; } mapJoinColumns(type, join) { if (join.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(join.prop.kind)) { return join.prop.fieldNames.map((_fieldName, idx) => { const columns = join.prop.owner ? join.joinColumns : join.inverseJoinColumns; return this.mapper(`${join.alias}.${columns[idx]}`, type, undefined, `${join.alias}__${columns[idx]}`); }); } return [ ...join.joinColumns.map(col => this.mapper(`${join.alias}.${col}`, type, undefined, `fk__${col}`)), ...join.inverseJoinColumns.map(col => this.mapper(`${join.alias}.${col}`, type, undefined, `fk__${col}`)), ]; } isOneToOneInverse(field, meta) { meta ??= this.#metadata.find(this.#entityName); const prop = meta.properties[field.replace(/:ref$/, '')]; return prop?.kind === ReferenceKind.ONE_TO_ONE && !prop.owner; } getTableName(entityName) { const meta = this.#metadata.find(entityName); return meta?.tableName ?? Utils.className(entityName); } /** * Checks whether the RE can be rewritten to simple LIKE query */ isSimpleRegExp(re) { if (!(re instanceof RegExp)) { return false; } if (re.flags.includes('i')) { return false; } // when including the opening bracket/paren we consider it complex return !/[{[(]/.exec(re.source); } getRegExpParam(re) { const value = re.source .replace(/\.\*/g, '%') // .* -> % .replace(/\./g, '_') // . -> _ .replace(/\\_/g, '.') // \. -> . .replace(/^\^/g, '') // remove ^ from start .replace(/\$$/g, ''); // remove $ from end if (re.source.startsWith('^') && re.source.endsWith('$')) { return value; } if (re.source.startsWith('^')) { return value + '%'; } if (re.source.endsWith('$')) { return '%' + value; } return `%${value}%`; } appendOnConflictClause(type, onConflict, qb) { onConflict.forEach(item => { const { fields, ignore } = item; const sub = qb.onConflict({ fields, ignore }); Utils.runIfNotEmpty(() => { let mergeParam = item.merge; if (Utils.isObject(item.merge)) { mergeParam = {}; Utils.keys(item.merge).forEach(key => { const k = this.mapper(key, type); mergeParam[k] = item.merge[key]; }); } if (Array.isArray(item.merge)) { mergeParam = item.merge.map(key => this.mapper(key, type)); } sub.merge = mergeParam ?? []; if (item.where) { sub.where = this._appendQueryCondition(type, item.where); } }, 'merge' in item); }); } appendQueryCondition(type, cond, qb, operator, method = 'where') { const { sql, params } = this._appendQueryCondition(type, cond, operator); qb[method](sql, params); } _appendQueryCondition(type, cond, operator) { const parts = []; const params = []; for (const k of Utils.getObjectQueryKeys(cond)) { if (k === '$and' || k === '$or') { if (operator) { this.append(() => this.appendGroupCondition(type, k, cond[k]), parts, params, operator); continue; } this.append(() => this.appendGroupCondition(type, k, cond[k]), parts, params); continue; } if (k === '$not') { const res = this._appendQueryCondition(type, cond[k]); parts.push(`not (${res.sql})`); res.params.forEach(p => params.push(p)); continue; } this.append(() => this.appendQuerySubCondition(type, cond, k), parts, params); } return { sql: parts.join(' and '), params }; } append(cb, parts, params, operator) { const res = cb(); if (['', '()'].includes(res.sql)) { return; } parts.push(operator === '$or' ? `(${res.sql})` : res.sql); res.params.forEach(p => params.push(p)); } appendQuerySubCondition(type, cond, key) { const parts = []; const params = []; if (this.isSimpleRegExp(cond[key])) { parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type))} like ?`); params.push(this.getRegExpParam(cond[key])); return { sql: parts.join(' and '), params }; } if (Utils.isPlainObject(cond[key]) && !Raw.isKnownFragmentSymbol(key)) { const [a, f] = this.splitField(key); const prop = this.getProperty(f, a); if (prop?.kind === ReferenceKind.EMBEDDED && prop.array) { const keys = Object.keys(cond[key]); const hasOnlyArrayOps = keys.every(k => EMBEDDABLE_ARRAY_OPS.includes(k)); if (!hasOnlyArrayOps) { return this.processEmbeddedArrayCondition(cond[key], prop, a); } } // $elemMatch on JSON properties — iterate array elements via EXISTS subquery. // When combined with other operators (e.g. $contains), processObjectSubCondition // splits them first (size > 1), so $elemMatch arrives here alone. if (prop && cond[key].$elemMatch != null && Utils.getObjectKeysSize(cond[key]) === 1) { if (!(prop.customType instanceof JsonType)) { throw new ValidationError( `$elemMatch can only be used on JSON array properties, but '${this.#entityName}.${prop.name}' has type '${prop.type}'`, ); } return this.processJsonElemMatch(cond[key].$elemMatch, prop, a); } } if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) { return this.processObjectSubCondition(cond, key, type); } const op = cond[key] === null ? 'is' : '='; if (Raw.isKnownFragmentSymbol(key)) { const raw = Raw.getKnownFragment(key); const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this.#alias); const value = Utils.asArray(cond[key]); params.push(...raw.params); if (value.length > 0) { const k = key; const val = this.getValueReplacement([k], value[0], params, k); parts.push(`${sql} ${op} ${val}`); return { sql: parts.join(' and '), params }; } parts.push(sql); return { sql: parts.join(' and '), params }; } const fields = Utils.splitPrimaryKeys(key); if (this.#subQueries[key]) { const val = this.getValueReplacement(fields, cond[key], params, key); parts.push(`(${this.#subQueries[key]}) ${op} ${val}`); return { sql: parts.join(' and '), params }; } const val = this.getValueReplacement(fields, cond[key], params, key); parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type, cond[key], null))} ${op} ${val}`); return { sql: parts.join(' and '), params }; } processObjectSubCondition(cond, key, type) { const parts = []; const params = []; let value = cond[key]; const size = Utils.getObjectKeysSize(value); if (Utils.isPlainObject(value) && size === 0) { return { sql: '', params }; } // grouped condition for one field, e.g. `{ age: { $gte: 10, $lt: 50 } }` if (size > 1) { const subCondition = Object.entries(value).map(([subKey, subValue]) => { return { [key]: { [subKey]: subValue } }; }); for (const sub of subCondition) { this.append(() => this._appendQueryCondition(type, sub, '$and'), parts, params); } return { sql: parts.join(' and '), params }; } if (value instanceof RegExp) { value = this.#platform.getRegExpValue(value); } // operators const op = Object.keys(QueryOperator).find(op => op in value); /* v8 ignore next */ if (!op) { throw ValidationError.invalidQueryCondition(cond); } const replacement = this.getOperatorReplacement(op, value); const rawField = Raw.isKnownFragmentSymbol(key); const fields = rawField ? [key] : Utils.splitPrimaryKeys(key); if (fields.length > 1 && Array.isArray(value[op])) { const singleTuple = !value[op].every(v => Array.isArray(v)); if (!this.#platform.allowsComparingTuples()) { const mapped = fields.map(f => this.mapper(f, type)); if (op === '$in') { const conds = value[op].map(() => { return `(${mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`).join(' and ')})`; }); parts.push(`(${conds.join(' or ')})`); params.push(...Utils.flatten(value[op])); return { sql: parts.join(' and '), params }; } parts.push(...mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`)); params.push(...Utils.flatten(value[op])); return { sql: parts.join(' and '), params }; } if (singleTuple) { const tmp = value[op].length === 1 && Utils.isPlainObject(value[op][0]) ? fields.map(f => value[op][0][f]) : value[op]; const sql = `(${fields.map(() => '?').join(', ')})`; value[op] = raw(sql, tmp); } } if (this.#subQueries[key]) { const val = this.getValueReplacement(fields, value[op], params, op); parts.push(`(${this.#subQueries[key]}) ${replacement} ${val}`); return { sql: parts.join(' and '), params }; } const [a, f] = rawField ? [] : this.splitField(key); const prop = f && this.getProperty(f, a); if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) { return { sql: '', params }; } if (op === '$fulltext') { /* v8 ignore next */ if (!prop) { throw new Error(`Cannot use $fulltext operator on ${String(key)}, property not found`); } const { sql, params: params2 } = raw(this.#platform.getFullTextWhereClause(prop), { column: this.mapper(key, type, undefined, null), query: value[op], }); parts.push(sql); params.push(...params2); } else if (['$in', '$nin'].includes(op) && Array.isArray(value[op]) && value[op].length === 0) { parts.push(`1 = ${op === '$in' ? 0 : 1}`); } else if (op === '$re') { const mappedKey = this.mapper(key, type, value[op], null); const processed = this.#platform.mapRegExpCondition(mappedKey, value); parts.push(processed.sql); params.push(...processed.params); } else if (value[op] instanceof Raw || typeof value[op]?.toRaw === 'function') { const query = value[op] instanceof Raw ? value[op] : value[op].toRaw(); const mappedKey = this.mapper(key, type, query, null); let sql = query.sql; if (['$in', '$nin'].includes(op)) { sql = `(${sql})`; } parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`); params.push(...query.params); } else { const mappedKey = this.mapper(key, type, value[op], null); const val = this.getValueReplacement(fields, value[op], params, op, prop); parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`); } return { sql: parts.join(' and '), params }; } getValueReplacement(fields, value, params, key, prop) { if (Array.isArray(value)) { if (fields.length > 1) { const tmp = []; for (const field of value) { tmp.push(`(${field.map(() => '?').join(', ')})`); params.push(...field); } return `(${tmp.join(', ')})`; } if (prop?.customType instanceof ArrayType) { const item = prop.customType.convertToDatabaseValue(value, this.#platform, { fromQuery: true, key, mode: 'query', }); params.push(item); } else { value.forEach(p => params.push(p)); } return `(${value.map(() => '?').join(', ')})`; } if (value === null) { return 'null'; } params.push(value); return '?'; } getOperatorReplacement(op, value) { let replacement = QueryOperator[op]; if (op === '$exists') { replacement = value[op] ? 'is not' : 'is'; value[op] = null; } if (value[op] === null && ['$eq', '$ne'].includes(op)) { replacement = op === '$eq' ? 'is' : 'is not'; } if (op === '$re') { replacement = this.#platform.getRegExpOperator(value[op], value.$flags); } if (replacement.includes('?')) { replacement = replacement.replaceAll('?', '\\?'); } return replacement; } validateQueryOrder(orderBy) { const strKeys = []; const rawKeys = []; for (const key of Utils.getObjectQueryKeys(orderBy)) { const raw = Raw.getKnownFragment(key); if (raw) { rawKeys.push(raw); } else { strKeys.push(key); } } if (strKeys.length > 0 && rawKeys.length > 0) { const example = [ ...strKeys.map(key => ({ [key]: orderBy[key] })), ...rawKeys.map(rawKey => ({ [`raw('${rawKey.sql}')`]: orderBy[rawKey] })), ]; throw new Error( [ `Invalid "orderBy": You are mixing field-based keys and raw SQL fragments inside a single object.`, `This is not allowed because object key order cannot reliably preserve evaluation order.`, `To fix this, split them into separate objects inside an array:\n`, `orderBy: ${inspect(example, { depth: 5 }).replace(/"raw\('(.*)'\)"/g, `[raw('$1')]`)}`, ].join('\n'), ); } } getQueryOrder(type, orderBy, populate, collation) { if (Array.isArray(orderBy)) { return orderBy.flatMap(o => this.getQueryOrder(type, o, populate, collation)); } return this.getQueryOrderFromObject(type, orderBy, populate, collation); } getQueryOrderFromObject(type, orderBy, populate, collation) { const ret = []; for (const key of Utils.getObjectQueryKeys(orderBy)) { const direction = orderBy[key]; const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction; if (Raw.isKnownFragmentSymbol(key)) { const raw = Raw.getKnownFragment(key); ret.push( ...this.#platform.getOrderByExpression(this.#platform.formatQuery(raw.sql, raw.params), order, collation), ); continue; } for (const f of Utils.splitPrimaryKeys(key)) { // eslint-disable-next-line prefer-const let [alias, field] = this.splitField(f, true); alias = populate[alias] || alias; const prop = this.getProperty(field, alias); const noPrefix = (prop?.persist === false && !prop.formula && !prop.embedded) || Raw.isKnownFragment(f); const column = this.mapper(noPrefix ? field : `${alias}.${field}`, type, undefined, null); /* v8 ignore next */ const rawColumn = typeof column === 'string' ? column .split('.') .map(e => this.#platform.quoteIdentifier(e)) .join('.') : column; const customOrder = prop?.customOrder; let colPart = customOrder ? this.#platform.generateCustomOrder(rawColumn, customOrder) : rawColumn; if (isRaw(colPart)) { colPart = this.#platform.formatQuery(colPart.sql, colPart.params); } if (Array.isArray(order)) { order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate, collation))); } else { ret.push(...this.#platform.getOrderByExpression(colPart, order, collation)); } } } return ret; } splitField(field, greedyAlias = false) { const parts = field.split('.'); const ref = parts[parts.length - 1].split(':')[1]; if (ref) { parts[parts.length - 1] = parts[parts.length - 1].substring(0, parts[parts.length - 1].indexOf(':')); } if (parts.length === 1) { return [this.#alias, parts[0], ref]; } if (greedyAlias) { const fromField = parts.pop(); const fromAlias = parts.join('.'); return [fromAlias, fromField, ref]; } const fromAlias = parts.shift(); const fromField = parts.join('.'); return [fromAlias, fromField, ref]; } getLockSQL(qb, lockMode, lockTables = [], joinsMap) { const meta = this.#metadata.find(this.#entityName); if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) { throw OptimisticLockError.lockFailed(Utils.className(this.#entityName)); } if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) { const joins = Object.values(joinsMap); const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type), ); if (joins.length > innerJoins.length) { lockTables.push(this.#alias, ...innerJoins.map(join => join.alias)); } } qb.lockMode(lockMode, lockTables); } updateVersionProperty(qb, data) { const meta = this.#metadata.find(this.#entityName); if (!meta?.versionProperty || meta.versionProperty in data) { return; } const versionProperty = meta.properties[meta.versionProperty]; let sql = this.#platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1'; if (versionProperty.runtimeType === 'Date') { sql = this.#platform.getCurrentTimestampSQL(versionProperty.length); } qb.update({ [versionProperty.fieldNames[0]]: raw(sql) }); } prefix(field, always = false, quote = false, idx) { let ret; if (!this.isPrefixed(field)) { // For TPT inheritance, resolve the correct alias for this property const tptAlias = this.getTPTAliasForProperty(field, this.#alias); const alias = always ? (quote ? tptAlias : this.#platform.quoteIdentifier(tptAlias)) + '.' : ''; const fieldName = this.fieldName(field, tptAlias, always, idx); if (fieldName instanceof Raw) { return fieldName.sql; } ret = alias + fieldName; } else { const [a, ...rest] = field.split('.'); const f = rest.join('.'); // For TPT inheritance, resolve the correct alias for this property // Only apply TPT resolution when `a` is an actual table alias (in aliasMap), // not when it's an embedded property name like 'profile1.identity.links' const isTableAlias = !!this.#aliasMap[a]; const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(f, a) : a; const fieldName = this.fieldName(f, resolvedAlias, always, idx); if (fieldName instanceof Raw) { return fieldName.sql; } ret = resolvedAlias + '.' + fieldName; } if (quote) { return this.#platform.quoteIdentifier(ret); } return ret; } appendGroupCondition(type, operator, subCondition) { const parts = []; const params = []; // single sub-condition can be ignored to reduce nesting of parens if (subCondition.length === 1 || operator === '$and') { for (const sub of subCondition) { this.append(() => this._appendQueryCondition(type, sub), parts, params); } return { sql: parts.join(' and '), params }; } for (const sub of subCondition) { // skip nesting parens if the value is simple = scalar or object without operators or with only single key, being the operator const keys = Utils.getObjectQueryKeys(sub); const val = sub[keys[0]]; const simple = !Utils.isPlainObject(val) || Utils.getObjectKeysSize(val) === 1 || Object.keys(val).every(k => !Utils.isOperator(k)); if (keys.length === 1 && simple) { this.append(() => this._appendQueryCondition(type, sub, operator), parts, params); continue; } this.append(() => this._appendQueryCondition(type, sub), parts, params, operator); } return { sql: `(${parts.join(' or ')})`, params }; } isPrefixed(field) { return !!/[\w`"[\]]+\./.exec(field); } fieldName(field, alias, always, idx = 0) { const prop = this.getProperty(field, alias); if (!prop) { return field; } if (prop.fieldNameRaw) { if (!always) { return raw( prop.fieldNameRaw .replace(new RegExp(ALIAS_REPLACEMENT_RE + '\\.?', 'g'), '') .replace(this.#platform.quoteIdentifier('') + '.', ''), ); } if (alias) { return raw(prop.fieldNameRaw.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), alias)); } /* v8 ignore next */ return raw(prop.fieldNameRaw); } /* v8 ignore next */ return prop.fieldNames?.[idx] ?? field; } getProperty(field, alias) { const entityName = this.#aliasMap[alias]?.entityName || this.#entityName; const meta = this.#metadata.find(entityName); // raw table name (e.g. CTE) — no metadata available if (!meta) { return undefined; } // check if `alias` is not matching an embedded property name instead of alias, e.g. `address.city` if (alias) { const prop = meta.properties[alias]; if (prop?.kind === ReferenceKind.EMBEDDED) { const parts = field.split('.'); const nest = p => (parts.length > 0 ? nest(p.embeddedProps[parts.shift()]) : p); return nest(prop); } } if (meta.properties[field]) { return meta.properties[field]; } return meta.relations.find(prop => prop.fieldNames?.some(name => field === name)); } isTableNameAliasRequired(type) { return [QueryType.SELECT, QueryType.COUNT].includes(type); } processEmbeddedArrayCondition(cond, prop, alias) { const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`); const resolveProperty = key => { const { embProp, jsonPropName } = this.resolveEmbeddedProp(prop, key); return { name: jsonPropName, type: embProp.runtimeType ?? 'string' }; }; const invalidObjectError = key => ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type); const parts = []; const allParams = []; // Top-level $not generates NOT EXISTS (no element matches the inner condition). const { $not, ...rest } = cond; if (Utils.hasObjectKeys(rest)) { const result = this.buildJsonArrayExists(rest, column, false, resolveProperty, invalidObjectError); if (result) { parts.push(result.sql); allParams.push(...result.params); } } if ($not != null) { if (!Utils.isPlainObject($not)) { throw new ValidationError(`Invalid query: $not in embedded array queries expects an object value`); } const result = this.buildJsonArrayExists($not, column, true, resolveProperty, invalidObjectError); if (result) { parts.push(result.sql); allParams.push(...result.params); } } if (parts.length === 0) { return { sql: '1 = 1', params: [] }; } return { sql: parts.join(' and '), params: allParams }; } buildJsonArrayExists(cond, column, negate, resolveProperty, invalidObjectError) { const jeAlias = `__je${this.#jsonAliasCounter++}`; const referencedProps = new Map(); const { sql: whereSql, params } = this.buildArrayElementWhere( cond, jeAlias, referencedProps, resolveProperty, invalidObjectError, ); if (!whereSql) { return null; } const from = this.#platform.getJsonArrayFromSQL(column, jeAlias, [...referencedProps.values()]); const exists = this.#platform.getJsonArrayExistsSQL(from, whereSql); return { sql: negate ? `not ${exists}` : exists, params }; } resolveEmbeddedProp(prop, key) { const embProp = prop.embeddedProps[key] ?? Object.values(prop.embeddedProps).find(p => p.name === key); if (!embProp) { throw ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type); } const prefix = `${prop.fieldNames[0]}~`; const raw = embProp.fieldNames[0]; const jsonPropName = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw; return { embProp, jsonPropName }; } buildEmbeddedArrayOperatorCondition(lhs, value, params) { const supported = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$not', '$like', '$exists']); const parts = []; // Clone to avoid getOperatorReplacement mutating the original (it sets value[op] = null for $exists). value = { ...value }; for (const op of Object.keys(value)) { if (!supported.has(op)) { throw new ValidationError(`Operator ${op} is not supported in embedded array queries`); } const replacement = this.getOperatorReplacement(op, value); const val = value[op]; if (['$in', '$nin'].includes(op)) { if (!Array.isArray(val)) { throw new ValidationError(`Invalid query: ${op} operator expects an array value`); } else if (val.length === 0) { parts.push(`1 = ${op === '$in' ? 0 : 1}`); } else { val.forEach(v => params.push(v)); parts.push(`${lhs} ${replacement} (${val.map(() => '?').join(', ')})`); } } else if (op === '$exists') { parts.push(`${lhs} ${replacement} null`); } else if (val === null) { parts.push(`${lhs} ${replacement} null`); } else { parts.push(`${lhs} ${replacement} ?`); params.push(val); } } return parts.join(' and '); } processJsonElemMatch(cond, prop, alias) { const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`); const result = this.buildJsonArrayExists( cond, column, false, (key, value) => { this.#platform.validateJsonPropertyName(key); return { name: key, type: this.inferJsonValueType(value) }; }, () => ValidationError.invalidQueryCondition(cond), ); return result ?? { sql: '1 = 1', params: [] }; } /** * Shared logic for building WHERE conditions inside JSON array EXISTS subqueries. * Used by both embedded array queries (metadata-driven) and $elemMatch (type-inferred). */ buildArrayElementWhere(cond, jeAlias, referencedProps, resolveProperty, invalidObjectError) { const parts = []; const params = []; for (const k of Object.keys(cond)) { if (k === '$and' || k === '$or') { const items = cond[k]; if (items.length === 0) { continue; } const subParts = []; for (const item of items) { const sub = this.buildArrayElementWhere(item, jeAlias, referencedProps, resolveProperty, invalidObjectError); if (sub.sql) { subParts.push(sub.sql); params.push(...sub.params); } } if (subParts.length > 0) { const joiner = k === '$or' ? ' or ' : ' and '; parts.push(`(${subParts.join(joiner)})`); } continue; } // Within $or/$and scope, $not provides element-level negation: // "this element does not match the condition". if (k === '$not') { const sub = this.buildArrayElementWhere(cond[k], jeAlias, referencedProps, resolveProperty, invalidObjectError); if (sub.sql) { parts.push(`not (${sub.sql})`); params.push(...sub.params); } continue; } const value = cond[k]; const { name, type } = resolveProperty(k, value); referencedProps.set(k, { name, type }); const lhs = this.#platform.getJsonArrayElementPropertySQL(jeAlias, name, type); if (Utils.isPlainObject(value)) { const valueKeys = Object.keys(value); if (valueKeys.some(vk => !Utils.isOperator(vk))) { throw invalidObjectError(k); } const sub = this.buildEmbeddedArrayOperatorCondition(lhs, value, params); parts.push(sub); } else if (value === null) { parts.push(`${lhs} is null`); } else { parts.push(`${lhs} = ?`); params.push(value); } } return { sql: parts.join(' and '), params }; } inferJsonValueType(value) { if (typeof value === 'number') { return 'number'; } if (typeof value === 'boolean') { return 'boolean'; } if (typeof value === 'bigint') { return 'bigint'; } if (Utils.isPlainObject(value)) { for (const v of Object.values(value)) { if (typeof v === 'number') { return 'number'; } if (typeof v === 'boolean') { return 'boolean'; } if (typeof v === 'bigint') { return 'bigint'; } if (Array.isArray(v) && v.length > 0) { if (typeof v[0] === 'number') { return 'number'; } if (typeof v[0] === 'boolean') { return 'boolean'; } } } } return 'string'; } processOnConflictCondition(cond, schema) { const meta = this.#metadata.get(this.#entityName); const tableName = meta.tableName; for (const key of Object.keys(cond)) { const mapped = this.mapper(key, QueryType.UPSERT); Utils.renameKey(cond, key, tableName + '.' + mapped); } return cond; } 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, }; } }