Skip to content

Commit

Permalink
feat: query builder negating with "NotBrackets" for complex expressio…
Browse files Browse the repository at this point in the history
…ns (#8476)

Co-authored-by: Christian Forgács <christian@wunderbit.de>
  • Loading branch information
christian-forgacs and Christian Forgács committed Jan 15, 2022
1 parent 546b3ed commit fe7f328
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 2 deletions.
18 changes: 18 additions & 0 deletions docs/select-query-builder.md
Expand Up @@ -403,6 +403,24 @@ Which will produce the following SQL query:
SELECT ... FROM users user WHERE user.registered = true AND (user.firstName = 'Timber' OR user.lastName = 'Saw')
```


You can add a negated complex `WHERE` expression into an existing `WHERE` using `NotBrackets`

```typescript
createQueryBuilder("user")
.where("user.registered = :registered", { registered: true })
.andWhere(new NotBrackets(qb => {
qb.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
}))
```

Which will produce the following SQL query:

```sql
SELECT ... FROM users user WHERE user.registered = true AND NOT((user.firstName = 'Timber' OR user.lastName = 'Saw'))
```

You can combine as many `AND` and `OR` expressions as you need.
If you use `.where` more than once you'll override all previous `WHERE` expressions.

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -129,6 +129,7 @@ export {InsertQueryBuilder} from "./query-builder/InsertQueryBuilder";
export {UpdateQueryBuilder} from "./query-builder/UpdateQueryBuilder";
export {RelationQueryBuilder} from "./query-builder/RelationQueryBuilder";
export {Brackets} from "./query-builder/Brackets";
export {NotBrackets} from "./query-builder/NotBrackets";
export {WhereExpressionBuilder} from "./query-builder/WhereExpressionBuilder";
export {WhereExpression} from "./query-builder/WhereExpressionBuilder";
export {InsertResult} from "./query-builder/result/InsertResult";
Expand Down
9 changes: 9 additions & 0 deletions src/query-builder/NotBrackets.ts
@@ -0,0 +1,9 @@
import {Brackets} from "./Brackets";

/**
* Syntax sugar.
* Allows to use negate brackets in WHERE expressions for better syntax.
*/
export class NotBrackets extends Brackets {

}
5 changes: 3 additions & 2 deletions src/query-builder/QueryBuilder.ts
Expand Up @@ -24,6 +24,7 @@ import {In} from "../find-options/operator/In";
import {EntityColumnNotFound} from "../error/EntityColumnNotFound";
import { TypeORMError } from "../error";
import { WhereClause, WhereClauseCondition } from "./WhereClause";
import {NotBrackets} from "./NotBrackets";

// todo: completely cover query builder with tests
// todo: entityOrProperty can be target name. implement proper behaviour if it is.
Expand Down Expand Up @@ -1169,7 +1170,7 @@ export abstract class QueryBuilder<Entity> {
}
}

protected getWhereCondition(where: string|((qb: this) => string)|Brackets|ObjectLiteral|ObjectLiteral[]): WhereClauseCondition {
protected getWhereCondition(where: string|((qb: this) => string)|Brackets|NotBrackets|ObjectLiteral|ObjectLiteral[]): WhereClauseCondition {
if (typeof where === "string") {
return where;
}
Expand All @@ -1189,7 +1190,7 @@ export abstract class QueryBuilder<Entity> {
where.whereFactory(whereQueryBuilder as any);

return {
operator: "brackets",
operator: where instanceof NotBrackets ? "not" : "brackets",
condition: whereQueryBuilder.expressionMap.wheres
};
}
Expand Down
20 changes: 20 additions & 0 deletions test/functional/query-builder/not/entity/User.ts
@@ -0,0 +1,20 @@
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;

}
118 changes: 118 additions & 0 deletions test/functional/query-builder/not/query-builder-not.ts
@@ -0,0 +1,118 @@
import "reflect-metadata";
import {expect} from "chai";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {User} from "./entity/User";
import {NotBrackets} from "../../../../src/query-builder/NotBrackets";

describe("query builder > not", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: [ "sqlite" ],
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should put negation in the SQL with one condition", () => Promise.all(connections.map(async connection => {
const sql = await connection.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.andWhere(new NotBrackets(qb => {
qb.where("user.firstName = :firstName1", { firstName1: "Hello" })
}))
.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.isAdmin = ? " +
"AND NOT(user.firstName = ?)"
)
})));

it("should put negation in the SQL with two condition", () => Promise.all(connections.map(async connection => {
const sql = await connection.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.andWhere(new NotBrackets(qb => {
qb.where("user.firstName = :firstName1", { firstName1: "Hello" })
.andWhere("user.lastName = :lastName1", { lastName1: "Mars" });
}))
.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.isAdmin = ? " +
"AND NOT((user.firstName = ? AND user.lastName = ?))"
)
})));

it("should put negation correctly into WHERE expression with one condition", () => Promise.all(connections.map(async connection => {

const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

const users = await connection.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.andWhere(new NotBrackets(qb => {
qb.where("user.firstName = :firstName1", { firstName1: "Timber" })
}))
.getMany();

expect(users.length).to.be.equal(1);

})));

it("should put negation correctly into WHERE expression with two conditions", () => Promise.all(connections.map(async connection => {

const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

const users = await connection.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.andWhere(new NotBrackets(qb => {
qb.where("user.firstName = :firstName1", { firstName1: "Timber" })
.andWhere("user.lastName = :lastName1", { lastName1: "Saw" });
}))
.getMany();

expect(users.length).to.be.equal(1);

})));

});

0 comments on commit fe7f328

Please sign in to comment.