From e42b9874ea156bdfb8468d61c346cd768aa1aaf4 Mon Sep 17 00:00:00 2001 From: Denys Otrishko Date: Fri, 18 Aug 2023 13:29:32 +0300 Subject: [PATCH 1/4] feat(select): support custom function in select --- doc/sql.md | 6 ++++++ lib/conditions-builder.js | 7 ++++-- lib/query-builder.js | 22 +++++++++++++++++++ lib/raw-builder.js | 21 ------------------ lib/select-builder.js | 13 +++++++++++- sql.js | 3 +-- test/sqlgen.js | 41 +++++++++++++++++++++++++++++++++++- types/lib/query-builder.d.ts | 11 ++++++++++ types/lib/raw-builder.d.ts | 12 ----------- types/sql.d.ts | 1 - 10 files changed, 97 insertions(+), 40 deletions(-) delete mode 100644 lib/raw-builder.js delete mode 100644 types/lib/raw-builder.d.ts diff --git a/doc/sql.md b/doc/sql.md index 6bf0643..5556544 100644 --- a/doc/sql.md +++ b/doc/sql.md @@ -7,6 +7,7 @@ - [QueryBuilder.prototype.buildParams](#querybuilderprototypebuildparams) - [QueryBuilder.prototype.makeKeyOrExpr](#querybuilderprototypemakekeyorexprvalue-wrap--false) - [QueryBuilder.prototype.makeParamValue](#querybuilderprototypemakeparamvaluevalue) + - [QueryBuilder.prototype.raw](#querybuilderprototyperawsqltemplate) - [QueryConditionsBuilder](#class-queryconditionsbuilder-extends-querybuilder) - [QueryConditionsBuilder.prototype.constructor](#queryconditionsbuilderprototypeconstructorparams-options) - [QueryConditionsBuilder.prototype.\_whereValueMapper](#queryconditionsbuilderprototype_wherevaluemappervalue) @@ -69,6 +70,7 @@ - [SelectBuilder.prototype.orderBy](#selectbuilderprototypeorderbyfield-dir--asc) - [SelectBuilder.prototype.select](#selectbuilderprototypeselectfields) - [SelectBuilder.prototype.selectAs](#selectbuilderprototypeselectasfield-alias) + - [SelectBuilder.prototype.selectRaw](#selectbuilderprototypeselectrawsqlorbuilder) - [SelectBuilder.prototype.sum](#selectbuilderprototypesumfield-alias) - [RawBuilder](#class-rawbuilder-extends-querybuilder) - [RawBuilder.prototype.constructor](#rawbuilderprototypeconstructorsqltemplate) @@ -187,6 +189,8 @@ Build params for this query #### QueryBuilder.prototype.makeParamValue(value) +#### QueryBuilder.prototype.raw(sqlTemplate) + ### class QueryConditionsBuilder extends [QueryBuilder][sql-querybuilder] #### QueryConditionsBuilder.prototype.constructor(params, options) @@ -311,6 +315,8 @@ Build params for this query #### SelectBuilder.prototype.selectAs(field, alias) +#### SelectBuilder.prototype.selectRaw(sqlOrBuilder) + #### SelectBuilder.prototype.sum(field, alias) ### class RawBuilder extends [QueryBuilder][sql-querybuilder] diff --git a/lib/conditions-builder.js b/lib/conditions-builder.js index 683e740..ffa7565 100644 --- a/lib/conditions-builder.js +++ b/lib/conditions-builder.js @@ -1,8 +1,11 @@ 'use strict'; -const { makeParamValue, QueryBuilder } = require('./query-builder.js'); +const { + makeParamValue, + QueryBuilder, + RawBuilder, +} = require('./query-builder.js'); const { mapJoinIterable } = require('./utils.js'); -const { RawBuilder } = require('./raw-builder.js'); const allowedConditions = new Set([ '=', diff --git a/lib/query-builder.js b/lib/query-builder.js index 543e838..f8397ab 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -29,6 +29,11 @@ class QueryBuilder { return this.escapeKey(value); } + raw(sqlTemplate) { + // eslint-disable-next-line no-use-before-define + return new RawBuilder(sqlTemplate, this.params, this.options); + } + // Build and return the SQL query // Returns: build() { @@ -42,6 +47,22 @@ class QueryBuilder { } } +class RawBuilder extends QueryBuilder { + // sqlTemplate function or sql string + // params + // Returns: query + constructor(sqlTemplate, params, options) { + super(params, options); + this.sqlTemplate = sqlTemplate; + } + + build() { + return typeof this.sqlTemplate === 'function' + ? this.sqlTemplate(this.params) + : this.sqlTemplate; + } +} + function makeParamValue(value, params) { if (value instanceof QueryBuilder) { return '(' + value.build() + ')'; @@ -70,6 +91,7 @@ function checkTypeOrQuery(value, name, type) { module.exports = { QueryBuilder, + RawBuilder, makeParamValue, makeKeyWithAlias, checkTypeOrQuery, diff --git a/lib/raw-builder.js b/lib/raw-builder.js deleted file mode 100644 index febe16d..0000000 --- a/lib/raw-builder.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const { QueryBuilder } = require('./query-builder.js'); - -class RawBuilder extends QueryBuilder { - // sqlTemplate function or sql string - // params - // Returns: query - constructor(sqlTemplate, params, options) { - super(params, options); - this.sqlTemplate = sqlTemplate; - } - - build() { - return typeof this.sqlTemplate === 'function' - ? this.sqlTemplate(this.params) - : this.sqlTemplate; - } -} - -module.exports = { RawBuilder }; diff --git a/lib/select-builder.js b/lib/select-builder.js index 8c3f3d6..f7bd798 100644 --- a/lib/select-builder.js +++ b/lib/select-builder.js @@ -2,7 +2,11 @@ const { iter } = require('@metarhia/iterator'); -const { checkTypeOrQuery, makeKeyWithAlias } = require('./query-builder.js'); +const { + QueryBuilder, + checkTypeOrQuery, + makeKeyWithAlias, +} = require('./query-builder.js'); const { QueryConditionsBuilder } = require('./query-conditions-builder.js'); const { mapJoinIterable } = require('./utils.js'); @@ -49,6 +53,11 @@ class SelectBuilder extends QueryConditionsBuilder { return this._addSelectClause(undefined, field, alias); } + selectRaw(sqlOrBuilder) { + this.operations.select.add({ sql: sqlOrBuilder }); + return this; + } + innerJoin(tableName, leftKey, rightKey) { this.operations.innerJoin.push({ table: this.escapeIdentifier(tableName), @@ -141,6 +150,8 @@ class SelectBuilder extends QueryConditionsBuilder { select, (op) => { if (typeof op === 'string') return op; + if (typeof op.sql === 'string') return op.sql; + if (op.sql instanceof QueryBuilder) return op.sql.build(); let clause = op.type === undefined ? op.field : functionHandlers[op.type](op); if (op.alias) clause += ` AS ${op.alias}`; diff --git a/sql.js b/sql.js index 2bb18bb..92eee6c 100644 --- a/sql.js +++ b/sql.js @@ -1,6 +1,6 @@ 'use strict'; -const { QueryBuilder } = require('./lib/query-builder.js'); +const { QueryBuilder, RawBuilder } = require('./lib/query-builder.js'); const { QueryConditionsBuilder } = require('./lib/query-conditions-builder.js'); const { SelectBuilder } = require('./lib/select-builder.js'); const { UpdateBuilder } = require('./lib/update-builder.js'); @@ -8,7 +8,6 @@ const { DeleteBuilder } = require('./lib/delete-builder.js'); const { InsertBuilder } = require('./lib/insert-builder.js'); const { PgInsertBuilder } = require('./lib/pg-insert-builder.js'); const { PgSelectBuilder } = require('./lib/pg-select-builder.js'); -const { RawBuilder } = require('./lib/raw-builder.js'); const { ConditionsBuilder } = require('./lib/conditions-builder.js'); const { ParamsBuilder } = require('./lib/params-builder.js'); const { PostgresParamsBuilder } = require('./lib/pg-params-builder.js'); diff --git a/test/sqlgen.js b/test/sqlgen.js index b5b6333..485f496 100644 --- a/test/sqlgen.js +++ b/test/sqlgen.js @@ -2,7 +2,7 @@ const { testSync } = require('metatests'); const { SelectBuilder } = require('../lib/select-builder'); -const { RawBuilder } = require('../lib/raw-builder'); +const { RawBuilder } = require('../lib/query-builder.js'); const { PostgresParamsBuilder } = require('../lib/pg-params-builder'); const allowedConditions = new Set([ @@ -573,6 +573,45 @@ test.testSync('Select multiple operations', (test, { builder, params }) => { test.strictSame(params.build(), []); }); +test.testSync('Select custom operation', (test, { builder, params }) => { + builder.from('table').selectRaw('array_agg("f1")').whereEq('f2', 42); + const query = builder.build(); + test.strictSame(query, 'SELECT array_agg("f1") FROM "table" WHERE "f2" = $1'); + test.strictSame(params.build(), [42]); +}); + +test.testSync( + 'Select custom operation with QueryBuilder', + (test, { builder, params }) => { + builder + .from('table') + .selectRaw(new RawBuilder('array_agg("f1")')) + .whereEq('f2', 42); + const query = builder.build(); + test.strictSame( + query, + 'SELECT array_agg("f1") FROM "table" WHERE "f2" = $1' + ); + test.strictSame(params.build(), [42]); + } +); + +test.testSync( + 'Select custom operation with QueryBuilder', + (test, { builder, params }) => { + builder + .from('table') + .selectRaw(builder.raw('array_agg("f1")')) + .whereEq('f2', 42); + const query = builder.build(); + test.strictSame( + query, + 'SELECT array_agg("f1") FROM "table" WHERE "f2" = $1' + ); + test.strictSame(params.build(), [42]); + } +); + test.testSync( 'Select multiple operations order', (test, { builder, params }) => { diff --git a/types/lib/query-builder.d.ts b/types/lib/query-builder.d.ts index c739f50..83e1c37 100644 --- a/types/lib/query-builder.d.ts +++ b/types/lib/query-builder.d.ts @@ -12,8 +12,19 @@ export class QueryBuilder { makeKeyOrExpr(value: string | QueryBuilder): string; + raw(sqlTemplate: SqlTemplate): RawBuilder; // Build and return the SQL query. build(): string; buildParams(): unknown[]; } + +export type SqlTemplate = string | ((p: ParamsBuilder) => string); + +export class RawBuilder extends QueryBuilder { + constructor( + sqlTemplate: SqlTemplate, + params: ParamsBuilder, + options?: QueryBuilderOptions + ); +} diff --git a/types/lib/raw-builder.d.ts b/types/lib/raw-builder.d.ts deleted file mode 100644 index 62c74bb..0000000 --- a/types/lib/raw-builder.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ParamsBuilder } from './params-builder'; -import { QueryBuilder, QueryBuilderOptions } from './query-builder'; - -export type SqlTemplate = string | ((p: ParamsBuilder) => string); - -export class RawBuilder extends QueryBuilder { - constructor( - sqlTemplate: SqlTemplate, - params: ParamsBuilder, - options?: QueryBuilderOptions - ); -} diff --git a/types/sql.d.ts b/types/sql.d.ts index 6bafb63..35ce300 100644 --- a/types/sql.d.ts +++ b/types/sql.d.ts @@ -1,6 +1,5 @@ export * from './lib/query-builder'; export * from './lib/query-conditions-builder'; -export * from './lib/raw-builder'; export { SelectBuilder, SelectBuilderOptions } from './lib/select-builder'; export { ConditionsBuilder, From 02e2254d1a106cc34860e93763fe4de77aa368d9 Mon Sep 17 00:00:00 2001 From: Denys Otrishko Date: Fri, 18 Aug 2023 13:39:39 +0300 Subject: [PATCH 2/4] feat(select): support select function in select --- doc/sql.md | 3 +++ lib/select-builder.js | 19 ++++++++++--------- test/sqlgen.js | 17 +++++++++++++++++ types/lib/select-builder.d.ts | 2 ++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/doc/sql.md b/doc/sql.md index 5556544..b21e7dd 100644 --- a/doc/sql.md +++ b/doc/sql.md @@ -70,6 +70,7 @@ - [SelectBuilder.prototype.orderBy](#selectbuilderprototypeorderbyfield-dir--asc) - [SelectBuilder.prototype.select](#selectbuilderprototypeselectfields) - [SelectBuilder.prototype.selectAs](#selectbuilderprototypeselectasfield-alias) + - [SelectBuilder.prototype.selectFn](#selectbuilderprototypeselectfnfn-field-alias) - [SelectBuilder.prototype.selectRaw](#selectbuilderprototypeselectrawsqlorbuilder) - [SelectBuilder.prototype.sum](#selectbuilderprototypesumfield-alias) - [RawBuilder](#class-rawbuilder-extends-querybuilder) @@ -315,6 +316,8 @@ Build params for this query #### SelectBuilder.prototype.selectAs(field, alias) +#### SelectBuilder.prototype.selectFn(fn, field, alias) + #### SelectBuilder.prototype.selectRaw(sqlOrBuilder) #### SelectBuilder.prototype.sum(field, alias) diff --git a/lib/select-builder.js b/lib/select-builder.js index f7bd798..293ceae 100644 --- a/lib/select-builder.js +++ b/lib/select-builder.js @@ -11,11 +11,7 @@ const { QueryConditionsBuilder } = require('./query-conditions-builder.js'); const { mapJoinIterable } = require('./utils.js'); const functionHandlers = { - count: (op) => `count(${op.field})`, - avg: (op) => `avg(${op.field})`, - min: (op) => `min(${op.field})`, - max: (op) => `max(${op.field})`, - sum: (op) => `sum(${op.field})`, + default: (op) => `${op.type}(${op.field})`, }; class SelectBuilder extends QueryConditionsBuilder { @@ -53,6 +49,10 @@ class SelectBuilder extends QueryConditionsBuilder { return this._addSelectClause(undefined, field, alias); } + selectFn(fn, field, alias) { + return this._addSelectClause(fn, field, alias); + } + selectRaw(sqlOrBuilder) { this.operations.select.add({ sql: sqlOrBuilder }); return this; @@ -152,10 +152,11 @@ class SelectBuilder extends QueryConditionsBuilder { if (typeof op === 'string') return op; if (typeof op.sql === 'string') return op.sql; if (op.sql instanceof QueryBuilder) return op.sql.build(); - let clause = - op.type === undefined ? op.field : functionHandlers[op.type](op); - if (op.alias) clause += ` AS ${op.alias}`; - return clause; + if (op.type) { + const handler = functionHandlers[op.type] ?? functionHandlers.default; + return handler(op) + (op.alias ? ` AS ${op.alias}` : ''); + } + return op.field + (op.alias ? ` AS ${op.alias}` : ''); }, ', ' ); diff --git a/test/sqlgen.js b/test/sqlgen.js index 485f496..07d609f 100644 --- a/test/sqlgen.js +++ b/test/sqlgen.js @@ -573,6 +573,23 @@ test.testSync('Select multiple operations', (test, { builder, params }) => { test.strictSame(params.build(), []); }); +test.testSync('Select fn operation', (test, { builder, params }) => { + builder.from('table').selectFn('my_function', 'f1').whereEq('f2', 42); + const query = builder.build(); + test.strictSame( + query, + 'SELECT my_function("f1") FROM "table" WHERE "f2" = $1' + ); + test.strictSame(params.build(), [42]); +}); + +test.testSync('Select fn operation', (test, { builder, params }) => { + builder.from('table').selectFn('array_agg', 'f1').whereEq('f2', 42); + const query = builder.build(); + test.strictSame(query, 'SELECT array_agg("f1") FROM "table" WHERE "f2" = $1'); + test.strictSame(params.build(), [42]); +}); + test.testSync('Select custom operation', (test, { builder, params }) => { builder.from('table').selectRaw('array_agg("f1")').whereEq('f2', 42); const query = builder.build(); diff --git a/types/lib/select-builder.d.ts b/types/lib/select-builder.d.ts index 33befe9..91ad40d 100644 --- a/types/lib/select-builder.d.ts +++ b/types/lib/select-builder.d.ts @@ -22,6 +22,8 @@ export class SelectBuilder extends QueryConditionsBuilder< selectAs(field: string, alias: string): this; + selectFn(fn: string, field: string, alias: string): this; + innerJoin(tableName: string, leftKey: string, rightKey: string): this; distinct(): this; From 626ee92b69e2b695b808fcff96dda63e4e93a877 Mon Sep 17 00:00:00 2001 From: Denys Otrishko Date: Fri, 18 Aug 2023 13:41:19 +0300 Subject: [PATCH 3/4] feat(select): support select function in select --- test/conditions-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/conditions-builder.js b/test/conditions-builder.js index 9149157..ec14a64 100644 --- a/test/conditions-builder.js +++ b/test/conditions-builder.js @@ -2,7 +2,7 @@ const { SelectBuilder } = require('../lib/select-builder'); const { testSync } = require('metatests'); -const { RawBuilder } = require('../lib/raw-builder'); +const { RawBuilder } = require('../lib/query-builder.js'); const { ConditionsBuilder } = require('../lib/conditions-builder'); const { PostgresParamsBuilder } = require('../lib/pg-params-builder'); From 5ea7674211493c01b819babfca47530433157640 Mon Sep 17 00:00:00 2001 From: Denys Otrishko Date: Fri, 18 Aug 2023 13:43:20 +0300 Subject: [PATCH 4/4] feat(select): support select function in select --- types/lib/conditions-builder.d.ts | 7 +++++-- types/lib/query-conditions-builder.d.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/types/lib/conditions-builder.d.ts b/types/lib/conditions-builder.d.ts index 81feebe..8ea9891 100644 --- a/types/lib/conditions-builder.d.ts +++ b/types/lib/conditions-builder.d.ts @@ -1,6 +1,9 @@ import { ParamsBuilder } from './params-builder'; -import { QueryBuilder, QueryBuilderOptions } from './query-builder'; -import { SqlTemplate } from './raw-builder'; +import { + QueryBuilder, + QueryBuilderOptions, + SqlTemplate, +} from './query-builder'; export interface ConditionsBuilderOptions extends QueryBuilderOptions {} diff --git a/types/lib/query-conditions-builder.d.ts b/types/lib/query-conditions-builder.d.ts index f4995df..75de897 100644 --- a/types/lib/query-conditions-builder.d.ts +++ b/types/lib/query-conditions-builder.d.ts @@ -1,7 +1,10 @@ import { ParamsBuilder } from './params-builder'; import { SelectQueryValue } from './select-builder'; -import { QueryBuilder, QueryBuilderOptions } from './query-builder'; -import { SqlTemplate } from './raw-builder'; +import { + QueryBuilder, + QueryBuilderOptions, + SqlTemplate, +} from './query-builder'; // Utility class for all proxy Conditions methods. export class QueryConditionsBuilder<