Skip to content

Commit

Permalink
feat: support query comments in the query builder (#6892)
Browse files Browse the repository at this point in the history
add a `comment` method to the QueryBuilder so we can include an
arbitrary comment in our queries for a variety of purposes -  from
query plan stability to SQL reverse proxy routing, to debugging.
the comment builder is supported in selects, inserts, deletes, and
updates

this is directly inspired by the functionality supported by hibernate
to handle SQL Query comments

it uses C-style queries which are ANSI SQL 2003 & supported in all
of the dialects of SQL that we support as drivers

fixes #3643
  • Loading branch information
imnotjames committed Oct 15, 2020
1 parent 4475d80 commit 84c18a9
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 4 deletions.
3 changes: 2 additions & 1 deletion src/query-builder/DeleteQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> implements
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createDeleteExpression();
let sql = this.createComment();
sql += this.createDeleteExpression();
return sql.trim();
}

Expand Down
3 changes: 2 additions & 1 deletion src/query-builder/InsertQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createInsertExpression();
let sql = this.createComment();
sql += this.createInsertExpression();
return sql.trim();
}

Expand Down
23 changes: 23 additions & 0 deletions src/query-builder/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ export abstract class QueryBuilder<Entity> {
return new (this.constructor as any)(this);
}

/**
* Includes a Query comment in the query builder. This is helpful for debugging purposes,
* such as finding a specific query in the database server's logs, or for categorization using
* an APM product.
*/
comment(comment: string): this {
this.expressionMap.comment = comment;
return this;
}

/**
* Disables escaping.
*/
Expand Down Expand Up @@ -613,6 +623,19 @@ export abstract class QueryBuilder<Entity> {
return statement;
}

protected createComment(): string {
if (!this.expressionMap.comment) {
return "";
}

// ANSI SQL 2003 support C style comments - comments that start with `/*` and end with `*/`
// In some dialects query nesting is available - but not all. Because of this, we'll need
// to scrub "ending" characters from the SQL but otherwise we can leave everything else
// as-is and it should be valid.

return `/* ${this.expressionMap.comment.replace("*/", "")} */ `;
}

/**
* Creates "WHERE" expression.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/query-builder/QueryExpressionMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ export class QueryExpressionMap {
*/
nativeParameters: ObjectLiteral = {};

/**
* Query Comment to include extra information for debugging or other purposes.
*/
comment?: string;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion src/query-builder/SelectQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createSelectExpression();
let sql = this.createComment();
sql += this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
Expand Down
3 changes: 2 additions & 1 deletion src/query-builder/UpdateQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> implements
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createUpdateExpression();
let sql = this.createComment();
sql += this.createUpdateExpression();
sql += this.createOrderByExpression();
sql += this.createLimitExpression();
return sql.trim();
Expand Down
10 changes: 10 additions & 0 deletions test/functional/query-builder/comment/entity/Test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";

@Entity()
export class Test {

@PrimaryGeneratedColumn()
id: number;

}
85 changes: 85 additions & 0 deletions test/functional/query-builder/comment/query-builder-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {Test} from "./entity/Test";
import {expect} from "chai";

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

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Test],
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should scrub end comment pattern from string", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.comment("Hello World */")
.getSql();

expect(sql).to.match(/^\/\* Hello World \*\/ SELECT/);
})));

it("should not allow an empty comment", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.comment("")
.getSql();

expect(sql).to.not.match(/^\/\* Hello World \*\/ SELECT/);
})));

it("should allow a comment with just whitespaces", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.comment(" ")
.getSql();

expect(sql).to.match(/^\/\* \*\/ SELECT/);
})));

it("should allow a multi-line comment", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.comment("Hello World\nIt's a beautiful day!")
.getSql();

expect(sql).to.match(/^\/\* Hello World\nIt's a beautiful day! \*\/ SELECT/);
})));

it("should include comment in select", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.comment("Hello World")
.getSql();

expect(sql).to.match(/^\/\* Hello World \*\/ SELECT/);
})));

it("should include comment in update", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.update()
.set({ id: 2 })
.comment("Hello World")
.getSql();

expect(sql).to.match(/^\/\* Hello World \*\/ UPDATE/);
})));

it("should include comment in insert", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.insert()
.values({ id: 1 })
.comment("Hello World")
.getSql();

expect(sql).to.match(/^\/\* Hello World \*\/ INSERT/);
})));

it("should include comment in delete", () => Promise.all(connections.map(async connection => {
const sql = connection.manager.createQueryBuilder(Test, "test")
.delete()
.comment("Hello World")
.getSql();

expect(sql).to.match(/^\/\* Hello World \*\/ DELETE/);
})));

});

0 comments on commit 84c18a9

Please sign in to comment.