diff --git a/.run/All Tests.run.xml b/.run/All Tests.run.xml index 7a1550c..dabda1f 100644 --- a/.run/All Tests.run.xml +++ b/.run/All Tests.run.xml @@ -5,8 +5,8 @@ - - + + diff --git a/.run/database query builder test.test aggregate functions with distinct.run.xml b/.run/database query builder test.test aggregate functions with distinct.run.xml new file mode 100644 index 0000000..23999f5 --- /dev/null +++ b/.run/database query builder test.test aggregate functions with distinct.run.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/fedaco/src/index.ts b/libs/fedaco/src/index.ts index b3f29c8..296f33c 100644 --- a/libs/fedaco/src/index.ts +++ b/libs/fedaco/src/index.ts @@ -175,7 +175,7 @@ export * from './query/ast/assignment-set-clause'; export * from './query/ast/query-expression'; export * from './query/ast/fragment/order/reject-order-element-expression'; export * from './query/ast/fragment/union-fragment'; -export * from './query/ast/fragment/aggregate-fragment'; +export * from './query/ast/fragment/aggregate-function-call-fragment'; export * from './query/ast/fragment/nested-expression'; export * from './query/ast/fragment/raw-fragment'; export * from './query/ast/fragment/expression/nested-predicate-expression'; diff --git a/libs/fedaco/src/query-builder/builder.ts b/libs/fedaco/src/query-builder/builder.ts index 93dcefc..748345a 100644 --- a/libs/fedaco/src/query-builder/builder.ts +++ b/libs/fedaco/src/query-builder/builder.ts @@ -7,7 +7,7 @@ import type { ColumnReferenceExpression } from '../query/ast/column-reference-expression'; import type { RawBindingExpression } from '../query/ast/expression/raw-binding-expression'; import type { RawExpression } from '../query/ast/expression/raw-expression'; -import type { AggregateFragment } from '../query/ast/fragment/aggregate-fragment'; +import type { AggregateFunctionCallFragment } from '../query/ast/fragment/aggregate-function-call-fragment'; import type { JoinFragment } from '../query/ast/fragment/join-fragment'; import type { UnionFragment } from '../query/ast/fragment/union-fragment'; import type { FromTable } from '../query/ast/from-table'; @@ -74,7 +74,7 @@ export abstract class Builder extends mixinJoin( 'insert' : [] }; /*An aggregate function and column to be run.*/ - _aggregate: AggregateFragment; + _aggregate: AggregateFunctionCallFragment; /*The columns that should be returned.*/ _columns: Array = []; /*Indicates if the query returns distinct results. diff --git a/libs/fedaco/src/query-builder/grammar.interface.ts b/libs/fedaco/src/query-builder/grammar.interface.ts index 2716676..99ca930 100644 --- a/libs/fedaco/src/query-builder/grammar.interface.ts +++ b/libs/fedaco/src/query-builder/grammar.interface.ts @@ -38,7 +38,9 @@ export interface GrammarInterface { compilePredicateFuncName(funcName: string): string; - distinct(builder: T, distinct: boolean | any[]): string; + distinct(distinct: boolean | any[]): string; + + distinctInAggregateFunctionCall(distinct: boolean | any[]): string; getOperators(): string[]; diff --git a/libs/fedaco/src/query-builder/grammar/postgres-query-grammar.ts b/libs/fedaco/src/query-builder/grammar/postgres-query-grammar.ts index f13bee1..abf6d42 100644 --- a/libs/fedaco/src/query-builder/grammar/postgres-query-grammar.ts +++ b/libs/fedaco/src/query-builder/grammar/postgres-query-grammar.ts @@ -71,14 +71,10 @@ export class PostgresQueryGrammar extends QueryGrammar implements GrammarInterfa } - distinct(query: QueryBuilder, distinct: boolean | any[]): string { - if (! isBlank(query._aggregate)) { - return ''; - } - - if(isArray(distinct)) { - return `DISTINCT ON (${this.columnize(distinct)})` - }else if (distinct === true) { + distinct(distinct: boolean | any[]): string { + if (isArray(distinct)) { + return `DISTINCT ON (${this.columnize(distinct)})`; + } else if (distinct === true) { return 'DISTINCT'; } else { return ''; diff --git a/libs/fedaco/src/query-builder/grammar/query-grammar.ts b/libs/fedaco/src/query-builder/grammar/query-grammar.ts index 97227ab..464037d 100644 --- a/libs/fedaco/src/query-builder/grammar/query-grammar.ts +++ b/libs/fedaco/src/query-builder/grammar/query-grammar.ts @@ -13,6 +13,7 @@ import { DeleteSpecification } from '../../query/ast/delete-specification'; import { ConditionExpression } from '../../query/ast/expression/condition-expression'; import { FunctionCallExpression } from '../../query/ast/expression/function-call-expression'; import { ParenthesizedExpression } from '../../query/ast/expression/parenthesized-expression'; +import { AggregateFunctionCallFragment } from '../../query/ast/fragment/aggregate-function-call-fragment'; import type { NestedExpression } from '../../query/ast/fragment/nested-expression'; import { FromClause } from '../../query/ast/from-clause'; import { FromTable } from '../../query/ast/from-table'; @@ -37,7 +38,7 @@ import { WhereClause } from '../../query/ast/where-clause'; import { SqlParser } from '../../query/parser/sql-parser'; import type { SqlNode } from '../../query/sql-node'; import type { SqlVisitor } from '../../query/sql-visitor'; -import { bindingVariable, createIdentifier, raw } from '../ast-factory'; +import { bindingVariable, createColumnReferenceExpression, createIdentifier, raw } from '../ast-factory'; import type { Builder } from '../builder'; import type { GrammarInterface } from '../grammar.interface'; import type { JoinClauseBuilder, QueryBuilder } from '../query-builder'; @@ -327,14 +328,18 @@ export abstract class QueryGrammar extends BaseGrammar implements GrammarInterfa return funcName; } - distinct(query: QueryBuilder, distinct: boolean | any[]): string { - // If the query is actually performing an aggregating select, we will let that - // compiler handle the building of the select clauses, as it will need some - // more syntax that is best handled by that function to keep things neat. - if (!isBlank(query._aggregate)) { + distinct(distinct: boolean | any[]): string { + if (distinct !== false) { + return 'DISTINCT'; + } else { return ''; } - if (distinct !== false) { + } + + distinctInAggregateFunctionCall(distinct: boolean | any[]): string { + if (isArray(distinct)) { + return `DISTINCT ${this.columnize(distinct)}`; + } else if (distinct !== false) { return 'DISTINCT'; } else { return ''; @@ -417,17 +422,17 @@ export abstract class QueryGrammar extends BaseGrammar implements GrammarInterfa } if (builder._aggregate && builder._unions.length === 0) { - selectClause = new SelectClause( + builder._aggregate.distinct = isArray(builder._distinct) ? + builder._distinct.map(it => createColumnReferenceExpression(it)) : + builder._distinct; + selectClause = new SelectClause( [ new SelectScalarExpression( - new FunctionCallExpression( - builder._aggregate.aggregateFunctionName, - builder._aggregate.aggregateColumns - ), + builder._aggregate, createIdentifier('aggregate') ) ], - builder._distinct + false ); } else { selectClause = new SelectClause( diff --git a/libs/fedaco/src/query-builder/grammar/sqlite-query-grammar.ts b/libs/fedaco/src/query-builder/grammar/sqlite-query-grammar.ts index 81af477..7f196b5 100644 --- a/libs/fedaco/src/query-builder/grammar/sqlite-query-grammar.ts +++ b/libs/fedaco/src/query-builder/grammar/sqlite-query-grammar.ts @@ -54,10 +54,7 @@ export class SqliteQueryGrammar extends QueryGrammar implements GrammarInterface return ast.accept(visitor); } - distinct(query: QueryBuilder, distinct: boolean | any[]): string { - if (! isBlank(query._aggregate)) { - return ''; - } + distinct(distinct: boolean | any[]): string { if (distinct !== false) { return 'DISTINCT'; } else { diff --git a/libs/fedaco/src/query-builder/grammar/sqlserver-query-grammar.ts b/libs/fedaco/src/query-builder/grammar/sqlserver-query-grammar.ts index 6b3c446..6e9c842 100644 --- a/libs/fedaco/src/query-builder/grammar/sqlserver-query-grammar.ts +++ b/libs/fedaco/src/query-builder/grammar/sqlserver-query-grammar.ts @@ -46,10 +46,7 @@ export class SqlserverQueryGrammar extends QueryGrammar implements GrammarInterf return snakeCase(funcName); } - distinct(query: QueryBuilder, distinct: boolean | any[]): string { - if (! isBlank(query._aggregate)) { - return ''; - } + distinct(distinct: boolean | any[]): string { if (distinct !== false) { return 'DISTINCT'; } else { diff --git a/libs/fedaco/src/query-builder/mixins/aggregate.ts b/libs/fedaco/src/query-builder/mixins/aggregate.ts index 11897d4..dec4158 100644 --- a/libs/fedaco/src/query-builder/mixins/aggregate.ts +++ b/libs/fedaco/src/query-builder/mixins/aggregate.ts @@ -7,7 +7,7 @@ import { isObject } from '@gradii/nanofn'; import type { Constructor } from '../../helper/constructor'; import { ColumnReferenceExpression } from '../../query/ast/column-reference-expression'; -import { AggregateFragment } from '../../query/ast/fragment/aggregate-fragment'; +import { AggregateFunctionCallFragment } from '../../query/ast/fragment/aggregate-function-call-fragment'; import { PathExpression } from '../../query/ast/path-expression'; import { SqlParser } from '../../query/parser/sql-parser'; import { createColumnReferenceExpression, createIdentifier, rawSqlBindings } from '../ast-factory'; @@ -42,7 +42,7 @@ export function mixinAggregate>(base: T): QueryBuilde /*Set the aggregate property without running the query.*/ _setAggregate(this: QueryBuilder & _Self, func: string, columns: Array) { - this._aggregate = new AggregateFragment( + this._aggregate = new AggregateFunctionCallFragment( createIdentifier(func), columns.map(it => createColumnReferenceExpression(it)) ); diff --git a/libs/fedaco/src/query-builder/query-builder-resolver-visitor.ts b/libs/fedaco/src/query-builder/query-builder-resolver-visitor.ts index 8834474..bf62b3f 100644 --- a/libs/fedaco/src/query-builder/query-builder-resolver-visitor.ts +++ b/libs/fedaco/src/query-builder/query-builder-resolver-visitor.ts @@ -50,7 +50,7 @@ export class QueryBuilderResolverVisitor implements SqlVisitor { throw new Error('Method not implemented.'); } - visitAggregateFragment(node: SqlNode): string { + visitAggregateFunctionCallFragment(node: SqlNode): string { throw new Error('Method not implemented.'); } diff --git a/libs/fedaco/src/query-builder/visitor/mysql-query-builder-visitor.ts b/libs/fedaco/src/query-builder/visitor/mysql-query-builder-visitor.ts index 86f155c..76141a9 100644 --- a/libs/fedaco/src/query-builder/visitor/mysql-query-builder-visitor.ts +++ b/libs/fedaco/src/query-builder/visitor/mysql-query-builder-visitor.ts @@ -4,6 +4,7 @@ * Use of this source code is governed by an MIT-style license */ +import { isArray } from '@gradii/nanofn'; import type { DeleteSpecification } from '../../query/ast/delete-specification'; import type { FunctionCallExpression } from '../../query/ast/expression/function-call-expression'; import type { UpdateSpecification } from '../../query/ast/update-specification'; diff --git a/libs/fedaco/src/query-builder/visitor/postgres-query-builder-visitor.ts b/libs/fedaco/src/query-builder/visitor/postgres-query-builder-visitor.ts index e1889b0..db44fa5 100644 --- a/libs/fedaco/src/query-builder/visitor/postgres-query-builder-visitor.ts +++ b/libs/fedaco/src/query-builder/visitor/postgres-query-builder-visitor.ts @@ -6,9 +6,7 @@ import { isString } from '@gradii/nanofn'; import type { ColumnReferenceExpression } from '../../query/ast/column-reference-expression'; -import type { - ComparisonPredicateExpression -} from '../../query/ast/expression/comparison-predicate-expression'; +import type { ComparisonPredicateExpression } from '../../query/ast/expression/comparison-predicate-expression'; import type { FunctionCallExpression } from '../../query/ast/expression/function-call-expression'; import type { JsonPathExpression } from '../../query/ast/json-path-expression'; import type { LockClause } from '../../query/ast/lock-clause'; diff --git a/libs/fedaco/src/query-builder/visitor/query-builder-visitor.ts b/libs/fedaco/src/query-builder/visitor/query-builder-visitor.ts index 5a58c34..6645671 100644 --- a/libs/fedaco/src/query-builder/visitor/query-builder-visitor.ts +++ b/libs/fedaco/src/query-builder/visitor/query-builder-visitor.ts @@ -31,7 +31,7 @@ import type { ParenthesizedExpression } from '../../query/ast/expression/parenth import type { RawBindingExpression } from '../../query/ast/expression/raw-binding-expression'; import { RawExpression } from '../../query/ast/expression/raw-expression'; import type { StringLiteralExpression } from '../../query/ast/expression/string-literal-expression'; -import type { AggregateFragment } from '../../query/ast/fragment/aggregate-fragment'; +import type { AggregateFunctionCallFragment } from '../../query/ast/fragment/aggregate-function-call-fragment'; import type { NestedPredicateExpression } from '../../query/ast/fragment/expression/nested-predicate-expression'; @@ -101,13 +101,20 @@ export class QueryBuilderVisitor implements SqlVisitor { return 'hello'; } - visitAggregateFragment(node: AggregateFragment): string { - throw new Error('not implement yet'); - // todo - // return this._grammar.compileAggregateFragment( - // node.aggregateFunctionName, - // node.aggregateColumns - // ); + visitAggregateFunctionCallFragment(node: AggregateFunctionCallFragment): string { + let funcName = node.aggregateFunctionName.accept(this); + funcName = this._grammar.compilePredicateFuncName(funcName); + let columns; + if (isArray(node.distinct)) { + const list = node.distinct.map((it: SqlNode) => it.accept(this)); + columns = this._grammar.distinctInAggregateFunctionCall(list); + } else { + columns = node.aggregateColumns.map(it => it.accept(this)).join(', '); + if (node.distinct === true && columns !== '*') { + columns = 'DISTINCT ' + columns; + } + } + return `${funcName}(${columns})`; } visitAsExpression(node: AsExpression): string { @@ -586,9 +593,9 @@ export class QueryBuilderVisitor implements SqlVisitor { return expression.accept(this); }); return `SELECT${node.distinct ? ` ${ - this._grammar.distinct(this._queryBuilder, node.distinct)} ` : ' '}${selectExpressions.join(', ')}`; + this._grammar.distinct(node.distinct)} ` : ' '}${selectExpressions.join(', ')}`; } else { - return `SELECT${node.distinct ? ` ${this._grammar.distinct(this._queryBuilder, node.distinct)} ` : ' '}*`; + return `SELECT${node.distinct ? ` ${this._grammar.distinct(node.distinct)} ` : ' '}*`; } } diff --git a/libs/fedaco/src/query-builder/visitor/sqlserver-query-builder-visitor.ts b/libs/fedaco/src/query-builder/visitor/sqlserver-query-builder-visitor.ts index 6ce493b..bd600b0 100644 --- a/libs/fedaco/src/query-builder/visitor/sqlserver-query-builder-visitor.ts +++ b/libs/fedaco/src/query-builder/visitor/sqlserver-query-builder-visitor.ts @@ -99,14 +99,10 @@ export class SqlserverQueryBuilderVisitor extends QueryBuilderVisitor { const selectExpressions = node.selectExpressions.map(expression => { return expression.accept(this); }); - return `SELECT${node.distinct ? ` ${this._grammar.distinct( - this._queryBuilder, - node.distinct)} ` : ' '}${topSql}${selectExpressions.join( + return `SELECT${node.distinct ? ` ${this._grammar.distinct(node.distinct)} ` : ' '}${topSql}${selectExpressions.join( ', ')}`; } else { - return `SELECT${node.distinct ? ` ${this._grammar.distinct( - this._queryBuilder, - node.distinct)} ` : ' '}${topSql}*`; + return `SELECT${node.distinct ? ` ${this._grammar.distinct(node.distinct)} ` : ' '}${topSql}*`; } } diff --git a/libs/fedaco/src/query/ast/fragment/aggregate-fragment.ts b/libs/fedaco/src/query/ast/fragment/aggregate-function-call-fragment.ts similarity index 63% rename from libs/fedaco/src/query/ast/fragment/aggregate-fragment.ts rename to libs/fedaco/src/query/ast/fragment/aggregate-function-call-fragment.ts index 64c75d2..f820740 100644 --- a/libs/fedaco/src/query/ast/fragment/aggregate-fragment.ts +++ b/libs/fedaco/src/query/ast/fragment/aggregate-function-call-fragment.ts @@ -9,15 +9,16 @@ import type { SqlVisitor } from '../../sql-visitor'; import type { Identifier } from '../identifier'; -export class AggregateFragment extends SqlNode { +export class AggregateFunctionCallFragment extends SqlNode { constructor( public aggregateFunctionName: Identifier, - public aggregateColumns: SqlNode[] + public aggregateColumns: SqlNode[], + public distinct: SqlNode[] | boolean = false ) { super(); } public accept(visitor: SqlVisitor) { - return visitor.visitAggregateFragment(this); + return visitor.visitAggregateFunctionCallFragment(this); } } diff --git a/libs/fedaco/src/query/sql-visitor.ts b/libs/fedaco/src/query/sql-visitor.ts index d60a9ff..8484dfb 100644 --- a/libs/fedaco/src/query/sql-visitor.ts +++ b/libs/fedaco/src/query/sql-visitor.ts @@ -13,7 +13,7 @@ export interface SqlVisitor { // visit(node: SqlNode) { // } - visitAggregateFragment(node: SqlNode): string; + visitAggregateFunctionCallFragment(node: SqlNode): string; visitAsExpression(node: SqlNode): string; diff --git a/libs/fedaco/test/query-builder.spec.ts b/libs/fedaco/test/query-builder.spec.ts index 2c3d8f0..b493cc3 100644 --- a/libs/fedaco/test/query-builder.spec.ts +++ b/libs/fedaco/test/query-builder.spec.ts @@ -2415,6 +2415,46 @@ describe('database query builder test', () => { expect(results).toBe(1); }); + it('test aggregate functions with distinct', async () => { + let spySelect, spyProcessSelect, results + builder = getBuilder(); + spySelect = jest.spyOn(builder._connection, 'select').mockReturnValue(Promise.resolve([ + { + 'aggregate': 1 + } + ])); + + spyProcessSelect = jest.spyOn(builder._processor, 'processSelect').mockImplementation( + (builder, results) => { + return results; + }); + + results = await builder.distinct('foo').from('users').count(); + expect(spySelect).toBeCalledWith('SELECT count(DISTINCT `foo`) AS aggregate FROM `users`', [], true); + expect(results).toBe(1); + + spySelect = jest.spyOn(builder._connection, 'select').mockReturnValue(Promise.resolve([ + { + 'aggregate': 2 + } + ])); + + spyProcessSelect = jest.spyOn(builder._processor, 'processSelect').mockImplementation( + (builder, results) => { + return results; + }); + + results = await builder.distinct().from('users').count('*'); + expect(spySelect).toBeCalledWith('SELECT count(*) AS aggregate FROM `users`', [], true); + expect(results).toBe(2); + + results = await builder.distinct().from('users').count('bar'); + expect(spySelect).toBeCalledWith('SELECT count(DISTINCT `bar`) AS aggregate FROM `users`', [], true); + + }); + + + /* it('test sql server exists', () => { builder = getSqlServerBuilder();