diff --git a/src/query-builder/DeleteQueryBuilder.ts b/src/query-builder/DeleteQueryBuilder.ts index 81b062aa8..e0e2a6266 100644 --- a/src/query-builder/DeleteQueryBuilder.ts +++ b/src/query-builder/DeleteQueryBuilder.ts @@ -50,8 +50,24 @@ export class DeleteQueryBuilder extends QueryBuilder { * calling this function will override previously set WHERE conditions. * Additionally you can add parameters used in where expression. */ - where(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "simple", condition: where }); + where(where: string, parameters?: ObjectLiteral): 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: (qb: this) => string, parameters?: ObjectLiteral): 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: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres = [{ type: "simple", condition: typeof where === "string" ? where : where(this) }]; if (parameters) this.setParameters(parameters); return this; } @@ -60,8 +76,20 @@ export class DeleteQueryBuilder extends QueryBuilder { * Adds new AND WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - andWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "and", condition: where }); + andWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "and", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -70,8 +98,20 @@ export class DeleteQueryBuilder extends QueryBuilder { * Adds new OR WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - orWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "or", condition: where }); + orWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "or", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -81,7 +121,7 @@ export class DeleteQueryBuilder extends QueryBuilder { */ whereInIds(ids: any[]): this { const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); + this.where(whereExpression, parameters); return this; } diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index 0636244a7..2de30c597 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -380,7 +380,7 @@ export abstract class QueryBuilder { * Specifies FROM which entity's table select/update/delete will be executed. * Also sets a main string alias of the selection data. */ - protected setMainAlias(entityTarget: Function|string, aliasName?: string): this { + protected setMainAlias(entityTarget: Function|string|((qb: SelectQueryBuilder) => SelectQueryBuilder), aliasName?: string): this { // if table has a metadata then find it to properly escape its properties // const metadata = this.connection.entityMetadatas.find(metadata => metadata.tableName === tableName); @@ -391,8 +391,16 @@ export abstract class QueryBuilder { }); } else { + let subQuery: string = ""; + if (entityTarget instanceof Function) { + const subQueryBuilder: SelectQueryBuilder = (entityTarget as any)(((this as any) as SelectQueryBuilder).subQuery()); + this.setParameters(subQueryBuilder.getParameters()); + subQuery = subQueryBuilder.getQuery(); + + } else { + subQuery = entityTarget; + } const isSubQuery = entityTarget instanceof Function || entityTarget.substr(0, 1) === "(" && entityTarget.substr(-1) === ")"; - const subQuery = entityTarget instanceof Function ? entityTarget(((this as any) as SelectQueryBuilder).subQuery()).getQuery() : entityTarget; this.expressionMap.createMainAlias({ name: aliasName, tableName: isSubQuery === false ? entityTarget as string : undefined, diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index b535f768f..902ee05d2 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -100,7 +100,19 @@ export class SelectQueryBuilder extends QueryBuilder { * Specifies FROM which entity's table select/update/delete will be executed. * Also sets a main string alias of the selection data. */ - from(entityTarget: ObjectType|string, aliasName: string): SelectQueryBuilder { + from(entityTarget: (qb: SelectQueryBuilder) => SelectQueryBuilder, aliasName: string): SelectQueryBuilder; + + /** + * Specifies FROM which entity's table select/update/delete will be executed. + * Also sets a main string alias of the selection data. + */ + from(entityTarget: ObjectType|string, aliasName: string): SelectQueryBuilder; + + /** + * Specifies FROM which entity's table select/update/delete will be executed. + * Also sets a main string alias of the selection data. + */ + from(entityTarget: ObjectType|string|((qb: SelectQueryBuilder) => SelectQueryBuilder), aliasName: string): SelectQueryBuilder { this.setMainAlias(entityTarget, aliasName); return (this as any) as SelectQueryBuilder; } @@ -491,8 +503,24 @@ export class SelectQueryBuilder extends QueryBuilder { * calling this function will override previously set WHERE conditions. * Additionally you can add parameters used in where expression. */ - where(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "simple", condition: where }); + where(where: string, parameters?: ObjectLiteral): 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: (qb: this) => string, parameters?: ObjectLiteral): 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: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres = [{ type: "simple", condition: typeof where === "string" ? where : where(this) }]; if (parameters) this.setParameters(parameters); return this; } @@ -501,8 +529,20 @@ export class SelectQueryBuilder extends QueryBuilder { * Adds new AND WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - andWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "and", condition: where }); + andWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "and", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -511,8 +551,20 @@ export class SelectQueryBuilder extends QueryBuilder { * Adds new OR WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - orWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "or", condition: where }); + orWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "or", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -522,7 +574,7 @@ export class SelectQueryBuilder extends QueryBuilder { */ whereInIds(ids: any[]): this { const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); + this.where(whereExpression, parameters); return this; } diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index 393da83d7..39b1f3805 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -48,8 +48,24 @@ export class UpdateQueryBuilder extends QueryBuilder { * calling this function will override previously set WHERE conditions. * Additionally you can add parameters used in where expression. */ - where(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "simple", condition: where }); + where(where: string, parameters?: ObjectLiteral): 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: (qb: this) => string, parameters?: ObjectLiteral): 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: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres = [{ type: "simple", condition: typeof where === "string" ? where : where(this) }]; if (parameters) this.setParameters(parameters); return this; } @@ -58,8 +74,20 @@ export class UpdateQueryBuilder extends QueryBuilder { * Adds new AND WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - andWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "and", condition: where }); + andWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new AND WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + andWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "and", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -68,8 +96,20 @@ export class UpdateQueryBuilder extends QueryBuilder { * Adds new OR WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ - orWhere(where: string, parameters?: ObjectLiteral): this { - this.expressionMap.wheres.push({ type: "or", condition: where }); + orWhere(where: string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: (qb: this) => string, parameters?: ObjectLiteral): this; + + /** + * Adds new OR WHERE condition in the query builder. + * Additionally you can add parameters used in where expression. + */ + orWhere(where: string|((qb: this) => string), parameters?: ObjectLiteral): this { + this.expressionMap.wheres.push({ type: "or", condition: typeof where === "string" ? where : where(this) }); if (parameters) this.setParameters(parameters); return this; } @@ -79,7 +119,7 @@ export class UpdateQueryBuilder extends QueryBuilder { */ whereInIds(ids: any[]): this { const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); + this.where(whereExpression, parameters); return this; } diff --git a/test/functional/query-builder/subquery/entity/Post.ts b/test/functional/query-builder/subquery/entity/Post.ts new file mode 100644 index 000000000..b65476c52 --- /dev/null +++ b/test/functional/query-builder/subquery/entity/Post.ts @@ -0,0 +1,14 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + +} \ No newline at end of file diff --git a/test/functional/query-builder/subquery/entity/User.ts b/test/functional/query-builder/subquery/entity/User.ts new file mode 100644 index 000000000..b7e6aad97 --- /dev/null +++ b/test/functional/query-builder/subquery/entity/User.ts @@ -0,0 +1,17 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + registered: boolean; + +} \ No newline at end of file diff --git a/test/functional/query-builder/subquery/query-builder-subquery.ts b/test/functional/query-builder/subquery/query-builder-subquery.ts new file mode 100644 index 000000000..3473c131c --- /dev/null +++ b/test/functional/query-builder/subquery/query-builder-subquery.ts @@ -0,0 +1,203 @@ +import "reflect-metadata"; +import * as chai from "chai"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; +import {Connection} from "../../../../src/connection/Connection"; +import {User} from "./entity/User"; +import {Post} from "./entity/Post"; + +const should = chai.should(); + +describe.only("query builder > sub-query", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + async function prepare(connection: Connection) { + + const user1 = new User(); + user1.name = "Alex Messer"; + user1.registered = true; + await connection.manager.save(user1); + + const user2 = new User(); + user2.name = "Dima Zotov"; + user2.registered = true; + await connection.manager.save(user2); + + const user3 = new User(); + user3.name = "Umed Khudoiberdiev"; + user3.registered = false; + await connection.manager.save(user3); + + const post1 = new Post(); + post1.title = "Alex Messer"; + await connection.manager.save(post1); + + const post2 = new Post(); + post2.title = "Dima Zotov"; + await connection.manager.save(post2); + + const post3 = new Post(); + post3.title = "Umed Khudoiberdiev"; + await connection.manager.save(post3); + } + + it("should execute sub query in where string using subQuery method", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const qb = await connection.getRepository(Post).createQueryBuilder("post"); + const posts = await qb + .where("post.title IN " + qb.subQuery().select("user.name").from(User, "user").where("user.registered = :registered").getQuery()) + .setParameter("registered", true) + .getMany(); + + posts.should.be.eql([ + { id: 1, title: "Alex Messer" }, + { id: 2, title: "Dima Zotov" }, + ]); + }))); + + it("should execute sub query in where function using subQuery method", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const posts = await connection.getRepository(Post) + .createQueryBuilder("post") + .where(qb => { + const subQuery = qb.subQuery() + .select("user.name") + .from(User, "user") + .where("user.registered = :registered") + .getQuery(); + return "post.title IN " + subQuery; + }) + .setParameter("registered", true) + .getMany(); + + posts.should.be.eql([ + { id: 1, title: "Alex Messer" }, + { id: 2, title: "Dima Zotov" }, + ]); + }))); + + it("should execute sub query in where function using subQuery method", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const posts = await connection.getRepository(Post) + .createQueryBuilder("post") + .where(qb => { + const subQuery = qb.subQuery() + .select("user.name") + .from(User, "user") + .where("user.registered = :registered") + .getQuery(); + return "post.title IN " + subQuery; + }) + .setParameter("registered", true) + .getMany(); + + posts.should.be.eql([ + { id: 1, title: "Alex Messer" }, + { id: 2, title: "Dima Zotov" }, + ]); + }))); + + it("should execute sub query using different query builder", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const userQb = await connection.getRepository(User) + .createQueryBuilder("user") + .select("user.name") + .where("user.registered = :registered", { registered: true }); + + const posts = await connection.getRepository(Post) + .createQueryBuilder("post") + .where("post.title IN (" + userQb.getQuery() + ")") + .setParameters(userQb.getParameters()) + .getMany(); + + posts.should.be.eql([ + { id: 1, title: "Alex Messer" }, + { id: 2, title: "Dima Zotov" }, + ]); + }))); + + it("should execute sub query in from expression (using different query builder)", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const userQb = await connection.getRepository(User) + .createQueryBuilder("user") + .select("user.name", "name") + .where("user.registered = :registered", { registered: true }); + + const posts = await connection + .createQueryBuilder() + .select("user.name", "name") + .from("(" + userQb.getQuery() + ")", "user") + .setParameters(userQb.getParameters()) + .getRawMany(); + + posts.should.be.eql([ + { name: "Alex Messer" }, + { name: "Dima Zotov" }, + ]); + }))); + + it("should execute sub query in from expression (using from's query builder)", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const userQb = await connection.getRepository(User) + .createQueryBuilder("user") + .select("user.name", "name") + .where("user.registered = :registered", { registered: true }); + + const posts = await connection + .createQueryBuilder() + .select("user.name", "name") + .from(subQuery => { + return subQuery + .select("user.name", "name") + .from(User, "user") + .where("user.registered = :registered", { registered: true }); + }, "user") + .setParameters(userQb.getParameters()) + .getRawMany(); + + posts.should.be.eql([ + { name: "Alex Messer" }, + { name: "Dima Zotov" }, + ]); + }))); + + it.only("should execute sub query in from expression (using from's query builder)", () => Promise.all(connections.map(async connection => { + await prepare(connection); + + const userQb = await connection.getRepository(User) + .createQueryBuilder("user") + .select("user.name", "name") + .where("user.registered = :registered", { registered: true }); + + const posts = await connection + .createQueryBuilder() + .select("user.name", "name") + .from(subQuery => { + return subQuery + .select("user.name", "name") + .from(User, "user") + .where("user.registered = :registered", { registered: true }); + }, "user") + .setParameters(userQb.getParameters()) + .getRawMany(); + + posts.should.be.eql([ + { name: "Alex Messer" }, + { name: "Dima Zotov" }, + ]); + }))); + +}); \ No newline at end of file