diff --git a/src/parser/update-set-parser.ts b/src/parser/update-set-parser.ts index 2c9e1ef5a..deb5ec0d3 100644 --- a/src/parser/update-set-parser.ts +++ b/src/parser/update-set-parser.ts @@ -38,6 +38,14 @@ export type UpdateObjectExpression< UT extends keyof DB = TB, > = UpdateObject | UpdateObjectFactory +export type UpdateObjectWithRef< + DB, + TB extends keyof DB, + UT extends keyof DB = TB, +> = DrainOuterGeneric<{ + [C in AnyColumn]?: ReferenceExpression +}> + export type ExtractUpdateTypeFromReferenceExpression< DB, TB extends keyof DB, @@ -62,6 +70,36 @@ export function parseUpdate( return parseUpdateObjectExpression(args[0]) } +export function parseUpdateWithRef( + ...args: + | [ReferenceExpression, ReferenceExpression] + | [UpdateObjectWithRef] +): ReadonlyArray { + if (args.length === 2) { + return [ + ColumnUpdateNode.create( + parseReferenceExpression(args[0]), + parseReferenceExpression(args[1]), + ), + ] + } + + return parseUpdateObjectWithRef(args[0]) +} + +export function parseUpdateObjectWithRef( + update: UpdateObjectWithRef, +): ReadonlyArray { + return Object.entries(update) + .filter(([_, value]) => value !== undefined) + .map(([key, value]) => { + return ColumnUpdateNode.create( + ColumnNode.create(key), + parseReferenceExpression(value!), + ) + }) +} + export function parseUpdateObjectExpression( update: UpdateObjectExpression, ): ReadonlyArray { diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index 12984650d..8412c238d 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -40,6 +40,7 @@ import { type UpdateObjectExpression, type ExtractUpdateTypeFromReferenceExpression, parseUpdate, + parseUpdateWithRef, } from '../parser/update-set-parser.js' import type { Compilable } from '../util/compilable.js' import type { QueryExecutor } from '../query-executor/query-executor.js' @@ -743,6 +744,81 @@ export class UpdateQueryBuilder }) } + /** + * Sets the values to update for an {@link Kysely.updateTable | update} query + * using column references instead of values. + * + * This method is similar to {@link set} but allows you to update columns by + * referencing other columns instead of providing literal values. This is useful + * when you want to copy values from one column to another or perform updates + * based on existing column values. + * + * You can provide either two arguments (column name and reference) or a single + * object where keys are column names and values are column references. + * + * ### Examples + * + * Update a column by referencing another column using the two-argument form: + * + * ```ts + * const result = await db + * .updateTable('person') + * .setRef('last_name', 'first_name') + * .where('id', '=', 1) + * .executeTakeFirst() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * update "person" set "last_name" = "first_name" where "id" = $1 + * ``` + * + * You can reference columns from joined tables in a PostgreSQL `from` query: + * + * ```ts + * const result = await db + * .updateTable('person') + * .from('pet') + * .setRef({ + * first_name: 'pet.name', + * }) + * .whereRef('pet.owner_id', '=', 'person.id') + * .executeTakeFirst() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * update "person" + * set "first_name" = "pet"."name" + * from "pet" + * where "pet"."owner_id" = "person"."id" + * ``` + */ + setRef>( + key: RE, + value: RE, + ): UpdateQueryBuilder + + setRef( + updates: UpdateObjectWithRef, + ): UpdateQueryBuilder + + setRef( + ...args: + | [ReferenceExpression, ReferenceExpression] + | [UpdateObjectWithRef] + ): UpdateQueryBuilder { + return new UpdateQueryBuilder({ + ...this.#props, + queryNode: UpdateQueryNode.cloneWithUpdates( + this.#props.queryNode, + parseUpdateWithRef(...args), + ), + }) + } + returning>( selections: ReadonlyArray, ): UpdateQueryBuilder> diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts index a88d6d7a7..9224f53d5 100644 --- a/test/node/src/update.test.ts +++ b/test/node/src/update.test.ts @@ -355,6 +355,96 @@ for (const dialect of DIALECTS) { expect(jennifer.last_name).to.equal('Jennifer') }) + it('should update one row using setRef', async () => { + const query = ctx.db + .updateTable('person') + .setRef('last_name', 'first_name') + .where('first_name', '=', 'Jennifer') + + testSql(query, dialect, { + postgres: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = $1', + parameters: ['Jennifer'], + }, + mysql: { + sql: 'update `person` set `last_name` = `first_name` where `first_name` = ?', + parameters: ['Jennifer'], + }, + mssql: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = @1', + parameters: ['Jennifer'], + }, + sqlite: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = ?', + parameters: ['Jennifer'], + }, + }) + + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(UpdateResult) + expect(result.numUpdatedRows).to.equal(1n) + if (sqlSpec === 'mysql') { + expect(result.numChangedRows).to.equal(1n) + } else { + expect(result.numChangedRows).to.undefined + } + + const jennifer = await ctx.db + .selectFrom('person') + .where('first_name', '=', 'Jennifer') + .select('last_name') + .executeTakeFirstOrThrow() + + expect(jennifer.last_name).to.equal('Jennifer') + }) + + it('should update one row using setRef with object', async () => { + const query = ctx.db + .updateTable('person') + .setRef({ + last_name: 'first_name', + }) + .where('first_name', '=', 'Jennifer') + + testSql(query, dialect, { + postgres: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = $1', + parameters: ['Jennifer'], + }, + mysql: { + sql: 'update `person` set `last_name` = `first_name` where `first_name` = ?', + parameters: ['Jennifer'], + }, + mssql: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = @1', + parameters: ['Jennifer'], + }, + sqlite: { + sql: 'update "person" set "last_name" = "first_name" where "first_name" = ?', + parameters: ['Jennifer'], + }, + }) + + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(UpdateResult) + expect(result.numUpdatedRows).to.equal(1n) + if (sqlSpec === 'mysql') { + expect(result.numChangedRows).to.equal(1n) + } else { + expect(result.numChangedRows).to.undefined + } + + const jennifer = await ctx.db + .selectFrom('person') + .where('first_name', '=', 'Jennifer') + .select('last_name') + .executeTakeFirstOrThrow() + + expect(jennifer.last_name).to.equal('Jennifer') + }) + it('should update one row while ignoring undefined values', async () => { const query = ctx.db .updateTable('person') diff --git a/test/typings/test-d/update.test-d.ts b/test/typings/test-d/update.test-d.ts index acd561ac1..3bcdcf7db 100644 --- a/test/typings/test-d/update.test-d.ts +++ b/test/typings/test-d/update.test-d.ts @@ -47,6 +47,13 @@ async function testUpdate(db: Kysely) { .execute() expectType<{ fn: string; person_id: number }[]>(r5) + const r6 = await db + .updateTable('pet as p') + .where('p.id', '=', '1') + .setRef('name', 'species') + .executeTakeFirst() + expectType(r6) + // Non-existent column expectError( db @@ -55,6 +62,27 @@ async function testUpdate(db: Kysely) { .set({ name: 'Fluffy', not_a_column: 'not_a_column' }), ) + expectError( + db + .updateTable('pet as p') + .where('p.id', '=', '1') + .setRef('name', 'not_a_column'), + ) + + expectError( + db + .updateTable('pet as p') + .where('p.id', '=', '1') + .setRef({ name: 'not_a_column' }), + ) + + expectError( + db + .updateTable('pet as p') + .where('p.id', '=', '1') + .setRef({ not_a_column: 'not_a_column' }), + ) + // Non-existent column in a callback expectError( db