From d1c1322b5128bd95aea5fdb0a2044fa78112bc88 Mon Sep 17 00:00:00 2001 From: Igal Klebanov Date: Sat, 24 Feb 2024 17:32:45 +0200 Subject: [PATCH] add `TOP` clause support. (#821) --- .prettierrc.json | 5 +- src/index.ts | 1 + src/operation-node/delete-query-node.ts | 2 + src/operation-node/insert-query-node.ts | 2 + src/operation-node/merge-query-node.ts | 2 + .../operation-node-transformer.ts | 15 ++ src/operation-node/operation-node-visitor.ts | 3 + src/operation-node/operation-node.ts | 1 + src/operation-node/query-node.ts | 9 ++ src/operation-node/select-query-node.ts | 2 + src/operation-node/top-node.ts | 27 ++++ src/operation-node/update-query-node.ts | 2 + src/parser/top-parser.ts | 25 +++ src/query-builder/delete-query-builder.ts | 125 ++++++++++----- src/query-builder/insert-query-builder.ts | 61 ++++++- src/query-builder/merge-query-builder.ts | 77 +++++++++ src/query-builder/select-query-builder.ts | 151 ++++++++++++++---- src/query-builder/update-query-builder.ts | 125 ++++++++++----- src/query-compiler/default-query-compiler.ts | 39 ++++- test/node/src/clear.test.ts | 2 +- test/node/src/delete.test.ts | 45 ++++++ test/node/src/insert.test.ts | 46 ++++++ test/node/src/join.test.ts | 6 +- test/node/src/merge.test.ts | 50 ++++++ test/node/src/select.test.ts | 99 +++++++++++- test/node/src/test-setup.ts | 2 +- test/node/src/update.test.ts | 42 ++++- 27 files changed, 847 insertions(+), 119 deletions(-) create mode 100644 src/operation-node/top-node.ts create mode 100644 src/parser/top-parser.ts diff --git a/.prettierrc.json b/.prettierrc.json index 00fbdb185..e3b414c7e 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,5 @@ { "semi": false, - "singleQuote": true -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "all" +} diff --git a/src/index.ts b/src/index.ts index 89554605e..ba895445f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -202,6 +202,7 @@ export * from './operation-node/tuple-node.js' export * from './operation-node/merge-query-node.js' export * from './operation-node/matched-node.js' export * from './operation-node/fetch-node.js' +export * from './operation-node/top-node.js' export * from './util/column-type.js' export * from './util/compilable.js' diff --git a/src/operation-node/delete-query-node.ts b/src/operation-node/delete-query-node.ts index 9085b7a2e..c949fc33f 100644 --- a/src/operation-node/delete-query-node.ts +++ b/src/operation-node/delete-query-node.ts @@ -10,6 +10,7 @@ import { OrderByNode } from './order-by-node.js' import { OrderByItemNode } from './order-by-item-node.js' import { ExplainNode } from './explain-node.js' import { UsingNode } from './using-node.js' +import { TopNode } from './top-node.js' export interface DeleteQueryNode extends OperationNode { readonly kind: 'DeleteQueryNode' @@ -22,6 +23,7 @@ export interface DeleteQueryNode extends OperationNode { readonly orderBy?: OrderByNode readonly limit?: LimitNode readonly explain?: ExplainNode + readonly top?: TopNode } /** diff --git a/src/operation-node/insert-query-node.ts b/src/operation-node/insert-query-node.ts index 045d4793b..5a69dccfe 100644 --- a/src/operation-node/insert-query-node.ts +++ b/src/operation-node/insert-query-node.ts @@ -6,6 +6,7 @@ import { OnDuplicateKeyNode } from './on-duplicate-key-node.js' import { OperationNode } from './operation-node.js' import { ReturningNode } from './returning-node.js' import { TableNode } from './table-node.js' +import { TopNode } from './top-node.js' import { WithNode } from './with-node.js' export type InsertQueryNodeProps = Omit @@ -23,6 +24,7 @@ export interface InsertQueryNode extends OperationNode { readonly replace?: boolean readonly explain?: ExplainNode readonly defaultValues?: boolean + readonly top?: TopNode } /** diff --git a/src/operation-node/merge-query-node.ts b/src/operation-node/merge-query-node.ts index a65b36a94..fff9eaa5a 100644 --- a/src/operation-node/merge-query-node.ts +++ b/src/operation-node/merge-query-node.ts @@ -3,6 +3,7 @@ import { AliasNode } from './alias-node.js' import { JoinNode } from './join-node.js' import { OperationNode } from './operation-node.js' import { TableNode } from './table-node.js' +import { TopNode } from './top-node.js' import { WhenNode } from './when-node.js' import { WithNode } from './with-node.js' @@ -12,6 +13,7 @@ export interface MergeQueryNode extends OperationNode { readonly using?: JoinNode readonly whens?: ReadonlyArray readonly with?: WithNode + readonly top?: TopNode } /** diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index bc0828b02..3036051b7 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -92,6 +92,7 @@ import { MatchedNode } from './matched-node.js' import { AddIndexNode } from './add-index-node.js' import { CastNode } from './cast-node.js' import { FetchNode } from './fetch-node.js' +import { TopNode } from './top-node.js' /** * Transforms an operation node tree into another one. @@ -218,6 +219,7 @@ export class OperationNodeTransformer { AddIndexNode: this.transformAddIndex.bind(this), CastNode: this.transformCast.bind(this), FetchNode: this.transformFetch.bind(this), + TopNode: this.transformTop.bind(this), }) transformNode(node: T): T { @@ -265,6 +267,7 @@ export class OperationNodeTransformer { explain: this.transformNode(node.explain), setOperations: this.transformNodeList(node.setOperations), fetch: this.transformNode(node.fetch), + top: this.transformNode(node.top), }) } @@ -380,6 +383,7 @@ export class OperationNodeTransformer { replace: node.replace, explain: this.transformNode(node.explain), defaultValues: node.defaultValues, + top: this.transformNode(node.top), }) } @@ -402,6 +406,7 @@ export class OperationNodeTransformer { orderBy: this.transformNode(node.orderBy), limit: this.transformNode(node.limit), explain: this.transformNode(node.explain), + top: this.transformNode(node.top), }) } @@ -507,6 +512,7 @@ export class OperationNodeTransformer { with: this.transformNode(node.with), explain: this.transformNode(node.explain), limit: this.transformNode(node.limit), + top: this.transformNode(node.top), }) } @@ -1000,6 +1006,7 @@ export class OperationNodeTransformer { using: this.transformNode(node.using), whens: this.transformNodeList(node.whens), with: this.transformNode(node.with), + top: this.transformNode(node.top), }) } @@ -1038,6 +1045,14 @@ export class OperationNodeTransformer { }) } + protected transformTop(node: TopNode): TopNode { + return requireAllProps({ + kind: 'TopNode', + expression: node.expression, + modifiers: node.modifiers, + }) + } + protected transformDataType(node: DataTypeNode): DataTypeNode { // An Object.freezed leaf node. No need to clone. return node diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index a258d4455..a3ea9d740 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -94,6 +94,7 @@ import { MatchedNode } from './matched-node.js' import { AddIndexNode } from './add-index-node.js' import { CastNode } from './cast-node.js' import { FetchNode } from './fetch-node.js' +import { TopNode } from './top-node.js' export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -195,6 +196,7 @@ export abstract class OperationNodeVisitor { AddIndexNode: this.visitAddIndex.bind(this), CastNode: this.visitCast.bind(this), FetchNode: this.visitFetch.bind(this), + TopNode: this.visitTop.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -304,4 +306,5 @@ export abstract class OperationNodeVisitor { protected abstract visitAddIndex(node: AddIndexNode): void protected abstract visitCast(node: CastNode): void protected abstract visitFetch(node: FetchNode): void + protected abstract visitTop(node: TopNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index 1a968737b..c58f3c75e 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -90,6 +90,7 @@ export type OperationNodeKind = | 'AddIndexNode' | 'CastNode' | 'FetchNode' + | 'TopNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/operation-node/query-node.ts b/src/operation-node/query-node.ts index c4c203fe3..0d0848eee 100644 --- a/src/operation-node/query-node.ts +++ b/src/operation-node/query-node.ts @@ -12,6 +12,7 @@ import { ExplainNode } from './explain-node.js' import { ExplainFormat } from '../util/explainable.js' import { Expression } from '../expression/expression.js' import { MergeQueryNode } from './merge-query-node.js' +import { TopNode } from './top-node.js' export type QueryNode = | SelectQueryNode @@ -24,6 +25,7 @@ type HasJoins = { joins?: ReadonlyArray } type HasWhere = { where?: WhereNode } type HasReturning = { returning?: ReturningNode } type HasExplain = { explain?: ExplainNode } +type HasTop = { top?: TopNode } /** * @internal @@ -91,4 +93,11 @@ export const QueryNode = freeze({ explain: ExplainNode.create(format, options?.toOperationNode()), }) }, + + cloneWithTop(node: T, top: TopNode): T { + return freeze({ + ...node, + top, + }) + }, }) diff --git a/src/operation-node/select-query-node.ts b/src/operation-node/select-query-node.ts index 7cf00cdc8..2d79b7dda 100644 --- a/src/operation-node/select-query-node.ts +++ b/src/operation-node/select-query-node.ts @@ -16,6 +16,7 @@ import { SelectModifierNode } from './select-modifier-node.js' import { ExplainNode } from './explain-node.js' import { SetOperationNode } from './set-operation-node.js' import { FetchNode } from './fetch-node.js' +import { TopNode } from './top-node.js' export interface SelectQueryNode extends OperationNode { readonly kind: 'SelectQueryNode' @@ -35,6 +36,7 @@ export interface SelectQueryNode extends OperationNode { readonly explain?: ExplainNode readonly setOperations?: ReadonlyArray readonly fetch?: FetchNode + readonly top?: TopNode } /** diff --git a/src/operation-node/top-node.ts b/src/operation-node/top-node.ts new file mode 100644 index 000000000..06692611f --- /dev/null +++ b/src/operation-node/top-node.ts @@ -0,0 +1,27 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' + +export type TopModifier = 'percent' | 'with ties' | 'percent with ties' + +export interface TopNode extends OperationNode { + readonly kind: 'TopNode' + readonly expression: number | bigint + readonly modifiers?: TopModifier +} + +/** + * @internal + */ +export const TopNode = freeze({ + is(node: OperationNode): node is TopNode { + return node.kind === 'TopNode' + }, + + create(expression: number | bigint, modifiers?: TopModifier): TopNode { + return freeze({ + kind: 'TopNode', + expression, + modifiers, + }) + }, +}) diff --git a/src/operation-node/update-query-node.ts b/src/operation-node/update-query-node.ts index 9a372d8a2..8bdb9a73f 100644 --- a/src/operation-node/update-query-node.ts +++ b/src/operation-node/update-query-node.ts @@ -10,6 +10,7 @@ import { WithNode } from './with-node.js' import { FromNode } from './from-node.js' import { ExplainNode } from './explain-node.js' import { LimitNode } from './limit-node.js' +import { TopNode } from './top-node.js' export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode @@ -24,6 +25,7 @@ export interface UpdateQueryNode extends OperationNode { readonly with?: WithNode readonly explain?: ExplainNode readonly limit?: LimitNode + readonly top?: TopNode } /** diff --git a/src/parser/top-parser.ts b/src/parser/top-parser.ts new file mode 100644 index 000000000..6a2fce445 --- /dev/null +++ b/src/parser/top-parser.ts @@ -0,0 +1,25 @@ +import { TopModifier, TopNode } from '../operation-node/top-node.js' +import { isBigInt, isNumber, isUndefined } from '../util/object-utils.js' + +export function parseTop( + expression: number | bigint, + modifiers?: TopModifier, +): TopNode { + if (!isNumber(expression) && !isBigInt(expression)) { + throw new Error(`Invalid top expression: ${expression}`) + } + + if (!isUndefined(modifiers) && !isTopModifiers(modifiers)) { + throw new Error(`Invalid top modifiers: ${modifiers}`) + } + + return TopNode.create(expression, modifiers) +} + +function isTopModifiers(modifiers: string): modifiers is TopModifier { + return ( + modifiers === 'percent' || + modifiers === 'with ties' || + modifiers === 'percent with ties' + ) +} diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts index c0daf3b5c..529dc8091 100644 --- a/src/query-builder/delete-query-builder.ts +++ b/src/query-builder/delete-query-builder.ts @@ -71,6 +71,7 @@ import { ValueExpression, parseValueExpression, } from '../parser/value-parser.js' +import { parseTop } from '../parser/top-parser.js' export class DeleteQueryBuilder implements @@ -134,6 +135,58 @@ export class DeleteQueryBuilder }) } + /** + * Changes a `delete from` query into a `delete top from` query. + * + * `top` clause is only supported by some dialects like MS SQL Server. + * + * ### Examples + * + * Delete the first 5 rows: + * + * ```ts + * await db + * .deleteFrom('person') + * .top(5) + * .where('age', '>', 18) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * delete top(5) from "person" where "age" > @1 + * ``` + * + * Delete the first 50% of rows: + * + * ```ts + * await db + * .deleteFrom('person') + * .top(50, 'percent') + * .where('age', '>', 18) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * delete top(50) percent from "person" where "age" > @1 + * ``` + */ + top( + expression: number | bigint, + modifiers?: 'percent', + ): DeleteQueryBuilder { + return new DeleteQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } + /** * Adds a `using` clause to the query. * @@ -783,8 +836,8 @@ export class DeleteQueryBuilder ): O2 extends DeleteResult ? DeleteQueryBuilder : O2 extends O & infer E - ? DeleteQueryBuilder> - : DeleteQueryBuilder> { + ? DeleteQueryBuilder> + : DeleteQueryBuilder> { if (condition) { return func(this) as any } @@ -1033,12 +1086,12 @@ export type DeleteQueryBuilderWithInnerJoin< ? InnerJoinedBuilder : never : TE extends keyof DB - ? DeleteQueryBuilder - : TE extends AliasedExpression - ? InnerJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? InnerJoinedBuilder - : never + ? DeleteQueryBuilder + : TE extends AliasedExpression + ? InnerJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? InnerJoinedBuilder + : never type InnerJoinedBuilder< DB, @@ -1065,12 +1118,12 @@ export type DeleteQueryBuilderWithLeftJoin< ? LeftJoinedBuilder : never : TE extends keyof DB - ? LeftJoinedBuilder - : TE extends AliasedExpression - ? LeftJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? LeftJoinedBuilder - : never + ? LeftJoinedBuilder + : TE extends AliasedExpression + ? LeftJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? LeftJoinedBuilder + : never type LeftJoinedBuilder< DB, @@ -1087,8 +1140,8 @@ type LeftJoinedDB = DrainOuterGeneric<{ [C in keyof DB | A]: C extends A ? Nullable : C extends keyof DB - ? DB[C] - : never + ? DB[C] + : never }> export type DeleteQueryBuilderWithRightJoin< @@ -1101,12 +1154,12 @@ export type DeleteQueryBuilderWithRightJoin< ? RightJoinedBuilder : never : TE extends keyof DB - ? RightJoinedBuilder - : TE extends AliasedExpression - ? RightJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? RightJoinedBuilder - : never + ? RightJoinedBuilder + : TE extends AliasedExpression + ? RightJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? RightJoinedBuilder + : never type RightJoinedBuilder< DB, @@ -1125,10 +1178,10 @@ type RightJoinedDB< [C in keyof DB | A]: C extends A ? R : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> export type DeleteQueryBuilderWithFullJoin< @@ -1141,12 +1194,12 @@ export type DeleteQueryBuilderWithFullJoin< ? OuterJoinedBuilder : never : TE extends keyof DB - ? OuterJoinedBuilder - : TE extends AliasedExpression - ? OuterJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? OuterJoinedBuilder - : never + ? OuterJoinedBuilder + : TE extends AliasedExpression + ? OuterJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? OuterJoinedBuilder + : never type OuterJoinedBuilder< DB, @@ -1165,8 +1218,8 @@ type OuterJoinedBuilderDB< [C in keyof DB | A]: C extends A ? Nullable : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index 05168695a..2a5deded4 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -58,6 +58,7 @@ import { Explainable, ExplainFormat } from '../util/explainable.js' import { Expression } from '../expression/expression.js' import { KyselyTypeError } from '../util/type-error.js' import { Streamable } from '../util/streamable.js' +import { parseTop } from '../parser/top-parser.js' export class InsertQueryBuilder implements @@ -369,6 +370,62 @@ export class InsertQueryBuilder }) } + /** + * Changes an `insert into` query to an `insert top into` query. + * + * `top` clause is only supported by some dialects like MS SQL Server. + * + * ### Examples + * + * Insert the first 5 rows: + * + * ```ts + * await db.insertInto('person') + * .top(5) + * .columns(['first_name', 'gender']) + * .expression( + * (eb) => eb.selectFrom('pet').select(['name', sql.lit('other').as('gender')]) + * ) + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * insert top(5) into "person" ("first_name", "gender") select "name", 'other' as "gender" from "pet" + * ``` + * + * Insert the first 50 percent of rows: + * + * ```ts + * await db.insertInto('person') + * .top(50, 'percent') + * .columns(['first_name', 'gender']) + * .expression( + * (eb) => eb.selectFrom('pet').select(['name', sql.lit('other').as('gender')]) + * ) + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * insert top(50) percent into "person" ("first_name", "gender") select "name", 'other' as "gender" from "pet" + * ``` + */ + top( + expression: number | bigint, + modifiers?: 'percent', + ): InsertQueryBuilder { + return new InsertQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } + /** * Adds an `on conflict` clause to the query. * @@ -700,8 +757,8 @@ export class InsertQueryBuilder ): O2 extends InsertResult ? InsertQueryBuilder : O2 extends O & infer E - ? InsertQueryBuilder> - : InsertQueryBuilder> { + ? InsertQueryBuilder> + : InsertQueryBuilder> { if (condition) { return func(this) as any } diff --git a/src/query-builder/merge-query-builder.ts b/src/query-builder/merge-query-builder.ts index c00951997..fc3c7a1cb 100644 --- a/src/query-builder/merge-query-builder.ts +++ b/src/query-builder/merge-query-builder.ts @@ -23,6 +23,7 @@ import { import { parseMergeThen, parseMergeWhen } from '../parser/merge-parser.js' import { ReferenceExpression } from '../parser/reference-parser.js' import { TableExpression } from '../parser/table-parser.js' +import { parseTop } from '../parser/top-parser.js' import { ExtractUpdateTypeFromReferenceExpression, UpdateObject, @@ -58,6 +59,66 @@ export class MergeQueryBuilder { this.#props = freeze(props) } + /** + * Changes a `merge into` query to an `merge top into` query. + * + * `top` clause is only supported by some dialects like MS SQL Server. + * + * ### Examples + * + * Affect 5 matched rows at most: + * + * ```ts + * await db.mergeInto('person') + * .top(5) + * .using('pet', 'person.id', 'pet.owner_id') + * .whenMatched() + * .thenDelete() + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * merge top(5) into "person" + * using "pet" on "person"."id" = "pet"."owner_id" + * when matched then + * delete + * ``` + * + * Affect 50% of matched rows: + * + * ```ts + * await db.mergeInto('person') + * .top(50, 'percent') + * .using('pet', 'person.id', 'pet.owner_id') + * .whenMatched() + * .thenDelete() + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * merge top(50) percent into "person" + * using "pet" on "person"."id" = "pet"."owner_id" + * when matched then + * delete + * ``` + */ + top( + expression: number | bigint, + modifiers?: 'percent', + ): MergeQueryBuilder { + return new MergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } + /** * Adds the `using` clause to the query. * @@ -137,6 +198,22 @@ export class WheneableMergeQueryBuilder< this.#props = freeze(props) } + /** + * See {@link MergeQueryBuilder.top}. + */ + top( + expression: number | bigint, + modifiers?: 'percent', + ): WheneableMergeQueryBuilder { + return new WheneableMergeQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } + /** * Adds a simple `when matched` clause to the query. * diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 5b56a1311..edc0ef9d7 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -81,6 +81,8 @@ import { } from '../parser/value-parser.js' import { FetchModifier } from '../operation-node/fetch-node.js' import { parseFetch } from '../parser/fetch-parser.js' +import { TopModifier } from '../operation-node/top-node.js' +import { parseTop } from '../parser/top-parser.js' export interface SelectQueryBuilder extends WhereInterface, @@ -1035,6 +1037,13 @@ export interface SelectQueryBuilder * .selectFrom('person') * .select('first_name') * .limit(10) + * .execute() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select "first_name" from "person" limit $1 * ``` * * Select rows from index 10 to index 19 of the result: @@ -1043,8 +1052,15 @@ export interface SelectQueryBuilder * return await db * .selectFrom('person') * .select('first_name') - * .offset(10) * .limit(10) + * .offset(10) + * .execute() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select "first_name" from "person" limit $1 offset $2 * ``` */ limit( @@ -1064,6 +1080,13 @@ export interface SelectQueryBuilder * .select('first_name') * .limit(10) * .offset(10) + * .execute() + * ``` + * + * The generated SQL (PostgreSQL): + * + * ```sql + * select "first_name" from "person" limit $1 offset $2 * ``` */ offset( @@ -1102,6 +1125,51 @@ export interface SelectQueryBuilder modifier?: FetchModifier, ): SelectQueryBuilder + /** + * Adds a `top` clause to the query. + * + * This clause is only supported by some dialects like MS SQL Server. + * + * ### Examples + * + * Select 10 biggest ages: + * + * ```ts + * return await db + * .selectFrom('person') + * .select('age') + * .top(10) + * .orderBy('age desc') + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * select top(10) "age" from "person" order by "age" desc + * ``` + * + * Select 10% first rows: + * + * ```ts + * return await db + * .selectFrom('person') + * .selectAll() + * .top(10, 'percent') + * .execute() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * select top(10) percent * from "person" + * ``` + */ + top( + expression: number | bigint, + modifiers?: TopModifier, + ): SelectQueryBuilder + /** * Combines another select query or raw expression to this query using `union`. * @@ -2040,6 +2108,19 @@ class SelectQueryBuilderImpl ), }) } + + top( + expression: number | bigint, + modifiers?: TopModifier, + ): SelectQueryBuilder { + return new SelectQueryBuilderImpl({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } union( expression: SetOperandExpression, @@ -2350,12 +2431,12 @@ export type SelectQueryBuilderWithInnerJoin< ? InnerJoinedBuilder : never : TE extends keyof DB - ? SelectQueryBuilder - : TE extends AliasedExpression - ? InnerJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? InnerJoinedBuilder - : never + ? SelectQueryBuilder + : TE extends AliasedExpression + ? InnerJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? InnerJoinedBuilder + : never type InnerJoinedBuilder< DB, @@ -2382,12 +2463,12 @@ export type SelectQueryBuilderWithLeftJoin< ? LeftJoinedBuilder : never : TE extends keyof DB - ? LeftJoinedBuilder - : TE extends AliasedExpression - ? LeftJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? LeftJoinedBuilder - : never + ? LeftJoinedBuilder + : TE extends AliasedExpression + ? LeftJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? LeftJoinedBuilder + : never type LeftJoinedBuilder< DB, @@ -2404,8 +2485,8 @@ type LeftJoinedDB = DrainOuterGeneric<{ [C in keyof DB | A]: C extends A ? Nullable : C extends keyof DB - ? DB[C] - : never + ? DB[C] + : never }> export type SelectQueryBuilderWithRightJoin< @@ -2418,12 +2499,12 @@ export type SelectQueryBuilderWithRightJoin< ? RightJoinedBuilder : never : TE extends keyof DB - ? RightJoinedBuilder - : TE extends AliasedExpression - ? RightJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? RightJoinedBuilder - : never + ? RightJoinedBuilder + : TE extends AliasedExpression + ? RightJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? RightJoinedBuilder + : never type RightJoinedBuilder< DB, @@ -2442,10 +2523,10 @@ type RightJoinedDB< [C in keyof DB | A]: C extends A ? R : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> export type SelectQueryBuilderWithFullJoin< @@ -2458,12 +2539,12 @@ export type SelectQueryBuilderWithFullJoin< ? OuterJoinedBuilder : never : TE extends keyof DB - ? OuterJoinedBuilder - : TE extends AliasedExpression - ? OuterJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? OuterJoinedBuilder - : never + ? OuterJoinedBuilder + : TE extends AliasedExpression + ? OuterJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? OuterJoinedBuilder + : never type OuterJoinedBuilder< DB, @@ -2482,10 +2563,10 @@ type OuterJoinedBuilderDB< [C in keyof DB | A]: C extends A ? Nullable : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> type TableOrList = diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index 28c8632fe..31333ca58 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -71,6 +71,7 @@ import { parseValueExpression, } from '../parser/value-parser.js' import { LimitNode } from '../operation-node/limit-node.js' +import { parseTop } from '../parser/top-parser.js' export class UpdateQueryBuilder implements @@ -134,6 +135,58 @@ export class UpdateQueryBuilder }) } + /** + * Changes an `update` query into a `update top` query. + * + * `top` clause is only supported by some dialects like MS SQL Server. + * + * ### Examples + * + * Update the first row: + * + * ```ts + * await db.updateTable('person') + * .top(1) + * .set({ first_name: 'Foo' }) + * .where('age', '>', 18) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * update top(1) "person" set "first_name" = @1 where "age" > @2 + * ``` + * + * Update the 50% first rows: + * + * ```ts + * await db.updateTable('person') + * .top(50, 'percent') + * .set({ first_name: 'Foo' }) + * .where('age', '>', 18) + * .executeTakeFirstOrThrow() + * ``` + * + * The generated SQL (MS SQL Server): + * + * ```sql + * update top(50) percent "person" set "first_name" = @1 where "age" > @2 + * ``` + */ + top( + expression: number | bigint, + modifiers?: 'percent', + ): UpdateQueryBuilder { + return new UpdateQueryBuilder({ + ...this.#props, + queryNode: QueryNode.cloneWithTop( + this.#props.queryNode, + parseTop(expression, modifiers), + ), + }) + } + /** * Adds a from clause to the update query. * @@ -724,8 +777,8 @@ export class UpdateQueryBuilder ): O2 extends UpdateResult ? UpdateQueryBuilder : O2 extends O & infer E - ? UpdateQueryBuilder> - : UpdateQueryBuilder> { + ? UpdateQueryBuilder> + : UpdateQueryBuilder> { if (condition) { return func(this) as any } @@ -982,12 +1035,12 @@ export type UpdateQueryBuilderWithInnerJoin< ? InnerJoinedBuilder : never : TE extends keyof DB - ? UpdateQueryBuilder - : TE extends AliasedExpression - ? InnerJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? InnerJoinedBuilder - : never + ? UpdateQueryBuilder + : TE extends AliasedExpression + ? InnerJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? InnerJoinedBuilder + : never type InnerJoinedBuilder< DB, @@ -1016,12 +1069,12 @@ export type UpdateQueryBuilderWithLeftJoin< ? LeftJoinedBuilder : never : TE extends keyof DB - ? LeftJoinedBuilder - : TE extends AliasedExpression - ? LeftJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? LeftJoinedBuilder - : never + ? LeftJoinedBuilder + : TE extends AliasedExpression + ? LeftJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? LeftJoinedBuilder + : never type LeftJoinedBuilder< DB, @@ -1039,8 +1092,8 @@ type LeftJoinedDB = DrainOuterGeneric<{ [C in keyof DB | A]: C extends A ? Nullable : C extends keyof DB - ? DB[C] - : never + ? DB[C] + : never }> export type UpdateQueryBuilderWithRightJoin< @@ -1054,12 +1107,12 @@ export type UpdateQueryBuilderWithRightJoin< ? RightJoinedBuilder : never : TE extends keyof DB - ? RightJoinedBuilder - : TE extends AliasedExpression - ? RightJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? RightJoinedBuilder - : never + ? RightJoinedBuilder + : TE extends AliasedExpression + ? RightJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? RightJoinedBuilder + : never type RightJoinedBuilder< DB, @@ -1079,10 +1132,10 @@ type RightJoinedDB< [C in keyof DB | A]: C extends A ? R : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> export type UpdateQueryBuilderWithFullJoin< @@ -1096,12 +1149,12 @@ export type UpdateQueryBuilderWithFullJoin< ? OuterJoinedBuilder : never : TE extends keyof DB - ? OuterJoinedBuilder - : TE extends AliasedExpression - ? OuterJoinedBuilder - : TE extends (qb: any) => AliasedExpression - ? OuterJoinedBuilder - : never + ? OuterJoinedBuilder + : TE extends AliasedExpression + ? OuterJoinedBuilder + : TE extends (qb: any) => AliasedExpression + ? OuterJoinedBuilder + : never type OuterJoinedBuilder< DB, @@ -1121,8 +1174,8 @@ type OuterJoinedBuilderDB< [C in keyof DB | A]: C extends A ? Nullable : C extends TB - ? Nullable - : C extends keyof DB - ? DB[C] - : never + ? Nullable + : C extends keyof DB + ? DB[C] + : never }> diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 2bd556035..bbdbdb81e 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -109,6 +109,7 @@ import { MatchedNode } from '../operation-node/matched-node.js' import { AddIndexNode } from '../operation-node/add-index-node.js' import { CastNode } from '../operation-node/cast-node.js' import { FetchNode } from '../operation-node/fetch-node.js' +import { TopNode } from '../operation-node/top-node.js' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -173,6 +174,11 @@ export class DefaultQueryCompiler this.compileList(node.frontModifiers, ' ') } + if (node.top) { + this.append(' ') + this.visitNode(node.top) + } + if (node.selections) { this.append(' ') this.compileList(node.selections) @@ -306,6 +312,11 @@ export class DefaultQueryCompiler this.append(' ignore') } + if (node.top) { + this.append(' ') + this.visitNode(node.top) + } + if (node.into) { this.append(' into ') this.visitNode(node.into) @@ -370,6 +381,12 @@ export class DefaultQueryCompiler } this.append('delete ') + + if (node.top) { + this.visitNode(node.top) + this.append(' ') + } + this.visitNode(node.from) if (node.using) { @@ -738,6 +755,11 @@ export class DefaultQueryCompiler this.append('update ') + if (node.top) { + this.visitNode(node.top) + this.append(' ') + } + if (node.table) { this.visitNode(node.table) this.append(' ') @@ -1476,7 +1498,14 @@ export class DefaultQueryCompiler this.append(' ') } - this.append('merge into ') + this.append('merge ') + + if (node.top) { + this.visitNode(node.top) + this.append(' ') + } + + this.append('into ') this.visitNode(node.into) if (node.using) { @@ -1539,6 +1568,14 @@ export class DefaultQueryCompiler this.append(` rows ${node.modifier}`) } + protected override visitTop(node: TopNode): void { + this.append(`top(${node.expression})`) + + if (node.modifiers) { + this.append(` ${node.modifiers}`) + } + } + protected append(str: string): void { this.#sql += str } diff --git a/test/node/src/clear.test.ts b/test/node/src/clear.test.ts index 4accd1e0e..788894989 100644 --- a/test/node/src/clear.test.ts +++ b/test/node/src/clear.test.ts @@ -386,7 +386,7 @@ for (const dialect of DIALECTS) { parameters: [1], }, mssql: { - sql: `select top 1 * from "person"`, + sql: `select top(1) * from "person"`, parameters: [], }, sqlite: { diff --git a/test/node/src/delete.test.ts b/test/node/src/delete.test.ts index 4954e43f4..2ed2d3f50 100644 --- a/test/node/src/delete.test.ts +++ b/test/node/src/delete.test.ts @@ -867,5 +867,50 @@ for (const dialect of DIALECTS) { ) }) } + + if (dialect === 'mssql') { + it('should delete top', async () => { + const query = ctx.db + .deleteFrom('person') + .top(1) + .where('gender', '=', 'male') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete top(1) from "person" where "gender" = @1', + parameters: ['male'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(DeleteResult) + expect(result.numDeletedRows).to.equal(1n) + }) + + it('should delete top percent', async () => { + const query = ctx.db + .deleteFrom('person') + .top(50, 'percent') + .where('gender', '=', 'male') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'delete top(50) percent from "person" where "gender" = @1', + parameters: ['male'], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(DeleteResult) + }) + } }) } diff --git a/test/node/src/insert.test.ts b/test/node/src/insert.test.ts index c97ece4a6..49057105e 100644 --- a/test/node/src/insert.test.ts +++ b/test/node/src/insert.test.ts @@ -902,6 +902,52 @@ for (const dialect of DIALECTS) { expect(people).to.eql(values) }) } + + if (dialect === 'mssql') { + it('should insert top', async () => { + const query = ctx.db + .insertInto('person') + .top(1) + .columns(['first_name', 'gender']) + .expression((eb) => + eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]) + ) + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'insert top(1) into "person" ("first_name", "gender") select "name", @1 as "gender" from "pet"', + parameters: ['other'], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.executeTakeFirstOrThrow() + }) + + it('should insert top percent', async () => { + const query = ctx.db + .insertInto('person') + .top(50, 'percent') + .columns(['first_name', 'gender']) + .expression((eb) => + eb.selectFrom('pet').select(['name', eb.val('other').as('gender')]) + ) + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'insert top(50) percent into "person" ("first_name", "gender") select "name", @1 as "gender" from "pet"', + parameters: ['other'], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.executeTakeFirstOrThrow() + }) + } }) async function getNewestPerson( diff --git a/test/node/src/join.test.ts b/test/node/src/join.test.ts index 41b550c08..6031f7520 100644 --- a/test/node/src/join.test.ts +++ b/test/node/src/join.test.ts @@ -273,7 +273,7 @@ for (const dialect of DIALECTS) { `inner join "pet"`, `on "pet"."owner_id" = "person"."id"`, `and "pet"."name" in (@1, @2, @3)`, - `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top 1 'hamster' as "hamster" from "pet"))`, + `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top(1) 'hamster' as "hamster" from "pet"))`, `order by "person"."first_name"`, ], parameters: ['Catto', 'Doggo', 'Hammo', 'cat', 'dog'], @@ -348,7 +348,7 @@ for (const dialect of DIALECTS) { `inner join "pet"`, `on "pet"."owner_id" = "person"."id"`, `and "pet"."name" in (@1, @2, @3)`, - `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top 1 'hamster' as "hamster" from "pet"))`, + `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top(1) 'hamster' as "hamster" from "pet"))`, `order by "person"."first_name"`, ], parameters: ['Catto', 'Doggo', 'Hammo', 'cat', 'dog'], @@ -593,7 +593,7 @@ for (const dialect of DIALECTS) { `left join "pet"`, `on "pet"."owner_id" = "person"."id"`, `and "pet"."name" in (@1, @2, @3)`, - `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top 1 'hamster' as "hamster" from "pet"))`, + `and ("pet"."species" = @4 or "species" = @5 or "species" = (select top(1) 'hamster' as "hamster" from "pet"))`, `order by "person"."first_name"`, ], parameters: ['Catto', 'Doggo', 'Hammo', 'cat', 'dog'], diff --git a/test/node/src/merge.test.ts b/test/node/src/merge.test.ts index 73b803f20..9fbe1bb22 100644 --- a/test/node/src/merge.test.ts +++ b/test/node/src/merge.test.ts @@ -955,5 +955,55 @@ for (const dialect of DIALECTS.filter( }) } }) + + if (dialect === 'mssql') { + it('should perform a merge top...using table simple on...when matched then delete query', async () => { + const query = ctx.db + .mergeInto('person') + .top(1) + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'merge top(1) into "person" using "pet" on "pet"."owner_id" = "person"."id" when matched then delete;', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirstOrThrow() + + expect(result).to.be.instanceOf(MergeResult) + expect(result.numChangedRows).to.equal(1n) + }) + + it('should perform a merge top percent...using table simple on...when matched then delete query', async () => { + const query = ctx.db + .mergeInto('person') + .top(50, 'percent') + .using('pet', 'pet.owner_id', 'person.id') + .whenMatched() + .thenDelete() + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'merge top(50) percent into "person" using "pet" on "pet"."owner_id" = "person"."id" when matched then delete;', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + const result = await query.executeTakeFirstOrThrow() + + expect(result).to.be.instanceOf(MergeResult) + expect(result.numChangedRows).to.equal(2n) + }) + } }) } diff --git a/test/node/src/select.test.ts b/test/node/src/select.test.ts index cdc9d5dcc..00477a19d 100644 --- a/test/node/src/select.test.ts +++ b/test/node/src/select.test.ts @@ -1143,7 +1143,7 @@ for (const dialect of DIALECTS) { parameters: [1], }, mssql: { - sql: `select (select top 1 "first_name" from "person" order by "first_name") as "person_first_name"`, + sql: `select (select top(1) "first_name" from "person" order by "first_name") as "person_first_name"`, parameters: [], }, sqlite: { @@ -1237,5 +1237,102 @@ for (const dialect of DIALECTS) { await query.execute() }) } + + if (dialect === 'mssql') { + it('should create a select query with top', async () => { + const query = ctx.db.selectFrom('person').select('first_name').top(2) + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'select top(2) "first_name" from "person"', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should create a select query with top and order by', async () => { + const query = ctx.db + .selectFrom('person') + .select('first_name') + .top(2) + .orderBy('first_name') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'select top(2) "first_name" from "person" order by "first_name"', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should create a select query with top percent', async () => { + const query = ctx.db + .selectFrom('person') + .select('first_name') + .top(50, 'percent') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'select top(50) percent "first_name" from "person"', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should create a select query with top with ties', async () => { + const query = ctx.db + .selectFrom('person') + .select('first_name') + .top(2, 'with ties') + .orderBy('first_name') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'select top(2) with ties "first_name" from "person" order by "first_name"', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should create a select query with top percent with ties', async () => { + const query = ctx.db + .selectFrom('person') + .select('first_name') + .top(50, 'percent with ties') + .orderBy('first_name') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'select top(50) percent with ties "first_name" from "person" order by "first_name"', + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + } }) } diff --git a/test/node/src/test-setup.ts b/test/node/src/test-setup.ts index f05753540..222834578 100644 --- a/test/node/src/test-setup.ts +++ b/test/node/src/test-setup.ts @@ -510,7 +510,7 @@ export function limit>( ): (qb: QB) => QB { return (qb) => { if (dialect === 'mssql') { - return qb.modifyFront(sql`top ${sql.lit(limit)}`) as QB + return qb.top(limit) as QB } return qb.limit(limit) as QB diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts index d4d783dd7..73f7e272f 100644 --- a/test/node/src/update.test.ts +++ b/test/node/src/update.test.ts @@ -575,7 +575,7 @@ for (const dialect of DIALECTS) { parameters: ['Jennifer', 1], }, mssql: { - sql: 'with "jennifer_id" as (select top 1 "id" from "person" where "first_name" = @1) update "pet" set "owner_id" = (select "id" from "jennifer_id")', + sql: 'with "jennifer_id" as (select top(1) "id" from "person" where "first_name" = @1) update "pet" set "owner_id" = (select "id" from "jennifer_id")', parameters: ['Jennifer'], }, sqlite: { @@ -670,6 +670,46 @@ for (const dialect of DIALECTS) { expect(pet.person_name).to.equal(pet.pet_name) } }) + + it('should update top', async () => { + const query = ctx.db + .updateTable('pet') + .top(1) + .set({ name: 'Lucky' }) + .where('species', '=', 'dog') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'update top(1) "pet" set "name" = @1 where "species" = @2', + parameters: ['Lucky', 'dog'], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should update top percent', async () => { + const query = ctx.db + .updateTable('pet') + .top(50, 'percent') + .set({ name: 'Lucky' }) + .where('species', '=', 'dog') + + testSql(query, dialect, { + postgres: NOT_SUPPORTED, + mysql: NOT_SUPPORTED, + mssql: { + sql: 'update top(50) percent "pet" set "name" = @1 where "species" = @2', + parameters: ['Lucky', 'dog'], + }, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) } }) }