Skip to content

Commit

Permalink
feat(sql): allow specifying query comments
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Aug 30, 2023
1 parent 01d1ad7 commit 06d4d20
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 7 deletions.
16 changes: 16 additions & 0 deletions packages/core/src/drivers/IDatabaseDriver.ts
Expand Up @@ -109,15 +109,25 @@ export interface FindOptions<T, P extends string = never> {
fields?: readonly EntityField<T, P>[];
schema?: string;
flags?: QueryFlag[];
/** sql only */
groupBy?: string | string[];
having?: QBFilterQuery<T>;
/** sql only */
strategy?: LoadStrategy;
flushMode?: FlushMode;
filters?: Dictionary<boolean | Dictionary> | string[] | boolean;
/** sql only */
lockMode?: Exclude<LockMode, LockMode.OPTIMISTIC>;
/** sql only */
lockTableAliases?: string[];
ctx?: Transaction;
connectionType?: ConnectionType;
/** sql only */
indexHint?: string;
/** sql only */
comments?: string | string[];
/** sql only */
hintComments?: string | string[];
}

export interface FindOneOptions<T extends object, P extends string = never> extends Omit<FindOptions<T, P>, 'limit' | 'lockMode'> {
Expand Down Expand Up @@ -151,6 +161,12 @@ export interface CountOptions<T extends object, P extends string = never> {
populate?: readonly AutoPath<T, P>[] | boolean;
ctx?: Transaction;
connectionType?: ConnectionType;
/** sql only */
indexHint?: string;
/** sql only */
comments?: string | string[];
/** sql only */
hintComments?: string | string[];
}

export interface UpdateOptions<T> {
Expand Down
29 changes: 22 additions & 7 deletions packages/knex/src/AbstractSqlDriver.ts
Expand Up @@ -59,6 +59,9 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
.orderBy([...Utils.asArray(options.orderBy), ...joinedPropsOrderBy])
.groupBy(options.groupBy!)
.having(options.having!)
.indexHint(options.indexHint!)
.comment(options.comments!)
.hintComment(options.hintComments!)
.withSchema(this.getSchemaName(meta, options));

if (options.limit !== undefined) {
Expand Down Expand Up @@ -152,7 +155,10 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
protected async wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>): Promise<T[]>;
protected async wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>, type = QueryType.SELECT): Promise<unknown> {
const qb = this.createQueryBuilder(meta.className, options?.ctx, options.connectionType, options.convertCustomTypes)
.limit(options?.limit, options?.offset);
.limit(options?.limit, options?.offset)
.indexHint(options.indexHint!)
.comment(options.comments!)
.hintComment(options.hintComments!);

if (options.orderBy) {
qb.orderBy(options.orderBy);
Expand Down Expand Up @@ -279,6 +285,9 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
}

const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false)
.indexHint(options.indexHint!)
.comment(options.comments!)
.hintComment(options.hintComments!)
.groupBy(options.groupBy!)
.having(options.having!)
.populate(options.populate as unknown as PopulateOptions<T>[] ?? [])
Expand Down Expand Up @@ -624,24 +633,30 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
where = { ...(where as Dictionary), ...cond } as FilterQuery<T>;
}

/* istanbul ignore if */
options = { ...options };

orderBy = this.getPivotOrderBy(prop, orderBy);
const qb = this.createQueryBuilder<T>(prop.type, ctx, options?.connectionType)
const qb = this.createQueryBuilder<T>(prop.type, ctx, options.connectionType)
.indexHint(options.indexHint!)
.comment(options.comments!)
.hintComment(options.hintComments!)
.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES)
.withSchema(this.getSchemaName(prop.targetMeta, options));
const populate = this.autoJoinOneToOneOwner(prop.targetMeta!, [{ field: prop.pivotEntity }]);
const fields = this.buildFields(prop.targetMeta!, (options?.populate ?? []) as unknown as PopulateOptions<T>[], [], qb, options?.fields as Field<T>[]);
qb.select(fields).populate(populate).where(where).orderBy(orderBy!).setLockMode(options?.lockMode, options?.lockTableAliases);
const fields = this.buildFields(prop.targetMeta!, (options.populate ?? []) as unknown as PopulateOptions<T>[], [], qb, options.fields as Field<T>[]);
qb.select(fields).populate(populate).where(where).orderBy(orderBy!).setLockMode(options.lockMode, options.lockTableAliases);

