From 17fb3a2eda04af54d1240520643de5e122253e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20Koskim=C3=A4ki?= Date: Sat, 16 Apr 2022 10:57:10 +0300 Subject: [PATCH] add update from queries --- package.json | 2 +- .../operation-node-transformer.ts | 1 + src/operation-node/update-query-node.ts | 18 ++- src/parser/update-set-parser.ts | 6 +- src/parser/with-parser.ts | 4 +- src/query-builder/insert-query-builder.ts | 2 +- src/query-builder/on-conflict-builder.ts | 6 +- src/query-builder/update-query-builder.ts | 112 ++++++++++++++---- src/query-compiler/default-query-compiler.ts | 5 + src/query-creator.ts | 1 + test/node/src/update.test.ts | 24 ++++ 11 files changed, 151 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index d8e1b8304..3698e4d2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely", - "version": "0.17.3", + "version": "0.18.0", "description": "Type safe SQL query builder", "repository": { "type": "git", diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 6a7ad54d1..14edf28ce 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -439,6 +439,7 @@ export class OperationNodeTransformer { return { kind: 'UpdateQueryNode', table: this.transformNode(node.table), + from: this.transformNode(node.from), joins: this.transformNodeList(node.joins), where: this.transformNode(node.where), updates: this.transformNodeList(node.updates), diff --git a/src/operation-node/update-query-node.ts b/src/operation-node/update-query-node.ts index 8d5597a5a..3fe57b496 100644 --- a/src/operation-node/update-query-node.ts +++ b/src/operation-node/update-query-node.ts @@ -8,12 +8,14 @@ import { ReturningNode } from './returning-node.js' import { ValueListNode } from './value-list-node.js' import { WhereNode } from './where-node.js' import { WithNode } from './with-node.js' +import { FromNode } from './from-node.js' export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode export interface UpdateQueryNode extends OperationNode { readonly kind: 'UpdateQueryNode' readonly table: TableExpressionNode + readonly from?: FromNode readonly joins?: ReadonlyArray readonly where?: WhereNode readonly updates?: ReadonlyArray @@ -30,11 +32,23 @@ export const UpdateQueryNode = freeze({ }, create(table: TableExpressionNode, withNode?: WithNode): UpdateQueryNode { - return { + return freeze({ kind: 'UpdateQueryNode', table, ...(withNode && { with: withNode }), - } + }) + }, + + cloneWithFromItems( + updateQuery: UpdateQueryNode, + fromItems: ReadonlyArray + ): UpdateQueryNode { + return freeze({ + ...updateQuery, + from: updateQuery.from + ? FromNode.cloneWithFroms(updateQuery.from, fromItems) + : FromNode.create(fromItems), + }) }, cloneWithUpdates( diff --git a/src/parser/update-set-parser.ts b/src/parser/update-set-parser.ts index f45309d11..cd00624ca 100644 --- a/src/parser/update-set-parser.ts +++ b/src/parser/update-set-parser.ts @@ -3,12 +3,12 @@ import { ColumnUpdateNode } from '../operation-node/column-update-node.js' import { UpdateKeys, UpdateType } from '../util/column-type.js' import { parseValueExpression, ValueExpression } from './value-parser.js' -export type MutationObject = { - [C in UpdateKeys]?: ValueExpression> +export type MutationObject = { + [C in UpdateKeys]?: ValueExpression> } export function parseUpdateObject( - row: MutationObject + row: MutationObject ): ReadonlyArray { return Object.entries(row) .filter(([_, value]) => value !== undefined) diff --git a/src/parser/with-parser.ts b/src/parser/with-parser.ts index 9a483a7b3..e838456db 100644 --- a/src/parser/with-parser.ts +++ b/src/parser/with-parser.ts @@ -38,7 +38,7 @@ export type QueryCreatorWithCommonTableExpression< type CommonTableExpressionOutput = | SelectQueryBuilder> | InsertQueryBuilder> - | UpdateQueryBuilder> + | UpdateQueryBuilder> | DeleteQueryBuilder> | RawBuilder> @@ -55,7 +55,7 @@ type ExtractRowFromCommonTableExpression = CTE extends ( ? QO : Q extends InsertQueryBuilder ? QO - : Q extends UpdateQueryBuilder + : Q extends UpdateQueryBuilder ? QO : Q extends DeleteQueryBuilder ? QO diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index ac098a88f..c39a0878a 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -475,7 +475,7 @@ export class InsertQueryBuilder * ``` */ onDuplicateKeyUpdate( - updates: MutationObject + updates: MutationObject ): InsertQueryBuilder { return new InsertQueryBuilder({ ...this.#props, diff --git a/src/query-builder/on-conflict-builder.ts b/src/query-builder/on-conflict-builder.ts index a8bf6cef9..831570f75 100644 --- a/src/query-builder/on-conflict-builder.ts +++ b/src/query-builder/on-conflict-builder.ts @@ -305,7 +305,11 @@ export class OnConflictBuilder * ``` */ doUpdateSet( - updates: MutationObject, OnConflictTables> + updates: MutationObject< + OnConflictDatabase, + OnConflictTables, + OnConflictTables + > ): OnConflictUpdateBuilder, OnConflictTables> { return new OnConflictUpdateBuilder({ ...this.#props, diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index b0e476bce..bb555a03e 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -12,6 +12,8 @@ import { LeftJoinTableExpressionDatabase, RightJoinTableExpressionDatabase, FullJoinTableExpressionDatabase, + parseTableExpressionOrList, + TableExpressionOrList, } from '../parser/table-parser.js' import { parseSelectExpressionOrList, @@ -55,7 +57,7 @@ import { ReturningInterface } from './returning-interface.js' import { NoResultError, NoResultErrorConstructor } from './no-result-error.js' import { Selectable } from '../util/column-type.js' -export class UpdateQueryBuilder +export class UpdateQueryBuilder implements WhereInterface, JoinInterface, @@ -73,10 +75,10 @@ export class UpdateQueryBuilder lhs: RE, op: FilterOperator, rhs: FilterValueExpressionOrList - ): UpdateQueryBuilder + ): UpdateQueryBuilder - where(grouper: WhereGrouper): UpdateQueryBuilder - where(raw: AnyRawBuilder): UpdateQueryBuilder + where(grouper: WhereGrouper): UpdateQueryBuilder + where(raw: AnyRawBuilder): UpdateQueryBuilder where(...args: any[]): any { return new UpdateQueryBuilder({ @@ -92,7 +94,7 @@ export class UpdateQueryBuilder lhs: ReferenceExpression, op: FilterOperator, rhs: ReferenceExpression - ): UpdateQueryBuilder { + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithWhere( @@ -106,10 +108,10 @@ export class UpdateQueryBuilder lhs: RE, op: FilterOperator, rhs: FilterValueExpressionOrList - ): UpdateQueryBuilder + ): UpdateQueryBuilder - orWhere(grouper: WhereGrouper): UpdateQueryBuilder - orWhere(raw: AnyRawBuilder): UpdateQueryBuilder + orWhere(grouper: WhereGrouper): UpdateQueryBuilder + orWhere(raw: AnyRawBuilder): UpdateQueryBuilder orWhere(...args: any[]): any { return new UpdateQueryBuilder({ @@ -125,7 +127,7 @@ export class UpdateQueryBuilder lhs: ReferenceExpression, op: FilterOperator, rhs: ReferenceExpression - ): UpdateQueryBuilder { + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithOrWhere( @@ -135,7 +137,9 @@ export class UpdateQueryBuilder }) } - whereExists(arg: ExistsExpression): UpdateQueryBuilder { + whereExists( + arg: ExistsExpression + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithWhere( @@ -145,7 +149,9 @@ export class UpdateQueryBuilder }) } - whereNotExists(arg: ExistsExpression): UpdateQueryBuilder { + whereNotExists( + arg: ExistsExpression + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithWhere( @@ -155,7 +161,9 @@ export class UpdateQueryBuilder }) } - orWhereExists(arg: ExistsExpression): UpdateQueryBuilder { + orWhereExists( + arg: ExistsExpression + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithOrWhere( @@ -167,7 +175,7 @@ export class UpdateQueryBuilder orWhereNotExists( arg: ExistsExpression - ): UpdateQueryBuilder { + ): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithOrWhere( @@ -177,6 +185,61 @@ export class UpdateQueryBuilder }) } + /** + * Adds a from clause to the update query. + * + * This is supported only on some databases like PostgreSQL. + * + * The API is the same as {@link QueryCreator.selectFrom}. + * + * ### Examples + * + * ```ts + * db.updateTable('person') + * .from('pet') + * .set({ + * first_name: (eb) => eb.ref('pet.name') + * }) + * .whereRef('pet.owner_id', '=', 'person.id') + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * update "person" + * set "first_name" = "pet"."name" + * from "pet" + * where "pet"."owner_id" = "person"."id" + * ``` + */ + from>( + table: TE + ): UpdateQueryBuilder< + TableExpressionDatabase, + UT, + TableExpressionTables, + O + > + + from>( + table: TE[] + ): UpdateQueryBuilder< + TableExpressionDatabase, + UT, + TableExpressionTables, + O + > + + from(from: TableExpressionOrList): any { + return new UpdateQueryBuilder({ + ...this.#props, + queryNode: UpdateQueryNode.cloneWithFromItems( + this.#props.queryNode, + parseTableExpressionOrList(from) + ), + }) + } + innerJoin< TE extends TableExpression, K1 extends JoinReferenceExpression, @@ -187,6 +250,7 @@ export class UpdateQueryBuilder k2: K2 ): UpdateQueryBuilder< TableExpressionDatabase, + UT, TableExpressionTables, O > @@ -199,6 +263,7 @@ export class UpdateQueryBuilder callback: FN ): UpdateQueryBuilder< TableExpressionDatabase, + UT, TableExpressionTables, O > @@ -223,6 +288,7 @@ export class UpdateQueryBuilder k2: K2 ): UpdateQueryBuilder< LeftJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -235,6 +301,7 @@ export class UpdateQueryBuilder callback: FN ): UpdateQueryBuilder< LeftJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -259,6 +326,7 @@ export class UpdateQueryBuilder k2: K2 ): UpdateQueryBuilder< RightJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -271,6 +339,7 @@ export class UpdateQueryBuilder callback: FN ): UpdateQueryBuilder< RightJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -295,6 +364,7 @@ export class UpdateQueryBuilder k2: K2 ): UpdateQueryBuilder< FullJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -307,6 +377,7 @@ export class UpdateQueryBuilder callback: FN ): UpdateQueryBuilder< FullJoinTableExpressionDatabase, + UT, TableExpressionTables, O > @@ -408,7 +479,7 @@ export class UpdateQueryBuilder * where "id" = $4 * ``` */ - set(row: MutationObject): UpdateQueryBuilder { + set(row: MutationObject): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, queryNode: UpdateQueryNode.cloneWithUpdates( @@ -420,11 +491,11 @@ export class UpdateQueryBuilder returning>( selections: ReadonlyArray - ): UpdateQueryBuilder> + ): UpdateQueryBuilder> returning>( selection: SE - ): UpdateQueryBuilder> + ): UpdateQueryBuilder> returning(selection: SelectExpressionOrList): any { return new UpdateQueryBuilder({ @@ -436,7 +507,7 @@ export class UpdateQueryBuilder }) } - returningAll(): UpdateQueryBuilder> { + returningAll(): UpdateQueryBuilder> { return new UpdateQueryBuilder({ ...this.#props, queryNode: QueryNode.cloneWithReturning( @@ -510,9 +581,10 @@ export class UpdateQueryBuilder */ if( condition: boolean, - func: (qb: this) => UpdateQueryBuilder + func: (qb: this) => UpdateQueryBuilder ): UpdateQueryBuilder< DB, + UT, TB, O2 extends UpdateResult ? UpdateResult @@ -535,14 +607,14 @@ export class UpdateQueryBuilder * You should only use this method as the last resort if the types * don't support your use case. */ - castTo(): UpdateQueryBuilder { + castTo(): UpdateQueryBuilder { return new UpdateQueryBuilder(this.#props) } /** * Returns a copy of this UpdateQueryBuilder instance with the given plugin installed. */ - withPlugin(plugin: KyselyPlugin): UpdateQueryBuilder { + withPlugin(plugin: KyselyPlugin): UpdateQueryBuilder { return new UpdateQueryBuilder({ ...this.#props, executor: this.#props.executor.withPlugin(plugin), diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index c31fae0e4..4b382a29c 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -627,6 +627,11 @@ export class DefaultQueryCompiler this.compileList(node.updates) } + if (node.from) { + this.append(' ') + this.visitNode(node.from) + } + if (node.joins) { this.append(' ') this.compileList(node.joins, ' ') diff --git a/src/query-creator.ts b/src/query-creator.ts index caaa91e11..9f8ac676c 100644 --- a/src/query-creator.ts +++ b/src/query-creator.ts @@ -287,6 +287,7 @@ export class QueryCreator { ): UpdateQueryBuilder< TableExpressionDatabase, TableExpressionTables, + TableExpressionTables, UpdateResult > { return new UpdateQueryBuilder({ diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts index 48b9943e6..5626c40e7 100644 --- a/test/node/src/update.test.ts +++ b/test/node/src/update.test.ts @@ -207,6 +207,30 @@ for (const dialect of BUILT_IN_DIALECTS) { const result = await query.executeTakeFirstOrThrow() expect(result.last_name).to.equal('Barson') }) + + it('should join a table when `from` is called', async () => { + const query = ctx.db + .updateTable('person') + .from('pet') + .set({ + first_name: (eb) => eb.ref('pet.name'), + }) + .whereRef('pet.owner_id', '=', 'person.id') + .where('person.first_name', '=', 'Arnold') + .returning('first_name') + + testSql(query, dialect, { + postgres: { + sql: 'update "person" set "first_name" = "pet"."name" from "pet" where "pet"."owner_id" = "person"."id" and "person"."first_name" = $1 returning "first_name"', + parameters: ['Arnold'], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.execute() + expect(result[0].first_name).to.equal('Doggo') + }) } }) }