1114 lines
43 KiB
JavaScript
1114 lines
43 KiB
JavaScript
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,
|
|
};
|
|
}
|
|
}
|