if (owners.length === 1 && (options?.offset != null || options?.limit != null)) {
if (owners.length === 1 && (options.offset != null || options.limit != null)) {
qb.limit(options.limit, options.offset);
}

if (prop.targetMeta!.schema !== '*' && pivotMeta.schema === '*' && options?.schema) {
if (prop.targetMeta!.schema !== '*' && pivotMeta.schema === '*' && options.schema) {
// eslint-disable-next-line dot-notation
qb['finalize']();
// eslint-disable-next-line dot-notation
Object.values(qb['_joins']).forEach(join => {
join.schema = options.schema;
join.schema = options!.schema;
});
}

Expand Down
25 changes: 25 additions & 0 deletions packages/knex/src/query/QueryBuilder.ts
Expand Up @@ -106,6 +106,8 @@ export class QueryBuilder<T extends object = AnyEntity> {
private _joinedProps = new Map<string, PopulateOptions<any>>();
private _cache?: boolean | number | [string, number];
private _indexHint?: string;
private _comments: string[] = [];
private _hintComments: string[] = [];
private flushMode?: FlushMode;
private lockMode?: LockMode;
private lockTables?: string[];
Expand Down Expand Up @@ -485,6 +487,26 @@ export class QueryBuilder<T extends object = AnyEntity> {
return this;
}

/**
* Prepend comment to the sql query using the syntax `/* ... *&#8205;/`. Some characters are forbidden such as `/*, *&#8205;/` and `?`.
*/
comment(comment: string | string[]): this {
this.ensureNotFinalized();
this._comments.push(...Utils.asArray(comment));
return this;
}

/**
* Add hints to the query using comment-like syntax `/*+ ... *&#8205;/`. MySQL and Oracle use this syntax for optimizer hints.
* Also various DB proxies and routers use this syntax to pass hints to alter their behavior. In other dialects the hints
* are ignored as simple comments.
*/
hintComment(comment: string | string[]): this {
this.ensureNotFinalized();
this._hintComments.push(...Utils.asArray(comment));
return this;
}

/**
* Specifies FROM which entity's table select/update/delete will be executed, removing all previously set FROM-s.
* Allows setting a main string alias of the selection data.
Expand Down Expand Up @@ -526,6 +548,8 @@ export class QueryBuilder<T extends object = AnyEntity> {
}, this._orderBy);
Utils.runIfNotEmpty(() => qb.limit(this._limit!), this._limit != null);
Utils.runIfNotEmpty(() => qb.offset(this._offset!), this._offset);
Utils.runIfNotEmpty(() => this._comments.forEach(comment => qb.comment(comment)), this._comments);
Utils.runIfNotEmpty(() => this._hintComments.forEach(comment => qb.hintComment(comment)), this._hintComments);
Utils.runIfNotEmpty(() => this.helper.appendOnConflictClause(this.type ?? QueryType.SELECT, this._onConflict!, qb), this._onConflict);

if (this.type === QueryType.TRUNCATE && this.platform.usesCascadeStatement()) {
Expand Down Expand Up @@ -764,6 +788,7 @@ export class QueryBuilder<T extends object = AnyEntity> {
const properties = [
'flags', '_populate', '_populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy',
'_schema', '_indexHint', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
'_comments', '_hintComments',
];
properties.forEach(prop => (qb as any)[prop] = Utils.copy(this[prop]));

Expand Down
14 changes: 14 additions & 0 deletions tests/EntityManager.mysql.test.ts
Expand Up @@ -2405,6 +2405,20 @@ describe('EntityManagerMySql', () => {
expect(res2).toMatchObject([{ count: 1 }]);
});

test('query comments', async () => {
const mock = mockLogger(orm, ['query']);
await orm.em.find(Author2, {}, {
comments: ['foo'],
hintComments: 'bar',
indexHint: 'force index(custom_email_index_name)',
});
expect(mock.mock.calls[0][0]).toMatch(
'/* foo */ select /*+ bar */ `a0`.*, `a1`.`author_id` as `address_author_id`' +
' from `author2` as `a0` force index(custom_email_index_name)' +
' left join `address2` as `a1` on `a0`.`id` = `a1`.`author_id`',
);
});

