diff --git a/site/docs/examples/update/0010-single-row.js b/site/docs/examples/update/0010-single-row.js
index e82a23a65..b30a98077 100644
--- a/site/docs/examples/update/0010-single-row.js
+++ b/site/docs/examples/update/0010-single-row.js
@@ -5,6 +5,4 @@ export const singleRow = `const result = await db
last_name: 'Aniston'
})
.where('id', '=', '1')
- .executeTakeFirst()
-
-console.log(result.numUpdatedRows)`
\ No newline at end of file
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0020-complex-values.js b/site/docs/examples/update/0020-complex-values.js
index 3b88fee1e..caad0ebb6 100644
--- a/site/docs/examples/update/0020-complex-values.js
+++ b/site/docs/examples/update/0020-complex-values.js
@@ -6,6 +6,4 @@ export const complexValues = `const result = await db
last_name: 'updated',
}))
.where('id', '=', '1')
- .executeTakeFirst()
-
-console.log(result.numUpdatedRows)`
\ No newline at end of file
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0030-my-sql-joins.js b/site/docs/examples/update/0030-my-sql-joins.js
new file mode 100644
index 000000000..400b23c44
--- /dev/null
+++ b/site/docs/examples/update/0030-my-sql-joins.js
@@ -0,0 +1,7 @@
+export const mySqlJoins = `const result = await db
+ .updateTable(['person', 'pet'])
+ .set('person.first_name', 'Updated person')
+ .set('pet.name', 'Updated doggo')
+ .whereRef('person.id', '=', 'pet.owner_id')
+ .where('person.id', '=', '1')
+ .executeTakeFirst()`
\ No newline at end of file
diff --git a/site/docs/examples/update/0030-my-sql-joins.mdx b/site/docs/examples/update/0030-my-sql-joins.mdx
new file mode 100644
index 000000000..82c4db139
--- /dev/null
+++ b/site/docs/examples/update/0030-my-sql-joins.mdx
@@ -0,0 +1,40 @@
+---
+title: 'MySQL joins'
+---
+
+# MySQL joins
+
+MySQL allows you to join tables directly to the "main" table and update
+rows of all joined tables. This is possible by passing all tables to the
+`updateTable` method as a list and adding the `ON` conditions as `WHERE`
+statements. You can then use the `set(column, value)` variant to update
+columns using table qualified names.
+
+The `UpdateQueryBuilder` also has `innerJoin` etc. join methods, but those
+can only be used as part of a PostgreSQL `update set from join` query.
+Due to type complexity issues, we unfortunately can't make the same
+methods work in both cases.
+
+import {
+ Playground,
+ exampleSetup,
+} from '../../../src/components/Playground'
+
+import {
+ mySqlJoins
+} from './0030-my-sql-joins'
+
+
+
+:::info[More examples]
+The API documentation is packed with examples. The API docs are hosted [here](https://kysely-org.github.io/kysely-apidoc/),
+but you can access the same documentation by hovering over functions/methods/classes in your IDE. The examples are always
+just one hover away!
+
+For example, check out these sections:
+ - [set method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#set)
+ - [returning method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#returning)
+ - [updateTable method](https://kysely-org.github.io/kysely-apidoc/classes/Kysely.html#updateTable)
+:::
diff --git a/src/operation-node/update-query-node.ts b/src/operation-node/update-query-node.ts
index c4188205a..a815a45c9 100644
--- a/src/operation-node/update-query-node.ts
+++ b/src/operation-node/update-query-node.ts
@@ -12,6 +12,7 @@ import { ExplainNode } from './explain-node.js'
import { LimitNode } from './limit-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
+import { ListNode } from './list-node.js'
export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode
@@ -39,10 +40,15 @@ export const UpdateQueryNode = freeze({
return node.kind === 'UpdateQueryNode'
},
- create(table: OperationNode, withNode?: WithNode): UpdateQueryNode {
+ create(
+ tables: ReadonlyArray,
+ withNode?: WithNode,
+ ): UpdateQueryNode {
return freeze({
kind: 'UpdateQueryNode',
- table,
+ // For backwards compatibility, use the raw table node when there's only one table
+ // and don't rename the property to something like `tables`.
+ table: tables.length === 1 ? tables[0] : ListNode.create(tables),
...(withNode && { with: withNode }),
})
},
diff --git a/src/parser/update-set-parser.ts b/src/parser/update-set-parser.ts
index c3ce3fe4c..248491956 100644
--- a/src/parser/update-set-parser.ts
+++ b/src/parser/update-set-parser.ts
@@ -12,12 +12,19 @@ import {
parseReferenceExpression,
ReferenceExpression,
} from './reference-parser.js'
+import { AnyColumn, DrainOuterGeneric } from '../util/type-utils.js'
-export type UpdateObject = {
- [C in UpdateKeys]?:
- | ValueExpression>
- | undefined
-}
+export type UpdateObject<
+ DB,
+ TB extends keyof DB,
+ UT extends keyof DB = TB,
+> = DrainOuterGeneric<{
+ [C in AnyColumn]?: {
+ [T in UT]: C extends keyof DB[T]
+ ? ValueExpression> | undefined
+ : never
+ }[UT]
+}>
export type UpdateObjectFactory<
DB,
diff --git a/src/plugin/with-schema/with-schema-transformer.ts b/src/plugin/with-schema/with-schema-transformer.ts
index af14b0b3d..f036d67d4 100644
--- a/src/plugin/with-schema/with-schema-transformer.ts
+++ b/src/plugin/with-schema/with-schema-transformer.ts
@@ -1,5 +1,6 @@
import { AliasNode } from '../../operation-node/alias-node.js'
import { IdentifierNode } from '../../operation-node/identifier-node.js'
+import { ListNode } from '../../operation-node/list-node.js'
import { OperationNodeTransformer } from '../../operation-node/operation-node-transformer.js'
import { OperationNode } from '../../operation-node/operation-node.js'
import { ReferencesNode } from '../../operation-node/references-node.js'
@@ -157,14 +158,14 @@ export class WithSchemaTransformer extends OperationNodeTransformer {
node: OperationNode,
schemableIds: Set,
): void {
- const table = TableNode.is(node)
- ? node
- : AliasNode.is(node) && TableNode.is(node.node)
- ? node.node
- : null
-
- if (table) {
- this.#collectSchemableId(table.table, schemableIds)
+ if (TableNode.is(node)) {
+ this.#collectSchemableId(node.table, schemableIds)
+ } else if (AliasNode.is(node) && TableNode.is(node.node)) {
+ this.#collectSchemableId(node.node.table, schemableIds)
+ } else if (ListNode.is(node)) {
+ for (const table of node.items) {
+ this.#collectSchemableIdsFromTableExpr(table, schemableIds)
+ }
}
}
diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts
index b68704198..bf0605a66 100644
--- a/src/query-builder/update-query-builder.ts
+++ b/src/query-builder/update-query-builder.ts
@@ -521,8 +521,6 @@ export class UpdateQueryBuilder
* })
* .where('id', '=', '1')
* .executeTakeFirst()
- *
- * console.log(result.numUpdatedRows)
* ```
*
* The generated SQL (PostgreSQL):
@@ -546,8 +544,6 @@ export class UpdateQueryBuilder
* }))
* .where('id', '=', '1')
* .executeTakeFirst()
- *
- * console.log(result.numUpdatedRows)
* ```
*
* The generated SQL (PostgreSQL):
@@ -630,6 +626,43 @@ export class UpdateQueryBuilder
* "last_name" = $3 || $4
* where "id" = $5
* ```
+ *
+ *
+ *
+ * MySQL allows you to join tables directly to the "main" table and update
+ * rows of all joined tables. This is possible by passing all tables to the
+ * `updateTable` method as a list and adding the `ON` conditions as `WHERE`
+ * statements. You can then use the `set(column, value)` variant to update
+ * columns using table qualified names.
+ *
+ * The `UpdateQueryBuilder` also has `innerJoin` etc. join methods, but those
+ * can only be used as part of a PostgreSQL `update set from join` query.
+ * Due to type complexity issues, we unfortunately can't make the same
+ * methods work in both cases.
+ *
+ * ```ts
+ * const result = await db
+ * .updateTable(['person', 'pet'])
+ * .set('person.first_name', 'Updated person')
+ * .set('pet.name', 'Updated doggo')
+ * .whereRef('person.id', '=', 'pet.owner_id')
+ * .where('person.id', '=', '1')
+ * .executeTakeFirst()
+ * ```
+ *
+ * The generated SQL (MySQL):
+ *
+ * ```sql
+ * update
+ * `person`,
+ * `pet`
+ * set
+ * `person`.`first_name` = ?,
+ * `pet`.`name` = ?
+ * where
+ * `person`.`id` = `pet`.`owner_id`
+ * and `person`.`id` = ?
+ * ```
*/
set(
update: UpdateObjectExpression,
diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts
index 836bdc72c..591424e7d 100644
--- a/src/query-compiler/default-query-compiler.ts
+++ b/src/query-compiler/default-query-compiler.ts
@@ -808,6 +808,10 @@ export class DefaultQueryCompiler
}
if (node.joins) {
+ if (!node.from) {
+ throw new Error("Joins in an update query are only supported as a part of a PostgreSQL 'update set from join' query. If you want to create a MySQL 'update join set' query, see https://kysely.dev/docs/examples/update/my-sql-joins")
+ }
+
this.append(' ')
this.compileList(node.joins, ' ')
}
diff --git a/src/query-creator.ts b/src/query-creator.ts
index 5291fee28..b698d9a51 100644
--- a/src/query-creator.ts
+++ b/src/query-creator.ts
@@ -464,39 +464,48 @@ export class QueryCreator {
* console.log(result.numUpdatedRows)
* ```
*/
- updateTable(
- table: TR,
+ updateTable(
+ from: TE[],
): UpdateQueryBuilder<
DB,
- ExtractTableAlias,
- ExtractTableAlias,
+ ExtractTableAlias,
+ ExtractTableAlias,
UpdateResult
>
- updateTable>(
- table: TR,
+ updateTable>(
+ from: TE[],
): UpdateQueryBuilder<
- DB & PickTableWithAlias,
- ExtractTableAlias, TR>,
- ExtractTableAlias, TR>,
+ From,
+ FromTables,
+ FromTables,
UpdateResult
>
- updateTable>(
- table: TR,
+ updateTable(
+ from: TE,
): UpdateQueryBuilder<
- From,
- FromTables,
- FromTables,
+ DB,
+ ExtractTableAlias,
+ ExtractTableAlias,
UpdateResult
>
- updateTable>(table: TR): any {
+ updateTable>(
+ from: TE,
+ ): UpdateQueryBuilder<
+ DB & PickTableWithAlias,
+ ExtractTableAlias, TE>,
+ ExtractTableAlias, TE>,
+ UpdateResult
+ >
+
+ updateTable(tables: TableExpressionOrList): any {
return new UpdateQueryBuilder({
queryId: createQueryId(),
executor: this.#props.executor,
queryNode: UpdateQueryNode.create(
- parseTableExpression(table),
+ parseTableExpressionOrList(tables),
this.#props.withNode,
),
})
diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts
index b4afdc650..adcc8849e 100644
--- a/test/node/src/update.test.ts
+++ b/test/node/src/update.test.ts
@@ -577,6 +577,123 @@ for (const dialect of DIALECTS) {
expect(people).to.have.length(2)
})
+
+ it('should update joined table using set(column, value) function', async () => {
+ const query = ctx.db
+ .updateTable(['person', 'pet'])
+ .set('person.first_name', 'Jennifer 2')
+ .set('pet.name', 'Doggo 2')
+ .where('person.first_name', '=', 'Jennifer')
+ .whereRef('person.id', '=', 'pet.owner_id')
+
+ testSql(query, dialect, {
+ postgres: NOT_SUPPORTED,
+ mysql: {
+ sql: 'update `person`, `pet` set `person`.`first_name` = ?, `pet`.`name` = ? where `person`.`first_name` = ? and `person`.`id` = `pet`.`owner_id`',
+ parameters: ['Jennifer 2', 'Doggo 2', 'Jennifer'],
+ },
+ mssql: NOT_SUPPORTED,
+ sqlite: NOT_SUPPORTED,
+ })
+
+ await query.execute()
+
+ const jennifer = await ctx.db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ .where('first_name', '=', 'Jennifer 2')
+ .execute()
+
+ const doggo = await ctx.db
+ .selectFrom('pet')
+ .select(['name', 'owner_id'])
+ .where('name', '=', 'Doggo 2')
+ .execute()
+
+ expect(jennifer).to.have.length(1)
+ expect(doggo).to.have.length(1)
+ expect(doggo[0].owner_id).to.equal(jennifer[0].id)
+ })
+
+ it('should update joined aliased table using set(column, value) function and', async () => {
+ const query = ctx.db
+ .updateTable(['person as per', 'pet as p'])
+ .set('per.first_name', 'Jennifer 2')
+ .set('p.name', 'Doggo 2')
+ .where('per.first_name', '=', 'Jennifer')
+ .whereRef('per.id', '=', 'p.owner_id')
+
+ testSql(query, dialect, {
+ postgres: NOT_SUPPORTED,
+ mysql: {
+ sql: 'update `person` as `per`, `pet` as `p` set `per`.`first_name` = ?, `p`.`name` = ? where `per`.`first_name` = ? and `per`.`id` = `p`.`owner_id`',
+ parameters: ['Jennifer 2', 'Doggo 2', 'Jennifer'],
+ },
+ mssql: NOT_SUPPORTED,
+ sqlite: NOT_SUPPORTED,
+ })
+
+ await query.execute()
+
+ const jennifer = await ctx.db
+ .selectFrom('person')
+ .select(['id', 'first_name'])
+ .where('first_name', '=', 'Jennifer 2')
+ .execute()
+
+ const doggo = await ctx.db
+ .selectFrom('pet')
+ .select(['name', 'owner_id'])
+ .where('name', '=', 'Doggo 2')
+ .execute()
+
+ expect(jennifer).to.have.length(1)
+ expect(doggo).to.have.length(1)
+ expect(doggo[0].owner_id).to.equal(jennifer[0].id)
+ })
+
+ it('should join expressions', async () => {
+ const query = ctx.db
+ .updateTable([
+ 'person',
+ ctx.db.selectFrom('pet').selectAll().as('pet'),
+ ])
+ .set('person.first_name', 'Jennifer 2')
+ .where('person.first_name', '=', 'Jennifer')
+ .whereRef('person.id', '=', 'pet.owner_id')
+
+ testSql(query, dialect, {
+ postgres: NOT_SUPPORTED,
+ mysql: {
+ sql: 'update `person`, (select * from `pet`) as `pet` set `person`.`first_name` = ? where `person`.`first_name` = ? and `person`.`id` = `pet`.`owner_id`',
+ parameters: ['Jennifer 2', 'Jennifer'],
+ },
+ mssql: NOT_SUPPORTED,
+ sqlite: NOT_SUPPORTED,
+ })
+
+ await query.execute()
+ })
+
+ it('should update joined table using set(object) function', async () => {
+ const query = ctx.db
+ .updateTable(['person', 'pet'])
+ .set({ name: 'Doggo 2' })
+ .where('person.first_name', '=', 'Jennifer')
+ .whereRef('person.id', '=', 'pet.owner_id')
+
+ testSql(query, dialect, {
+ postgres: NOT_SUPPORTED,
+ mysql: {
+ sql: 'update `person`, `pet` set `name` = ? where `person`.`first_name` = ? and `person`.`id` = `pet`.`owner_id`',
+ parameters: ['Doggo 2', 'Jennifer'],
+ },
+ mssql: NOT_SUPPORTED,
+ sqlite: NOT_SUPPORTED,
+ })
+
+ await query.execute()
+ })
}
it('should create an update query that uses a CTE', async () => {