From 72b00544860c3835aa0d05fabaf9ab64afbc97c7 Mon Sep 17 00:00:00 2001 From: Igal Klebanov Date: Thu, 15 Dec 2022 09:58:56 +0200 Subject: [PATCH] add `filter` clause support at `AggregateFunctionBuilder`. (#208) * add filter to AggregateFunctionNode. * add filter handling @ DefaultQueryCompiler.visitAggregateFunction. * add filter field @ OperationNodeTransformer.transformAggregateFunction. * add filter methods @ AggregateFunctionBuilder. * fix postgres-json test tsc error. * add some filter unit tests @ aggregate-function. * rename `filter` methods to `filterWhere`. Have slept on it, think this is more aligned with Kysely's API philosophy. * align with recent WhereNode changes. * align with recent filter-parser refactor. * remove .only from aggregate function module unit tests. * AggregateFunctionBuilder.filterWhere typings tests. * AggregateFunctionBuilder.filterWhereRef typings tests. * add ts docs to filterWhere methods @ `AggregateFunctionBuilder`. --- src/operation-node/aggregate-function-node.ts | 34 ++ .../operation-node-transformer.ts | 1 + .../aggregate-function-builder.ts | 323 +++++++++++++++++- src/query-builder/where-interface.ts | 6 +- src/query-compiler/default-query-compiler.ts | 6 + test/node/src/aggregate-function.test.ts | 161 +++++++++ test/node/src/postgres-json.test.ts | 10 +- test/typings/test-d/function-module.test-d.ts | 297 +++++++++++++++- 8 files changed, 828 insertions(+), 10 deletions(-) diff --git a/src/operation-node/aggregate-function-node.ts b/src/operation-node/aggregate-function-node.ts index 7859af518..f4b26e542 100644 --- a/src/operation-node/aggregate-function-node.ts +++ b/src/operation-node/aggregate-function-node.ts @@ -2,6 +2,7 @@ import { freeze } from '../util/object-utils.js' import { OperationNode } from './operation-node.js' import { OverNode } from './over-node.js' import { SimpleReferenceExpressionNode } from './simple-reference-expression-node.js' +import { WhereNode } from './where-node.js' type AggregateFunction = 'avg' | 'count' | 'max' | 'min' | 'sum' @@ -10,6 +11,7 @@ export interface AggregateFunctionNode extends OperationNode { readonly func: AggregateFunction readonly column: SimpleReferenceExpressionNode readonly distinct?: boolean + readonly filter?: WhereNode readonly over?: OverNode } @@ -41,6 +43,38 @@ export const AggregateFunctionNode = freeze({ }) }, + cloneWithFilter( + aggregateFunctionNode: AggregateFunctionNode, + filter: OperationNode + ): AggregateFunctionNode { + return freeze({ + ...aggregateFunctionNode, + filter: aggregateFunctionNode.filter + ? WhereNode.cloneWithOperation( + aggregateFunctionNode.filter, + 'And', + filter + ) + : WhereNode.create(filter), + }) + }, + + cloneWithOrFilter( + aggregateFunctionNode: AggregateFunctionNode, + filter: OperationNode + ): AggregateFunctionNode { + return freeze({ + ...aggregateFunctionNode, + filter: aggregateFunctionNode.filter + ? WhereNode.cloneWithOperation( + aggregateFunctionNode.filter, + 'Or', + filter + ) + : WhereNode.create(filter), + }) + }, + cloneWithOver( aggregateFunctionNode: AggregateFunctionNode, over?: OverNode diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 6bd63a2ea..931623ca1 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -825,6 +825,7 @@ export class OperationNodeTransformer { kind: 'AggregateFunctionNode', column: this.transformNode(node.column), distinct: node.distinct, + filter: this.transformNode(node.filter), func: node.func, over: this.transformNode(node.over), }) diff --git a/src/query-builder/aggregate-function-builder.ts b/src/query-builder/aggregate-function-builder.ts index 613420acd..7dd1bef31 100644 --- a/src/query-builder/aggregate-function-builder.ts +++ b/src/query-builder/aggregate-function-builder.ts @@ -6,6 +6,19 @@ import { preventAwait } from '../util/prevent-await.js' import { OverBuilder } from './over-builder.js' import { createOverBuilder } from '../parser/parse-utils.js' import { AliasedExpression, Expression } from '../expression/expression.js' +import { ReferenceExpression } from '../parser/reference-parser.js' +import { + ComparisonOperatorExpression, + OperandValueExpressionOrList, + parseReferentialFilter, + parseWhere, + WhereGrouper, +} from '../parser/binary-operation-parser.js' +import { + ExistsExpression, + parseExists, + parseNotExists, +} from '../parser/unary-operation-parser.js' export class AggregateFunctionBuilder implements Expression @@ -53,13 +66,15 @@ export class AggregateFunctionBuilder } /** - * Adds a distinct clause inside the function. + * Adds a `distinct` clause inside the function. + * + * ### Examples * * ```ts * const result = await db * .selectFrom('person') - * .select( - * eb => eb.fn.count('first_name').distinct().as('first_name_count') + * .select((eb) => + * eb.fn.count('first_name').distinct().as('first_name_count') * ) * .executeTakeFirstOrThrow() * ``` @@ -81,7 +96,307 @@ export class AggregateFunctionBuilder } /** - * Adds an over clause (window functions) after the function. + * Adds a `filter` clause with a nested `where` clause after the function. + * + * Similar to {@link WhereInterface}'s `where` method. + * + * Also see {@link orFilterWhere}, {@link filterWhereExists} and {@link filterWhereRef}. + * + * ### Examples + * + * Count by gender: + * + * ```ts + * const result = await db + * .selectFrom('person') + * .select([ + * (eb) => + * eb.fn + * .count('id') + * .filterWhere('gender', '=', 'female') + * .as('female_count'), + * (eb) => + * eb.fn + * .count('id') + * .filterWhere('gender', '=', 'male') + * .as('male_count'), + * (eb) => + * eb.fn + * .count('id') + * .filterWhere('gender', '=', 'other') + * .as('other_count'), + * ]) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * count("id") filter(where "gender" = $1) as "female_count", + * count("id") filter(where "gender" = $2) as "male_count", + * count("id") filter(where "gender" = $3) as "other_count" + * from "person" + * ``` + */ + filterWhere>( + lhs: RE, + op: ComparisonOperatorExpression, + rhs: OperandValueExpressionOrList + ): AggregateFunctionBuilder + + filterWhere( + grouper: WhereGrouper + ): AggregateFunctionBuilder + + filterWhere(expression: Expression): AggregateFunctionBuilder + + filterWhere(...args: any[]): any { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithFilter( + this.#props.aggregateFunctionNode, + parseWhere(args) + ), + }) + } + + /** + * Adds a `filter` clause with a nested `where exists` clause after the function. + * + * Similar to {@link WhereInterface}'s `whereExists` method. + * + * ### Examples + * + * Count pet owners versus general public: + * + * ```ts + * const result = await db + * .selectFrom('person') + * .select([ + * (eb) => + * eb.fn + * .count('person.id') + * .filterWhereExists((qb) => + * qb + * .selectFrom('pet') + * .select('pet.id') + * .whereRef('pet.owner_id', '=', 'person.id') + * ) + * .as('pet_owner_count'), + * (eb) => eb.fn.count('person.id').as('total_count'), + * ]) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select count("person"."id") filter(where exists ( + * select "pet"."id" + * from "pet" + * where "pet"."owner_id" = "person"."id" + * )) as "pet_ower_count", + * count("person"."id") as "total_count" + * from "person" + * ``` + */ + filterWhereExists( + arg: ExistsExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithFilter( + this.#props.aggregateFunctionNode, + parseExists(arg) + ), + }) + } + + /** + * Just like {@link filterWhereExists} but creates a `not exists` clause inside + * the `filter` clause. + */ + filterWhereNotExists( + arg: ExistsExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithFilter( + this.#props.aggregateFunctionNode, + parseNotExists(arg) + ), + }) + } + + /** + * Adds a `filter` clause with a nested `where` clause after the function, where + * both sides of the operator are references to columns. + * + * Similar to {@link WhereInterface}'s `whereRef` method. + * + * ### Examples + * + * Count people with same first and last names versus general public: + * + * ```ts + * const result = await db + * .selectFrom('person') + * .select([ + * (eb) => + * eb.fn + * .count('id') + * .filterWhereRef('first_name', '=', 'last_name') + * .as('repeat_name_count'), + * (eb) => eb.fn.count('id').as('total_count'), + * ]) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * count("id") filter(where "first_name" = "last_name") as "repeat_name_count", + * count("id") as "total_count" + * from "person" + * ``` + */ + filterWhereRef( + lhs: ReferenceExpression, + op: ComparisonOperatorExpression, + rhs: ReferenceExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithFilter( + this.#props.aggregateFunctionNode, + parseReferentialFilter(lhs, op, rhs) + ), + }) + } + + /** + * Adds a `filter` clause with a nested `or where` clause after the function. + * Otherwise works just like {@link filterWhere}. + * + * Similar to {@link WhereInterface}'s `orWhere` method. + * + * ### Examples + * + * For some reason you're tasked with counting adults (18+) or people called + * "Bob" versus general public: + * + * ```ts + * const result = await db + * .selectFrom('person') + * .select([ + * (eb) => + * eb.fn + * .count('id') + * .filterWhere('age', '>=', '18') + * .orFilterWhere('first_name', '=', 'Bob') + * .as('adult_or_bob_count'), + * (eb) => eb.fn.count('id').as('total_count'), + * ]) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * count("id") filter(where "age" >= $1 or "first_name" = $2) as "adult_or_bob_count", + * count("id") as "total_count" + * from "person" + * ``` + */ + orFilterWhere>( + lhs: RE, + op: ComparisonOperatorExpression, + rhs: OperandValueExpressionOrList + ): AggregateFunctionBuilder + + orFilterWhere( + grouper: WhereGrouper + ): AggregateFunctionBuilder + + orFilterWhere( + expression: Expression + ): AggregateFunctionBuilder + + orFilterWhere(...args: any[]): any { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithOrFilter( + this.#props.aggregateFunctionNode, + parseWhere(args) + ), + }) + } + + /** + * Just like {@link filterWhereExists} but creates an `or exists` clause inside + * the `filter` clause. + * + * Similar to {@link WhereInterface}'s `orWhereExists` method. + */ + orFilterWhereExists( + arg: ExistsExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithOrFilter( + this.#props.aggregateFunctionNode, + parseExists(arg) + ), + }) + } + + /** + * Just like {@link filterWhereExists} but creates an `or not exists` clause inside + * the `filter` clause. + * + * Similar to {@link WhereInterface}'s `orWhereNotExists` method. + */ + orFilterWhereNotExists( + arg: ExistsExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithOrFilter( + this.#props.aggregateFunctionNode, + parseNotExists(arg) + ), + }) + } + + /** + * Adds an `or where` clause inside the `filter` clause. Otherwise works just + * like {@link filterWhereRef}. + * + * Also see {@link orFilterWhere} and {@link filterWhere}. + * + * Similar to {@link WhereInterface}'s `orWhereRef` method. + */ + orFilterWhereRef( + lhs: ReferenceExpression, + op: ComparisonOperatorExpression, + rhs: ReferenceExpression + ): AggregateFunctionBuilder { + return new AggregateFunctionBuilder({ + ...this.#props, + aggregateFunctionNode: AggregateFunctionNode.cloneWithOrFilter( + this.#props.aggregateFunctionNode, + parseReferentialFilter(lhs, op, rhs) + ), + }) + } + + /** + * Adds an `over` clause (window functions) after the function. + * + * ### Examples * * ```ts * const result = await db diff --git a/src/query-builder/where-interface.ts b/src/query-builder/where-interface.ts index d81c8b2ca..3dc58cf53 100644 --- a/src/query-builder/where-interface.ts +++ b/src/query-builder/where-interface.ts @@ -208,7 +208,7 @@ export interface WhereInterface { * * The normal `where` method treats the right hand side argument as a * value by default. `whereRef` treats it as a column reference. This method is - * expecially useful with joins and correclated subqueries. + * expecially useful with joins and correlated subqueries. * * ### Examples * @@ -411,12 +411,12 @@ export interface WhereInterface { whereNotExists(arg: ExistsExpression): WhereInterface /** - * Just like {@link whereExists} but creates a `or exists` clause. + * Just like {@link whereExists} but creates an `or exists` clause. */ orWhereExists(arg: ExistsExpression): WhereInterface /** - * Just like {@link whereExists} but creates a `or not exists` clause. + * Just like {@link whereExists} but creates an `or not exists` clause. */ orWhereNotExists(arg: ExistsExpression): WhereInterface } diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 3c7b79e5a..7ed6b0d89 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -1200,6 +1200,12 @@ export class DefaultQueryCompiler this.visitNode(node.column) this.append(')') + if (node.filter) { + this.append(' filter(') + this.visitNode(node.filter) + this.append(')') + } + if (node.over) { this.append(' ') this.visitNode(node.over) diff --git a/test/node/src/aggregate-function.test.ts b/test/node/src/aggregate-function.test.ts index 2ced51dd3..81abec34c 100644 --- a/test/node/src/aggregate-function.test.ts +++ b/test/node/src/aggregate-function.test.ts @@ -8,6 +8,7 @@ import { Database, destroyTest, initTest, + NOT_SUPPORTED, TestContext, testSql, } from './test-setup.js' @@ -357,6 +358,166 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) }) + + if (dialect === 'postgres' || dialect === 'sqlite') { + it(`should execute a query with ${funcName}(...) filter(where ...) in select clause`, async () => { + const query = ctx.db + .selectFrom('person') + .select([ + func('person.id') + .filterWhere('person.gender', '=', 'female') + .as(funcName), + (eb) => + getFuncFromExpressionBuilder(eb, funcName)('person.id') + .filterWhere('person.gender', '=', 'female') + .as(`another_${funcName}`), + ]) + + testSql(query, dialect, { + postgres: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = $1) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = $2) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + mysql: NOT_SUPPORTED, + sqlite: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = ?) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = ?) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + }) + + await query.execute() + }) + + it(`should execute a query with ${funcName}(...) filter(where ... and ...) in select clause`, async () => { + const query = ctx.db + .selectFrom('person') + .select([ + func('person.id') + .filterWhere('person.gender', '=', 'female') + .filterWhere('person.middle_name', 'is not', null) + .as(funcName), + (eb) => + getFuncFromExpressionBuilder(eb, funcName)('person.id') + .filterWhere('person.gender', '=', 'female') + .filterWhere('person.middle_name', 'is not', null) + .as(`another_${funcName}`), + ]) + + testSql(query, dialect, { + postgres: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = $1 and "person"."middle_name" is not null) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = $2 and "person"."middle_name" is not null) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + mysql: NOT_SUPPORTED, + sqlite: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = ? and "person"."middle_name" is not null) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = ? and "person"."middle_name" is not null) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + }) + + await query.execute() + }) + + it(`should execute a query with ${funcName}(...) filter(where ... or ...) in select clause`, async () => { + const query = ctx.db + .selectFrom('person') + .select([ + func('person.id') + .filterWhere('person.gender', '=', 'female') + .orFilterWhere('person.middle_name', 'is not', null) + .as(funcName), + (eb) => + getFuncFromExpressionBuilder(eb, funcName)('person.id') + .filterWhere('person.gender', '=', 'female') + .orFilterWhere('person.middle_name', 'is not', null) + .as(`another_${funcName}`), + ]) + + testSql(query, dialect, { + postgres: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = $1 or "person"."middle_name" is not null) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = $2 or "person"."middle_name" is not null) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + mysql: NOT_SUPPORTED, + sqlite: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = ? or "person"."middle_name" is not null) as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = ? or "person"."middle_name" is not null) as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + }) + + await query.execute() + }) + + it(`should execute a query with ${funcName}(...) filter(where ...) over() in select clause`, async () => { + const query = ctx.db + .selectFrom('person') + .select([ + func('person.id') + .filterWhere('person.gender', '=', 'female') + .over() + .as(funcName), + (eb) => + getFuncFromExpressionBuilder(eb, funcName)('person.id') + .filterWhere('person.gender', '=', 'female') + .over() + .as(`another_${funcName}`), + ]) + + testSql(query, dialect, { + postgres: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = $1) over() as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = $2) over() as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + mysql: NOT_SUPPORTED, + sqlite: { + sql: [ + `select`, + `${funcName}("person"."id") filter(where "person"."gender" = ?) over() as "${funcName}",`, + `${funcName}("person"."id") filter(where "person"."gender" = ?) over() as "another_${funcName}"`, + `from "person"`, + ], + parameters: ['female', 'female'], + }, + }) + + await query.execute() + }) + } }) } }) diff --git a/test/node/src/postgres-json.test.ts b/test/node/src/postgres-json.test.ts index 0ed58c32a..5fddc029e 100644 --- a/test/node/src/postgres-json.test.ts +++ b/test/node/src/postgres-json.test.ts @@ -1,6 +1,12 @@ import { Generated, Kysely, RawBuilder, sql } from '../../../' -import { destroyTest, initTest, TestContext, expect } from './test-setup.js' +import { + destroyTest, + initTest, + TestContext, + expect, + Database, +} from './test-setup.js' interface JsonTable { id: Generated @@ -14,7 +20,7 @@ interface JsonTable { describe(`postgres json tests`, () => { let ctx: TestContext - let db: Kysely<{ json_table: JsonTable }> + let db: Kysely before(async function () { ctx = await initTest(this, 'postgres') diff --git a/test/typings/test-d/function-module.test-d.ts b/test/typings/test-d/function-module.test-d.ts index 155193da5..c4423559f 100644 --- a/test/typings/test-d/function-module.test-d.ts +++ b/test/typings/test-d/function-module.test-d.ts @@ -1,5 +1,5 @@ import { expectError, expectAssignable, expectNotAssignable } from 'tsd' -import { Kysely } from '..' +import { Kysely, sql } from '..' import { Database } from '../shared' async function testSelectWithoutAs(db: Kysely) { @@ -279,6 +279,301 @@ async function testSelectWithDistinct(db: Kysely) { expectAssignable(result.total_age) } +async function testWithFilterWhere(db: Kysely) { + const { avg, count, max, min, sum } = db.fn + + // Column name + db.selectFrom('person') + .select(avg('age').filterWhere('gender', '=', 'female').as('avg_age')) + .select(count('id').filterWhere('gender', '=', 'female').as('female_count')) + .select(max('age').filterWhere('gender', '=', 'female').as('max_age')) + .select(min('age').filterWhere('gender', '=', 'female').as('min_age')) + .select(sum('age').filterWhere('gender', '=', 'female').as('total_age')) + + // Table and column + db.selectFrom('person').select( + avg('age').filterWhere('person.gender', '=', 'female').as('avg_age') + ) + + // Schema, table and column + db.selectFrom('some_schema.movie').select( + avg('stars').filterWhere('some_schema.movie.stars', '>', 0).as('avg_stars') + ) + + // Subquery in LHS + db.selectFrom('person').select( + avg('age') + .filterWhere((qb) => qb.selectFrom('movie').select('stars'), '>', 0) + .as('avg_age') + ) + + // Subquery in RHS + db.selectFrom('movie').select( + avg('stars') + .filterWhere(sql`${'female'}`, '=', (qb) => + qb.selectFrom('person').select('gender') + ) + .as('avg_stars') + ) + + // Raw expression + db.selectFrom('person').select( + avg('age') + .filterWhere('first_name', '=', sql`'foo'`) + .filterWhere('first_name', '=', sql`'foo'`) + .filterWhere(sql`whatever`, '=', 1) + .filterWhere(sql`whatever`, '=', true) + .filterWhere(sql`whatever`, '=', '1') + .as('avg_age') + ) + + // List value + db.selectFrom('person').select( + avg('age').filterWhere('gender', 'in', ['female', 'male']).as('avg_age') + ) + + // Raw operator + db.selectFrom('person').select( + avg('age') + .filterWhere('person.age', sql`lol`, 25) + .as('avg_age') + ) + + // Invalid operator + expectError( + db + .selectFrom('person') + .select(avg('age').filterWhere('person.age', 'lol', 25).as('avg_age')) + ) + + // Invalid table + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhere('movie.stars', '=', 25).as('avg_age') + ) + ) + + // Invalid column + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhere('stars', '=', 25).as('avg_age') + ) + ) + + // Invalid type for column + expectError( + db + .selectFrom('person') + .select(avg('age').filterWhere('first_name', '=', 25).as('avg_age')) + ) + + // Invalid type for column + expectError( + db + .selectFrom('person') + .select( + avg('age').filterWhere('gender', '=', 'not_a_gender').as('avg_age') + ) + ) + + // Invalid type for column + expectError( + db + .selectFrom('person') + .select( + avg('age') + .filterWhere('gender', 'in', ['female', 'not_a_gender']) + .as('avg_age') + ) + ) + + // Invalid type for column + expectError( + db + .selectFrom('some_schema.movie') + .select( + avg('stars').filterWhere('some_schema.movie.id', '=', 1).as('avg_stars') + ) + ) + + // Invalid type for column + expectError( + db.selectFrom('some_schema.movie').select( + avg('stars') + .filterWhere( + (qb) => qb.selectFrom('person').select('gender'), + '=', + 'not_a_gender' + ) + .as('avg_stars') + ) + ) + + // Invalid type for column + expectError( + db.selectFrom('person').select( + avg('age') + .filterWhere('first_name', '=', sql`1`) + .as('avg_age') + ) + ) + + // Invalid type for column + expectError( + db.selectFrom('person').select( + avg('age') + .filterWhere(sql`first_name`, '=', 1) + .as('avg_age') + ) + ) +} + +async function testWithFilterWhereRef(db: Kysely) { + const { avg, count, max, min, sum } = db.fn + + // Column name + db.selectFrom('person') + .select( + avg('age').filterWhereRef('first_name', '=', 'last_name').as('avg_age') + ) + .select( + count('id').filterWhereRef('first_name', '=', 'last_name').as('count') + ) + .select( + max('age').filterWhereRef('first_name', '=', 'last_name').as('max_age') + ) + .select( + min('age').filterWhereRef('first_name', '=', 'last_name').as('min_age') + ) + .select( + sum('age').filterWhereRef('first_name', '=', 'last_name').as('total_age') + ) + + // Table and column + db.selectFrom('person') + .select( + avg('age') + .filterWhereRef('person.first_name', '=', 'last_name') + .as('avg_age') + ) + .select( + count('id') + .filterWhereRef('first_name', '=', 'person.last_name') + .as('count') + ) + .select( + max('age') + .filterWhereRef('person.first_name', '=', 'person.last_name') + .as('max_age') + ) + + // Schema, table and column + db.selectFrom('movie') + .select( + avg('stars') + .filterWhereRef('some_schema.movie.id', '=', 'stars') + .as('avg_stars') + ) + .select( + count('id') + .filterWhereRef('some_schema.movie.id', '=', 'movie.stars') + .as('count') + ) + .select( + max('stars') + .filterWhereRef('some_schema.movie.id', '=', 'some_schema.movie.stars') + .as('max_stars') + ) + .select( + min('stars') + .filterWhereRef('movie.id', '=', 'some_schema.movie.stars') + .as('min_stars') + ) + .select( + sum('stars') + .filterWhereRef('id', '=', 'some_schema.movie.stars') + .as('total_stars') + ) + + // Subquery in LHS + db.selectFrom('person').select( + avg('age') + .filterWhereRef( + (qb) => qb.selectFrom('movie').select('stars'), + '>', + 'age' + ) + .as('avg_age') + ) + + // Subquery in RHS + db.selectFrom('person').select( + avg('age') + .filterWhereRef('age', '>', (qb) => + qb.selectFrom('movie').select('stars') + ) + .as('avg_age') + ) + + // Raw operator + db.selectFrom('person').select( + avg('age') + .filterWhereRef('first_name', sql`lol`, 'last_name') + .as('avg_age') + ) + + // Invalid operator + expectError( + db + .selectFrom('person') + .select( + avg('age') + .filterWhereRef('first_name', 'lol', 'last_name') + .as('avg_age') + ) + ) + + // Invalid table LHS + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhereRef('movie.stars', '>', 'age').as('avg_age') + ) + ) + + // Invalid table RHS + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhereRef('age', '>', 'movie.stars').as('avg_age') + ) + ) + + // Invalid column LHS + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhereRef('stars', '>', 'age').as('avg_age') + ) + ) + + // Invalid column RHS + expectError( + db + .selectFrom('person') + .select((eb) => + eb.fn.avg('age').filterWhereRef('age', '>', 'stars').as('avg_age') + ) + ) +} + async function testSelectWithOver(db: Kysely) { const { avg, count, max, min, sum } = db.fn