Skip to content

Commit

Permalink
Support arbitrary expressions in min and max aggregate functions
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Dec 30, 2023
1 parent 1df47ee commit f17b6a2
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 112 deletions.
82 changes: 25 additions & 57 deletions src/query-builder/function-module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DynamicReferenceBuilder } from '../dynamic/dynamic-reference-builder.js'
import { ExpressionWrapper } from '../expression/expression-wrapper.js'
import { Expression } from '../expression/expression.js'
import { AggregateFunctionNode } from '../operation-node/aggregate-function-node.js'
Expand All @@ -19,7 +18,7 @@ import {
} from '../parser/reference-parser.js'
import { parseSelectAll } from '../parser/select-parser.js'
import { KyselyTypeError } from '../util/type-error.js'
import { Equals, IsAny } from '../util/type-utils.js'
import { IsNever } from '../util/type-utils.js'
import { AggregateFunctionBuilder } from './aggregate-function-builder.js'
import { SelectQueryBuilderExpression } from '../query-builder/select-query-builder-expression.js'
import { isString } from '../util/object-utils.js'
Expand Down Expand Up @@ -226,9 +225,9 @@ export interface FunctionModule<DB, TB extends keyof DB> {
*/
avg<
O extends number | string | null = number | string,
C extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
column: C
expr: RE
): AggregateFunctionBuilder<DB, TB, O>

/**
Expand Down Expand Up @@ -395,9 +394,9 @@ export interface FunctionModule<DB, TB extends keyof DB> {
*/
count<
O extends number | string | bigint,
C extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
column: C
expr: RE
): AggregateFunctionBuilder<DB, TB, O>

/**
Expand Down Expand Up @@ -521,29 +520,23 @@ export interface FunctionModule<DB, TB extends keyof DB> {
*
* ```ts
* db.selectFrom('toy')
* .select((eb) => eb.fn.max<number | null, 'price'>('price').as('max_price'))
* .select((eb) => eb.fn.max<number | null>('price').as('max_price'))
* .execute()
* ```
*/
max<C extends StringReference<DB, TB>>(
column: C
max<
O extends number | string | bigint | null = never,
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
expr: RE
): AggregateFunctionBuilder<
DB,
TB,
ExtractTypeFromReferenceExpression<DB, TB, C>
IsNever<O> extends true
? ExtractTypeFromReferenceExpression<DB, TB, RE, number | string | bigint>
: O
>

max<
O extends number | string | bigint | null,
C extends StringReference<DB, TB> = StringReference<DB, TB>
>(
column: OutputBoundStringReference<DB, TB, C, O>
): StringReferenceBoundAggregateFunctionBuilder<DB, TB, C, O>

max<O extends number | string | bigint | null = number | string | bigint>(
column: DynamicReferenceBuilder
): AggregateFunctionBuilder<DB, TB, O>

/**
* Calls the `min` function for the column or expression given as the argument.
*
Expand Down Expand Up @@ -586,29 +579,23 @@ export interface FunctionModule<DB, TB extends keyof DB> {
*
* ```ts
* db.selectFrom('toy')
* .select((eb) => eb.fn.min<number | null, 'price'>('price').as('min_price'))
* .select((eb) => eb.fn.min<number | null>('price').as('min_price'))
* .execute()
* ```
*/
min<C extends StringReference<DB, TB>>(
column: C
min<
O extends number | string | bigint | null = never,
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
expr: RE
): AggregateFunctionBuilder<
DB,
TB,
ExtractTypeFromReferenceExpression<DB, TB, C>
IsNever<O> extends true
? ExtractTypeFromReferenceExpression<DB, TB, RE, number | string | bigint>
: O
>

min<
O extends number | string | bigint | null,
C extends StringReference<DB, TB> = StringReference<DB, TB>
>(
column: OutputBoundStringReference<DB, TB, C, O>
): StringReferenceBoundAggregateFunctionBuilder<DB, TB, C, O>

min<O extends number | string | bigint | null = number | string | bigint>(
column: DynamicReferenceBuilder
): AggregateFunctionBuilder<DB, TB, O>

/**
* Calls the `sum` function for the column or expression given as the argument.
*
Expand Down Expand Up @@ -669,9 +656,9 @@ export interface FunctionModule<DB, TB extends keyof DB> {
*/
sum<
O extends number | string | bigint | null = number | string | bigint,
C extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
column: C
expr: RE
): AggregateFunctionBuilder<DB, TB, O>