// this should run in ~800ms (when running single test locally)
test('perf: batch insert and update', async () => {
const authors = new Set<Author2>();
Expand Down
59 changes: 59 additions & 0 deletions tests/QueryBuilder.test.ts
Expand Up @@ -2757,6 +2757,36 @@ describe('QueryBuilder', () => {
expect(qb.getParams()).toEqual(['^c.o.*l-te.*st.c.m$']);
}

// query comments
{
const sql1 = pg.em.createQueryBuilder(Author2)
.comment('test 123')
.hintComment('test 123')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
expect(sql1).toBe(`/* test 123 */ select /*+ test 123 */ "a0".* from "author2" as "a0" where "a0"."favourite_book_uuid_pk" in ('1', '2', '3')`);

const sql2 = pg.em.createQueryBuilder(Author2).withSchema('my_schema')
.comment('test 123')
.comment('test 456')
.hintComment('test 123')
.hintComment('test 456')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
expect(sql2).toBe(`/* test 123 */ /* test 456 */ select /*+ test 123 test 456 */ "a0".* from "my_schema"."author2" as "a0" where "a0"."favourite_book_uuid_pk" in ('1', '2', '3')`);

const sql3 = pg.em.createQueryBuilder(Author2).withSchema('my_schema')
.update({ name: '...' })
.comment('test 123')
.comment('test 456')
.hintComment('test 123')
.hintComment('test 456')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
// works only with select queries
expect(sql3).toBe(`update "my_schema"."author2" set "name" = '...' where "favourite_book_uuid_pk" in ('1', '2', '3')`);
}

await pg.close(true);
});

Expand Down Expand Up @@ -2887,6 +2917,35 @@ describe('QueryBuilder', () => {
expect(sql3).toBe("update `my_schema`.`author2` force index(custom_email_index_name) set `name` = '...' where `favourite_book_uuid_pk` in ('1', '2', '3')");
});

test('query comments', async () => {
const sql1 = orm.em.createQueryBuilder(Author2)
.comment('test 123')
.hintComment('test 123')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
expect(sql1).toBe("/* test 123 */ select /*+ test 123 */ `e0`.* from `author2` as `e0` where `e0`.`favourite_book_uuid_pk` in ('1', '2', '3')");

const sql2 = orm.em.createQueryBuilder(Author2).withSchema('my_schema')
.comment('test 123')
.comment('test 456')
.hintComment('test 123')
.hintComment('test 456')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
expect(sql2).toBe("/* test 123 */ /* test 456 */ select /*+ test 123 test 456 */ `e0`.* from `my_schema`.`author2` as `e0` where `e0`.`favourite_book_uuid_pk` in ('1', '2', '3')");

const sql3 = orm.em.createQueryBuilder(Author2).withSchema('my_schema')
.update({ name: '...' })
.comment('test 123')
.comment('test 456')
.hintComment('test 123')
.hintComment('test 456')
.where({ favouriteBook: { $in: ['1', '2', '3'] } })
.getFormattedQuery();
// works only with select queries
expect(sql3).toBe("update `my_schema`.`author2` set `name` = '...' where `favourite_book_uuid_pk` in ('1', '2', '3')");
});

test('$or operator inside auto-joined relation', async () => {
const query = {
author: {
Expand Down

0 comments on commit 06d4d20

Please sign in to comment.