Skip to content

Commit

Permalink
Support update table1, table2, ... query support. Closes #192 (#1079)
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Jul 16, 2024
1 parent 3837d8e commit bec9a9c
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 45 deletions.
4 changes: 1 addition & 3 deletions site/docs/examples/update/0010-single-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@ export const singleRow = `const result = await db
last_name: 'Aniston'
})
.where('id', '=', '1')
.executeTakeFirst()
console.log(result.numUpdatedRows)`
.executeTakeFirst()`
4 changes: 1 addition & 3 deletions site/docs/examples/update/0020-complex-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ export const complexValues = `const result = await db
last_name: 'updated',
}))
.where('id', '=', '1')
.executeTakeFirst()
console.log(result.numUpdatedRows)`
.executeTakeFirst()`
7 changes: 7 additions & 0 deletions site/docs/examples/update/0030-my-sql-joins.js
Original file line number Diff line number Diff line change
@@ -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()`
40 changes: 40 additions & 0 deletions site/docs/examples/update/0030-my-sql-joins.mdx
Original file line number Diff line number Diff line change
@@ -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'

<div style={{ marginBottom: '1em' }}>
<Playground code={mySqlJoins} setupCode={exampleSetup} />
</div>

:::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)
:::
2 changes: 1 addition & 1 deletion site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions src/operation-node/update-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -39,10 +40,15 @@ export const UpdateQueryNode = freeze({
return node.kind === 'UpdateQueryNode'
},

create(table: OperationNode, withNode?: WithNode): UpdateQueryNode {
create(
tables: ReadonlyArray<OperationNode>,
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 }),
})
},
Expand Down
7 changes: 4 additions & 3 deletions src/parser/select-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export type SelectArg<
| ReadonlyArray<SE>
| ((eb: ExpressionBuilder<DB, TB>) => ReadonlyArray<SE>)

type FlattenSelectExpression<SE> = SE extends DynamicReferenceBuilder<infer RA>
? { [R in RA]: DynamicReferenceBuilder<R> }[RA]
: SE
type FlattenSelectExpression<SE> =
SE extends DynamicReferenceBuilder<infer RA>
? { [R in RA]: DynamicReferenceBuilder<R> }[RA]
: SE

type ExtractAliasFromSelectExpression<SE> = SE extends string
? ExtractAliasFromStringSelectExpression<SE>
Expand Down
17 changes: 12 additions & 5 deletions src/parser/update-set-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ import {
parseReferenceExpression,
ReferenceExpression,
} from './reference-parser.js'
import { AnyColumn, DrainOuterGeneric } from '../util/type-utils.js'

export type UpdateObject<DB, TB extends keyof DB, UT extends keyof DB = TB> = {
[C in UpdateKeys<DB[UT]>]?:
| ValueExpression<DB, TB, UpdateType<DB[UT][C]>>
| undefined
}
export type UpdateObject<
DB,
TB extends keyof DB,
UT extends keyof DB = TB,
> = DrainOuterGeneric<{
[C in AnyColumn<DB, UT>]?: {
[T in UT]: C extends keyof DB[T]
? ValueExpression<DB, TB, UpdateType<DB[T][C]>> | undefined
: never
}[UT]
}>

export type UpdateObjectFactory<
DB,
Expand Down
17 changes: 9 additions & 8 deletions src/plugin/with-schema/with-schema-transformer.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -157,14 +158,14 @@ export class WithSchemaTransformer extends OperationNodeTransformer {
node: OperationNode,
schemableIds: Set<string>,
): 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)
}
}
}

Expand Down
41 changes: 37 additions & 4 deletions src/query-builder/update-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,6 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
* })
* .where('id', '=', '1')
* .executeTakeFirst()
*
* console.log(result.numUpdatedRows)
* ```
*
* The generated SQL (PostgreSQL):
Expand All @@ -546,8 +544,6 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
* }))
* .where('id', '=', '1')
* .executeTakeFirst()
*
* console.log(result.numUpdatedRows)
* ```
*
* The generated SQL (PostgreSQL):
Expand Down Expand Up @@ -630,6 +626,43 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
* "last_name" = $3 || $4
* where "id" = $5
* ```
*
* <!-- siteExample("update", "MySQL joins", 30) -->
*
* 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<DB, TB, UT>,
Expand Down
4 changes: 4 additions & 0 deletions src/query-compiler/default-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, ' ')
}
Expand Down
41 changes: 25 additions & 16 deletions src/query-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,39 +464,48 @@ export class QueryCreator<DB> {
* console.log(result.numUpdatedRows)
* ```
*/
updateTable<TR extends keyof DB & string>(
table: TR,
updateTable<TE extends keyof DB & string>(
from: TE[],
): UpdateQueryBuilder<
DB,
ExtractTableAlias<DB, TR>,
ExtractTableAlias<DB, TR>,
ExtractTableAlias<DB, TE>,
ExtractTableAlias<DB, TE>,
UpdateResult
>

updateTable<TR extends AnyAliasedTable<DB>>(
table: TR,
updateTable<TE extends TableExpression<DB, never>>(
from: TE[],
): UpdateQueryBuilder<
DB & PickTableWithAlias<DB, TR>,
ExtractTableAlias<DB & PickTableWithAlias<DB, TR>, TR>,
ExtractTableAlias<DB & PickTableWithAlias<DB, TR>, TR>,
From<DB, TE>,
FromTables<DB, never, TE>,
FromTables<DB, never, TE>,
UpdateResult
>

updateTable<TR extends TableReference<DB>>(
table: TR,
updateTable<TE extends keyof DB & string>(
from: TE,
): UpdateQueryBuilder<
From<DB, TR>,
FromTables<DB, never, TR>,
FromTables<DB, never, TR>,
DB,
ExtractTableAlias<DB, TE>,
ExtractTableAlias<DB, TE>,
UpdateResult
>

updateTable<TR extends TableReference<DB>>(table: TR): any {
updateTable<TE extends AnyAliasedTable<DB>>(
from: TE,
): UpdateQueryBuilder<
DB & PickTableWithAlias<DB, TE>,
ExtractTableAlias<DB & PickTableWithAlias<DB, TE>, TE>,
ExtractTableAlias<DB & PickTableWithAlias<DB, TE>, TE>,
UpdateResult
>

updateTable(tables: TableExpressionOrList<any, any>): any {
return new UpdateQueryBuilder({
queryId: createQueryId(),
executor: this.#props.executor,
queryNode: UpdateQueryNode.create(
parseTableExpression(table),
parseTableExpressionOrList(tables),
this.#props.withNode,
),
})
Expand Down
Loading

0 comments on commit bec9a9c

Please sign in to comment.