typeorm
Version:
Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB databases.
1,200 lines • 124 kB
JavaScript
import { RawSqlResultsToEntityTransformer } from "./transformer/RawSqlResultsToEntityTransformer";
import { PessimisticLockTransactionRequiredError } from "../error/PessimisticLockTransactionRequiredError";
import { NoVersionOrUpdateDateColumnError } from "../error/NoVersionOrUpdateDateColumnError";
import { OptimisticLockVersionMismatchError } from "../error/OptimisticLockVersionMismatchError";
import { OptimisticLockCanNotBeUsedError } from "../error/OptimisticLockCanNotBeUsedError";
import { JoinAttribute } from "./JoinAttribute";
import { RelationIdAttribute } from "./relation-id/RelationIdAttribute";
import { RelationCountAttribute } from "./relation-count/RelationCountAttribute";
import { RelationIdLoader } from "./relation-id/RelationIdLoader";
import { RelationIdLoader as QueryStrategyRelationIdLoader } from "./RelationIdLoader";
import { RelationIdMetadataToAttributeTransformer } from "./relation-id/RelationIdMetadataToAttributeTransformer";
import { RelationCountLoader } from "./relation-count/RelationCountLoader";
import { RelationCountMetadataToAttributeTransformer } from "./relation-count/RelationCountMetadataToAttributeTransformer";
import { QueryBuilder } from "./QueryBuilder";
import { LockNotSupportedOnGivenDriverError } from "../error/LockNotSupportedOnGivenDriverError";
import { OffsetWithoutLimitNotSupportedError } from "../error/OffsetWithoutLimitNotSupportedError";
import { ObjectUtils } from "../util/ObjectUtils";
import { DriverUtils } from "../driver/DriverUtils";
import { EntityNotFoundError } from "../error/EntityNotFoundError";
import { TypeORMError } from "../error";
import { FindOptionsUtils } from "../find-options/FindOptionsUtils";
import { OrmUtils } from "../util/OrmUtils";
import { EntityPropertyNotFoundError } from "../error/EntityPropertyNotFoundError";
import { InstanceChecker } from "../util/InstanceChecker";
import { FindOperator } from "../find-options/FindOperator";
import { ApplyValueTransformers } from "../util/ApplyValueTransformers";
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
*/
export class SelectQueryBuilder extends QueryBuilder {
constructor() {
super(...arguments);
this["@instanceof"] = Symbol.for("SelectQueryBuilder");
this.findOptions = {};
this.selects = [];
this.joins = [];
this.conditions = "";
this.orderBys = [];
this.relationMetadatas = [];
}
// -------------------------------------------------------------------------
// 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) {
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.
*/
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.
*/
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.
*/
distinct(distinct = true) {
this.expressionMap.selectDistinct = distinct;
return this;
}
/**
* Sets the distinct on clause for Postgres.
*/
distinctOn(distinctOn) {
this.expressionMap.selectDistinctOn = distinctOn;
return this;
}
fromDummy() {
return this.from(this.connection.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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
loadRelationIdAndMap(mapToProperty, relationName, aliasNameOrOptions, queryBuilderFactory) {
const relationIdAttribute = new 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;
}
/**
* Counts number of entities of entity's relation and maps the value into some entity's property.
* Optionally, you can add condition and parameters used in condition.
*/
loadRelationCountAndMap(mapToProperty, relationName, aliasName, queryBuilderFactory) {
const relationCountAttribute = new RelationCountAttribute(this.expressionMap);
relationCountAttribute.mapToProperty = mapToProperty;
relationCountAttribute.relationName = relationName;
relationCountAttribute.alias = aliasName;
relationCountAttribute.queryBuilderFactory = queryBuilderFactory;
this.expressionMap.relationCountAttributes.push(relationCountAttribute);
this.expressionMap.createAlias({
type: "other",
name: relationCountAttribute.junctionAlias,
});
if (relationCountAttribute.relation.junctionEntityMetadata) {
this.expressionMap.createAlias({
type: "other",
name: relationCountAttribute.junctionAlias,
metadata: relationCountAttribute.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.
*/
loadAllRelationIds(options) {
// todo: add skip relations
this.expressionMap.mainAlias.metadata.relations.forEach((relation) => {
if (options !== undefined &&
options.relations !== undefined &&
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.
*/
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.
*/
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.
*/
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
*/
whereExists(subQuery) {
return this.where(...this.getExistsCondition(subQuery));
}
/**
* Adds a new AND where EXISTS clause
*/
andWhereExists(subQuery) {
return this.andWhere(...this.getExistsCondition(subQuery));
}
/**
* Adds a new OR where EXISTS clause
*/
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 }, ...]
*/
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 }, ...]
*/
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 }, ...]
*/
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.
*/
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.
*/
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.
*/
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.
*/
groupBy(groupBy) {
if (groupBy) {
this.expressionMap.groupBys = [groupBy];
}
else {
this.expressionMap.groupBys = [];
}
return this;
}
/**
* Adds GROUP BY condition in the query builder.
*/
addGroupBy(groupBy) {
this.expressionMap.groupBys.push(groupBy);
return this;
}
/**
* Enables time travelling for the current query (only supported by cockroach currently)
*/
timeTravelQuery(timeTravelFn) {
if (this.connection.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.
*/
orderBy(sort, order = "ASC", nulls) {
if (order !== undefined && order !== "ASC" && order !== "DESC")
throw new TypeORMError(`SelectQueryBuilder.addOrderBy "order" can accept only "ASC" and "DESC" values.`);
if (nulls !== undefined &&
nulls !== "NULLS FIRST" &&
nulls !== "NULLS LAST")
throw new TypeORMError(`SelectQueryBuilder.addOrderBy "nulls" can accept only "NULLS FIRST" and "NULLS LAST" values.`);
if (sort) {
if (typeof sort === "object") {
this.expressionMap.orderBys = sort;
}
else {
if (nulls) {
this.expressionMap.orderBys = {
[sort]: { order, nulls },
};
}
else {
this.expressionMap.orderBys = { [sort]: order };
}
}
}
else {
this.expressionMap.orderBys = {};
}
return this;
}
/**
* Adds ORDER BY condition in the query builder.
*/
addOrderBy(sort, order = "ASC", nulls) {
if (order !== undefined && order !== "ASC" && order !== "DESC")
throw new TypeORMError(`SelectQueryBuilder.addOrderBy "order" can accept only "ASC" and "DESC" values.`);
if (nulls !== undefined &&
nulls !== "NULLS FIRST" &&
nulls !== "NULLS LAST")
throw new TypeORMError(`SelectQueryBuilder.addOrderBy "nulls" can accept only "NULLS FIRST" and "NULLS LAST" values.`);
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.
*/
limit(limit) {
this.expressionMap.limit = this.normalizeNumber(limit);
if (this.expressionMap.limit !== undefined &&
isNaN(this.expressionMap.limit))
throw new TypeORMError(`Provided "limit" value is not a number. Please provide a numeric value.`);
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.
*/
offset(offset) {
this.expressionMap.offset = this.normalizeNumber(offset);
if (this.expressionMap.offset !== undefined &&
isNaN(this.expressionMap.offset))
throw new TypeORMError(`Provided "offset" value is not a number. Please provide a numeric value.`);
return this;
}
/**
* Sets maximal number of entities to take.
*/
take(take) {
this.expressionMap.take = this.normalizeNumber(take);
if (this.expressionMap.take !== undefined &&
isNaN(this.expressionMap.take))
throw new TypeORMError(`Provided "take" value is not a number. Please provide a numeric value.`);
return this;
}
/**
* Sets number of entities to skip.
*/
skip(skip) {
this.expressionMap.skip = this.normalizeNumber(skip);
if (this.expressionMap.skip !== undefined &&
isNaN(this.expressionMap.skip))
throw new TypeORMError(`Provided "skip" value is not a number. Please provide a numeric value.`);
return this;
}
/**
* Set certain index to be used by the query.
*
* @param index Name of index to be used.
*/
useIndex(index) {
this.expressionMap.useIndex = index;
return this;
}
/**
* Sets locking mode.
*/
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.
*/
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();
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(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(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(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();
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();
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();
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();
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;
const cacheId = this.expressionMap.cacheId;
// Creates a new cacheId for the count query, or it will retreive the above query results
// and count will return 0.
this.expressionMap.cacheId = cacheId ? `${cacheId}-count` : cacheId;
const 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();
}
}
/**
* 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.
*/
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.
*/
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(this.connection, 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 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,
});
if (joinAttribute.relation &&
joinAttribute.relation.junctionEntityMetadata) {
this.expressionMap.createAlias({
type: "join",
name: joinAttribute.junctionAlias,
metadata: joinAttribute.relation.junctionEntityMetadata,
});
}
}
else {
let subQuery = "";
if (typeof entityOrProperty === "function") {
const subQueryBuilder = entityOrProperty(this.subQuery());
this.setParameters(subQueryBuilder.getParameters());
subQuery = subQueryBuilder.getQuery();
}
else {
subQuery = entityOrProperty;
}
const isSubQuery = typeof entityOrProperty === "function" ||
(entityOrProperty.substr(0, 1) === "(" &&
entityOrProperty.substr(-1) === ")");
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 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: this.replacePropertyNames(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) {
if (DriverUtils.isMySQLFamily(this.connection.driver)) {
useIndex = ` USE INDEX (${this.expressionMap.useIndex})`;
}
}
// 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.connection;
let select = "SELECT ";
if (maxExecutionTime > 0) {
if (DriverUtils.isMySQLFamily(driver)) {
select += `/*+ MAX_EXECUTION_TIME(${this.expressionMap.maxExecutionTime}) */ `;
}
}
if (DriverUtils.isPostgresFamily(driver) &&
selectDistinctOn.length > 0) {
const selectDistinctOnMap = selectDistinctOn
.map((on) => this.replacePropertyNames(on))
.join(", ");
select = `SELECT DISTINCT ON (${selectDistinctOnMap}) `;
}
else if (selectDistinct) {
select = "SELECT DISTINCT ";
}
return select;
}
/**
* Creates "JOIN" part of SQL query.
*/
createJoinExpression() {
// examples:
// select from owning side
// qb.select("post")
// .leftJoinAndSelect("post.category", "category");
// select from non-owning side
// qb.select("category")
// .leftJoinAndSelect("category.post", "post");
const joins = this.expressionMap.joinAttributes.map((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;
// 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
? joinAttr.alias.subQuery
: this.getTableName(destinationTableName);
return (" " +
joinAttr.direction +
" JOIN " +
destinationJoin +
" " +
this.escape(destinationTableAlias) +
this.createTableLockExpression() +
(joinAttr.condition
? " ON " + this.replacePropertyNames(joinAttr.condition)
: ""));
}
// 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 (" " +
joinAttr.direction +
" JOIN " +
this.getTableName(destinationTableName) +
" " +
this.escape(destinationTableAlias) +
this.createTableLockExpression() +
" ON " +
this.replacePropertyNames(condition + appendedCondition));
}
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 TypeORMError(`Relation ${relation.entityMetadata.name}.${relation.propertyName} does not have join columns.`);
return (" " +
joinAttr.direction +
" JOIN " +
this.getTable