Skip to content

Commit

Permalink
Add ability to prepend query comments (#5289)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Fink <ryanjfink@gmail.com>
  • Loading branch information
francois2metz and rfink committed Apr 14, 2023
1 parent ce56f40 commit 704d12e
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/dialects/mssql/query/mssql-querycompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
} = require('../../../formatter/wrappingFormatter');

const components = [
'comments',
'columns',
'join',
'lock',
Expand Down
1 change: 1 addition & 0 deletions lib/dialects/oracle/query/oracle-querycompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { ReturningHelper } = require('../utils');
const { isString } = require('../../../util/is');

const components = [
'comments',
'columns',
'join',
'where',
Expand Down
17 changes: 17 additions & 0 deletions lib/query/querybuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Builder extends EventEmitter {
this.client = client;
this.and = this;
this._single = {};
this._comments = [];
this._statements = [];
this._method = 'select';
if (client.config) {
Expand Down Expand Up @@ -88,6 +89,7 @@ class Builder extends EventEmitter {
const cloned = new this.constructor(this.client);
cloned._method = this._method;
cloned._single = clone(this._single);
cloned._comments = clone(this._comments);
cloned._statements = clone(this._statements);
cloned._debug = this._debug;

Expand Down Expand Up @@ -235,6 +237,21 @@ class Builder extends EventEmitter {
return this;
}

// Adds a comment to the query
comment(txt) {
if (!isString(txt)) {
throw new Error('Comment must be a string');
}
const forbiddenChars = ['/*', '*/', '?'];
if (forbiddenChars.some((chars) => txt.includes(chars))) {
throw new Error(`Cannot include ${forbiddenChars.join(', ')} in comment`);
}
this._comments.push({
comment: txt,
});
return this;
}

// Allow for a sub-select to be explicitly aliased as a column,
// without needing to compile the query in a where.
as(column) {
Expand Down
11 changes: 11 additions & 0 deletions lib/query/querycompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
const debugBindings = debug('knex:bindings');

const components = [
'comments',
'columns',
'join',
'where',
Expand All @@ -50,6 +51,7 @@ class QueryCompiler {
this.method = builder._method || 'select';
this.options = builder._options;
this.single = builder._single;
this.queryComments = builder._comments;
this.timeout = builder._timeout || false;
this.cancelOnTimeout = builder._cancelOnTimeout || false;
this.grouped = groupBy(builder._statements, 'grouping');
Expand Down Expand Up @@ -138,6 +140,7 @@ class QueryCompiler {
case 'union':
unionStatement = statement;
break;
case 'comments':
case 'columns':
case 'join':
case 'where':
Expand Down Expand Up @@ -317,6 +320,14 @@ class QueryCompiler {
);
}

// Add comments to the query
comments() {
if (!this.queryComments.length) return '';
return this.queryComments
.map((comment) => `/* ${comment.comment} */`)
.join(' ');
}

_aggregate(stmt, { aliasSeparator = ' as ', distinctParentheses } = {}) {
const value = stmt.value;
const method = stmt.method;
Expand Down
10 changes: 10 additions & 0 deletions test/integration2/query/select/selects.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,16 @@ describe('Selects', function () {
});
});

it('#1982 - Allow comments in db', async function () {
await knex('accounts')
.comment('Integration Comment')
.select('first_name', 'email')
.limit(1)
.then(function(results) {
expect(results).to.have.length(1);
});
});

describe('recursive CTE support', function () {
before(async function () {
await knex.schema.dropTableIfExists('rcte');
Expand Down
150 changes: 150 additions & 0 deletions test/unit/query/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -9440,6 +9440,156 @@ describe('QueryBuilder', () => {
);
});

it('#1982 - should allow query comments in querybuilder', () => {
testsql(
qb()
.from('testtable')
.comment('Added comment 1')
.comment('Added comment 2'),
{
mysql: {
sql:
'/* Added comment 1 */ /* Added comment 2 */ select * from `testtable`',
bindings: [],
},
oracledb: {
sql:
'/* Added comment 1 */ /* Added comment 2 */ select * from "testtable"',
bindings: [],
},
mssql: {
sql:
'/* Added comment 1 */ /* Added comment 2 */ select * from [testtable]',
bindings: [],
},
pg: {
sql:
'/* Added comment 1 */ /* Added comment 2 */ select * from "testtable"',
bindings: [],
},
'pg-redshift': {
sql:
'/* Added comment 1 */ /* Added comment 2 */ select * from "testtable"',
bindings: [],
},
}
);
});

it('#1982 (2) - should throw error on non string', () => {
try {
testsql(
qb()
.from('testtable')
.comment({ prop: 'val' }),
{
mysql: {
sql: '',
bindings: [],
},
oracledb: {
sql: '',
bindings: [],
},
mssql: {
sql: '',
bindings: [],
},
pg: {
sql: '',
bindings: [],
},
'pg-redshift': {
sql: '',
bindings: [],
},
}
);
expect(true).to.equal(
false,
'Expected to throw error in compilation about non-string'
);
} catch (error) {
expect(error.message).to.contain('Comment must be a string');
}
});

it('#1982 (3) - should throw error when there is subcomments', () => {
try {
testsql(
qb()
.from('testtable')
.comment('/* Hello world'),
{
mysql: {
sql: '',
bindings: [],
},
oracledb: {
sql: '',
bindings: [],
},
mssql: {
sql: '',
bindings: [],
},
pg: {
sql: '',
bindings: [],
},
'pg-redshift': {
sql: '',
bindings: [],
},
}
);
expect(true).to.equal(
false,
'Expected to throw error in compilation about non-string'
);
} catch (error) {
expect(error.message).to.contain('Cannot include /*, */, ? in comment');
}
});

it('#1982 (4) - should throw error when there is question mark', () => {
try {
testsql(
qb()
.from('testtable')
.comment('?'),
{
mysql: {
sql: '',
bindings: [],
},
oracledb: {
sql: '',
bindings: [],
},
mssql: {
sql: '',
bindings: [],
},
pg: {
sql: '',
bindings: [],
},
'pg-redshift': {
sql: '',
bindings: [],
},
}
);
expect(true).to.equal(
false,
'Expected to throw error in compilation about non-string'
);
} catch (error) {
expect(error.message).to.contain('Cannot include /*, */, ? in comment');
}
});

it('#4199 - allows hint comments in subqueries', () => {
testsql(
qb()
Expand Down
5 changes: 5 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ export declare namespace Knex {
as: As<TRecord, TResult>;
columns: Select<TRecord, TResult>;
column: Select<TRecord, TResult>;
comment: Comment<TRecord, TResult>;
hintComment: HintComment<TRecord, TResult>;
from: Table<TRecord, TResult>;
fromRaw: Table<TRecord, TResult>;
Expand Down Expand Up @@ -1359,6 +1360,10 @@ export declare namespace Knex {
): QueryBuilder<TRecord, TResult>;
}

interface Comment<TRecord extends {} = any, TResult = any> {
(comment: string): QueryBuilder<TRecord, TResult>;
}

interface HintComment<TRecord extends {} = any, TResult = any> {
(hint: string): QueryBuilder<TRecord, TResult>;
(hints: readonly string[]): QueryBuilder<TRecord, TResult>;
Expand Down

0 comments on commit 704d12e

Please sign in to comment.