diff --git a/src/expression/expression-builder.ts b/src/expression/expression-builder.ts index 7ab6a4b98..83a9102d8 100644 --- a/src/expression/expression-builder.ts +++ b/src/expression/expression-builder.ts @@ -72,6 +72,17 @@ import { Selection, parseSelectArg, } from '../parser/select-parser.js' +import { + RefTuple2, + RefTuple3, + RefTuple4, + RefTuple5, + ValTuple2, + ValTuple3, + ValTuple4, + ValTuple5, +} from '../parser/tuple-parser.js' +import { TupleNode } from '../operation-node/tuple-node.js' export interface ExpressionBuilder { /** @@ -474,6 +485,184 @@ export interface ExpressionBuilder { value: VE ): ExpressionWrapper> + /** + * Creates a tuple expression. + * + * This creates a tuple using column references by default. See {@link tuple} + * if you need to create value tuples. + * + * ### Examples + * + * ```ts + * db.selectFrom('person') + * .selectAll('person') + * .where(({ eb, refTuple, tuple }) => eb( + * refTuple('first_name', 'last_name'), + * 'in', + * [ + * tuple('Jennifer', 'Aniston'), + * tuple('Sylvester', 'Stallone') + * ] + * )) + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * "person".* + * from + * "person" + * where + * ("first_name", "last_name") + * in + * ( + * ($1, $2), + * ($3, $4) + * ) + * ``` + * + * In the next example a reference tuple is compared to a subquery. Note that + * in this case you need to use the {@link @SelectQueryBuilder.$asTuple | $asTuple} + * function: + * + * ```ts + * db.selectFrom('person') + * .selectAll('person') + * .where(({ eb, refTuple, selectFrom }) => eb( + * refTuple('first_name', 'last_name'), + * 'in', + * selectFrom('pet') + * .select(['name', 'species']) + * .where('species', '!=', 'cat') + * .$asTuple('name', 'species') + * )) + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * "person".* + * from + * "person" + * where + * ("first_name", "last_name") + * in + * ( + * select "name", "species" + * from "pet" + * where "species" != $1 + * ) + * ``` + */ + refTuple< + R1 extends ReferenceExpression, + R2 extends ReferenceExpression + >( + value1: R1, + value2: R2 + ): ExpressionWrapper> + + refTuple< + R1 extends ReferenceExpression, + R2 extends ReferenceExpression, + R3 extends ReferenceExpression + >( + value1: R1, + value2: R2, + value3: R3 + ): ExpressionWrapper> + + refTuple< + R1 extends ReferenceExpression, + R2 extends ReferenceExpression, + R3 extends ReferenceExpression, + R4 extends ReferenceExpression + >( + value1: R1, + value2: R2, + value3: R3, + value4: R4 + ): ExpressionWrapper> + + refTuple< + R1 extends ReferenceExpression, + R2 extends ReferenceExpression, + R3 extends ReferenceExpression, + R4 extends ReferenceExpression, + R5 extends ReferenceExpression + >( + value1: R1, + value2: R2, + value3: R3, + value4: R4, + value5: R5 + ): ExpressionWrapper> + + /** + * Creates a value tuple expression. + * + * This creates a tuple using values by default. See {@link refTuple} if you need to create + * tuples using column references. + * + * ### Examples + * + * ```ts + * db.selectFrom('person') + * .selectAll('person') + * .where(({ eb, refTuple, tuple }) => eb( + * refTuple('first_name', 'last_name'), + * 'in', + * [ + * tuple('Jennifer', 'Aniston'), + * tuple('Sylvester', 'Stallone') + * ] + * )) + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select + * "person".* + * from + * "person" + * where + * ("first_name", "last_name") + * in + * ( + * ($1, $2), + * ($3, $4) + * ) + * ``` + */ + tuple( + value1: V1, + value2: V2 + ): ExpressionWrapper> + + tuple( + value1: V1, + value2: V2, + value3: V3 + ): ExpressionWrapper> + + tuple( + value1: V1, + value2: V2, + value3: V3, + value4: V4 + ): ExpressionWrapper> + + tuple( + value1: V1, + value2: V2, + value3: V3, + value4: V4, + value5: V5 + ): ExpressionWrapper> + /** * Returns a literal value expression. * @@ -950,6 +1139,20 @@ export function createExpressionBuilder( return new ExpressionWrapper(parseValueExpressionOrList(value)) }, + refTuple( + ...values: ReadonlyArray> + ): ExpressionWrapper { + return new ExpressionWrapper( + TupleNode.create(values.map(parseReferenceExpression)) + ) + }, + + tuple(...values: ReadonlyArray): ExpressionWrapper { + return new ExpressionWrapper( + TupleNode.create(values.map(parseValueExpression)) + ) + }, + lit( value: VE ): ExpressionWrapper { diff --git a/src/index.ts b/src/index.ts index 0cc1d911e..1ce029264 100644 --- a/src/index.ts +++ b/src/index.ts @@ -189,6 +189,7 @@ export * from './operation-node/json-reference-node.js' export * from './operation-node/json-path-leg-node.js' export * from './operation-node/json-path-node.js' export * from './operation-node/json-operator-chain-node.js' +export * from './operation-node/tuple-node.js' export * from './util/column-type.js' export * from './util/compilable.js' diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 95fdecab0..17fb5d0e7 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -86,6 +86,7 @@ import { JSONReferenceNode } from './json-reference-node.js' import { JSONPathNode } from './json-path-node.js' import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' +import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' /** @@ -208,6 +209,7 @@ export class OperationNodeTransformer { JSONPathNode: this.transformJSONPath.bind(this), JSONPathLegNode: this.transformJSONPathLeg.bind(this), JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this), + TupleNode: this.transformTuple.bind(this), }) transformNode(node: T): T { @@ -978,6 +980,13 @@ export class OperationNodeTransformer { }) } + protected transformTuple(node: TupleNode): TupleNode { + return requireAllProps({ + kind: 'TupleNode', + values: this.transformNodeList(node.values), + }) + } + protected transformDataType(node: DataTypeNode): DataTypeNode { // An Object.freezed leaf node. No need to clone. return node diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index 031b57920..b340c716c 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -88,9 +88,9 @@ import { JSONReferenceNode } from './json-reference-node.js' import { JSONPathNode } from './json-path-node.js' import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' +import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' - export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -186,6 +186,7 @@ export abstract class OperationNodeVisitor { JSONPathNode: this.visitJSONPath.bind(this), JSONPathLegNode: this.visitJSONPathLeg.bind(this), JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this), + TupleNode: this.visitTuple.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -290,4 +291,5 @@ export abstract class OperationNodeVisitor { protected abstract visitJSONPath(node: JSONPathNode): void protected abstract visitJSONPathLeg(node: JSONPathLegNode): void protected abstract visitJSONOperatorChain(node: JSONOperatorChainNode): void + protected abstract visitTuple(node: TupleNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index 7c3fa6546..baf180b24 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -85,6 +85,7 @@ export type OperationNodeKind = | 'JSONPathNode' | 'JSONPathLegNode' | 'JSONOperatorChainNode' + | 'TupleNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/operation-node/tuple-node.ts b/src/operation-node/tuple-node.ts new file mode 100644 index 000000000..bb4ae261f --- /dev/null +++ b/src/operation-node/tuple-node.ts @@ -0,0 +1,23 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' + +export interface TupleNode extends OperationNode { + readonly kind: 'TupleNode' + readonly values: ReadonlyArray +} + +/** + * @internal + */ +export const TupleNode = freeze({ + is(node: OperationNode): node is TupleNode { + return node.kind === 'TupleNode' + }, + + create(values: ReadonlyArray): TupleNode { + return freeze({ + kind: 'TupleNode', + values: freeze(values), + }) + }, +}) diff --git a/src/parser/select-parser.ts b/src/parser/select-parser.ts index f48a02d09..ce46a7801 100644 --- a/src/parser/select-parser.ts +++ b/src/parser/select-parser.ts @@ -42,11 +42,13 @@ export type SelectArg< | ReadonlyArray | ((eb: ExpressionBuilder) => ReadonlyArray) -export type Selection = { - [A in ExtractAliasFromSelectExpression]: SelectType< - ExtractTypeFromSelectExpression - > -} +export type Selection = [SE] extends [unknown] + ? { + [A in ExtractAliasFromSelectExpression]: SelectType< + ExtractTypeFromSelectExpression + > + } + : never type ExtractAliasFromSelectExpression = SE extends string ? ExtractAliasFromStringSelectExpression @@ -151,11 +153,13 @@ type ExtractTypeFromStringSelectExpression< : never : never -export type AllSelection = Selectable<{ - [C in AnyColumn]: { - [T in TB]: C extends keyof DB[T] ? DB[T][C] : never - }[TB] -}> +export type AllSelection = [DB] extends [unknown] + ? Selectable<{ + [C in AnyColumn]: { + [T in TB]: C extends keyof DB[T] ? DB[T][C] : never + }[TB] + }> + : never export function parseSelectArg( selection: SelectArg> diff --git a/src/parser/tuple-parser.ts b/src/parser/tuple-parser.ts new file mode 100644 index 000000000..7283241c8 --- /dev/null +++ b/src/parser/tuple-parser.ts @@ -0,0 +1,73 @@ +import { ExtractTypeFromReferenceExpression } from './reference-parser.js' +import { ExtractTypeFromValueExpression } from './value-parser.js' + +export type RefTuple2 = [R1] extends [unknown] + ? [ + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression + ] + : never + +export type RefTuple3 = [R1] extends [ + unknown +] + ? [ + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression + ] + : never + +export type RefTuple4 = [R1] extends [ + unknown +] + ? [ + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression + ] + : never + +export type RefTuple5 = [ + R1 +] extends [unknown] + ? [ + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression, + ExtractTypeFromReferenceExpression + ] + : never + +export type ValTuple2 = [V1] extends [unknown] + ? [ExtractTypeFromValueExpression, ExtractTypeFromValueExpression] + : never + +export type ValTuple3 = V1 extends any + ? [ + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression + ] + : never + +export type ValTuple4 = [V1] extends [unknown] + ? [ + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression + ] + : never + +export type ValTuple5 = [V1] extends [unknown] + ? [ + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression, + ExtractTypeFromValueExpression + ] + : never diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 1ad8e6616..8c5b2fddb 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -71,6 +71,7 @@ import { KyselyTypeError } from '../util/type-error.js' import { Selectable } from '../util/column-type.js' import { Streamable } from '../util/streamable.js' import { ExpressionOrFactory } from '../parser/expression-parser.js' +import { ExpressionWrapper } from '../expression/expression-wrapper.js' export interface SelectQueryBuilder extends WhereInterface, @@ -1414,6 +1415,99 @@ export interface SelectQueryBuilder */ $castTo(): SelectQueryBuilder + /** + * Changes the output type from an object to a tuple. + * + * This doesn't affect the generated SQL in any way. This function is + * just a necessary evil when you need to convert a query's output + * record type to a tuple type. Typescript doesn't currently offer + * tools to do this automatically (without insane hackery). + * + * The returned object can no longer be executed. It can only be used + * as a subquery. + * + * ### Examples + * + * ```ts + * const result = await db + * .selectFrom('person') + * .selectAll('person') + * .where(({ eb, refTuple, selectFrom }) => eb( + * refTuple('first_name', 'last_name'), + * 'in', + * selectFrom('pet') + * .select(['name', 'species']) + * .where('pet.species', '!=', 'cat') + * .$asTuple('name', 'species') + * )) + * ``` + * + * The generated SQL(PostgreSQL): + * + * ```sql + * select + * "person".* + * from + * "person" + * where + * ("first_name", "last_name") + * in + * ( + * select "name", "species" + * from "pet" + * where "pet"."species" != $1 + * ) + * ``` + */ + $asTuple>( + key1: K1, + key2: K2 + ): keyof O extends K1 | K2 + ? ExpressionWrapper + : KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'> + + $asTuple< + K1 extends keyof O, + K2 extends Exclude, + K3 extends Exclude + >( + key1: K1, + key2: K2, + key3: K3 + ): keyof O extends K1 | K2 | K3 + ? ExpressionWrapper + : KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'> + + $asTuple< + K1 extends keyof O, + K2 extends Exclude, + K3 extends Exclude, + K4 extends Exclude + >( + key1: K1, + key2: K2, + key3: K3, + key4: K4 + ): keyof O extends K1 | K2 | K3 | K4 + ? ExpressionWrapper + : KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'> + + $asTuple< + K1 extends keyof O, + K2 extends Exclude, + K3 extends Exclude, + K4 extends Exclude, + K5 extends Exclude + >( + key1: K1, + key2: K2, + key3: K3, + key4: K4, + key5: K5 + ): keyof O extends K1 | K2 | K3 | K4 | K5 + ? ExpressionWrapper + : KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'> + /** * Narrows (parts of) the output type of the query. * @@ -1966,6 +2060,10 @@ class SelectQueryBuilderImpl return new SelectQueryBuilderImpl(this.#props) as unknown as any } + $asTuple(): ExpressionWrapper { + return new ExpressionWrapper(this.toOperationNode()) + } + withPlugin(plugin: KyselyPlugin): SelectQueryBuilder { return new SelectQueryBuilderImpl({ ...this.#props, diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index a32bc352b..ba01f504a 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -100,9 +100,9 @@ import { JSONReferenceNode } from '../operation-node/json-reference-node.js' import { JSONPathNode } from '../operation-node/json-path-node.js' import { JSONPathLegNode } from '../operation-node/json-path-leg-node.js' import { JSONOperatorChainNode } from '../operation-node/json-operator-chain-node.js' +import { TupleNode } from '../operation-node/tuple-node.js' import { AddIndexNode } from '../operation-node/add-index-node.js' - export class DefaultQueryCompiler extends OperationNodeVisitor implements QueryCompiler @@ -452,6 +452,12 @@ export class DefaultQueryCompiler this.append(')') } + protected override visitTuple(node: TupleNode): void { + this.append('(') + this.compileList(node.values) + this.append(')') + } + protected override visitPrimitiveValueList( node: PrimitiveValueListNode ): void { diff --git a/src/util/column-type.ts b/src/util/column-type.ts index cdbf0d5d5..c52cb3dd7 100644 --- a/src/util/column-type.ts +++ b/src/util/column-type.ts @@ -149,9 +149,11 @@ export type UpdateKeys = { * // } * ``` */ -export type Selectable = { - [K in NonNeverSelectKeys]: SelectType -} +export type Selectable = [R] extends [unknown] + ? { + [K in NonNeverSelectKeys]: SelectType + } + : never /** * Given a table interface, extracts the insert type from all @@ -174,11 +176,13 @@ export type Selectable = { * // } * ``` */ -export type Insertable = { - [K in NonNullableInsertKeys]: InsertType -} & { - [K in NullableInsertKeys]?: InsertType -} +export type Insertable = [R] extends [unknown] + ? { + [K in NonNullableInsertKeys]: InsertType + } & { + [K in NullableInsertKeys]?: InsertType + } + : never /** * Given a table interface, extracts the update type from all @@ -200,6 +204,8 @@ export type Insertable = { * // } * ``` */ -export type Updateable = { - [K in UpdateKeys]?: UpdateType -} +export type Updateable = [R] extends [unknown] + ? { + [K in UpdateKeys]?: UpdateType + } + : never diff --git a/src/util/type-utils.ts b/src/util/type-utils.ts index c1d35408c..46028f50a 100644 --- a/src/util/type-utils.ts +++ b/src/util/type-utils.ts @@ -35,19 +35,23 @@ import { KyselyTypeError } from './type-error.js' * // Columns == 'id' | 'name' | 'species' * ``` */ -export type AnyColumn = DrainOuterGeneric< - { - [T in TB]: keyof DB[T] - }[TB] & - string -> +export type AnyColumn = [DB] extends [unknown] + ? { + [T in TB]: keyof DB[T] + }[TB] & + string + : never /** * Extracts a column type. */ -export type ExtractColumnType = { - [T in TB]: C extends keyof DB[T] ? DB[T][C] : never -}[TB] +export type ExtractColumnType = [C] extends [ + unknown +] + ? { + [T in TB]: C extends keyof DB[T] ? DB[T][C] : never + }[TB] + : never /** * Given a database type and a union of table names in that db, returns diff --git a/test/node/src/select.test.ts b/test/node/src/select.test.ts index 5e7efb9ec..ab8131676 100644 --- a/test/node/src/select.test.ts +++ b/test/node/src/select.test.ts @@ -936,39 +936,5 @@ for (const dialect of DIALECTS) { expect(result[0]).to.eql({ one: 1, person_first_name: 'Arnold' }) } }) - - it.skip('perf', async () => { - const ids = Array.from({ length: 100 }).map(() => - Math.round(Math.random() * 1000) - ) - - function test() { - return ctx.db - .updateTable('person') - .set({ - first_name: 'foo', - last_name: 'bar', - id: 100, - gender: 'other', - }) - .where('id', 'in', ids) - .compile() - } - - // Warmup - for (let i = 0; i < 1000; ++i) { - test() - } - - const time = Date.now() - const N = 100000 - - for (let i = 0; i < N; ++i) { - test() - } - - const endTime = Date.now() - console.log((endTime - time) / N) - }) }) } diff --git a/test/node/src/where.test.ts b/test/node/src/where.test.ts index 0d1038cfe..3ec26f671 100644 --- a/test/node/src/where.test.ts +++ b/test/node/src/where.test.ts @@ -455,6 +455,97 @@ for (const dialect of DIALECTS) { ]) }) + it('a `where in` query with tuples', async () => { + const query = ctx.db + .selectFrom('person') + .selectAll() + .where((eb) => + eb(eb.refTuple('first_name', 'last_name'), 'in', [ + eb.tuple('Jennifer', 'Aniston'), + eb.tuple('Sylvester', 'Stallone'), + ]) + ) + .orderBy('first_name asc') + + testSql(query, dialect, { + postgres: { + sql: 'select * from "person" where ("first_name", "last_name") in (($1, $2), ($3, $4)) order by "first_name" asc', + parameters: ['Jennifer', 'Aniston', 'Sylvester', 'Stallone'], + }, + mysql: { + sql: 'select * from `person` where (`first_name`, `last_name`) in ((?, ?), (?, ?)) order by `first_name` asc', + parameters: ['Jennifer', 'Aniston', 'Sylvester', 'Stallone'], + }, + sqlite: { + sql: 'select * from "person" where ("first_name", "last_name") in ((?, ?), (?, ?)) order by "first_name" asc', + parameters: ['Jennifer', 'Aniston', 'Sylvester', 'Stallone'], + }, + }) + + const persons = await query.execute() + expect(persons).to.have.length(2) + expect(persons).to.containSubset([ + { + first_name: 'Jennifer', + last_name: 'Aniston', + gender: 'female', + }, + { + first_name: 'Sylvester', + last_name: 'Stallone', + gender: 'male', + }, + ]) + }) + + it('a `where in` query with tuples and a subquery', async () => { + const query = ctx.db + .selectFrom('person') + .selectAll() + .where((eb) => + eb( + eb.refTuple('first_name', 'last_name'), + 'in', + eb + .selectFrom('person as p2') + .select(['p2.first_name', 'p2.last_name']) + .where('first_name', 'in', ['Arnold', 'Sylvester']) + .$asTuple('first_name', 'last_name') + ) + ) + .orderBy('first_name asc') + + testSql(query, dialect, { + postgres: { + sql: 'select * from "person" where ("first_name", "last_name") in (select "p2"."first_name", "p2"."last_name" from "person" as "p2" where "first_name" in ($1, $2)) order by "first_name" asc', + parameters: ['Arnold', 'Sylvester'], + }, + mysql: { + sql: 'select * from `person` where (`first_name`, `last_name`) in (select `p2`.`first_name`, `p2`.`last_name` from `person` as `p2` where `first_name` in (?, ?)) order by `first_name` asc', + parameters: ['Arnold', 'Sylvester'], + }, + sqlite: { + sql: 'select * from "person" where ("first_name", "last_name") in (select "p2"."first_name", "p2"."last_name" from "person" as "p2" where "first_name" in (?, ?)) order by "first_name" asc', + parameters: ['Arnold', 'Sylvester'], + }, + }) + + const persons = await query.execute() + expect(persons).to.have.length(2) + expect(persons).to.containSubset([ + { + first_name: 'Arnold', + last_name: 'Schwarzenegger', + gender: 'male', + }, + { + first_name: 'Sylvester', + last_name: 'Stallone', + gender: 'male', + }, + ]) + }) + it('two where expressions', async () => { const query = ctx.db .selectFrom('person') diff --git a/test/typings/test-d/expression.test-d.ts b/test/typings/test-d/expression.test-d.ts index b3badd873..1afcff920 100644 --- a/test/typings/test-d/expression.test-d.ts +++ b/test/typings/test-d/expression.test-d.ts @@ -221,3 +221,68 @@ async function textExpressionBuilderAny( // Not an array expectError(eb(eb.val('Jen'), '=', eb.fn.any('id'))) } + +function testExpressionBuilderTuple(db: Kysely) { + db.selectFrom('person') + .selectAll() + .where(({ eb, refTuple, tuple }) => + eb(refTuple('first_name', 'last_name'), 'in', [ + tuple('Jennifer', 'Aniston'), + tuple('Sylvester', 'Stallone'), + ]) + ) + + db.selectFrom('person') + .selectAll() + .where(({ eb, refTuple, selectFrom }) => + eb( + refTuple('first_name', 'last_name'), + 'in', + selectFrom('person') + .select(['first_name', 'last_name']) + .$asTuple('first_name', 'last_name') + ) + ) + + // Wrong tuple type + expectError( + db + .selectFrom('person') + .where(({ eb, refTuple, tuple }) => + eb(refTuple('first_name', 'last_name'), 'in', [ + tuple('Jennifer', 'Aniston'), + tuple('Sylvester', 1), + ]) + ) + ) + + // Wrong tuple length + expectError( + db + .selectFrom('person') + .where(({ eb, refTuple, tuple }) => + eb(refTuple('first_name', 'last_name'), 'in', [ + tuple('Jennifer', 'Aniston', 'Extra'), + tuple('Sylvester', 'Stallone'), + ]) + ) + ) + + // Not all selected columns provided for $asTuple + expectType< + KyselyTypeError<'$asTuple() call failed: All selected columns must be provided as arguments'> + >( + db + .selectFrom('person') + .select(['first_name', 'last_name', 'age']) + .$asTuple('first_name', 'last_name') + ) + + // Duplicate column provided for $asTuple + expectError( + db + .selectFrom('person') + .select(['first_name', 'last_name']) + .$asTuple('first_name', 'last_name', 'last_name') + ) +}