From 3cda7ec39d145f4f37f74bf40906565e472852ed Mon Sep 17 00:00:00 2001 From: Tait Clarridge Date: Tue, 2 Jan 2024 01:54:43 -0500 Subject: [PATCH] feat: add isolated where statements (#10213) - Add `isolateWhereStatements` to the `BaseDataSourceOptions` object and automatically isolate where statements in the query builder if it is true --- docs/data-source-options.md | 3 ++ src/data-source/BaseDataSourceOptions.ts | 5 ++ src/query-builder/QueryBuilder.ts | 26 +++++++++- .../isolated-where/entity/User.ts | 18 +++++++ .../query-builder-isolated-where.ts | 48 +++++++++++++++++++ test/utils/test-utils.ts | 8 ++++ 6 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 test/functional/query-builder/isolated-where/entity/User.ts create mode 100644 test/functional/query-builder/isolated-where/query-builder-isolated-where.ts diff --git a/docs/data-source-options.md b/docs/data-source-options.md index 2ea012a0f..7f77a9f70 100644 --- a/docs/data-source-options.md +++ b/docs/data-source-options.md @@ -98,6 +98,9 @@ Different RDBMS-es have their own specific options. - `cache` - Enables entity result caching. You can also configure cache type and other cache options here. Read more about caching [here](caching.md). +- `isolateWhereStatements` - Enables where statement isolation, wrapping each where clause in brackets automatically. + eg. `.where("user.firstName = :search OR user.lastName = :search")` becomes `WHERE (user.firstName = ? OR user.lastName = ?)` instead of `WHERE user.firstName = ? OR user.lastName = ?` + ## `mysql` / `mariadb` data source options - `url` - Connection url where perform connection to. Please note that other data source options will override parameters set from url. diff --git a/src/data-source/BaseDataSourceOptions.ts b/src/data-source/BaseDataSourceOptions.ts index dcd5f2b2f..03aa17d0b 100644 --- a/src/data-source/BaseDataSourceOptions.ts +++ b/src/data-source/BaseDataSourceOptions.ts @@ -208,4 +208,9 @@ export interface BaseDataSourceOptions { */ readonly ignoreErrors?: boolean } + + /** + * Allows automatic isolation of where clauses + */ + readonly isolateWhereStatements?: boolean } diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index de5c8a394..4fa1486e8 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -1008,9 +1008,31 @@ export abstract class QueryBuilder { switch (clause.type) { case "and": - return (index > 0 ? "AND " : "") + expression + return ( + (index > 0 ? "AND " : "") + + `${ + this.connection.options.isolateWhereStatements + ? "(" + : "" + }${expression}${ + this.connection.options.isolateWhereStatements + ? ")" + : "" + }` + ) case "or": - return (index > 0 ? "OR " : "") + expression + return ( + (index > 0 ? "OR " : "") + + `${ + this.connection.options.isolateWhereStatements + ? "(" + : "" + }${expression}${ + this.connection.options.isolateWhereStatements + ? ")" + : "" + }` + ) } return expression diff --git a/test/functional/query-builder/isolated-where/entity/User.ts b/test/functional/query-builder/isolated-where/entity/User.ts new file mode 100644 index 000000000..8958728be --- /dev/null +++ b/test/functional/query-builder/isolated-where/entity/User.ts @@ -0,0 +1,18 @@ +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() + firstName: string + + @Column() + lastName: string + + @Column() + isAdmin: boolean +} diff --git a/test/functional/query-builder/isolated-where/query-builder-isolated-where.ts b/test/functional/query-builder/isolated-where/query-builder-isolated-where.ts new file mode 100644 index 000000000..dc5a07451 --- /dev/null +++ b/test/functional/query-builder/isolated-where/query-builder-isolated-where.ts @@ -0,0 +1,48 @@ +import "../../../utils/test-setup" +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases, +} from "../../../utils/test-utils" +import { expect } from "chai" +import { DataSource } from "../../../../src" +import { User } from "./entity/User" + +describe("query builder > isolated-where", () => { + let connections: DataSource[] + before( + async () => + (connections = await createTestingConnections({ + entities: [User], + enabledDrivers: ["sqlite"], + isolateWhereStatements: true, + })), + ) + beforeEach(() => reloadTestingDatabases(connections)) + after(() => closeTestingConnections(connections)) + + it("should correctly apply brackets when where statement isolation is enabled", () => + Promise.all( + connections.map(async (connection) => { + const sql = connection.manager + .createQueryBuilder(User, "user") + .where("user.id = :userId", { userId: "user-id" }) + .andWhere( + "user.firstName = :search OR user.lastName = :search", + { + search: "search-term", + }, + ) + .disableEscaping() + .getSql() + + expect(sql).to.be.equal( + "SELECT user.id AS user_id, user.firstName AS user_firstName, " + + "user.lastName AS user_lastName, user.isAdmin AS user_isAdmin " + + "FROM user user " + + "WHERE user.id = ? " + + "AND (user.firstName = ? OR user.lastName = ?)", + ) + }), + )) +}) diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index 0f34f2632..49680e1fc 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -159,6 +159,11 @@ export interface TestingOptions { | Logger relationLoadStrategy?: "join" | "query" + + /** + * Allows automatic isolation of where clauses + */ + isolateWhereStatements?: boolean } /** @@ -295,6 +300,9 @@ export function setupTestingConnections( newOptions.metadataTableName = options.metadataTableName if (options && options.relationLoadStrategy) newOptions.relationLoadStrategy = options.relationLoadStrategy + if (options && options.isolateWhereStatements) + newOptions.isolateWhereStatements = + options.isolateWhereStatements newOptions.baseDirectory = path.dirname(getOrmFilepath())