Skip to content

Commit

Permalink
feat: implement exists query method (#9303)
Browse files Browse the repository at this point in the history
Adding `Exists` method to query builder and EntityManager, to check whether a row exists given the conditions

Closes: #2815

Co-authored-by: mortzprk <mortz.prk@gmail.com>
  • Loading branch information
mortezaPRK and mortzprk committed Dec 3, 2022
1 parent 53fad8f commit 598e269
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export interface Driver {

cteCapabilities: CteCapabilities

/**
* Dummy table name
*/
dummyTableName?: string

/**
* Performs connection to the database.
* Depend on driver type it may create a connection pool.
Expand Down
2 changes: 2 additions & 0 deletions src/driver/oracle/OracleDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ export class OracleDriver implements Driver {
enabled: false, // TODO: enable
}

dummyTableName = "DUAL";

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions src/driver/sap/SapDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export class SapDriver implements Driver {
enabled: true,
}

dummyTableName = `SYS.DUMMY`;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down
17 changes: 17 additions & 0 deletions src/entity-manager/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,23 @@ export class EntityManager {
}
}

/**
* Checks whether any entity exists with the given condition
*/
exists<Entity>(
entityClass: EntityTarget<Entity>,
options?: FindManyOptions<Entity>,
): Promise<boolean> {
const metadata = this.connection.getMetadata(entityClass)
return this.createQueryBuilder(
entityClass,
FindOptionsUtils.extractFindManyOptionsAlias(options) ||
metadata.name,
)
.setFindOptions(options || {})
.getExists()
}

/**
* Counts entities that match given options.
* Useful for pagination.
Expand Down
15 changes: 15 additions & 0 deletions src/query-builder/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,21 @@ export abstract class QueryBuilder<Entity extends ObjectLiteral> {
})
}

protected getExistsCondition(subQuery: any): [string, any[]] {
const query = subQuery
.clone()
.orderBy()
.groupBy()
.offset(undefined)
.limit(undefined)
.skip(undefined)
.take(undefined)
.select("1")
.setOption("disable-global-order")

return [`EXISTS (${query.getQuery()})`, query.getParameters()]
}

private findColumnsForPropertyPath(
propertyPath: string,
): [Alias, string[], ColumnMetadata[]] {
Expand Down
83 changes: 83 additions & 0 deletions src/query-builder/SelectQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
return this
}

fromDummy(): SelectQueryBuilder<any> {
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.
Expand Down Expand Up @@ -1190,6 +1194,27 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
return this
}

/**
* Sets a new where EXISTS clause
*/
whereExists(subQuery: SelectQueryBuilder<any>): this {
return this.where(...this.getExistsCondition(subQuery))
}

/**
* Adds a new AND where EXISTS clause
*/
andWhereExists(subQuery: SelectQueryBuilder<any>): this {
return this.andWhere(...this.getExistsCondition(subQuery))
}

/**
* Adds a new OR where EXISTS clause
*/
orWhereExists(subQuery: SelectQueryBuilder<any>): this {
return this.orWhere(...this.getExistsCondition(subQuery))
}

/**
* Adds new AND WHERE with conditions for the given ids.
*
Expand Down Expand Up @@ -1752,6 +1777,50 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
}
}

/**
* Gets exists
* Returns whether any rows exists matching current query.
*/
async getExists(): Promise<boolean> {
if (this.expressionMap.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError()

const queryRunner = this.obtainQueryRunner()
let transactionStartedByUs: boolean = 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.
Expand Down Expand Up @@ -2912,6 +2981,20 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
return parseInt(results[0]["cnt"])
}

protected async executeExistsQuery(
queryRunner: QueryRunner,
): Promise<boolean> {
const results = await this.connection
.createQueryBuilder()
.fromDummy()
.select("1", "row_exists")
.whereExists(this)
.limit(1)
.loadRawResults(queryRunner)

return results.length > 0
}

protected applyFindOptions() {
// todo: convert relations: string[] to object map to simplify code
// todo: same with selects
Expand Down
7 changes: 7 additions & 0 deletions src/repository/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,13 @@ export class Repository<Entity extends ObjectLiteral> {
)
}

/**
* Checks whether any entity exists that match given options.
*/
exist(options?: FindManyOptions<Entity>): Promise<boolean> {

This comment has been minimized.

Copy link
@nebkat

nebkat Dec 10, 2022

Contributor

typo?

This comment has been minimized.

Copy link
@pleerock

pleerock Dec 10, 2022

Member

oh yeah it should have a consistent name (exists) like in manager and everywhere else @mortezaPRK

return this.manager.exists(this.metadata.target, options)
}

/**
* Counts entities that match given options.
* Useful for pagination.
Expand Down
7 changes: 7 additions & 0 deletions test/functional/query-builder/exists/entity/Test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity, PrimaryColumn } from "../../../../../src"

@Entity("tests")
export class Test {
@PrimaryColumn()
id: string
}
45 changes: 45 additions & 0 deletions test/functional/query-builder/exists/query-builder-exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../utils/test-utils"
import { DataSource } from "../../../../src/data-source/DataSource"
import { expect } from "chai"
import { Test } from "./entity/Test"

describe("query builder > exist", () => {
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [Test],
schemaCreate: true,
dropSchema: true,
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))

it("Exists query of empty table should be false", () =>
Promise.all(
connections.map(async (connection) => {
const repo = connection.getRepository(Test)

const exist = await repo.exist()
expect(exist).to.be.equal(false)
}),
))

it("Exists query of non empty table should be true", () =>
Promise.all(
connections.map(async (connection) => {
const repo = connection.getRepository(Test)

await repo.save({ id: "ok" })
await repo.save({ id: "nok" })

const exist = await repo.exist()
expect(exist).to.be.equal(true)
}),
))
})
113 changes: 113 additions & 0 deletions test/functional/repository/find-methods/repostiory-find-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,119 @@ describe("repository > find methods", () => {
))
})

describe("exists", function () {
it("should return a True when no criteria given", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)

for (let i = 0; i < 100; i++) {
const post = new Post()
post.id = i
post.title = "post #" + i
post.categoryName = "other"
await postRepository.save(post)
}

// check exist method
const exists = await postRepository.exist({
order: { id: "ASC" },
})
exists.should.be.equal(true)
}),
))

it("should return True when matches the given criteria", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
for (let i = 1; i <= 100; i++) {
const post = new Post()
post.id = i
post.title = "post #" + i
post.categoryName = i % 2 === 0 ? "even" : "odd"
await postRepository.save(post)
}

// check exist method
const exists = await postRepository.exist({
where: { categoryName: "odd" },
order: { id: "ASC" },
})
exists.should.be.equal(true)
}),
))

it("should return True when matches the given multiple criteria", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
for (let i = 1; i <= 100; i++) {
const post = new Post()
post.id = i
post.title = "post #" + i
post.categoryName = i % 2 === 0 ? "even" : "odd"
post.isNew = i > 90
await postRepository.save(post)
}

// check exist method
const exists = await postRepository.exist({
where: { categoryName: "odd", isNew: true },
order: { id: "ASC" },
})
exists.should.be.equal(true)
}),
))

it("should return True when matches the given find options", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
for (let i = 1; i <= 100; i++) {
const post = new Post()
post.id = i
post.isNew = i > 90
post.title = post.isNew
? "new post #" + i
: "post #" + i
post.categoryName = i % 2 === 0 ? "even" : "odd"
await postRepository.save(post)
}

// check exist method
const exists = await postRepository.exist()
exists.should.be.equal(true)
}),
))

it("should return True when matches both criteria and find options", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
for (let i = 1; i <= 100; i++) {
const post = new Post()
post.id = i
post.isNew = i > 90
post.title = post.isNew
? "new post #" + i
: "post #" + i
post.categoryName = i % 2 === 0 ? "even" : "odd"
await postRepository.save(post)
}

// check exist method
const exists = await postRepository.exist({
where: { categoryName: "even", isNew: true },
skip: 1,
take: 2,
order: { id: "ASC" },
})
exists.should.be.equal(true)
}),
))
})

describe("find and findAndCount", function () {
it("should return everything when no criteria given", () =>
Promise.all(
Expand Down

0 comments on commit 598e269

Please sign in to comment.