/**
Expand Down Expand Up @@ -872,22 +859,3 @@ export function createFunctionModule<DB, TB extends keyof DB>(): FunctionModule<
},
})
}

type OutputBoundStringReference<DB, TB extends keyof DB, C, O> = Equals<
ExtractTypeFromReferenceExpression<DB, TB, C> | null,
O | null
> extends true
? C
: never

type StringReferenceBoundAggregateFunctionBuilder<
DB,
TB extends keyof DB,
C,
O
> = AggregateFunctionBuilder<
DB,
TB,
| ExtractTypeFromReferenceExpression<DB, TB, C>
| (IsAny<O> extends true ? never : null extends O ? null : never) // output is nullable, but column type might not be nullable.
>
122 changes: 68 additions & 54 deletions test/typings/test-d/aggregate-function.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { expectError, expectAssignable, expectNotAssignable } from 'tsd'
import {
expectError,
expectAssignable,
expectNotAssignable,
expectType,
} from 'tsd'
import { Generated, Kysely, sql } from '..'
import { Database } from '../shared'

Expand Down Expand Up @@ -90,6 +95,65 @@ async function testSelectWithDefaultGenerics(db: Kysely<Database>) {
expectNotAssignable<null>(result.total_age)
}

async function testSelectExpressionBuilderWithDefaultGenerics(
db: Kysely<Database>
) {
const result = await db
.selectFrom('person')
.select((eb) => [
eb.fn.avg('age').as('avg_age'),
eb.fn.count('age').as('total_people'),
eb.fn.countAll().as('total_all'),
eb.fn.countAll('person').as('total_all_people'),
eb.fn.max('age').as('max_age'),
eb.fn.min('age').as('min_age'),
eb.fn.sum('age').as('total_age'),
])

.executeTakeFirstOrThrow()

expectAssignable<string | number>(result.avg_age)
expectNotAssignable<null>(result.avg_age)
expectAssignable<string | number | bigint>(result.total_people)
expectNotAssignable<null>(result.total_people)
expectAssignable<string | number | bigint>(result.total_all)
expectNotAssignable<null>(result.total_all)
expectAssignable<string | number | bigint>(result.total_all_people)
expectNotAssignable<null>(result.total_all_people)
expectAssignable<number>(result.max_age)
expectNotAssignable<string | bigint | null>(result.max_age)
expectAssignable<number>(result.min_age)
expectNotAssignable<string | bigint | null>(result.min_age)
expectAssignable<string | number | bigint>(result.total_age)
expectNotAssignable<null>(result.total_age)
}

async function testSelectExpressionBuilderWithSubExpressions(
db: Kysely<Database>
) {
const result = await db
.selectFrom('person')
.select((eb) => [
eb.fn.avg(eb.ref('age')).as('avg_age'),
eb.fn.count(eb.ref('age')).as('total_people'),
eb.fn.countAll().as('total_all'),
eb.fn.countAll('person').as('total_all_people'),
eb.fn.max(eb.ref('age').$castTo<bigint>()).as('max_age'),
eb.fn.min(eb.ref('age')).as('min_age'),
eb.fn.sum(eb.ref('age')).as('total_age'),
])

.executeTakeFirstOrThrow()

expectType<string | number>(result.avg_age)
expectType<string | number | bigint>(result.total_people)
expectType<string | number | bigint>(result.total_all)
expectType<string | number | bigint>(result.total_all_people)
expectType<bigint>(result.max_age)
expectType<number>(result.min_age)
expectType<string | number | bigint>(result.total_age)
}

async function testSelectWithCustomGenerics(db: Kysely<Database>) {
const { avg, count, countAll, max, min, sum } = db.fn

Expand All @@ -100,8 +164,8 @@ async function testSelectWithCustomGenerics(db: Kysely<Database>) {
.select(count<number>('age').as('total_people'))
.select(countAll<number>().as('total_all'))
.select(countAll<number>('person').as('total_all_people'))
.select(max<number | null, 'age'>('age').as('nullable_max_age'))
.select(min<number | null, 'age'>('age').as('nullable_min_age'))
.select(max<number | null>('age').as('nullable_max_age'))
.select(min<number | null>('age').as('nullable_min_age'))
.select(sum<number>('age').as('total_age'))
.select(sum<number | null>('age').as('nullable_total_age'))
.executeTakeFirstOrThrow()
Expand All @@ -124,56 +188,6 @@ async function testSelectWithCustomGenerics(db: Kysely<Database>) {
expectNotAssignable<string | bigint | null>(result.total_age)
expectAssignable<number | null>(result.nullable_total_age)
expectNotAssignable<string | bigint>(result.nullable_total_age)

expectError(
db
.selectFrom('person')
.select(max<string>('age').as('max_lie_return_type'))
.executeTakeFirstOrThrow()
)

expectError(
db
.selectFrom('person')
.select(max<string, 'age'>('age').as('another_max_lie_return_type'))
.executeTakeFirstOrThrow()
)

expectError(
db
.selectFrom('person')
.select(
max<number | null>('age').as(
'max_explicit_return_type_but_no_string_ref'
)
)
.executeTakeFirstOrThrow()
)

expectError(
db
.selectFrom('person')
.select(min<string>('age').as('min_lie_return_type'))
.executeTakeFirstOrThrow()
)

expectError(
db
.selectFrom('person')
.select(min<string, 'age'>('age').as('another_min_lie_return_type'))
.executeTakeFirstOrThrow()
)

expectError(
db
.selectFrom('person')
.select(
min<number | null>('age').as(
'min_explicit_return_type_but_no_string_ref'
)
)
.executeTakeFirstOrThrow()
)
}

async function testSelectUnexpectedColumn(db: Kysely<Database>) {
Expand Down Expand Up @@ -1083,7 +1097,7 @@ async function testIssue764(db: Kysely<DB764>) {
.end()
)
.when('OrderAggregates.itemType', '=', ItemType764.FEELING)
.then(eb.fn.max('MaxQuantity')) // <-- WOT?
.then(eb.fn.max('MaxQuantity'))
.else(0)
.end()
.as('totalQuantity')
Expand Down
2 changes: 1 addition & 1 deletion test/typings/test-d/coalesce.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async function testCoalesceMultiple(db: Kysely<Database>) {
.selectFrom('person')
.select(
coalesce(
db.fn.max<string | null, 'first_name'>('first_name'),
db.fn.max<string | null>('first_name'),
sql<string>`${sql.lit('N/A')}`
).as('max_first_name')
)
Expand Down

1 comment on commit f17b6a2

@vercel
Copy link

@vercel vercel bot commented on f17b6a2 Dec 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

kysely – ./

kysely.dev
kysely-git-master-kysely-team.vercel.app
kysely-kysely-team.vercel.app
www.kysely.dev

Please sign in to comment.