Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/parser/update-set-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export type UpdateObjectExpression<
UT extends keyof DB = TB,
> = UpdateObject<DB, TB, UT> | UpdateObjectFactory<DB, TB, UT>

export type UpdateObjectWithRef<
DB,
TB extends keyof DB,
UT extends keyof DB = TB,
> = DrainOuterGeneric<{
[C in AnyColumn<DB, UT>]?: ReferenceExpression<DB, TB>
}>

export type ExtractUpdateTypeFromReferenceExpression<
DB,
TB extends keyof DB,
Expand All @@ -62,6 +70,36 @@ export function parseUpdate(
return parseUpdateObjectExpression(args[0])
}

export function parseUpdateWithRef(
...args:
| [ReferenceExpression<any, any>, ReferenceExpression<any, any>]
| [UpdateObjectWithRef<any, any, any>]
): ReadonlyArray<ColumnUpdateNode> {
if (args.length === 2) {
return [
ColumnUpdateNode.create(
parseReferenceExpression(args[0]),
parseReferenceExpression(args[1]),
),
]
}

return parseUpdateObjectWithRef(args[0])
}

export function parseUpdateObjectWithRef(
update: UpdateObjectWithRef<any, any, any>,
): ReadonlyArray<ColumnUpdateNode> {
return Object.entries(update)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => {
return ColumnUpdateNode.create(
ColumnNode.create(key),
parseReferenceExpression(value!),
)
})
}

export function parseUpdateObjectExpression(
update: UpdateObjectExpression<any, any, any>,
): ReadonlyArray<ColumnUpdateNode> {
Expand Down
76 changes: 76 additions & 0 deletions src/query-builder/update-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -743,6 +744,81 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
})
}

/**
* 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<RE extends ReferenceExpression<DB, UT>>(
key: RE,
value: RE,
): UpdateQueryBuilder<DB, UT, TB, O>

setRef(
updates: UpdateObjectWithRef<DB, TB, UT>,
): UpdateQueryBuilder<DB, UT, TB, O>

setRef(
...args:
| [ReferenceExpression<DB, UT>, ReferenceExpression<DB, UT>]
| [UpdateObjectWithRef<DB, TB, UT>]
): UpdateQueryBuilder<DB, UT, TB, O> {
return new UpdateQueryBuilder({
...this.#props,
queryNode: UpdateQueryNode.cloneWithUpdates(
this.#props.queryNode,
parseUpdateWithRef(...args),
),
})
}

returning<SE extends SelectExpression<DB, TB>>(
selections: ReadonlyArray<SE>,
): UpdateQueryBuilder<DB, UT, TB, ReturningRow<DB, TB, O, SE>>
Expand Down
90 changes: 90 additions & 0 deletions test/node/src/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
28 changes: 28 additions & 0 deletions test/typings/test-d/update.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ async function testUpdate(db: Kysely<Database>) {
.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<UpdateResult>(r6)

// Non-existent column
expectError(
db
Expand All @@ -55,6 +62,27 @@ async function testUpdate(db: Kysely<Database>) {
.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
Expand Down