typeorm
Version:
Data-Mapper ORM for TypeScript and ES2023+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
2,803 lines • 129 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectQueryBuilder = void 0;
const DriverUtils_1 = require("../driver/DriverUtils");
const error_1 = require("../error");
const EntityNotFoundError_1 = require("../error/EntityNotFoundError");
const EntityPropertyNotFoundError_1 = require("../error/EntityPropertyNotFoundError");
const LockNotSupportedOnGivenDriverError_1 = require("../error/LockNotSupportedOnGivenDriverError");
const NoVersionOrUpdateDateColumnError_1 = require("../error/NoVersionOrUpdateDateColumnError");
const OffsetWithoutLimitNotSupportedError_1 = require("../error/OffsetWithoutLimitNotSupportedError");
const OptimisticLockCanNotBeUsedError_1 = require("../error/OptimisticLockCanNotBeUsedError");
const OptimisticLockVersionMismatchError_1 = require("../error/OptimisticLockVersionMismatchError");
const PessimisticLockTransactionRequiredError_1 = require("../error/PessimisticLockTransactionRequiredError");
const FindOptionsUtils_1 = require("../find-options/FindOptionsUtils");
const ApplyValueTransformers_1 = require("../util/ApplyValueTransformers");
const InstanceChecker_1 = require("../util/InstanceChecker");
const ObjectUtils_1 = require("../util/ObjectUtils");
const OrmUtils_1 = require("../util/OrmUtils");
const JoinAttribute_1 = require("./JoinAttribute");
const QueryBuilder_1 = require("./QueryBuilder");
const RelationIdAttribute_1 = require("./relation-id/RelationIdAttribute");
const RelationIdLoader_1 = require("./relation-id/RelationIdLoader");
const RelationIdMetadataToAttributeTransformer_1 = require("./relation-id/RelationIdMetadataToAttributeTransformer");
const RelationIdLoader_2 = require("./RelationIdLoader");
const RawSqlResultsToEntityTransformer_1 = require("./transformer/RawSqlResultsToEntityTransformer");
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
*/
class SelectQueryBuilder extends QueryBuilder_1.QueryBuilder {
constructor() {
super(...arguments);
this["@instanceof"] = Symbol.for("SelectQueryBuilder");
this.findOptions = {};
this.selects = [];
this.joins = [];
this.conditions = "";
this.orderBys = [];
this.relationMetadatas = [];
this.eagerLoadChain = new Set();
}
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated SQL query without parameters being replaced.
*/
getQuery() {
let sql = this.createComment();
sql += this.createCteExpression();
sql += this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
sql += this.createOrderByExpression();
sql += this.createLimitOffsetExpression();
sql += this.createLockExpression();
sql = sql.trim();
if (this.expressionMap.subQuery)
sql = "(" + sql + ")";
return this.replacePropertyNamesForTheWholeQuery(sql);
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
setFindOptions(findOptions) {
FindOptionsUtils_1.FindOptionsUtils.rejectJoinOption(findOptions);
FindOptionsUtils_1.FindOptionsUtils.rejectStringArraySelect(findOptions);
FindOptionsUtils_1.FindOptionsUtils.rejectStringArrayRelations(findOptions);
this.findOptions = findOptions;
this.applyFindOptions();
return this;
}
/**
* Creates a subquery - query that can be used inside other queries.
*/
subQuery() {
const qb = this.createQueryBuilder();
qb.expressionMap.subQuery = true;
qb.parentQueryBuilder = this;
return qb;
}
/**
* Creates SELECT query and selects given data.
* Replaces all previous selections if they exist.
*
* @param selection
* @param selectionAliasName
*/
select(selection, selectionAliasName) {
this.expressionMap.queryType = "select";
if (Array.isArray(selection)) {
this.expressionMap.selects = selection.map((selection) => ({
selection: selection,
}));
}
else if (typeof selection === "function") {
const subQueryBuilder = selection(this.subQuery());
this.setParameters(subQueryBuilder.getParameters());
this.expressionMap.selects.push({
selection: subQueryBuilder.getQuery(),
aliasName: selectionAliasName,
});
}
else if (selection) {
this.expressionMap.selects = [
{ selection: selection, aliasName: selectionAliasName },
];
}
return this;
}
/**
* Adds new selection to the SELECT query.
*
* @param selection
* @param selectionAliasName
*/
addSelect(selection, selectionAliasName) {
if (!selection)
return this;
if (Array.isArray(selection)) {
this.expressionMap.selects = this.expressionMap.selects.concat(selection.map((selection) => ({ selection: selection })));
}
else if (typeof selection === "function") {
const subQueryBuilder = selection(this.subQuery());
this.setParameters(subQueryBuilder.getParameters());
this.expressionMap.selects.push({
selection: subQueryBuilder.getQuery(),
aliasName: selectionAliasName,
});
}
else if (selection) {
this.expressionMap.selects.push({
selection: selection,
aliasName: selectionAliasName,
});
}
return this;
}
/**
* Set max execution time.
*
* @param milliseconds
*/
maxExecutionTime(milliseconds) {
this.expressionMap.maxExecutionTime = milliseconds;
return this;
}
/**
* Sets whether the selection is DISTINCT.
*
* @param distinct
*/
distinct(distinct = true) {
this.expressionMap.selectDistinct = distinct;
return this;
}
/**
* Sets the distinct on clause for Postgres.
*
* @param distinctOn
*/
distinctOn(distinctOn) {
this.expressionMap.selectDistinctOn = distinctOn;
return this;
}
fromDummy() {
return this.from(this.dataSource.driver.dummyTableName ??
"(SELECT 1 AS dummy_column)", "dummy_table");
}
/**
* Specifies FROM which entity's table select/update/delete will be executed.
* Also sets a main string alias of the selection data.
* Removes all previously set from-s.
*
* @param entityTarget
* @param aliasName
*/
from(entityTarget, aliasName) {
const mainAlias = this.createFromAlias(entityTarget, aliasName);
this.expressionMap.setMainAlias(mainAlias);
return this;
}
/**
* Specifies FROM which entity's table select/update/delete will be executed.
* Also sets a main string alias of the selection data.
*
* @param entityTarget
* @param aliasName
*/
addFrom(entityTarget, aliasName) {
const alias = this.createFromAlias(entityTarget, aliasName);
if (!this.expressionMap.mainAlias)
this.expressionMap.setMainAlias(alias);
return this;
}
/**
* INNER JOINs (without selection).
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
innerJoin(entityOrProperty, alias, condition, parameters) {
this.join("INNER", entityOrProperty, alias, condition, parameters);
return this;
}
/**
* LEFT JOINs (without selection).
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
leftJoin(entityOrProperty, alias, condition, parameters) {
this.join("LEFT", entityOrProperty, alias, condition, parameters);
return this;
}
/**
* INNER JOINs and adds all selection properties to SELECT.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
innerJoinAndSelect(entityOrProperty, alias, condition, parameters) {
this.addSelect(alias);
this.innerJoin(entityOrProperty, alias, condition, parameters);
return this;
}
/**
* LEFT JOINs and adds all selection properties to SELECT.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
leftJoinAndSelect(entityOrProperty, alias, condition, parameters) {
this.addSelect(alias);
this.leftJoin(entityOrProperty, alias, condition, parameters);
return this;
}
/**
* INNER JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property.
* This is extremely useful when you want to select some data and map it to some virtual property.
* It will assume that there are multiple rows of selecting data, and mapped result will be an array.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param mapToProperty
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
innerJoinAndMapMany(mapToProperty, entityOrProperty, alias, condition, parameters) {
this.addSelect(alias);
this.join("INNER", entityOrProperty, alias, condition, parameters, mapToProperty, true);
return this;
}
/**
* INNER JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property.
* This is extremely useful when you want to select some data and map it to some virtual property.
* It will assume that there is a single row of selecting data, and mapped result will be a single selected value.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param mapToProperty
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
* @param mapAsEntity
*/
innerJoinAndMapOne(mapToProperty, entityOrProperty, alias, condition, parameters, mapAsEntity) {
this.addSelect(alias);
this.join("INNER", entityOrProperty, alias, condition, parameters, mapToProperty, false, mapAsEntity);
return this;
}
/**
* LEFT JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property.
* This is extremely useful when you want to select some data and map it to some virtual property.
* It will assume that there are multiple rows of selecting data, and mapped result will be an array.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param mapToProperty
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
*/
leftJoinAndMapMany(mapToProperty, entityOrProperty, alias, condition, parameters) {
this.addSelect(alias);
this.join("LEFT", entityOrProperty, alias, condition, parameters, mapToProperty, true);
return this;
}
/**
* LEFT JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property.
* This is extremely useful when you want to select some data and map it to some virtual property.
* It will assume that there is a single row of selecting data, and mapped result will be a single selected value.
* You also need to specify an alias of the joined data.
* Optionally, you can add condition and parameters used in condition.
*
* @param mapToProperty
* @param entityOrProperty
* @param alias
* @param condition
* @param parameters
* @param mapAsEntity
*/
leftJoinAndMapOne(mapToProperty, entityOrProperty, alias, condition, parameters, mapAsEntity) {
this.addSelect(alias);
this.join("LEFT", entityOrProperty, alias, condition, parameters, mapToProperty, false, mapAsEntity);
return this;
}
/**
* LEFT JOINs relation id and maps it into some entity's property.
* Optionally, you can add condition and parameters used in condition.
*
* @param mapToProperty
* @param relationName
* @param aliasNameOrOptions
* @param queryBuilderFactory
*/
loadRelationIdAndMap(mapToProperty, relationName, aliasNameOrOptions, queryBuilderFactory) {
const relationIdAttribute = new RelationIdAttribute_1.RelationIdAttribute(this.expressionMap);
relationIdAttribute.mapToProperty = mapToProperty;
relationIdAttribute.relationName = relationName;
if (typeof aliasNameOrOptions === "string")
relationIdAttribute.alias = aliasNameOrOptions;
if (typeof aliasNameOrOptions === "object" &&
aliasNameOrOptions.disableMixedMap)
relationIdAttribute.disableMixedMap = true;
relationIdAttribute.queryBuilderFactory = queryBuilderFactory;
this.expressionMap.relationIdAttributes.push(relationIdAttribute);
if (relationIdAttribute.relation.junctionEntityMetadata) {
this.expressionMap.createAlias({
type: "other",
name: relationIdAttribute.junctionAlias,
metadata: relationIdAttribute.relation.junctionEntityMetadata,
});
}
return this;
}
/**
* Loads all relation ids for all relations of the selected entity.
* All relation ids will be mapped to relation property themself.
* If array of strings is given then loads only relation ids of the given properties.
*
* @param options
* @param options.relations
* @param options.disableMixedMap
*/
loadAllRelationIds(options) {
// todo: add skip relations
this.expressionMap.mainAlias.metadata.relations.forEach((relation) => {
if (options?.relations?.indexOf(relation.propertyPath) === -1)
return;
this.loadRelationIdAndMap(this.expressionMap.mainAlias.name +
"." +
relation.propertyPath, this.expressionMap.mainAlias.name +
"." +
relation.propertyPath, options);
});
return this;
}
/**
* Sets WHERE condition in the query builder.
* If you had previously WHERE expression defined,
* calling this function will override previously set WHERE conditions.
* Additionally you can add parameters used in where expression.
*
* @param where
* @param parameters
*/
where(where, parameters) {
this.expressionMap.wheres = []; // don't move this block below since computeWhereParameter can add where expressions
const condition = this.getWhereCondition(where);
if (condition) {
this.expressionMap.wheres = [
{ type: "simple", condition: condition },
];
}
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Adds new AND WHERE condition in the query builder.
* Additionally you can add parameters used in where expression.
*
* @param where
* @param parameters
*/
andWhere(where, parameters) {
this.expressionMap.wheres.push({
type: "and",
condition: this.getWhereCondition(where),
});
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Adds new OR WHERE condition in the query builder.
* Additionally you can add parameters used in where expression.
*
* @param where
* @param parameters
*/
orWhere(where, parameters) {
this.expressionMap.wheres.push({
type: "or",
condition: this.getWhereCondition(where),
});
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Sets a new where EXISTS clause
*
* @param subQuery
*/
whereExists(subQuery) {
return this.where(...this.getExistsCondition(subQuery));
}
/**
* Adds a new AND where EXISTS clause
*
* @param subQuery
*/
andWhereExists(subQuery) {
return this.andWhere(...this.getExistsCondition(subQuery));
}
/**
* Adds a new OR where EXISTS clause
*
* @param subQuery
*/
orWhereExists(subQuery) {
return this.orWhere(...this.getExistsCondition(subQuery));
}
/**
* Adds new AND WHERE with conditions for the given ids.
*
* Ids are mixed.
* It means if you have single primary key you can pass a simple id values, for example [1, 2, 3].
* If you have multiple primary keys you need to pass object with property names and values specified,
* for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...]
*
* @param ids
*/
whereInIds(ids) {
return this.where(this.getWhereInIdsCondition(ids));
}
/**
* Adds new AND WHERE with conditions for the given ids.
*
* Ids are mixed.
* It means if you have single primary key you can pass a simple id values, for example [1, 2, 3].
* If you have multiple primary keys you need to pass object with property names and values specified,
* for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...]
*
* @param ids
*/
andWhereInIds(ids) {
return this.andWhere(this.getWhereInIdsCondition(ids));
}
/**
* Adds new OR WHERE with conditions for the given ids.
*
* Ids are mixed.
* It means if you have single primary key you can pass a simple id values, for example [1, 2, 3].
* If you have multiple primary keys you need to pass object with property names and values specified,
* for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...]
*
* @param ids
*/
orWhereInIds(ids) {
return this.orWhere(this.getWhereInIdsCondition(ids));
}
/**
* Sets HAVING condition in the query builder.
* If you had previously HAVING expression defined,
* calling this function will override previously set HAVING conditions.
* Additionally you can add parameters used in where expression.
*
* @param having
* @param parameters
*/
having(having, parameters) {
this.expressionMap.havings.push({ type: "simple", condition: having });
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Adds new AND HAVING condition in the query builder.
* Additionally you can add parameters used in where expression.
*
* @param having
* @param parameters
*/
andHaving(having, parameters) {
this.expressionMap.havings.push({ type: "and", condition: having });
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Adds new OR HAVING condition in the query builder.
* Additionally you can add parameters used in where expression.
*
* @param having
* @param parameters
*/
orHaving(having, parameters) {
this.expressionMap.havings.push({ type: "or", condition: having });
if (parameters)
this.setParameters(parameters);
return this;
}
/**
* Sets GROUP BY condition in the query builder.
* If you had previously GROUP BY expression defined,
* calling this function will override previously set GROUP BY conditions.
*
* @param groupBy
*/
groupBy(groupBy) {
if (groupBy) {
this.expressionMap.groupBys = [groupBy];
}
else {
this.expressionMap.groupBys = [];
}
return this;
}
/**
* Adds GROUP BY condition in the query builder.
*
* @param groupBy
*/
addGroupBy(groupBy) {
this.expressionMap.groupBys.push(groupBy);
return this;
}
/**
* Enables time travelling for the current query (only supported by cockroach currently)
*
* @param timeTravelFn
*/
timeTravelQuery(timeTravelFn) {
if (this.dataSource.driver.options.type === "cockroachdb") {
if (timeTravelFn === undefined) {
this.expressionMap.timeTravel = "follower_read_timestamp()";
}
else {
this.expressionMap.timeTravel = timeTravelFn;
}
}
return this;
}
/**
* Sets ORDER BY condition in the query builder.
* If you had previously ORDER BY expression defined,
* calling this function will override previously set ORDER BY conditions.
*
* @param sort
* @param order
* @param nulls
*/
orderBy(sort, order = "ASC", nulls) {
if (!sort) {
this.expressionMap.orderBys = {};
return this;
}
if (typeof sort === "object") {
this.validateOrderByCondition(sort);
this.expressionMap.orderBys = sort;
return this;
}
const condition = nulls
? { [sort]: { order, nulls } }
: { [sort]: order };
this.validateOrderByCondition(condition);
this.expressionMap.orderBys = condition;
return this;
}
/**
* Adds ORDER BY condition in the query builder.
*
* @param sort
* @param order
* @param nulls
*/
addOrderBy(sort, order = "ASC", nulls) {
const condition = nulls
? { [sort]: { order, nulls } }
: { [sort]: order };
this.validateOrderByCondition(condition);
if (nulls) {
this.expressionMap.orderBys[sort] = { order, nulls };
}
else {
this.expressionMap.orderBys[sort] = order;
}
return this;
}
/**
* Sets LIMIT - maximum number of rows to be selected.
* NOTE that it may not work as you expect if you are using joins.
* If you want to implement pagination, and you are having join in your query,
* then use the take method instead.
*
* @param limit
*/
limit(limit) {
this.expressionMap.limit = this.validateNumericInput("limit", limit);
return this;
}
/**
* Sets OFFSET - selection offset.
* NOTE that it may not work as you expect if you are using joins.
* If you want to implement pagination, and you are having join in your query,
* then use the skip method instead.
*
* @param offset
*/
offset(offset) {
this.expressionMap.offset = this.validateNumericInput("offset", offset);
return this;
}
/**
* Sets maximal number of entities to take.
*
* @param take
*/
take(take) {
this.expressionMap.take = this.validateNumericInput("take", take);
return this;
}
/**
* Sets number of entities to skip.
*
* @param skip
*/
skip(skip) {
this.expressionMap.skip = this.validateNumericInput("skip", skip);
return this;
}
/**
* Set certain index(es) to be used by the query.
*
* @param indexes Name(s) of index(es) to be used.
*/
useIndex(indexes) {
this.expressionMap.useIndex = Array.isArray(indexes)
? indexes
: [indexes];
return this;
}
/**
* Sets locking mode.
*
* @param lockMode
* @param lockVersion
* @param lockTables
*/
setLock(lockMode, lockVersion, lockTables) {
this.expressionMap.lockMode = lockMode;
this.expressionMap.lockVersion = lockVersion;
this.expressionMap.lockTables = lockTables;
return this;
}
/**
* Sets lock handling by adding NO WAIT or SKIP LOCKED.
*
* @param onLocked
*/
setOnLocked(onLocked) {
this.expressionMap.onLocked = onLocked;
return this;
}
/**
* Disables the global condition of "non-deleted" for the entity with delete date columns.
*/
withDeleted() {
this.expressionMap.withDeleted = true;
return this;
}
/**
* Gets first raw result returned by execution of generated query builder sql.
*/
async getRawOne() {
return (await this.getRawMany())[0];
}
/**
* Gets all raw results returned by execution of generated query builder sql.
*/
async getRawMany() {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError_1.OptimisticLockCanNotBeUsedError();
this.expressionMap.queryEntity = false;
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
const results = await this.loadRawResults(queryRunner);
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
finally {
if (queryRunner !== this.queryRunner) {
// means we created our own query runner
await queryRunner.release();
}
}
}
/**
* Executes sql generated by query builder and returns object with raw results and entities created from them.
*/
async getRawAndEntities() {
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
this.expressionMap.queryEntity = true;
const results = await this.executeEntitiesAndRawResults(queryRunner);
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
finally {
if (queryRunner !== this.queryRunner)
// means we created our own query runner
await queryRunner.release();
}
}
/**
* Gets single entity returned by execution of generated query builder sql.
*/
async getOne() {
const results = await this.getRawAndEntities();
const result = results.entities[0];
if (result &&
this.expressionMap.lockMode === "optimistic" &&
this.expressionMap.lockVersion) {
const metadata = this.expressionMap.mainAlias.metadata;
if (this.expressionMap.lockVersion instanceof Date) {
const actualVersion = metadata.updateDateColumn.getEntityValue(result); // what if columns arent set?
if (actualVersion.getTime() !==
this.expressionMap.lockVersion.getTime())
throw new OptimisticLockVersionMismatchError_1.OptimisticLockVersionMismatchError(metadata.name, this.expressionMap.lockVersion, actualVersion);
}
else {
const actualVersion = metadata.versionColumn.getEntityValue(result); // what if columns arent set?
if (actualVersion !== this.expressionMap.lockVersion)
throw new OptimisticLockVersionMismatchError_1.OptimisticLockVersionMismatchError(metadata.name, this.expressionMap.lockVersion, actualVersion);
}
}
if (result === undefined) {
return null;
}
return result;
}
/**
* Gets the first entity returned by execution of generated query builder sql or rejects the returned promise on error.
*/
async getOneOrFail() {
const entity = await this.getOne();
if (!entity) {
throw new EntityNotFoundError_1.EntityNotFoundError(this.expressionMap.mainAlias.target, this.expressionMap.parameters);
}
return entity;
}
/**
* Gets entities returned by execution of generated query builder sql.
*/
async getMany() {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError_1.OptimisticLockCanNotBeUsedError();
const results = await this.getRawAndEntities();
return results.entities;
}
/**
* Gets count - number of entities selected by sql generated by this query builder.
* Count excludes all limitations set by offset, limit, skip, and take.
*/
async getCount() {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError_1.OptimisticLockCanNotBeUsedError();
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
this.expressionMap.queryEntity = false;
const results = await this.executeCountQuery(queryRunner);
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
finally {
if (queryRunner !== this.queryRunner)
// means we created our own query runner
await queryRunner.release();
}
}
/**
* Gets exists
* Returns whether any rows exists matching current query.
*/
async getExists() {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError_1.OptimisticLockCanNotBeUsedError();
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
this.expressionMap.queryEntity = false;
const results = await this.executeExistsQuery(queryRunner);
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
finally {
if (queryRunner !== this.queryRunner)
// means we created our own query runner
await queryRunner.release();
}
}
/**
* Executes built SQL query and returns entities and overall entities count (without limitation).
* This method is useful to build pagination.
*/
async getManyAndCount() {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError_1.OptimisticLockCanNotBeUsedError();
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
this.expressionMap.queryEntity = true;
const entitiesAndRaw = await this.executeEntitiesAndRawResults(queryRunner);
this.expressionMap.queryEntity = false;
let count = this.lazyCount(entitiesAndRaw);
if (count === undefined) {
const cacheId = this.expressionMap.cacheId;
// Creates a new cacheId for the count query, or it will retrieve the above query results
// and count will return 0.
if (cacheId) {
this.expressionMap.cacheId = `${cacheId}-count`;
}
count = await this.executeCountQuery(queryRunner);
}
const results = [entitiesAndRaw.entities, count];
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
finally {
if (queryRunner !== this.queryRunner)
// means we created our own query runner
await queryRunner.release();
}
}
lazyCount(entitiesAndRaw) {
const hasLimit = this.expressionMap.limit !== undefined &&
this.expressionMap.limit !== null;
if (this.expressionMap.joinAttributes.length > 0 && hasLimit) {
return undefined;
}
const hasTake = this.expressionMap.take !== undefined &&
this.expressionMap.take !== null;
// limit overrides take when no join is defined
const maxResults = hasLimit
? this.expressionMap.limit
: hasTake
? this.expressionMap.take
: undefined;
if (maxResults !== undefined &&
entitiesAndRaw.entities.length === maxResults) {
// stop here when the result set contains the max number of rows; we need to execute a full count
return undefined;
}
const hasSkip = this.expressionMap.skip !== undefined &&
this.expressionMap.skip !== null &&
this.expressionMap.skip > 0;
const hasOffset = this.expressionMap.offset !== undefined &&
this.expressionMap.offset !== null &&
this.expressionMap.offset > 0;
if (entitiesAndRaw.entities.length === 0 && (hasSkip || hasOffset)) {
// when skip or offset were used and no results found, we need to execute a full count
// (the given offset may have exceeded the actual number of rows)
return undefined;
}
// offset overrides skip when no join is defined
const previousResults = hasOffset
? this.expressionMap.offset
: hasSkip
? this.expressionMap.skip
: 0;
return entitiesAndRaw.entities.length + previousResults;
}
/**
* Executes built SQL query and returns raw data stream.
*/
async stream() {
this.expressionMap.queryEntity = false;
const [sql, parameters] = this.getQueryAndParameters();
const queryRunner = this.obtainQueryRunner();
let transactionStartedByUs = false;
try {
// start transaction if it was enabled
if (this.expressionMap.useTransaction === true &&
queryRunner.isTransactionActive === false) {
await queryRunner.startTransaction();
transactionStartedByUs = true;
}
const releaseFn = () => {
if (queryRunner !== this.queryRunner)
// means we created our own query runner
return queryRunner.release();
return;
};
const results = queryRunner.stream(sql, parameters, releaseFn, releaseFn);
// close transaction if we started it
if (transactionStartedByUs) {
await queryRunner.commitTransaction();
}
return results;
}
catch (error) {
// rollback transaction if we started it
if (transactionStartedByUs) {
try {
await queryRunner.rollbackTransaction();
}
catch (rollbackError) { }
}
throw error;
}
}
/**
* Enables or disables query result caching.
*
* @param enabledOrMillisecondsOrId
* @param maybeMilliseconds
*/
cache(enabledOrMillisecondsOrId, maybeMilliseconds) {
if (typeof enabledOrMillisecondsOrId === "boolean") {
this.expressionMap.cache = enabledOrMillisecondsOrId;
}
else if (typeof enabledOrMillisecondsOrId === "number") {
this.expressionMap.cache = true;
this.expressionMap.cacheDuration = enabledOrMillisecondsOrId;
}
else if (typeof enabledOrMillisecondsOrId === "string" ||
typeof enabledOrMillisecondsOrId === "number") {
this.expressionMap.cache = true;
this.expressionMap.cacheId = enabledOrMillisecondsOrId;
}
if (maybeMilliseconds) {
this.expressionMap.cacheDuration = maybeMilliseconds;
}
return this;
}
/**
* Sets extra options that can be used to configure how query builder works.
*
* @param option
*/
setOption(option) {
this.expressionMap.options.push(option);
return this;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
join(direction, entityOrProperty, aliasName, condition, parameters, mapToProperty, isMappingMany, mapAsEntity) {
if (parameters) {
this.setParameters(parameters);
}
const joinAttribute = new JoinAttribute_1.JoinAttribute(this.dataSource, this.expressionMap);
joinAttribute.direction = direction;
joinAttribute.mapAsEntity = mapAsEntity;
joinAttribute.mapToProperty = mapToProperty;
joinAttribute.isMappingMany = isMappingMany;
joinAttribute.entityOrProperty = entityOrProperty; // relationName
joinAttribute.condition = condition; // joinInverseSideCondition
// joinAttribute.junctionAlias = joinAttribute.relation.isOwning ? parentAlias + "_" + destinationTableAlias : destinationTableAlias + "_" + parentAlias;
this.expressionMap.joinAttributes.push(joinAttribute);
const isEntity = this.dataSource.hasMetadata(entityOrProperty);
const isSubQuery = (!isEntity && typeof entityOrProperty === "function") ||
(typeof entityOrProperty === "string" &&
entityOrProperty.startsWith("(") &&
entityOrProperty.endsWith(")"));
let subQuery = "";
if (isSubQuery) {
if (typeof entityOrProperty === "string") {
subQuery = entityOrProperty;
}
else {
const subQueryBuilder = entityOrProperty(this.subQuery());
this.setParameters(subQueryBuilder.getParameters());
subQuery = subQueryBuilder.getQuery();
}
}
const joinAttributeMetadata = joinAttribute.metadata;
if (joinAttributeMetadata) {
if (joinAttributeMetadata.deleteDateColumn &&
!this.expressionMap.withDeleted) {
const conditionDeleteColumn = `${aliasName}.${joinAttributeMetadata.deleteDateColumn.propertyName} IS NULL`;
joinAttribute.condition = joinAttribute.condition
? ` ${joinAttribute.condition} AND ${conditionDeleteColumn}`
: `${conditionDeleteColumn}`;
}
// todo: find and set metadata right there?
joinAttribute.alias = this.expressionMap.createAlias({
type: "join",
name: aliasName,
metadata: joinAttributeMetadata,
subQuery: isSubQuery ? subQuery : undefined,
});
if (joinAttribute.relation?.junctionEntityMetadata) {
this.expressionMap.createAlias({
type: "join",
name: joinAttribute.junctionAlias,
metadata: joinAttribute.relation.junctionEntityMetadata,
});
}
}
else {
joinAttribute.alias = this.expressionMap.createAlias({
type: "join",
name: aliasName,
tablePath: isSubQuery === false
? entityOrProperty
: undefined,
subQuery: isSubQuery === true ? subQuery : undefined,
});
}
}
/**
* Creates "SELECT FROM" part of SQL query.
*/
createSelectExpression() {
if (!this.expressionMap.mainAlias)
throw new error_1.TypeORMError("Cannot build query because main alias is not set (call qb#from method)");
// todo throw exception if selects or from is missing
const allSelects = [];
const excludedSelects = [];
if (this.expressionMap.mainAlias.hasMetadata) {
const metadata = this.expressionMap.mainAlias.metadata;
allSelects.push(...this.buildEscapedEntityColumnSelects(this.expressionMap.mainAlias.name, metadata));
excludedSelects.push(...this.findEntityColumnSelects(this.expressionMap.mainAlias.name, metadata));
}
// add selects from joins
this.expressionMap.joinAttributes.forEach((join) => {
if (join.metadata) {
allSelects.push(...this.buildEscapedEntityColumnSelects(join.alias.name, join.metadata));
excludedSelects.push(...this.findEntityColumnSelects(join.alias.name, join.metadata));
}
else {
const hasMainAlias = this.expressionMap.selects.some((select) => select.selection === join.alias.name);
if (hasMainAlias) {
allSelects.push({
selection: this.escape(join.alias.name) + ".*",
});
const excludedSelect = this.expressionMap.selects.find((select) => select.selection === join.alias.name);
excludedSelects.push(excludedSelect);
}
}
});
// add all other selects
this.expressionMap.selects
.filter((select) => excludedSelects.indexOf(select) === -1)
.forEach((select) => allSelects.push({
selection: select.selection,
aliasName: select.aliasName,
}));
// if still selection is empty, then simply set it to all (*)
if (allSelects.length === 0)
allSelects.push({ selection: "*" });
// Use certain index
let useIndex = "";
if (this.expressionMap.useIndex?.length) {
if (DriverUtils_1.DriverUtils.isMySQLFamily(this.dataSource.driver)) {
useIndex = ` USE INDEX (${this.expressionMap.useIndex
.map((i) => this.escape(i))
.join(", ")})`;
}
}
// create a selection query
const froms = this.expressionMap.aliases
.filter((alias) => alias.type === "from" &&
(alias.tablePath ?? alias.subQuery))
.map((alias) => {
if (alias.subQuery)
return alias.subQuery + " " + this.escape(alias.name);
return (this.getTableName(alias.tablePath) +
" " +
this.escape(alias.name));
});
const select = this.createSelectDistinctExpression();
const selection = allSelects
.map((select) => select.selection +
(select.aliasName
? " AS " + this.escape(select.aliasName)
: ""))
.join(", ");
return (select +
selection +
" FROM " +
froms.join(", ") +
this.createTableLockExpression() +
useIndex);
}
/**
* Creates select | select distinct part of SQL query.
*/
createSelectDistinctExpression() {
const { selectDistinct, selectDistinctOn, maxExecutionTime } = this.expressionMap;
const { driver } = this.dataSource;
let select = "SELECT ";
if (maxExecutionTime > 0) {
if (DriverUtils_1.DriverUtils.isMySQLFamily(driver)) {
select += `/*+ MAX_EXECUTION_TIME(${this.expressionMap.maxExecutionTime}) */ `;
}
}
if (DriverUtils_1.DriverUtils.isPostgresFamily(driver) &&
selectDistinctOn.length > 0) {
const selectDistinctOnMap = selectDistinctOn.join(", ");
select = `SELECT DISTINCT ON (${selectDistinctOnMap}) `;
}
else if (selectDistinct) {
select = "SELECT DISTINCT ";
}
return select;
}
/**
* Creates "JOIN" part of SQL query.
*
* @example
* // select from owning side
* qb.select("post")
* .leftJoinAndSelect("post.category", "category")
*
* @example
* // select from non-owning side
* qb.select("category")
* .leftJoinAndSelect("category.post", "post")
*
*/
createJoinExpression() {
// preprocess join attributes by nesting them when necessary
const joinAttributeTrees = this.expressionMap.joinAttributes.reduce(({ trees, mappedTrees }, joinAttribute) => {
const destinationTableAlias = joinAttribute.alias.name;
const existingTree = joinAttribute.parentAlias
? mappedTrees[joinAttribute.parentAlias]
: undefined;
const joinAttributeTree = {
children: [],
joinAttribute,
};
if (existingTree) {
existingTree.children.push(joinAttributeTree);
}
else {
trees.push(joinAttributeTree);
}
mappedTrees[destinationTableAlias] = joinAttributeTree;
return { trees, mappedTrees };
}, { trees: [], mappedTrees: {} });
const joinStatements = joinAttributeTrees.trees.map((joinTree) => {
return this.createJoinTreeRecursively(joinTree);
});
return joinStatements.join(" ");
}
createJoinTreeRecursively({ children, joinAttribute: joinAttr, }) {
const relation = joinAttr.relation;
const destinationTableName = joinAttr.tablePath;
const destinationTableAlias = joinAttr.alias.name;
let appendedCondition = joinAttr.condition
? " AND (" + joinAttr.condition + ")"
: "";
const parentAlias = joinAttr.parentAlias;
const childJoins = (children || [])
.map((joinTreeChild) => {
return this.createJoinTreeRecursively(joinTreeChild);
})
.join(" ");
// nesting is only needed to stop an inner child join from filtering out the rows its outer parent
// preserves. In every other combination the flat form is equivalent and leaves join order to the planner.
const nestChildJoins = joinAttr.direction === "LEFT" &&
(children || []).some((joinTreeChild) => joinTreeChild.joinAttribute.direction === "INNER");
// if join was build without relation (e.g. without "post.category") then it means that we have direct
// table to join, without junction table involved. This means we simply join direct table.
if (!parentAlias || !relation) {
const destinationJoin = joinAttr.alias.subQuery ??
this.getTableName(destinationTableName);
return this.buildJoinClause(joinAttr.direction, destinationJoin, destinationTableAlias, joinAttr.condition ?? "", childJoins, nestChildJoins);
}
// if real entity relation is involved
if (relation.isManyToOne || relation.isOneToOneOwner) {
// JOIN `category` `category` ON `category`.`id` = `post`.`categoryId`
const condition = relation.joinColumns
.map((joinColumn) => {
return (destinationTableAlias +
"." +
joinColumn.referencedColumn.propertyPath +
"=" +
parentAlias +
"." +
relation.propertyPath +
"." +
joinColumn.referencedColumn.propertyPath);
})
.join(" AND ");
return this.buildJoinClause(joinAttr.direction, this.getTableName(destinationTableName), destinationTableAlias, condition + appendedCondition, childJoins, nestChildJoins);
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
// JOIN `post` `post` ON `post`.`categoryId` = `category`.`id`
const condition = relation
.inverseRelation.joinColumns.map((joinColumn) => {
if (relation.inverseEntityMetadata.tableType ===
"entity-child" &&
relation.inverseEntityMetadata.discriminatorColumn) {
appendedCondition +=
" AND " +
destinationTableAlias +
"." +
relation.inverseEntityMetadata.discriminatorColumn
.databaseName +
"='" +
relation.inverseEntityMetadata.discriminatorValue +
"'";
}
return (destinationTableAlias +
"." +
relation.inverseRelation.propertyPath +
"." +
joinColumn.referencedColumn.propertyPath +
"=" +
parentAlias +
"." +
joinColumn.referencedColumn.propertyPath);
})
.join(" AND ");
if (!condition)
throw new error_1.TypeORMError(`Relation ${relation.entityMetadata.name}.${relation.propertyName} does not have join columns.`);
return this.buildJoinClause(joinAttr.direction, this.getTableName(destinationTableName), destinationTableAlias, condition + appendedCondition, childJoins, nestChildJoins);
}
else {
// means many-to-many
const junctionTableName = relation.junctionEntityMetadata.tablePath;
const junctionAlias = joinAttr.junctionAlias;
let junctionCondition = "", destinationCondition = "";
if (relation.isOwning) {
junctionCondition = relation.joinColumns
.map((joinColumn) => {
// `post_category`.`postId` = `post`.`id`
return (junctionAlias +
"." +
joinColumn.propertyPath +
"=" +
parentAlias +
"." +
joinColumn.referencedColumn.propertyPath);
})
.join(" AND ");
destinationCondition = relation.inverseJoinColumns
.map((joinColumn) => {
// `category`.`id` = `post_category`.`categoryId`
return (destinationTableAlias +
"." +
joinColumn.referencedColumn.propertyPath +
"=" +
junctionAlias +
"." +
joinColumn.propertyPath);
})
.join(" AND ");
}
else {
junctionCondition = relation
.inverseRelation.inverseJoinColumns.map((joinColumn) => {
// `post_category`.`categoryId` = `category`.`id`
return (junctionAlias +
"." +
joinColumn.propertyPath +
"=" +
parentAlias +
"." +
joinColumn.referencedColumn.propertyPath);
})
.join(" AND ");
destinationCondition = relation
.inverseRelation.joinColumns.map((joinColumn) => {
// `post`.`id` = `post_category`.`postId`
return (destinationTableAlias +
"." +
joinColumn.referencedColumn.propertyPath +
"=" +
junctionAlias +
"." +
joinColumn.propertyPath);
})
.join(" AND ");
}
return (this.buildJoinClause(joinAttr.direction, this.getTableName(junctionTableName), junctionAlias, junctionCondition) +
this.buildJoinClause(joinAttr.direction, this.getTableName(destinationTableName), destinationTableAlias, destinationCondition + appendedCondition, childJoins, nestChildJoins));
}
}
buildJoinClause(direction, tableName, alias, condition, childJoins = "", nestChildJoins = false) {
const joinedTable = nestChildJoins
? "(" + tableName + " " + this.escape(alias) + childJoins + ")"
: tableName + " " + this.escape(alias);
return (" " +
direction +
" JOIN " +
joinedTable +
this.createTableLockExpression() +
(condition ? " ON " + condition : "") +
(nestChildJoins ? "" : childJoins));
}
/**
* Creates "GROUP BY" part of SQL query.
*/
createGroupByExpression() {
if (!this.expressionMap.groupBys?.length)
return "";
return " GROUP BY " + this.expressionMap.groupBys.join(", ");
}
/**
* Creates "ORDER BY" part of SQL query.
*/
createOrderByExpression() {
const orderBys = this.expressionMap.allOrderBys;
if (Object.keys(orderBys).length === 0)
return "";
return (" ORDER BY " +
Object.keys(orderBys)
.map((columnName) => {
const orderValue = typeof orderBys[columnName] === "string"
? orderBys[columnName]
: orderBys[columnName].order +
" " +
orderBys[columnName].nulls;
if (/[;'"\\]/.test(orderValue))
throw new error_1.TypeORMError(`Unsafe order-by value "${orderValue}" for "${columnName}".`);
const selectionByAlias = this.expressionMap.selects.find((s) => s.aliasName === columnName);
if (selectionByAlias) {
return this.escape(columnName) + " " + orderValue;
}
const selection = this.expressionMap.selects.find((s) => s.selection === columnName);
if (selection &&
!selection.aliasName &&
columnName.indexOf(".") !== -1) {
const criteriaParts = columnName.split(".");
const aliasName = criteriaParts[0];
const propertyPath = criteriaParts.slice(1).join(".");
const alias = this.expressionMap.aliases.find((alias) => alias.name === aliasName);
if (alias?.hasMetadata) {
const column = alias.metadata.findColumnWithPropertyPath(propertyPath);
if (column) {
const orderAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, aliasName, column.databaseName);
return (this.escape(orderAlias) + " " + orderValue);
}
}
}
return columnName + " " + orderValue;
})
.join(", "));
}
/**
* Creates "LIMIT" and "OFFSET" parts of SQL query.
*/
createLimitOffsetExpression() {
// in the case if nothing is joined in the query builder we don't need to make two requests to get paginated results
// we can use regular limit / offset, that's why we add offset and limit construction here based on skip and take values
let offset = this.expressionMap.offset, limit = this.expressionMap.limit;
if (offset === undefined &&
limit === undefined &&
this.expressionMap.joinAttributes.length === 0) {
offset = this.expressionMap.skip;
limit = this.expressionMap.take;
}
// Helper functions to check if values are set (including 0)
const hasLimit = limit !== undefined && limit !== null;
const hasOffset = offset !== undefined && offset !== null;
if (this.dataSource.driver.options.type === "mssql") {
// Due to a limitation in SQL Server's parser implementation it does not support using
// OFFSET or FETCH NEXT without an ORDER BY clause being provided. In cases where the
// user does not request one we insert a dummy ORDER BY that does nothing and should
// have no effect on the query planner or on the order of the results returned.
// https://dba.stackexchange.com/a/193799
let prefix = "";
if ((hasLimit || hasOffset) &&
Object.keys(this.expressionMap.allOrderBys).length <= 0) {
prefix = " ORDER BY (SELECT NULL)";
}
if (hasLimit && hasOffset)
return (prefix +
" OFFSET " +
offset +
" ROWS FETCH NEXT " +
limit +
" ROWS ONLY");
if (hasLimit)
return (prefix + " OFFSET 0 ROWS FETCH NEXT " + limit + " ROWS ONLY");
if (hasOffset)
return prefix + " OFFSET " + offset + " ROWS";
}
else if (DriverUtils_1.DriverUtils.isMySQLFamily(this.dataSource.driver) ||
this.dataSource.driver.options.type === "aurora-mysql" ||
this.dataSource.driver.options.type === "sap" ||
this.dataSource.driver.options.type === "spanner") {
if (hasLimit && hasOffset)
return " LIMIT " + limit + " OFFSET " + offset;
if (hasLimit)
return " LIMIT " + limit;
if (hasOffset)
throw new OffsetWithoutLimitNotSupportedError_1.OffsetWithoutLimitNotSupportedError();
}
else if (DriverUtils_1.DriverUtils.isSQLiteFamily(this.dataSource.driver)) {
if (hasLimit && hasOffset)
return " LIMIT " + limit + " OFFSET " + offset;
if (hasLimit)
return " LIMIT " + limit;
if (hasOffset)
return " LIMIT -1 OFFSET " + offset;
}
else if (this.dataSource.driver.options.type === "oracle") {
if (hasLimit && hasOffset)
return (" OFFSET " +
offset +
" ROWS FETCH NEXT " +
limit +
" ROWS ONLY");
if (hasLimit)
return " FETCH NEXT " + limit + " ROWS ONLY";
if (hasOffset)
return " OFFSET " + offset + " ROWS";
}
else {
if (hasLimit && hasOffset)
return " LIMIT " + limit + " OFFSET " + offset;
if (hasLimit)
return " LIMIT " + limit;
if (hasOffset)
return " OFFSET " + offset;
}
return "";
}
/**
* Creates "LOCK" part of SELECT Query after table Clause
* ex.
* SELECT 1
* FROM USER U WITH (NOLOCK)
* JOIN ORDER O WITH (NOLOCK)
* ON U.ID=O.OrderID
*/
createTableLockExpression() {
if (this.dataSource.driver.options.type === "mssql") {
switch (this.expressionMap.lockMode) {
case "pessimistic_read":
return " WITH (HOLDLOCK, ROWLOCK)";
case "pessimistic_write":
return " WITH (UPDLOCK, ROWLOCK)";
case "dirty_read":
return " WITH (NOLOCK)";
}
}
return "";
}
/**
* @returns "LOCK" part of SQL query
*/
createLockExpression() {
const driver = this.dataSource.driver;
let lockTablesClause = "";
if (this.expressionMap.lockTables) {
if (!DriverUtils_1.DriverUtils.isPostgresFamily(driver)) {
throw new error_1.TypeORMError("Lock tables not supported in selected driver");
}
if (this.expressionMap.lockTables.length < 1) {
throw new error_1.TypeORMError("lockTables cannot be an empty array");
}
lockTablesClause = " OF " + this.expressionMap.lockTables.join(", ");
}
let onLockExpression = "";
if (this.expressionMap.onLocked === "nowait") {
onLockExpression = " NOWAIT";
}
else if (this.expressionMap.onLocked === "skip_locked") {
if (driver.options.type === "sap") {
onLockExpression = " IGNORE LOCKED";
}
else {
onLockExpression = " SKIP LOCKED";
}
}
switch (this.expressionMap.lockMode) {
case "pessimistic_read":
if (driver.options.type === "mysql" ||
driver.options.type === "aurora-mysql") {
if (DriverUtils_1.DriverUtils.isReleaseVersionOrGreater(driver, "8.0.0")) {
return (" FOR SHARE" + lockTablesClause + onLockExpression);
}
else {
return " LOCK IN SHARE MODE";
}
}
else if (driver.options.type === "mariadb") {
return " LOCK IN SHARE MODE";
}
else if (DriverUtils_1.DriverUtils.isPostgresFamily(driver)) {
return " FOR SHARE" + lockTablesClause + onLockExpression;
}
else if (driver.options.type === "sap") {
return (" FOR SHARE LOCK" + lockTablesClause + onLockExpression);
}
else if (driver.options.type === "oracle") {
return " FOR UPDATE";
}
else if (driver.options.type === "mssql") {
return "";
}
else {
throw new LockNotSupportedOnGivenDriverError_1.LockNotSupportedOnGivenDriverError();
}
case "pessimistic_write":
if (DriverUtils_1.DriverUtils.isMySQLFamily(driver) ||
driver.options.type === "aurora-mysql" ||
driver.options.type === "oracle") {
return " FOR UPDATE" + onLockExpression;
}
else if (DriverUtils_1.DriverUtils.isPostgresFamily(driver) ||
driver.options.type === "sap") {
return " FOR UPDATE" + lockTablesClause + onLockExpression;
}
else if (driver.options.type === "mssql") {
return "";
}
else {
throw new LockNotSupportedOnGivenDriverError_1.LockNotSupportedOnGivenDriverError();
}
case "for_no_key_update":
if (DriverUtils_1.DriverUtils.isPostgresFamily(driver)) {
return (" FOR NO KEY UPDATE" +
lockTablesClause +
onLockExpression);
}
else {
throw new LockNotSupportedOnGivenDriverError_1.LockNotSupportedOnGivenDriverError();
}
case "for_key_share":
if (DriverUtils_1.DriverUtils.isPostgresFamily(driver)) {
return (" FOR KEY SHARE" + lockTablesClause + onLockExpression);
}
else {
throw new LockNotSupportedOnGivenDriverError_1.LockNotSupportedOnGivenDriverError();
}
default:
return "";
}
}
/**
* Creates "HAVING" part of SQL query.
*/
createHavingExpression() {
if (!this.expressionMap.havings?.length)
return "";
const conditions = this.expressionMap.havings
.map((having, index) => {
switch (having.type) {
case "and":
return (index > 0 ? "AND " : "") + having.condition;
case "or":
return (index > 0 ? "OR " : "") + having.condition;
default:
return having.condition;
}
})
.join(" ");
if (!conditions.length)
return "";
return " HAVING " + conditions;
}
buildEscapedEntityColumnSelects(aliasName, metadata) {
const hasMainAlias = this.expressionMap.selects.some((select) => select.selection === aliasName);
const columns = [];
if (hasMainAlias) {
columns.push(...metadata.columns.filter((column) => column.isSelect === true));
}
const columnsMap = new Map(metadata.columns.map((col) => [
`${aliasName}.${col.propertyPath}`,
col,
]));
columns.push(...this.expressionMap.selects
.map((select) => columnsMap.get(select.selection))
.filter((col) => !!col));
// if user used partial selection and did not select some primary columns which are required to be selected
// we select those primary columns and mark them as "virtual". Later virtual column values will be removed from final entity
// to make entity contain exactly what user selected
if (columns.length === 0)
// however not in the case when nothing (even partial) was selected from this target (for example joins without selection)
return [];
const nonSelectedPrimaryColumns = this.expressionMap.queryEntity
? metadata.primaryColumns.filter((primaryColumn) => columns.indexOf(primaryColumn) === -1)
: [];
const allColumns = [...columns, ...nonSelectedPrimaryColumns];
const finalSelects = [];
allColumns.forEach((column) => {
const selectionPath = this.buildColumnSelectionExpression(aliasName, column);
const selections = this.expressionMap.selects.filter((select) => select.selection === aliasName + "." + column.propertyPath);
if (selections.length) {
selections.forEach((selection) => {
finalSelects.push({
selection: selectionPath,
aliasName: selection.aliasName ??
DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, aliasName, column.databaseName),
// todo: need to keep in mind that custom selection.aliasName breaks hydrator. fix it later!
virtual: selection.virtual,
});
});
}
else {
finalSelects.push({
selection: selectionPath,
aliasName: DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, aliasName, column.databaseName),
// todo: need to keep in mind that custom selection.aliasName breaks hydrator. fix it later!
virtual: hasMainAlias,
});
}
});
return finalSelects;
}
buildColumnSelectionExpression(aliasName, column, includeHydrationTransform = true) {
const escapedAliasName = this.escape(aliasName);
let selectionPath = escapedAliasName + "." + this.escape(column.databaseName);
if (column.isVirtualProperty && column.query) {
selectionPath = `(${column.query(escapedAliasName)})`;
}
if (DriverUtils_1.DriverUtils.isSQLiteFamily(this.dataSource.driver) &&
includeHydrationTransform) {
selectionPath = this.dataSource.driver.wrapWithJsonFunction(selectionPath, column, false);
}
if (this.dataSource.driver.spatialTypes.indexOf(column.type) !== -1) {
if ((DriverUtils_1.DriverUtils.isMySQLFamily(this.dataSource.driver) ||
this.dataSource.driver.options.type === "aurora-mysql") &&
includeHydrationTransform) {
const useLegacy = this.dataSource.driver.options.legacySpatialSupport;
const asText = useLegacy ? "AsText" : "ST_AsText";
selectionPath = `${asText}(${selectionPath})`;
}
if (DriverUtils_1.DriverUtils.isPostgresFamily(this.dataSource.driver) &&
includeHydrationTransform)
if (column.precision) {
// cast to JSON to trigger parsing in the driver
selectionPath = `ST_AsGeoJSON(${selectionPath}, ${column.precision})::json`;
}
else {
selectionPath = `ST_AsGeoJSON(${selectionPath})::json`;
}
if (this.dataSource.driver.options.type === "mssql")
selectionPath = `${selectionPath}.ToString()`;
}
return selectionPath;
}
findEntityColumnSelects(aliasName, metadata) {
return this.expressionMap.selects.filter((select) => select.selection === aliasName ||
metadata.columns.some((column) => select.selection ===
aliasName + "." + column.propertyPath));
}
computeCountExpression() {
const mainAlias = this.expressionMap.mainAlias.name; // todo: will this work with "fromTableName"?
const metadata = this.expressionMap.mainAlias.metadata;
const primaryColumns = metadata.primaryColumns;
const selectedColumns = [];
if (this.findOptions.select) {
selectedColumns.push(...this.computeSelectedColumnExpressions());
}
if (selectedColumns.length > 0) {
return this.computeDistinctCountExpression(selectedColumns);
}
// If we aren't doing anything that will create a join, we can use a simpler `COUNT` instead
// so we prevent poor query patterns in the most likely cases
if (this.expressionMap.joinAttributes.length === 0 &&
this.expressionMap.relationIdAttributes.length === 0) {
return "COUNT(1)";
}
// For everything else, we'll need to do some hackery to get the correct count values.
return this.computeDistinctCountExpression(primaryColumns.map((column) => `${this.escape(mainAlias)}.${this.escape(column.databaseName)}`));
}
computeSelectedColumnExpressions() {
const selectedColumns = new Set();
this.expressionMap.selects.forEach((select) => {
const criteriaParts = select.selection.split(".");
if (criteriaParts.length < 2)
return;
const aliasName = criteriaParts[0];
const propertyPath = criteriaParts.slice(1).join(".");
const alias = this.expressionMap.aliases.find((queryAlias) => queryAlias.name === aliasName && queryAlias.hasMetadata);
if (!alias)
return;
const column = alias.metadata.findColumnWithPropertyPath(propertyPath);
if (!column)
return;
selectedColumns.add(this.buildColumnSelectionExpression(aliasName, column, false));
});
return Array.from(selectedColumns);
}
computeDistinctCountExpression(columns) {
if (this.dataSource.driver.options.type === "cockroachdb" ||
DriverUtils_1.DriverUtils.isPostgresFamily(this.dataSource.driver)) {
// Postgres and CockroachDB can pass multiple parameters to the `DISTINCT` function
// https://www.postgresql.org/docs/9.5/sql-select.html#SQL-DISTINCT
return "COUNT(DISTINCT(" + columns.join(", ") + "))";
}
if (DriverUtils_1.DriverUtils.isMySQLFamily(this.dataSource.driver)) {
// MySQL & MariaDB can pass multiple parameters to the `DISTINCT` language construct
// https://mariadb.com/kb/en/count-distinct/
return "COUNT(DISTINCT " + columns.join(", ") + ")";
}
if (this.dataSource.driver.options.type === "mssql") {
// SQL Server has gotta be different from everyone else. They don't support
// distinct counting multiple columns & they don't have the same operator
// characteristic for concatenating, so we gotta use the `CONCAT` function.
// However, If it's exactly 1 column we can omit the `CONCAT` for better performance.
const columnsExpression = columns.join(", '|;|', ");
if (columns.length === 1) {
return `COUNT(DISTINCT(${columnsExpression}))`;
}
return `COUNT(DISTINCT(CONCAT(${columnsExpression})))`;
}
if (this.dataSource.driver.options.type === "spanner") {
// spanner also has gotta be different from everyone else.
// they do not support concatenation of different column types without casting them to string
if (columns.length === 1) {
return `COUNT(DISTINCT(${columns[0]}))`;
}
const columnsExpression = columns
.map((column) => `CAST(${column} AS STRING)`)
.join(", '|;|', ");
return `COUNT(DISTINCT(CONCAT(${columnsExpression})))`;
}
// If all else fails, fall back to a `COUNT` and `DISTINCT` across all the primary columns concatenated.
// Per the SQL spec, this is the canonical string concatenation mechanism which is most
// likely to work across servers implementing the SQL standard.
// Please note, if there is only one primary column that the concatenation does not occur in this
// query and the query is a standard `COUNT DISTINCT` in that case.
return `COUNT(DISTINCT(` + columns.join(" || '|;|' || ") + "))";
}
async executeCountQuery(queryRunner) {
const countSql = this.computeCountExpression();
const results = await this.clone()
.orderBy()
.groupBy()
.offset(undefined)
.limit(undefined)
.skip(undefined)
.take(undefined)
.select(countSql, "cnt")
.setOption("disable-global-order")
.loadRawResults(queryRunner);
if (!results?.[0]?.["cnt"])
return 0;
return parseInt(results[0]["cnt"]);
}
async executeExistsQuery(queryRunner) {
const results = await this.dataSource
.createQueryBuilder()
.fromDummy()
.select("1", "row_exists")
.whereExists(this)
.limit(1)
.loadRawResults(queryRunner);
return results.length > 0;
}
applyFindOptions() {
// todo: convert relations: string[] to object map to simplify code
// todo: same with selects
if (this.expressionMap.mainAlias.metadata) {
if (this.findOptions.relationLoadStrategy) {
this.expressionMap.relationLoadStrategy =
this.findOptions.relationLoadStrategy;
}
if (this.findOptions.comment) {
this.comment(this.findOptions.comment);
}
if (this.findOptions.withDeleted) {
this.withDeleted();
}
if (this.findOptions.select) {
this.buildSelect(this.findOptions.select, this.expressionMap.mainAlias.metadata, this.expressionMap.mainAlias.name);
}
if (this.selects.length) {
this.select(this.selects);
}
this.selects = [];
if (this.findOptions.relations) {
this.buildRelations(this.findOptions.relations, this.findOptions.select, this.expressionMap.mainAlias.metadata, this.expressionMap.mainAlias.name);
if (this.findOptions.loadEagerRelations !== false &&
this.expressionMap.relationLoadStrategy === "join") {
this.buildEagerRelations(this.findOptions.relations, this.findOptions.select, this.expressionMap.mainAlias.metadata, this.expressionMap.mainAlias.name);
}
}
if (this.selects.length) {
this.addSelect(this.selects);
}
if (this.findOptions.where) {
this.conditions = this.buildWhere(this.findOptions.where, this.expressionMap.mainAlias.metadata, this.expressionMap.mainAlias.name);
if (this.conditions.length)
this.andWhere(this.conditions.startsWith("(")
? this.conditions
: `(${this.conditions})`); // temporary and where and braces
}
if (this.findOptions.order) {
this.buildOrder(this.findOptions.order, this.expressionMap.mainAlias.metadata, this.expressionMap.mainAlias.name);
}
// apply joins
if (this.joins.length) {
this.joins.forEach((join) => {
if (join.select && !join.selection) {
// if (join.selection) {
//
// } else {
if (join.type === "inner") {
this.innerJoinAndSelect(`${join.parentAlias}.${join.relationMetadata.propertyPath}`, join.alias);
}
else {
this.leftJoinAndSelect(`${join.parentAlias}.${join.relationMetadata.propertyPath}`, join.alias);
}
// }
}
else {
if (join.type === "inner") {
this.innerJoin(`${join.parentAlias}.${join.relationMetadata.propertyPath}`, join.alias);
}
else {
this.leftJoin(`${join.parentAlias}.${join.relationMetadata.propertyPath}`, join.alias);
}
}
// if (join.select) {
// if (this.findOptions.loadEagerRelations !== false) {
// FindOptionsUtils.joinEagerRelations(
// this,
// join.alias,
// join.relationMetadata.inverseEntityMetadata
// );
// }
// }
});
}
// if (this.conditions.length) {
// this.where(this.conditions.join(" AND "));
// }
// apply offset
if (this.findOptions.skip !== undefined) {
// if (this.findOptions.options && this.findOptions.options.pagination === false) {
// this.offset(this.findOptions.skip);
// } else {
this.skip(this.findOptions.skip);
// }
}
// apply limit
if (this.findOptions.take !== undefined) {
// if (this.findOptions.options && this.findOptions.options.pagination === false) {
// this.limit(this.findOptions.take);
// } else {
this.take(this.findOptions.take);
// }
}
// apply caching options
if (typeof this.findOptions.cache === "number") {
this.cache(this.findOptions.cache);
}
else if (typeof this.findOptions.cache === "boolean") {
this.cache(this.findOptions.cache);
}
else if (typeof this.findOptions.cache === "object") {
this.cache(this.findOptions.cache.id, this.findOptions.cache.milliseconds);
}
if (this.findOptions.lock) {
if (this.findOptions.lock.mode === "optimistic") {
this.setLock(this.findOptions.lock.mode, this.findOptions.lock.version);
}
else if (this.findOptions.lock.mode === "pessimistic_read" ||
this.findOptions.lock.mode === "pessimistic_write" ||
this.findOptions.lock.mode === "dirty_read" ||
this.findOptions.lock.mode === "for_no_key_update" ||
this.findOptions.lock.mode === "for_key_share") {
const tableNames = this.findOptions.lock.tables
? this.findOptions.lock.tables.map((table) => {
const tableAlias = this.expressionMap.aliases.find((alias) => {
return (alias.metadata
.tableNameWithoutPrefix === table);
});
if (!tableAlias) {
throw new error_1.TypeORMError(`"${table}" is not part of this query`);
}
return this.escape(tableAlias.name);
})
: undefined;
this.setLock(this.findOptions.lock.mode, undefined, tableNames);
if (this.findOptions.lock.onLocked) {
this.setOnLocked(this.findOptions.lock.onLocked);
}
}
}
if (this.findOptions.loadRelationIds === true) {
this.loadAllRelationIds();
}
else if (typeof this.findOptions.loadRelationIds === "object") {
this.loadAllRelationIds(this.findOptions.loadRelationIds);
}
if (this.findOptions.loadEagerRelations !== false &&
this.expressionMap.mainAlias) {
if (this.expressionMap.relationLoadStrategy === "join") {
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(this, this.expressionMap.mainAlias.name, this.expressionMap.mainAlias.metadata);
}
else if (this.expressionMap.relationLoadStrategy === "query") {
this.concatRelationMetadata(...this.expressionMap.mainAlias.metadata.eagerRelations);
}
}
if (this.findOptions.transaction === true) {
this.expressionMap.useTransaction = true;
}
}
}
/**
* Registers relation metadata for loading via separate queries when using
* the "query" relation load strategy. Duplicates (by propertyPath) are
* silently skipped.
*
* @param relationMetadata - one or more relation metadata entries to register
*/
concatRelationMetadata(...relationMetadata) {
const newRelationMetadata = relationMetadata.filter((metadata) => !this.relationMetadatas.some((existentMetadata) => existentMetadata.propertyPath === metadata.propertyPath));
this.relationMetadatas.push(...newRelationMetadata);
}
/**
* Executes sql generated by query builder and returns object with raw results and entities created from them.
*
* @param queryRunner
*/
async executeEntitiesAndRawResults(queryRunner) {
if (!this.expressionMap.mainAlias)
throw new error_1.TypeORMError(`Alias is not set. Use "from" method to set an alias.`);
if ((this.expressionMap.lockMode === "pessimistic_read" ||
this.expressionMap.lockMode === "pessimistic_write" ||
this.expressionMap.lockMode === "for_no_key_update" ||
this.expressionMap.lockMode === "for_key_share") &&
!queryRunner.isTransactionActive)
throw new PessimisticLockTransactionRequiredError_1.PessimisticLockTransactionRequiredError();
if (this.expressionMap.lockMode === "optimistic") {
const metadata = this.expressionMap.mainAlias.metadata;
if (!metadata.versionColumn && !metadata.updateDateColumn)
throw new NoVersionOrUpdateDateColumnError_1.NoVersionOrUpdateDateColumnError(metadata.name);
}
const relationIdLoader = new RelationIdLoader_1.RelationIdLoader(this.dataSource, queryRunner, this.expressionMap.relationIdAttributes, this.expressionMap.withDeleted);
const relationIdMetadataTransformer = new RelationIdMetadataToAttributeTransformer_1.RelationIdMetadataToAttributeTransformer(this.expressionMap);
relationIdMetadataTransformer.transform();
let rawResults, entities = [];
// for pagination enabled (e.g. skip and take) its much more complicated - its a special process
// where we make two queries to find the data we need
// first query find ids in skip and take range
// and second query loads the actual data in given ids range
if ((this.expressionMap.skip || this.expressionMap.take) &&
this.expressionMap.joinAttributes.length > 0) {
// we are skipping order by here because its not working in subqueries anyway
// to make order by working we need to apply it on a distinct query
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
const metadata = this.expressionMap.mainAlias.metadata;
const mainAliasName = this.expressionMap.mainAlias.name;
const querySelects = metadata.primaryColumns.map((primaryColumn) => {
const distinctAlias = this.escape("distinctAlias");
const columnAlias = this.escape(DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, mainAliasName, primaryColumn.databaseName));
if (!orderBys[columnAlias])
// make sure we aren't overriding user-defined order in inverse direction
orderBys[columnAlias] = "ASC";
const alias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, "ids_" + mainAliasName, primaryColumn.databaseName);
return `${distinctAlias}.${columnAlias} AS ${this.escape(alias)}`;
});
const originalQuery = this.clone();
// preserve original timeTravel value since we set it to "false" in subquery
const originalQueryTimeTravel = originalQuery.expressionMap.timeTravel;
const paginationQueryBuilder = new SelectQueryBuilder(this.dataSource, queryRunner)
.select(`DISTINCT ${querySelects.join(", ")}`)
.addSelect(selects)
.from(`(${originalQuery
.orderBy()
.timeTravelQuery(false) // set it to "false" since time travel clause must appear at the very end and applies to the entire SELECT clause.
.getQuery()})`, "distinctAlias")
.timeTravelQuery(originalQueryTimeTravel)
.offset(this.expressionMap.skip)
.limit(this.expressionMap.take)
.orderBy(orderBys)
.cache(this.expressionMap.cache && this.expressionMap.cacheId
? `${this.expressionMap.cacheId}-pagination`
: this.expressionMap.cache, this.expressionMap.cacheDuration)
.setParameters(this.getParameters());
rawResults = await paginationQueryBuilder.getRawMany();
if (rawResults.length > 0) {
let condition;
const parameters = {};
if (metadata.hasMultiplePrimaryKeys) {
condition = rawResults
.map((result, index) => {
return metadata.primaryColumns
.map((primaryColumn) => {
const paramKey = `orm_distinct_ids_${index}_${primaryColumn.databaseName}`;
const paramKeyResult = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, "ids_" + mainAliasName, primaryColumn.databaseName);
parameters[paramKey] =
result[paramKeyResult];
return `${mainAliasName}.${primaryColumn.propertyPath}=:${paramKey}`;
})
.join(" AND ");
})
.join(" OR ");
}
else {
const alias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, "ids_" + mainAliasName, metadata.primaryColumns[0].databaseName);
const ids = rawResults.map((result) => result[alias]);
const areAllNumbers = ids.every((id) => typeof id === "number");
if (areAllNumbers) {
// fixes #190. if all numbers then its safe to perform query without parameter
condition = `${mainAliasName}.${metadata.primaryColumns[0].propertyPath} IN (${ids.join(", ")})`;
}
else {
parameters["orm_distinct_ids"] = ids;
condition =
mainAliasName +
"." +
metadata.primaryColumns[0].propertyPath +
" IN (:...orm_distinct_ids)";
}
}
rawResults = await this.clone()
.mergeExpressionMap({
extraAppendedAndWhereCondition: condition,
})
.setParameters(parameters)
.loadRawResults(queryRunner);
}
}
else {
rawResults = await this.loadRawResults(queryRunner);
}
if (rawResults.length > 0) {
// transform raw results into entities
const rawRelationIdResults = await relationIdLoader.load(rawResults);
const transformer = new RawSqlResultsToEntityTransformer_1.RawSqlResultsToEntityTransformer(this.expressionMap, this.dataSource.driver, rawRelationIdResults, this.queryRunner);
entities = transformer.transform(rawResults, this.expressionMap.mainAlias);
// broadcast all "after load" events
if (this.expressionMap.callListeners === true &&
this.expressionMap.mainAlias.hasMetadata) {
await queryRunner.broadcaster.broadcast("Load", this.expressionMap.mainAlias.metadata, entities);
}
}
if (this.expressionMap.relationLoadStrategy === "query") {
const queryStrategyRelationIdLoader = new RelationIdLoader_2.RelationIdLoader(this.dataSource, queryRunner, this.findOptions.loadEagerRelations);
const loadRelation = async (relation) => {
// Prevent infinite recursion from circular eager
// chains (e.g. A→B→C→A). Each branch maintains its
// own visited set so parallel branches don't interfere.
if (relation.isEager) {
const targetEntity = relation.inverseEntityMetadata.tablePath;
if (this.eagerLoadChain.has(targetEntity))
return;
}
const relationTarget = relation.inverseEntityMetadata.target;
const relationAlias = relation.inverseEntityMetadata.targetName;
const queryBuilder = this.createQueryBuilder(queryRunner)
.select(relationAlias)
.from(relationTarget, relationAlias);
// Propagate eager load chain with current entity
// added, so the child detects cycles in its branch
if (relation.isEager) {
queryBuilder.eagerLoadChain = new Set(this.eagerLoadChain);
queryBuilder.eagerLoadChain.add(relation.entityMetadata.tablePath);
}
queryBuilder.setFindOptions({
select: this.findOptions.select
? OrmUtils_1.OrmUtils.deepValue(this.findOptions.select, relation.propertyPath)
: undefined,
order: this.findOptions.order
? OrmUtils_1.OrmUtils.deepValue(this.findOptions.order, relation.propertyPath)
: undefined,
relations: this.findOptions.relations
? OrmUtils_1.OrmUtils.deepValue(this.findOptions.relations, relation.propertyPath)
: undefined,
withDeleted: this.findOptions.withDeleted,
relationLoadStrategy: this.findOptions.relationLoadStrategy,
loadEagerRelations: this.findOptions.loadEagerRelations,
});
if (entities.length > 0) {
const relatedEntityGroups = await queryStrategyRelationIdLoader.loadManyToManyRelationIdsAndGroup(relation, entities, undefined, queryBuilder);
entities.forEach((entity) => {
const relatedEntityGroup = relatedEntityGroups.find((group) => group.entity === entity);
if (relatedEntityGroup) {
const value = relatedEntityGroup.related === undefined
? null
: relatedEntityGroup.related;
relation.setEntityValue(entity, value);
}
});
}
};
// Avoid concurrent queries on the same pg client; see #12238.
// CockroachDB uses the pg package over a single connection too.
const driverType = this.dataSource.options.type;
if (driverType === "postgres" || driverType === "cockroachdb") {
for (const relation of this.relationMetadatas) {
await loadRelation(relation);
}
}
else {
await Promise.all(this.relationMetadatas.map(loadRelation));
}
}
return {
raw: rawResults,
entities: entities,
};
}
createOrderByCombinedWithSelectExpression(parentAlias) {
// if table has a default order then apply it
const orderBys = this.expressionMap.allOrderBys;
const selectString = Object.keys(orderBys)
.map((orderCriteria) => {
if (orderCriteria.indexOf(".") !== -1) {
const criteriaParts = orderCriteria.split(".");
const aliasName = criteriaParts[0];
const propertyPath = criteriaParts.slice(1).join(".");
const alias = this.expressionMap.findAliasByName(aliasName);
const column = alias.metadata.findColumnWithPropertyPath(propertyPath) ??
alias.metadata.findColumnWithDatabaseName(propertyPath);
const databaseName = column
? column.databaseName
: propertyPath;
return (this.escape(parentAlias) +
"." +
this.escape(DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, aliasName, databaseName)));
}
else {
if (this.expressionMap.selects.find((select) => select.selection === orderCriteria ||
select.aliasName === orderCriteria))
return (this.escape(parentAlias) +
"." +
this.escape(orderCriteria));
return "";
}
})
.join(", ");
const orderByObject = {};
Object.keys(orderBys).forEach((orderCriteria) => {
if (orderCriteria.indexOf(".") !== -1) {
const criteriaParts = orderCriteria.split(".");
const aliasName = criteriaParts[0];
const propertyPath = criteriaParts.slice(1).join(".");
const alias = this.expressionMap.findAliasByName(aliasName);
const column = alias.metadata.findColumnWithPropertyPath(propertyPath) ??
alias.metadata.findColumnWithDatabaseName(propertyPath);
const databaseName = column ? column.databaseName : propertyPath;
orderByObject[this.escape(parentAlias) +
"." +
this.escape(DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, aliasName, databaseName))] = orderBys[orderCriteria];
}
else {
if (this.expressionMap.selects.find((select) => select.selection === orderCriteria ||
select.aliasName === orderCriteria)) {
orderByObject[this.escape(parentAlias) +
"." +
this.escape(orderCriteria)] = orderBys[orderCriteria];
}
else {
orderByObject[orderCriteria] = orderBys[orderCriteria];
}
}
});
return [selectString, orderByObject];
}
/**
* Loads raw results from the database.
*
* @param queryRunner
*/
async loadRawResults(queryRunner) {
const [sql, parameters] = this.getQueryAndParameters();
const queryId = sql +
" -- PARAMETERS: " +
JSON.stringify(parameters, (_, value) => typeof value === "bigint" ? value.toString() : value);
const cacheOptions = typeof this.dataSource.options.cache === "object"
? this.dataSource.options.cache
: {};
let savedQueryResultCacheOptions = undefined;
const isCachingEnabled = cacheOptions.alwaysEnabled
? this.expressionMap.cache !== false
: this.expressionMap.cache === true;
let cacheError = false;
if (this.dataSource.queryResultCache && isCachingEnabled) {
try {
savedQueryResultCacheOptions =
await this.dataSource.queryResultCache.getFromCache({
identifier: this.expressionMap.cacheId,
query: queryId,
duration: this.expressionMap.cacheDuration ||
(cacheOptions.duration ?? 1000),
}, queryRunner);
if (savedQueryResultCacheOptions &&
!this.dataSource.queryResultCache.isExpired(savedQueryResultCacheOptions)) {
return JSON.parse(savedQueryResultCacheOptions.result);
}
}
catch (error) {
if (!cacheOptions.ignoreErrors) {
throw error;
}
cacheError = true;
}
}
const results = await queryRunner.query(sql, parameters, true);
if (!cacheError &&
this.dataSource.queryResultCache &&
isCachingEnabled) {
try {
await this.dataSource.queryResultCache.storeInCache({
identifier: this.expressionMap.cacheId,
query: queryId,
time: Date.now(),
duration: this.expressionMap.cacheDuration ||
(cacheOptions.duration ?? 1000),
result: JSON.stringify(results.records),
}, savedQueryResultCacheOptions, queryRunner);
}
catch (error) {
if (!cacheOptions.ignoreErrors) {
throw error;
}
}
}
return results.records;
}
/**
* Merges into expression map given expression map properties.
*
* @param expressionMap
*/
mergeExpressionMap(expressionMap) {
ObjectUtils_1.ObjectUtils.assign(this.expressionMap, expressionMap);
return this;
}
/**
* Creates a query builder used to execute sql queries inside this query builder.
*/
obtainQueryRunner() {
return (this.queryRunner ??
this.dataSource.createQueryRunner(this.dataSource.defaultReplicationModeForReads()));
}
buildSelect(select, metadata, alias, embedPrefix) {
for (const key in select) {
if (select[key] === undefined || select[key] === false)
continue;
const propertyPath = embedPrefix ? embedPrefix + "." + key : key;
const column = metadata.findColumnWithPropertyPathStrict(propertyPath);
const embed = metadata.findEmbeddedWithPropertyPath(propertyPath);
const relation = metadata.findRelationWithPropertyPath(propertyPath);
if (!embed && !column && !relation)
throw new EntityPropertyNotFoundError_1.EntityPropertyNotFoundError(propertyPath, metadata);
if (column) {
this.selects.push(alias + "." + propertyPath);
// this.addSelect(alias + "." + propertyPath);
}
else if (embed) {
this.buildSelect(select[key], metadata, alias, propertyPath);
// } else if (relation) {
// const joinAlias = alias + "_" + relation.propertyName;
// const existJoin = this.joins.find(join => join.alias === joinAlias);
// if (!existJoin) {
// this.joins.push({
// type: "left",
// select: false,
// alias: joinAlias,
// parentAlias: alias,
// relationMetadata: relation
// });
// }
// this.buildOrder(select[key] as FindOptionsOrder<any>, relation.inverseEntityMetadata, joinAlias);
}
}
}
getRelationJoinType(relation, parentJoinType = "inner") {
return FindOptionsUtils_1.FindOptionsUtils.getRelationJoinType(relation, this.expressionMap.withDeleted, parentJoinType);
}
buildRelations(relations, selection, metadata, alias, embedPrefix, parentJoinType = "inner") {
if (!relations)
return;
Object.keys(relations).forEach((relationName) => {
const relationValue = relations[relationName];
const propertyPath = embedPrefix
? embedPrefix + "." + relationName
: relationName;
const embed = metadata.findEmbeddedWithPropertyPath(propertyPath);
const relation = metadata.findRelationWithPropertyPath(propertyPath);
if (!embed && !relation)
throw new EntityPropertyNotFoundError_1.EntityPropertyNotFoundError(propertyPath, metadata);
if (embed) {
this.buildRelations(relationValue, typeof selection === "object"
? OrmUtils_1.OrmUtils.deepValue(selection, embed.propertyPath)
: undefined, metadata, alias, propertyPath);
}
else if (relation) {
let joinAlias = alias + "_" + propertyPath.replace(".", "_");
joinAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { joiner: "__" }, alias, joinAlias);
const joinType = this.getRelationJoinType(relation, parentJoinType);
if (relationValue === true ||
typeof relationValue === "object") {
if (this.expressionMap.relationLoadStrategy === "query") {
this.concatRelationMetadata(relation);
}
else {
this.joins.push({
type: joinType,
select: true,
selection: selection &&
typeof selection[relationName] === "object"
? selection[relationName]
: undefined,
alias: joinAlias,
parentAlias: alias,
relationMetadata: relation,
});
if (selection &&
typeof selection[relationName] === "object") {
this.buildSelect(selection[relationName], relation.inverseEntityMetadata, joinAlias);
}
}
}
if (typeof relationValue === "object" &&
this.expressionMap.relationLoadStrategy === "join") {
this.buildRelations(relationValue, typeof selection === "object"
? OrmUtils_1.OrmUtils.deepValue(selection, relation.propertyPath)
: undefined, relation.inverseEntityMetadata, joinAlias, undefined, joinType);
}
}
});
}
buildEagerRelations(relations, selection, metadata, alias, embedPrefix) {
if (!relations)
return;
Object.keys(relations).forEach((relationName) => {
const relationValue = relations[relationName];
const propertyPath = embedPrefix
? embedPrefix + "." + relationName
: relationName;
const embed = metadata.findEmbeddedWithPropertyPath(propertyPath);
const relation = metadata.findRelationWithPropertyPath(propertyPath);
if (!embed && !relation)
throw new EntityPropertyNotFoundError_1.EntityPropertyNotFoundError(propertyPath, metadata);
if (embed) {
this.buildEagerRelations(relationValue, typeof selection === "object"
? OrmUtils_1.OrmUtils.deepValue(selection, embed.propertyPath)
: undefined, metadata, alias, propertyPath);
}
else if (relation) {
let joinAlias = alias + "_" + propertyPath.replace(".", "_");
joinAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { joiner: "__" }, alias, joinAlias);
if ((relationValue === true ||
typeof relationValue === "object") &&
this.expressionMap.relationLoadStrategy === "join") {
// Determine this relation's join type to propagate to eager children
const parentJoin = this.joins.find((j) => j.alias === joinAlias);
const parentJoinType = parentJoin?.type ?? "inner";
relation.inverseEntityMetadata.eagerRelations.forEach((eagerRelation) => {
let eagerRelationJoinAlias = joinAlias +
"_" +
eagerRelation.propertyPath.replace(".", "_");
eagerRelationJoinAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { joiner: "__" }, joinAlias, eagerRelationJoinAlias);
const existJoin = this.joins.find((join) => join.alias === eagerRelationJoinAlias);
if (!existJoin) {
this.joins.push({
type: this.getRelationJoinType(eagerRelation, parentJoinType),
select: true,
alias: eagerRelationJoinAlias,
parentAlias: joinAlias,
selection: undefined,
relationMetadata: eagerRelation,
});
}
if (selection &&
typeof selection[relationName] === "object") {
this.buildSelect(selection[relationName], relation.inverseEntityMetadata, joinAlias);
}
});
}
if (typeof relationValue === "object") {
this.buildEagerRelations(relationValue, typeof selection === "object"
? OrmUtils_1.OrmUtils.deepValue(selection, relation.propertyPath)
: undefined, relation.inverseEntityMetadata, joinAlias, undefined);
}
}
});
}
buildOrder(order, metadata, alias, embedPrefix) {
for (const key in order) {
if (order[key] === undefined)
continue;
const propertyPath = embedPrefix ? embedPrefix + "." + key : key;
const column = metadata.findColumnWithPropertyPathStrict(propertyPath);
const embed = metadata.findEmbeddedWithPropertyPath(propertyPath);
const relation = metadata.findRelationWithPropertyPath(propertyPath);
if (!embed && !column && !relation)
throw new EntityPropertyNotFoundError_1.EntityPropertyNotFoundError(propertyPath, metadata);
if (column) {
let direction = typeof order[key] === "object"
? order[key].direction
: order[key];
direction =
direction === "DESC" ||
direction === "desc" ||
direction === -1
? "DESC"
: "ASC";
let nulls = typeof order[key] === "object"
? order[key].nulls
: undefined;
nulls =
nulls?.toLowerCase() === "first"
? "NULLS FIRST"
: nulls?.toLowerCase() === "last"
? "NULLS LAST"
: undefined;
const aliasPath = `${alias}.${propertyPath}`;
// const selection = this.expressionMap.selects.find(
// (s) => s.selection === aliasPath,
// )
// if (selection) {
// // this is not building correctly now???
// aliasPath = this.escape(
// DriverUtils.buildAlias(
// this.dataSource.driver,
// undefined,
// alias,
// column.databaseName,
// ),
// )
// // selection.aliasName = aliasPath
// } else {
// if (column.isVirtualProperty && column.query) {
// aliasPath = `(${column.query(alias)})`
// }
// }
// console.log("add sort", selection, aliasPath, direction, nulls)
this.addOrderBy(aliasPath, direction, nulls);
// this.orderBys.push({ alias: alias + "." + propertyPath, direction, nulls });
}
else if (embed) {
this.buildOrder(order[key], metadata, alias, propertyPath);
}
else if (relation) {
let joinAlias = alias + "_" + propertyPath.replace(".", "_");
joinAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { joiner: "__" }, alias, joinAlias);
const existJoin = this.joins.find((join) => join.alias === joinAlias);
if (!existJoin) {
const parentJoin = this.joins.find((j) => j.alias === alias);
this.joins.push({
type: this.getRelationJoinType(relation, parentJoin?.type ?? "inner"),
select: false,
alias: joinAlias,
parentAlias: alias,
selection: undefined,
relationMetadata: relation,
});
}
this.buildOrder(order[key], relation.inverseEntityMetadata, joinAlias);
}
}
}
buildWhere(where, metadata, alias, embedPrefix) {
let condition = "";
if (Array.isArray(where)) {
if (where.length) {
condition = where
.map((whereItem) => {
return this.buildWhere(whereItem, metadata, alias, embedPrefix);
})
.filter((condition) => !!condition)
.map((condition) => "(" + condition + ")")
.join(" OR ");
}
}
else {
const andConditions = [];
for (const key in where) {
let parameterValue = where[key];
const propertyPath = embedPrefix ? embedPrefix + "." + key : key;
const column = metadata.findColumnWithPropertyPathStrict(propertyPath);
const embed = metadata.findEmbeddedWithPropertyPath(propertyPath);
const relation = metadata.findRelationWithPropertyPath(propertyPath);
if (!embed && !column && !relation) {
throw new EntityPropertyNotFoundError_1.EntityPropertyNotFoundError(propertyPath, metadata);
}
if (parameterValue === undefined) {
const undefinedBehavior = this.dataSource.options.invalidWhereValuesBehavior
?.undefined ?? "throw";
if (undefinedBehavior === "throw") {
throw new error_1.TypeORMError(`Undefined value encountered in property '${alias}.${key}' of a where condition. ` +
`Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.`);
}
continue;
}
if (parameterValue === null) {
const nullBehavior = this.dataSource.options.invalidWhereValuesBehavior
?.null ?? "throw";
if (nullBehavior === "ignore") {
continue;
}
else if (nullBehavior === "throw") {
throw new error_1.TypeORMError(`Null value encountered in property '${alias}.${key}' of a where condition. ` +
`To match with SQL NULL, the IsNull() operator must be used. ` +
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`);
}
// 'sql-null' behavior continues to the next logic
}
if (column) {
let aliasPath = `${alias}.${propertyPath}`;
if (column.isVirtualProperty && column.query) {
aliasPath = `(${column.query(this.escape(alias))})`;
}
if (parameterValue === null) {
andConditions.push(`${aliasPath} IS NULL`);
continue;
}
// const parameterName = alias + "_" + propertyPath.split(".").join("_") + "_" + parameterIndex;
// todo: we need to handle other operators as well?
if (InstanceChecker_1.InstanceChecker.isEqualOperator(where[key])) {
parameterValue = where[key].value;
}
if (column.transformer) {
parameterValue = ApplyValueTransformers_1.ApplyValueTransformers.transformTo(column.transformer, parameterValue);
}
// MSSQL requires parameters to carry extra type information
if (this.dataSource.driver.options.type === "mssql") {
parameterValue = this.dataSource.driver.parametrizeValues(column, parameterValue);
}
andConditions.push(this.createWhereConditionExpression(this.getWherePredicateCondition(aliasPath, parameterValue)));
}
else if (embed) {
const condition = this.buildWhere(where[key], metadata, alias, propertyPath);
if (condition)
andConditions.push(condition);
}
else if (relation) {
if (where[key] === null) {
const nullBehavior = this.dataSource.options.invalidWhereValuesBehavior
?.null ?? "throw";
if (nullBehavior === "sql-null") {
andConditions.push(`${alias}.${propertyPath} IS NULL`);
}
else if (nullBehavior === "throw") {
throw new error_1.TypeORMError(`Null value encountered in property '${alias}.${key}' of a where condition. ` +
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`);
}
// 'ignore' behavior falls through to continue
continue;
}
// if all properties of where are undefined we don't need to join anything
// this can happen when user defines map with conditional queries inside
if (typeof where[key] === "object") {
const whereKeys = Object.keys(where[key]);
// empty object — no predicates to apply, skip the join
if (whereKeys.length === 0) {
continue;
}
const allUndefined = whereKeys.every((k) => where[key][k] === undefined);
if (allUndefined) {
const undefinedBehavior = this.dataSource.options
.invalidWhereValuesBehavior?.undefined ??
"throw";
if (undefinedBehavior === "throw") {
throw new error_1.TypeORMError(`Undefined value encountered in nested relation '${alias}.${key}' of a where condition. ` +
`All properties of the nested object are undefined. ` +
`Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.`);
}
continue;
}
}
if (InstanceChecker_1.InstanceChecker.isFindOperator(where[key])) {
if (where[key].type === "moreThan" ||
where[key].type === "lessThan" ||
where[key].type === "moreThanOrEqual" ||
where[key].type === "lessThanOrEqual") {
let sqlOperator = "";
if (where[key].type === "moreThan") {
sqlOperator = ">";
}
else if (where[key].type === "lessThan") {
sqlOperator = "<";
}
else if (where[key].type === "moreThanOrEqual") {
sqlOperator = ">=";
}
else if (where[key].type === "lessThanOrEqual") {
sqlOperator = "<=";
}
// basically relation count functionality
const qb = this.subQuery();
if (relation.isManyToManyOwner) {
qb.select("COUNT(*)")
.from(relation.joinTableName, relation.joinTableName)
.where(relation.joinColumns
.map((column) => {
return `${relation.joinTableName}.${column.propertyName} = ${alias}.${column.referencedColumn
.propertyName}`;
})
.join(" AND "));
}
else if (relation.isManyToManyNotOwner) {
qb.select("COUNT(*)")
.from(relation.inverseRelation.joinTableName, relation.inverseRelation.joinTableName)
.where(relation
.inverseRelation.inverseJoinColumns.map((column) => {
return `${relation
.inverseRelation
.joinTableName}.${column.propertyName} = ${alias}.${column.referencedColumn
.propertyName}`;
})
.join(" AND "));
}
else if (relation.isOneToMany) {
qb.select("COUNT(*)")
.from(relation.inverseEntityMetadata.target, relation.inverseEntityMetadata
.tableName)
.where(relation
.inverseRelation.joinColumns.map((column) => {
return `${relation
.inverseEntityMetadata
.tableName}.${column.propertyName} = ${alias}.${column.referencedColumn
.propertyName}`;
})
.join(" AND "));
}
else {
throw new Error(`This relation isn't supported by given find operator`);
}
// this
// .addSelect(qb.getSql(), relation.propertyAliasName + "_cnt")
// .andWhere(this.escape(relation.propertyAliasName + "_cnt") + " " + sqlOperator + " " + parseInt(where[key].value));
this.andWhere(qb.getSql() +
" " +
sqlOperator +
" " +
parseInt(where[key].value));
}
else {
if (relation.isManyToOne ||
(relation.isOneToOne &&
relation.isOneToOneOwner)) {
const aliasPath = `${alias}.${propertyPath}`;
andConditions.push(this.createWhereConditionExpression(this.getWherePredicateCondition(aliasPath, where[key])));
}
else {
throw new Error(`This relation isn't supported by given find operator`);
}
}
}
else {
// const joinAlias = alias + "_" + relation.propertyName;
let joinAlias = alias +
"_" +
relation.propertyPath.replace(".", "_");
joinAlias = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { joiner: "__" }, alias, joinAlias);
const existJoin = this.joins.find((join) => join.alias === joinAlias);
if (!existJoin) {
const parentJoin = this.joins.find((j) => j.alias === alias);
this.joins.push({
type: this.getRelationJoinType(relation, parentJoin?.type ?? "inner"),
select: false,
selection: undefined,
alias: joinAlias,
parentAlias: alias,
relationMetadata: relation,
});
}
const condition = this.buildWhere(where[key], relation.inverseEntityMetadata, joinAlias);
if (condition) {
andConditions.push(condition);
}
}
}
}
condition = andConditions.length
? "(" + andConditions.join(") AND (") + ")"
: andConditions.join(" AND ");
}
return condition.length ? "(" + condition + ")" : condition;
}
}
exports.SelectQueryBuilder = SelectQueryBuilder;
//# sourceMappingURL=SelectQueryBuilder.js.map