diff --git a/src/query-builder/function-module.ts b/src/query-builder/function-module.ts index 190231529..ca0c944c9 100644 --- a/src/query-builder/function-module.ts +++ b/src/query-builder/function-module.ts @@ -61,13 +61,13 @@ export class FunctionModule { * * If this is used in a `select` statement the type of the selected expression * will be `number | string` by default. This is because Kysely can't know the - * type the db driver outputs. Sometimes the output can be larger than the - * largest javascript number and a string is returned instead. Most drivers - * allow you to configure the output type of large numbers and Kysely can't - * know if you've done so. + * type the db driver outputs. Sometimes the output can be larger than the largest + * javascript number and a string is returned instead. Most drivers allow you + * to configure the output type of large numbers and Kysely can't know if you've + * done so. * - * You can specify the output type of the expression by providing - * the type as the first type argument: + * You can specify the output type of the expression by providing the type as + * the first type argument: * * ```ts * const { avg } = db.fn @@ -76,9 +76,22 @@ export class FunctionModule { * .select(avg('price').as('avg_price')) * .execute() * ``` + * + * Sometimes a null is returned, e.g. when row count is 0, and no `group by` + * was used. It is highly recommended to include null in the output type union + * and handle null values in post-execute code, or wrap the function with a `coalesce` + * function. + * + * ```ts + * const { avg } = db.fn + * + * db.selectFrom('toy') + * .select(avg('price').as('avg_price')) + * .execute() + * ``` */ avg< - O extends number | string, + O extends number | string | null = number | string, C extends SimpleReferenceExpression = SimpleReferenceExpression< DB, TB @@ -143,11 +156,11 @@ export class FunctionModule { * Calls the `count` function for the column given as the argument. * * If this is used in a `select` statement the type of the selected expression - * will be `number | string | bigint` by default. This is because Kysely can't - * know the type the db driver outputs. Sometimes the output can be larger than - * the largest javascript number and a string is returned instead. Most drivers - * allow you to configure the output type of large numbers and Kysely can't - * know if you've done so. + * will be `number | string | bigint` by default. This is because Kysely + * can't know the type the db driver outputs. Sometimes the output can be larger + * than the largest javascript number and a string is returned instead. Most + * drivers allow you to configure the output type of large numbers and Kysely + * can't know if you've done so. * * You can specify the output type of the expression by providing * the type as the first type argument: @@ -186,6 +199,9 @@ export class FunctionModule { /** * Calls the `max` function for the column given as the argument. * + * If this is used in a `select` statement the type of the selected expression + * will be the referenced column's type. + * * ### Examples * * ```ts @@ -195,16 +211,30 @@ export class FunctionModule { * .select(max('price').as('max_price')) * .execute() * ``` + * + * Sometimes a null is returned, e.g. when row count is 0, and no `group by` + * was used. It is highly recommended to include null in the output type union + * and handle null values in post-execute code, or wrap the function with a `coalesce` + * function. + * + * ```ts + * const { max } = db.fn + * + * db.selectFrom('toy') + * .select(max('price').as('max_price')) + * .execute() + * ``` */ max< - O extends number | string | bigint, - C extends SimpleReferenceExpression = DynamicReferenceBuilder + O extends number | string | bigint | null = number | string | bigint, + C extends SimpleReferenceExpression = DynamicReferenceBuilder >( column: C ): AggregateFunctionBuilder< DB, TB, - ExtractTypeFromReferenceExpression + | ExtractTypeFromReferenceExpression + | (null extends O ? null : never) > { return new AggregateFunctionBuilder({ aggregateFunctionNode: AggregateFunctionNode.create( @@ -217,6 +247,9 @@ export class FunctionModule { /** * Calls the `min` function for the column given as the argument. * + * If this is used in a `select` statement the type of the selected expression + * will be the referenced column's type. + * * ### Examples * * ```ts @@ -227,16 +260,29 @@ export class FunctionModule { * .execute() * ``` * + * Sometimes a null is returned, e.g. when row count is 0, and no `group by` + * was used. It is highly recommended to include null in the output type union + * and handle null values in post-execute code, or wrap the function with a `coalesce` + * function. + * + * ```ts + * const { min } = db.fn + * + * db.selectFrom('toy') + * .select(min('price').as('min_price')) + * .execute() + * ``` */ min< - O extends number | string | bigint, - C extends SimpleReferenceExpression = DynamicReferenceBuilder + O extends number | string | bigint | null = number | string | bigint, + C extends SimpleReferenceExpression = DynamicReferenceBuilder >( column: C ): AggregateFunctionBuilder< DB, TB, - ExtractTypeFromReferenceExpression + | ExtractTypeFromReferenceExpression + | (null extends O ? null : never) > { return new AggregateFunctionBuilder({ aggregateFunctionNode: AggregateFunctionNode.create( @@ -250,11 +296,11 @@ export class FunctionModule { * Calls the `sum` function for the column given as the argument. * * If this is used in a `select` statement the type of the selected expression - * will be `number | string | bigint` by default. This is because Kysely can't - * know the type the db driver outputs. Sometimes the output can be larger than - * the largest javascript number and a string is returned instead. Most drivers - * allow you to configure the output type of large numbers and Kysely can't - * know if you've done so. + * will be `number | string | bigint` by default. This is because Kysely + * can't know the type the db driver outputs. Sometimes the output can be larger + * than the largest javascript number and a string is returned instead. Most + * drivers allow you to configure the output type of large numbers and Kysely + * can't know if you've done so. * * You can specify the output type of the expression by providing * the type as the first type argument: @@ -266,9 +312,22 @@ export class FunctionModule { * .select(sum('price').as('total_price')) * .execute() * ``` + * + * Sometimes a null is returned, e.g. when row count is 0, and no `group by` + * was used. It is highly recommended to include null in the output type union + * and handle null values in post-execute code, or wrap the function with a `coalesce` + * function. + * + * ```ts + * const { sum } = db.fn + * + * db.selectFrom('toy') + * .select(sum('price').as('total_price')) + * .execute() + * ``` */ sum< - O extends number | string | bigint, + O extends number | string | bigint | null = number | string | bigint, C extends SimpleReferenceExpression = SimpleReferenceExpression< DB, TB diff --git a/test/typings/test-d/coalesce.test-d.ts b/test/typings/test-d/coalesce.test-d.ts index 14b77833b..5fcc3f1ec 100644 --- a/test/typings/test-d/coalesce.test-d.ts +++ b/test/typings/test-d/coalesce.test-d.ts @@ -90,6 +90,18 @@ async function testCoalesceMultiple(db: Kysely) { .groupBy(['last_name', 'first_name', 'age']) .execute() expectType<{ field: string }>(r2) + + // string | null, string -> string + const [r3] = await db + .selectFrom('person') + .select( + coalesce( + db.fn.max('first_name'), + sql`${sql.literal('N/A')}` + ).as('max_first_name') + ) + .execute() + expectType<{ max_first_name: string }>(r3) } function value(value: V): RawBuilder { diff --git a/test/typings/test-d/function-module.test-d.ts b/test/typings/test-d/function-module.test-d.ts index c3395f198..6dc92f900 100644 --- a/test/typings/test-d/function-module.test-d.ts +++ b/test/typings/test-d/function-module.test-d.ts @@ -54,10 +54,15 @@ async function testSelectWithDefaultGenerics(db: Kysely) { .executeTakeFirstOrThrow() expectAssignable(result.avg_age) + expectNotAssignable(result.avg_age) expectAssignable(result.total_people) + expectNotAssignable(result.total_people) expectAssignable(result.max_age) + expectNotAssignable(result.max_age) expectAssignable(result.min_age) + expectNotAssignable(result.min_age) expectAssignable(result.total_age) + expectNotAssignable(result.total_age) } async function testSelectWithCustomGenerics(db: Kysely) { @@ -66,22 +71,54 @@ async function testSelectWithCustomGenerics(db: Kysely) { const result = await db .selectFrom('person') .select(avg('age').as('avg_age')) + .select(avg('age').as('nullable_avg_age')) .select(count('age').as('total_people')) - .select(max('age').as('max_age')) - .select(min('age').as('min_age')) + .select(max('age').as('nullable_max_age')) + .select(max('age').as('lying_max_age')) + .select(max('age').as('nullable_lying_max_age')) + .select(min('age').as('nullable_min_age')) + .select(min('age').as('lying_min_age')) + .select(min('age').as('nullable_lying_min_age')) .select(sum('age').as('total_age')) + .select(sum('age').as('nullable_total_age')) .executeTakeFirstOrThrow() expectAssignable(result.avg_age) - expectNotAssignable(result.avg_age) + expectNotAssignable(result.avg_age) + expectAssignable(result.nullable_avg_age) + expectNotAssignable(result.nullable_avg_age) expectAssignable(result.total_people) - expectNotAssignable(result.total_people) - expectAssignable(result.max_age) - expectNotAssignable(result.max_age) - expectAssignable(result.min_age) - expectNotAssignable(result.min_age) + expectNotAssignable(result.total_people) + expectAssignable(result.nullable_max_age) + expectNotAssignable(result.nullable_max_age) + expectAssignable(result.lying_max_age) + expectNotAssignable(result.lying_max_age) + expectAssignable(result.nullable_lying_max_age) + expectNotAssignable(result.nullable_lying_max_age) + expectAssignable(result.nullable_min_age) + expectNotAssignable(result.nullable_min_age) + expectAssignable(result.lying_min_age) + expectNotAssignable(result.lying_min_age) + expectAssignable(result.nullable_lying_min_age) + expectNotAssignable(result.nullable_lying_min_age) expectAssignable(result.total_age) - expectNotAssignable(result.total_age) + expectNotAssignable(result.total_age) + expectAssignable(result.nullable_total_age) + expectNotAssignable(result.nullable_total_age) + + expectError( + db + .selectFrom('person') + .select(max('age').as('max_no_reference_generic')) + .executeTakeFirstOrThrow() + ) + + expectError( + db + .selectFrom('person') + .select(min('age').as('min_no_reference_generic')) + .executeTakeFirstOrThrow() + ) } async function testSelectUnexpectedColumn(db: Kysely) { @@ -155,32 +192,48 @@ async function testSelectWithDynamicReference(db: Kysely) { .selectFrom('person') .select(avg(dynamicReference).as('avg')) .select(avg(dynamicReference).as('another_avg')) + .select(avg(dynamicReference).as('nullable_avg')) .select(count(dynamicReference).as('count')) .select(count(dynamicReference).as('another_count')) .select(max(dynamicReference).as('max')) .select(max(dynamicReference).as('another_max')) + .select(max(dynamicReference).as('nullable_max')) .select(min(dynamicReference).as('min')) .select(min(dynamicReference).as('another_min')) + .select(max(dynamicReference).as('nullable_min')) .select(sum(dynamicReference).as('sum')) .select(sum(dynamicReference).as('another_sum')) + .select(sum(dynamicReference).as('nullable_sum')) .executeTakeFirstOrThrow() expectAssignable(result.avg) - expectNotAssignable(result.avg) + expectNotAssignable(result.avg) expectAssignable(result.another_avg) - expectNotAssignable(result.another_avg) + expectNotAssignable(result.another_avg) + expectAssignable(result.nullable_avg) + expectNotAssignable(result.nullable_avg) expectAssignable(result.count) + expectNotAssignable(result.count) expectAssignable(result.another_count) - expectNotAssignable(result.another_count) + expectNotAssignable(result.another_count) expectAssignable(result.max) + expectNotAssignable(result.max) expectAssignable(result.another_max) expectNotAssignable(result.another_max) + expectAssignable(result.nullable_max) + expectNotAssignable(result.nullable_max) expectAssignable(result.min) + expectNotAssignable(result.min) expectAssignable(result.another_min) - expectNotAssignable(result.another_min) + expectNotAssignable(result.another_min) + expectAssignable(result.nullable_min) + expectNotAssignable(result.nullable_min) expectAssignable(result.sum) + expectNotAssignable(result.sum) expectAssignable(result.another_sum) - expectNotAssignable(result.another_sum) + expectNotAssignable(result.another_sum) + expectAssignable(result.nullable_sum) + expectNotAssignable(result.nullable_sum) } async function testSelectWithDistinct(db: Kysely) {