diff --git a/src/parser/returning-parser.ts b/src/parser/returning-parser.ts index 8833705cb..abc0520a7 100644 --- a/src/parser/returning-parser.ts +++ b/src/parser/returning-parser.ts @@ -1,7 +1,7 @@ import { DeleteResult } from '../query-builder/delete-result.js' import { InsertResult } from '../query-builder/insert-result.js' import { UpdateResult } from '../query-builder/update-result.js' -import { Selection } from './select-parser.js' +import { Selection, AllSelection } from './select-parser.js' export type ReturningRow< DB, @@ -15,3 +15,11 @@ export type ReturningRow< : O extends UpdateResult ? Selection : O & Selection + +export type ReturningAllRow = O extends InsertResult + ? AllSelection + : O extends DeleteResult + ? AllSelection + : O extends UpdateResult + ? AllSelection + : O & AllSelection diff --git a/src/parser/select-parser.ts b/src/parser/select-parser.ts index e33c0155a..4b017391d 100644 --- a/src/parser/select-parser.ts +++ b/src/parser/select-parser.ts @@ -167,7 +167,7 @@ type ExtractTypeFromStringSelectExpression< : never : never -type AllSelection = Selectable<{ +export type AllSelection = Selectable<{ [C in AnyColumn]: { [T in TB]: C extends keyof DB[T] ? DB[T][C] : never }[TB] diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts index 5ae3adfd1..0a10ee7eb 100644 --- a/src/query-builder/delete-query-builder.ts +++ b/src/query-builder/delete-query-builder.ts @@ -18,7 +18,7 @@ import { SelectExpression, SelectExpressionOrList, } from '../parser/select-parser.js' -import { ReturningRow } from '../parser/returning-parser.js' +import { ReturningAllRow, ReturningRow } from '../parser/returning-parser.js' import { ReferenceExpression } from '../parser/reference-parser.js' import { QueryNode } from '../operation-node/query-node.js' import { @@ -38,7 +38,6 @@ import { ReturningInterface } from './returning-interface.js' import { NoResultError, NoResultErrorConstructor } from './no-result-error.js' import { DeleteResult } from './delete-result.js' import { DeleteQueryNode } from '../operation-node/delete-query-node.js' -import { Selectable } from '../util/column-type.js' import { LimitNode } from '../operation-node/limit-node.js' import { OrderByDirectionExpression, @@ -503,12 +502,114 @@ export class DeleteQueryBuilder }) } - returningAll(): DeleteQueryBuilder> { + /** + * Adds `returning *` or `returning table.*` clause to the query. + * + * ### Examples + * + * Return all columns. + * + * ```ts + * const pets = await db + * .deleteFrom('pet') + * .returningAll() + * .execute() + * ``` + * + * The generated SQL (PostgreSQL) + * + * ```sql + * delete from "pet" returning * + * ``` + * + * Return all columns from all tables + * + * ```ts + * const result = ctx.db + * .deleteFrom('toy') + * .using(['pet', 'person']) + * .whereRef('toy.pet_id', '=', 'pet.id') + * .whereRef('pet.owner_id', '=', 'person.id') + * .where('person.first_name', '=', 'Zoro') + * .returningAll() + * .execute() + * ``` + * + * The generated SQL (PostgreSQL) + * + * ```sql + * delete from "toy" + * using "pet", "person" + * where "toy"."pet_id" = "pet"."id" + * and "pet"."owner_id" = "person"."id" + * and "person"."first_name" = $1 + * returning * + * ``` + * + * Return all columns from a single table. + * + * ```ts + * const result = ctx.db + * .deleteFrom('toy') + * .using(['pet', 'person']) + * .whereRef('toy.pet_id', '=', 'pet.id') + * .whereRef('pet.owner_id', '=', 'person.id') + * .where('person.first_name', '=', 'Itachi') + * .returningAll('pet') + * .execute() + * ``` + * + * The generated SQL (PostgreSQL) + * + * ```sql + * delete from "toy" + * using "pet", "person" + * where "toy"."pet_id" = "pet"."id" + * and "pet"."owner_id" = "person"."id" + * and "person"."first_name" = $1 + * returning "pet".* + * ``` + * + * Return all columns from multiple tables. + * + * ```ts + * const result = ctx.db + * .deleteFrom('toy') + * .using(['pet', 'person']) + * .whereRef('toy.pet_id', '=', 'pet.id') + * .whereRef('pet.owner_id', '=', 'person.id') + * .where('person.first_name', '=', 'Luffy') + * .returningAll(['toy', 'pet']) + * .execute() + * ``` + * + * The generated SQL (PostgreSQL) + * + * ```sql + * delete from "toy" + * using "pet", "person" + * where "toy"."pet_id" = "pet"."id" + * and "pet"."owner_id" = "person"."id" + * and "person"."first_name" = $1 + * returning "toy".*, "pet".* + * ``` + */ + returningAll( + tables: ReadonlyArray + ): DeleteQueryBuilder> + + returningAll( + table: T + ): DeleteQueryBuilder> + + returningAll(): DeleteQueryBuilder> + + returningAll(table?: any): any { return new DeleteQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithReturning( this.#props.queryNode, - parseSelectAll() + parseSelectAll(table) ), }) } diff --git a/test/node/src/delete.test.ts b/test/node/src/delete.test.ts index 58b0b663c..a9e26a76c 100644 --- a/test/node/src/delete.test.ts +++ b/test/node/src/delete.test.ts @@ -244,6 +244,180 @@ for (const dialect of BUILT_IN_DIALECTS) { await query.execute() }) + + it('should delete from t1 returning *', async () => { + const query = ctx.db + .deleteFrom('pet') + .where('pet.species', '=', 'cat') + .returningAll() + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "pet"', + 'where "pet"."species" = $1', + 'returning *', + ], + parameters: ['cat'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 using t2, t3 returning *', async () => { + const query = ctx.db + .deleteFrom('toy') + .using(['pet', 'person']) + .whereRef('toy.pet_id', '=', 'pet.id') + .whereRef('pet.owner_id', '=', 'person.id') + .where('person.first_name', '=', 'Zoro') + .returningAll() + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "toy"', + 'using "pet", "person"', + 'where "toy"."pet_id" = "pet"."id"', + 'and "pet"."owner_id" = "person"."id"', + 'and "person"."first_name" = $1', + 'returning *', + ], + parameters: ['Zoro'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 using t2, t3 returning t1.*, t2.*', async () => { + const query = ctx.db + .deleteFrom('toy') + .using(['pet', 'person']) + .whereRef('toy.pet_id', '=', 'pet.id') + .whereRef('pet.owner_id', '=', 'person.id') + .where('person.first_name', '=', 'Luffy') + .returningAll(['toy', 'pet']) + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "toy"', + 'using "pet", "person"', + 'where "toy"."pet_id" = "pet"."id"', + 'and "pet"."owner_id" = "person"."id"', + 'and "person"."first_name" = $1', + 'returning "toy".*, "pet".*', + ], + parameters: ['Luffy'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 using t2, t3 returning t2.*', async () => { + const query = ctx.db + .deleteFrom('toy') + .using(['pet', 'person']) + .whereRef('toy.pet_id', '=', 'pet.id') + .whereRef('pet.owner_id', '=', 'person.id') + .where('person.first_name', '=', 'Itachi') + .returningAll('pet') + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "toy"', + 'using "pet", "person"', + 'where "toy"."pet_id" = "pet"."id"', + 'and "pet"."owner_id" = "person"."id"', + 'and "person"."first_name" = $1', + 'returning "pet".*', + ], + parameters: ['Itachi'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 returning t1.*', async () => { + const query = ctx.db + .deleteFrom('person') + .where('gender', '=', 'male') + .returningAll('person') + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "person"', + 'where "gender" = $1', + 'returning "person".*', + ], + parameters: ['male'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 returning *', async () => { + const query = ctx.db + .deleteFrom('person') + .where('gender', '=', 'male') + .returningAll() + + testSql(query, dialect, { + postgres: { + sql: ['delete from "person"', 'where "gender" = $1', 'returning *'], + parameters: ['male'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should delete from t1 using t2, t3 returning *', async () => { + const query = ctx.db + .deleteFrom('toy') + .using(['pet', 'person']) + .whereRef('toy.pet_id', '=', 'pet.id') + .whereRef('pet.owner_id', '=', 'person.id') + .where('person.first_name', '=', 'Bob') + .returningAll() + + testSql(query, dialect, { + postgres: { + sql: [ + 'delete from "toy"', + 'using "pet", "person"', + 'where "toy"."pet_id" = "pet"."id"', + 'and "pet"."owner_id" = "person"."id"', + 'and "person"."first_name" = $1', + 'returning *', + ], + parameters: ['Bob'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) } if (dialect === 'mysql') { diff --git a/test/typings/test-d/delete-query-builder.test-d.ts b/test/typings/test-d/delete-query-builder.test-d.ts index ee87c36ab..b302e817c 100644 --- a/test/typings/test-d/delete-query-builder.test-d.ts +++ b/test/typings/test-d/delete-query-builder.test-d.ts @@ -1,6 +1,6 @@ import { expectError, expectType } from 'tsd' -import { Kysely, DeleteResult } from '..' -import { Database } from '../shared' +import { Kysely, DeleteResult, Selectable } from '..' +import { Database, Person, Pet } from '../shared' async function testDelete(db: Kysely) { const r1 = await db.deleteFrom('pet').where('id', '=', '1').executeTakeFirst() @@ -79,4 +79,108 @@ async function testDelete(db: Kysely) { .using('pet') .leftJoin('person', 'NO_SUCH_COLUMN', 'pet.owner_id') ) + + const r8 = await db + .deleteFrom('person') + .using(['person', 'pet']) + .leftJoin('toy', 'toy.pet_id', 'pet.id') + .where('pet.species', '=', 'cat') + .orWhere('toy.price', '=', 0) + .returningAll('person') + .execute() + expectType[]>(r8) + + const r9 = await db + .deleteFrom('pet') + .where('pet.species', '=', 'cat') + .returningAll('pet') + .execute() + expectType[]>(r9) + + const r10 = await db + .deleteFrom('person') + .using(['person', 'pet']) + .leftJoin('toy', 'toy.pet_id', 'pet.id') + .where('pet.species', '=', 'cat') + .orWhere('toy.price', '=', 0) + .returningAll(['pet', 'toy', 'person']) + .execute() + expectType< + { + id: number | string | null + first_name: string + last_name: string | null + age: number + gender: 'male' | 'female' | 'other' + modified_at: Date + + name: string + owner_id: number + species: 'dog' | 'cat' + + price: number | null + pet_id: string | null + }[] + >(r10) + + const r11 = await db + .deleteFrom('person') + .innerJoin('pet', 'pet.owner_id', 'person.id') + .where('pet.species', '=', 'dog') + .returningAll(['person', 'pet']) + .execute() + expectType< + { + id: number | string + first_name: string + last_name: string | null + age: number + gender: 'male' | 'female' | 'other' + modified_at: Date + + name: string + owner_id: number + species: 'dog' | 'cat' + }[] + >(r11) + + const r12 = await db + .deleteFrom('pet') + .where('pet.species', '=', 'cat') + .returningAll(['pet']) + .execute() + expectType[]>(r12) + + const r13 = await db + .deleteFrom('pet') + .where('pet.species', '=', 'dog') + .returningAll() + .execute() + expectType[]>(r13) + + const r14 = await db + .deleteFrom('person') + .using(['person', 'pet']) + .leftJoin('toy', 'toy.pet_id', 'pet.id') + .where('pet.species', '=', 'cat') + .orWhere('toy.price', '=', 0) + .returningAll() + .execute() + expectType< + { + id: number | string | null + first_name: string + last_name: string | null + age: number + gender: 'male' | 'female' | 'other' + modified_at: Date + + name: string + owner_id: number + species: 'dog' | 'cat' + + price: number | null + pet_id: string | null + }[] + >(r14) }