diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 1adc18e79..b035dec3e 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -811,6 +811,7 @@ export class OperationNodeTransformer { kind: 'SelectModifierNode', modifier: node.modifier, rawModifier: this.transformNode(node.rawModifier), + of: this.transformNodeList(node.of), }) } diff --git a/src/operation-node/select-modifier-node.ts b/src/operation-node/select-modifier-node.ts index fa6a6f474..fd41751df 100644 --- a/src/operation-node/select-modifier-node.ts +++ b/src/operation-node/select-modifier-node.ts @@ -14,6 +14,7 @@ export interface SelectModifierNode extends OperationNode { readonly kind: 'SelectModifierNode' readonly modifier?: SelectModifier readonly rawModifier?: OperationNode + readonly of?: ReadonlyArray } /** @@ -24,10 +25,14 @@ export const SelectModifierNode = freeze({ return node.kind === 'SelectModifierNode' }, - create(modifier: SelectModifier): SelectModifierNode { + create( + modifier: SelectModifier, + of?: ReadonlyArray + ): SelectModifierNode { return freeze({ kind: 'SelectModifierNode', modifier, + of, }) }, diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 21005ab4e..7e70dfd2e 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -6,7 +6,7 @@ import { JoinReferenceExpression, parseJoin, } from '../parser/join-parser.js' -import { TableExpression } from '../parser/table-parser.js' +import { TableExpression, parseTable } from '../parser/table-parser.js' import { parseSelectArg, parseSelectAll, @@ -46,7 +46,7 @@ import { OffsetNode } from '../operation-node/offset-node.js' import { Compilable } from '../util/compilable.js' import { QueryExecutor } from '../query-executor/query-executor.js' import { QueryId } from '../util/query-id.js' -import { freeze } from '../util/object-utils.js' +import { asArray, freeze } from '../util/object-utils.js' import { GroupByArg, parseGroupBy } from '../parser/group-by-parser.js' import { KyselyPlugin } from '../plugin/kysely-plugin.js' import { WhereInterface } from './where-interface.js' @@ -429,22 +429,22 @@ export interface SelectQueryBuilder /** * Adds the `for update` modifier to a select query on supported databases. */ - forUpdate(): SelectQueryBuilder + forUpdate(of?: TableOrList): SelectQueryBuilder /** * Adds the `for share` modifier to a select query on supported databases. */ - forShare(): SelectQueryBuilder + forShare(of?: TableOrList): SelectQueryBuilder /** * Adds the `for key share` modifier to a select query on supported databases. */ - forKeyShare(): SelectQueryBuilder + forKeyShare(of?: TableOrList): SelectQueryBuilder /** * Adds the `for no key update` modifier to a select query on supported databases. */ - forNoKeyUpdate(): SelectQueryBuilder + forNoKeyUpdate(of?: TableOrList): SelectQueryBuilder /** * Adds the `skip locked` modifier to a select query on supported databases. @@ -1797,42 +1797,54 @@ class SelectQueryBuilderImpl }) } - forUpdate(): SelectQueryBuilder { + forUpdate(of?: TableOrList): SelectQueryBuilder { return new SelectQueryBuilderImpl({ ...this.#props, queryNode: SelectQueryNode.cloneWithEndModifier( this.#props.queryNode, - SelectModifierNode.create('ForUpdate') + SelectModifierNode.create( + 'ForUpdate', + of ? asArray(of).map(parseTable) : undefined + ) ), }) } - forShare(): SelectQueryBuilder { + forShare(of?: TableOrList): SelectQueryBuilder { return new SelectQueryBuilderImpl({ ...this.#props, queryNode: SelectQueryNode.cloneWithEndModifier( this.#props.queryNode, - SelectModifierNode.create('ForShare') + SelectModifierNode.create( + 'ForShare', + of ? asArray(of).map(parseTable) : undefined + ) ), }) } - forKeyShare(): SelectQueryBuilder { + forKeyShare(of?: TableOrList): SelectQueryBuilder { return new SelectQueryBuilderImpl({ ...this.#props, queryNode: SelectQueryNode.cloneWithEndModifier( this.#props.queryNode, - SelectModifierNode.create('ForKeyShare') + SelectModifierNode.create( + 'ForKeyShare', + of ? asArray(of).map(parseTable) : undefined + ) ), }) } - forNoKeyUpdate(): SelectQueryBuilder { + forNoKeyUpdate(of?: TableOrList): SelectQueryBuilder { return new SelectQueryBuilderImpl({ ...this.#props, queryNode: SelectQueryNode.cloneWithEndModifier( this.#props.queryNode, - SelectModifierNode.create('ForNoKeyUpdate') + SelectModifierNode.create( + 'ForNoKeyUpdate', + of ? asArray(of).map(parseTable) : undefined + ) ), }) } @@ -2413,3 +2425,7 @@ type OuterJoinedBuilderDB< ? DB[C] : never }> + +type TableOrList = + | (TB & string) + | ReadonlyArray diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 50f025709..d2be11aed 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -1219,6 +1219,11 @@ export class DefaultQueryCompiler } else { this.append(SELECT_MODIFIER_SQL[node.modifier!]) } + + if (node.of) { + this.append(' of ') + this.compileList(node.of, ', ') + } } protected override visitCreateType(node: CreateTypeNode): void { diff --git a/src/util/object-utils.ts b/src/util/object-utils.ts index 467c844f0..843f39d3f 100644 --- a/src/util/object-utils.ts +++ b/src/util/object-utils.ts @@ -76,8 +76,8 @@ export function freeze(obj: T): Readonly { return Object.freeze(obj) } -export function asArray(arg: T | T[]): T[] { - if (Array.isArray(arg)) { +export function asArray(arg: T | ReadonlyArray): ReadonlyArray { + if (isReadonlyArray(arg)) { return arg } else { return [arg] diff --git a/test/node/src/select.test.ts b/test/node/src/select.test.ts index e8515a712..f0953edd3 100644 --- a/test/node/src/select.test.ts +++ b/test/node/src/select.test.ts @@ -645,6 +645,32 @@ for (const dialect of DIALECTS) { expect(persons).to.eql([{ last_name: 'Aniston' }]) }) + it('should select a row for update of', async () => { + const query = ctx.db + .selectFrom('person') + .select('last_name') + .where('first_name', '=', 'Jennifer') + .forUpdate('person') + + testSql(query, dialect, { + postgres: { + sql: 'select "last_name" from "person" where "first_name" = $1 for update of "person"', + parameters: ['Jennifer'], + }, + mysql: { + sql: 'select `last_name` from `person` where `first_name` = ? for update of "person"', + parameters: ['Jennifer'], + }, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + const persons = await query.execute() + + expect(persons).to.have.length(1) + expect(persons).to.eql([{ last_name: 'Aniston' }]) + }) + it('should select a row for update with skip locked', async () => { const query = ctx.db .selectFrom('person') @@ -672,6 +698,33 @@ for (const dialect of DIALECTS) { expect(persons).to.eql([{ last_name: 'Aniston' }]) }) + it('should select a row for update of with skip locked', async () => { + const query = ctx.db + .selectFrom('person') + .select('last_name') + .where('first_name', '=', 'Jennifer') + .forUpdate(['person']) + .skipLocked() + + testSql(query, dialect, { + postgres: { + sql: 'select "last_name" from "person" where "first_name" = $1 for update of "person" skip locked', + parameters: ['Jennifer'], + }, + mysql: { + sql: 'select `last_name` from `person` where `first_name` = ? for update of "person" skip locked', + parameters: ['Jennifer'], + }, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + const persons = await query.execute() + + expect(persons).to.have.length(1) + expect(persons).to.eql([{ last_name: 'Aniston' }]) + }) + it('should select a row for update with skipLocked called before forUpdate', async () => { const query = ctx.db .selectFrom('person')