From 57589b352366b9907aafc03d1e6a99a533940426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20Koskim=C3=A4ki?= Date: Wed, 24 Nov 2021 23:31:51 +0200 Subject: [PATCH] Add first version of union and unionAll methods --- .../operation-node-transformer.ts | 11 + src/operation-node/operation-node-visitor.ts | 490 +++--------------- src/operation-node/operation-node.ts | 1 + src/operation-node/select-query-node.ts | 14 + src/operation-node/union-node.ts | 29 ++ src/parser/insert-values-parser.ts | 2 +- src/parser/union-parser.ts | 18 + src/parser/value-parser.ts | 2 +- src/query-builder/query-builder.ts | 59 ++- src/query-compiler/default-query-compiler.ts | 27 +- test/src/union.test.ts | 93 ++++ 11 files changed, 320 insertions(+), 426 deletions(-) create mode 100644 src/operation-node/union-node.ts create mode 100644 src/parser/union-parser.ts create mode 100644 test/src/union.test.ts diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 818689543..7d9418a12 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -58,6 +58,7 @@ import { ForeignKeyConstraintNode } from './foreign-key-constraint-node.js' import { ColumnDefinitionNode } from './column-definition-node.js' import { ModifyColumnNode } from './modify-column-node.js' import { OnDuplicateKeyNode } from './on-duplicate-key-node.js' +import { UnionNode } from './union-node.js' /** * Transforms an operation node tree into another one. @@ -150,6 +151,7 @@ export class OperationNodeTransformer { AddConstraintNode: this.transformAddConstraint.bind(this), DropConstraintNode: this.transformDropConstraint.bind(this), ForeignKeyConstraintNode: this.transformForeignKeyConstraint.bind(this), + UnionNode: this.transformUnion.bind(this), } readonly transformNode = ( @@ -192,6 +194,7 @@ export class OperationNodeTransformer { offset: this.transformNode(node.offset), with: this.transformNode(node.with), having: this.transformNode(node.having), + union: this.transformNodeList(node.union), } } @@ -519,6 +522,14 @@ export class OperationNodeTransformer { } } + protected transformUnion(node: UnionNode): UnionNode { + return { + kind: 'UnionNode', + union: this.transformNode(node.union), + all: node.all, + } + } + protected transformReferences(node: ReferencesNode): ReferencesNode { return { kind: 'ReferencesNode', diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index 3f1c86c54..4e448fc32 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -60,8 +60,9 @@ import { ForeignKeyConstraintNode } from './foreign-key-constraint-node.js' import { ColumnDefinitionNode } from './column-definition-node.js' import { ModifyColumnNode } from './modify-column-node.js' import { OnDuplicateKeyNode } from './on-duplicate-key-node.js' +import { UnionNode } from './union-node.js' -export class OperationNodeVisitor { +export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] readonly #visitors: Record = { @@ -123,6 +124,7 @@ export class OperationNodeVisitor { AddConstraintNode: this.visitAddConstraint.bind(this), DropConstraintNode: this.visitDropConstraint.bind(this), ForeignKeyConstraintNode: this.visitForeignKeyConstraint.bind(this), + UnionNode: this.visitUnion.bind(this), } protected readonly visitNode = (node: OperationNode): void => { @@ -131,425 +133,69 @@ export class OperationNodeVisitor { this.nodeStack.pop() } - protected visitSelectQuery(node: SelectQueryNode): void { - if (node.with) { - this.visitNode(node.with) - } - - if (node.distinctOnSelections) { - node.distinctOnSelections.forEach(this.visitNode) - } - - if (node.selections) { - node.selections.forEach(this.visitNode) - } - - this.visitNode(node.from) - - if (node.joins) { - node.joins.forEach(this.visitNode) - } - - if (node.where) { - this.visitNode(node.where) - } - - if (node.groupBy) { - this.visitNode(node.groupBy) - } - - if (node.having) { - this.visitNode(node.having) - } - - if (node.orderBy) { - this.visitNode(node.orderBy) - } - - if (node.limit) { - this.visitNode(node.limit) - } - - if (node.offset) { - this.visitNode(node.offset) - } - } - - protected visitSelection(node: SelectionNode): void { - this.visitNode(node.selection) - } - - protected visitColumn(node: ColumnNode): void { - this.visitNode(node.column) - } - - protected visitAlias(node: AliasNode): void { - this.visitNode(node.node) - } - - protected visitTable(node: TableNode): void { - if (node.schema) { - this.visitNode(node.schema) - } - - this.visitNode(node.table) - } - - protected visitFrom(node: FromNode): void { - node.froms.forEach(this.visitNode) - } - - protected visitReference(node: ReferenceNode): void { - this.visitNode(node.column) - this.visitNode(node.table) - } - - protected visitFilter(node: FilterNode): void { - if (node.left) { - this.visitNode(node.left) - } - - this.visitNode(node.op) - this.visitNode(node.right) - } - - protected visitAnd(node: AndNode): void { - this.visitNode(node.left) - this.visitNode(node.right) - } - - protected visitOr(node: OrNode): void { - this.visitNode(node.left) - this.visitNode(node.right) - } - - protected visitValueList(node: ValueListNode): void { - node.values.forEach(this.visitNode) - } - - protected visitParens(node: ParensNode): void { - this.visitNode(node.node) - } - - protected visitJoin(node: JoinNode): void { - this.visitNode(node.table) - - if (node.on) { - this.visitNode(node.on) - } - } - - protected visitRaw(node: RawNode): void { - node.params.forEach(this.visitNode) - } - - protected visitWhere(node: WhereNode): void { - this.visitNode(node.where) - } - - protected visitInsertQuery(node: InsertQueryNode): void { - if (node.with) { - this.visitNode(node.with) - } - - this.visitNode(node.into) - - if (node.columns) { - node.columns.forEach(this.visitNode) - } - - if (node.values) { - node.values.forEach(this.visitNode) - } - - if (node.returning) { - this.visitNode(node.returning) - } - } - - protected visitDeleteQuery(node: DeleteQueryNode): void { - if (node.with) { - this.visitNode(node.with) - } - - this.visitNode(node.from) - - if (node.joins) { - node.joins.forEach(this.visitNode) - } - - if (node.where) { - this.visitNode(node.where) - } - - if (node.returning) { - this.visitNode(node.returning) - } - } - - protected visitReturning(node: ReturningNode): void { - node.selections.forEach(this.visitNode) - } - - protected visitCreateTable(node: CreateTableNode): void { - this.visitNode(node.table) - - node.columns.forEach(this.visitNode) - - if (node.constraints) { - node.constraints.forEach(this.visitNode) - } - } - - protected visitAddColumn(node: AddColumnNode): void { - this.visitNode(node.column) - } - - protected visitColumnDefinition(node: ColumnDefinitionNode): void { - this.visitNode(node.column) - this.visitNode(node.dataType) - - if (node.defaultTo) { - this.visitNode(node.defaultTo) - } - - if (node.references) { - this.visitNode(node.references) - } - - if (node.check) { - this.visitNode(node.check) - } - } - - protected visitDropTable(node: DropTableNode): void { - this.visitNode(node.table) - } - - protected visitOrderBy(node: OrderByNode): void { - node.items.forEach(this.visitNode) - } - - protected visitOrderByItem(node: OrderByItemNode): void { - this.visitNode(node.orderBy) - } - - protected visitGroupBy(node: GroupByNode): void { - node.items.forEach(this.visitNode) - } - - protected visitGroupByItem(node: GroupByItemNode): void { - this.visitNode(node.groupBy) - } - - protected visitUpdateQuery(node: UpdateQueryNode): void { - if (node.with) { - this.visitNode(node.with) - } - - this.visitNode(node.table) - - if (node.updates) { - node.updates.forEach(this.visitNode) - } - - if (node.joins) { - node.joins.forEach(this.visitNode) - } - - if (node.where) { - this.visitNode(node.where) - } - - if (node.returning) { - this.visitNode(node.returning) - } - } - - protected visitColumnUpdate(node: ColumnUpdateNode): void { - this.visitNode(node.column) - this.visitNode(node.value) - } - - protected visitLimit(node: LimitNode): void { - this.visitNode(node.limit) - } - - protected visitOffset(node: OffsetNode): void { - this.visitNode(node.offset) - } - - protected visitOnConflict(node: OnConflictNode): void { - if (node.columns) { - node.columns.forEach(this.visitNode) - } - - if (node.constraint) { - this.visitNode(node.constraint) - } - - if (node.updates) { - node.updates.forEach(this.visitNode) - } - } - - protected visitOnDuplicateKey(node: OnDuplicateKeyNode): void { - node.updates.forEach(this.visitNode) - } - - protected visitCreateIndex(node: CreateIndexNode): void { - this.visitNode(node.name) - - if (node.table) { - this.visitNode(node.table) - } - - if (node.using) { - this.visitNode(node.using) - } - - if (node.expression) { - this.visitNode(node.expression) - } - } - - protected visitList(node: ListNode): void { - node.items.forEach(this.visitNode) - } - - protected visitDropIndex(node: DropIndexNode): void { - this.visitNode(node.name) - - if (node.table) { - this.visitNode(node.table) - } - } - - protected visitPrimaryKeyConstraint(node: PrimaryKeyConstraintNode): void { - if (node.name) { - this.visitNode(node.name) - } - - node.columns.forEach(this.visitNode) - } - - protected visitUniqueConstraint(node: UniqueConstraintNode): void { - if (node.name) { - this.visitNode(node.name) - } - - node.columns.forEach(this.visitNode) - } - - protected visitReferences(node: ReferencesNode): void { - node.columns.forEach(this.visitNode) - this.visitNode(node.table) - } - - protected visitCheckConstraint(node: CheckConstraintNode): void { - if (node.name) { - this.visitNode(node.name) - } - - this.visitNode(node.expression) - } - - protected visitWith(node: WithNode): void { - node.expressions.forEach(this.visitNode) - } - - protected visitCommonTableExpression(node: CommonTableExpressionNode): void { - this.visitNode(node.name) - this.visitNode(node.expression) - } - - protected visitHaving(node: HavingNode): void { - this.visitNode(node.having) - } - - protected visitCreateSchema(node: CreateSchemaNode): void { - this.visitNode(node.schema) - } - - protected visitDropSchema(node: DropSchemaNode): void { - this.visitNode(node.schema) - } - - protected visitAlterTable(node: AlterTableNode): void { - this.visitNode(node.table) - - if (node.renameTo) { - this.visitNode(node.renameTo) - } - - if (node.renameColumn) { - this.visitNode(node.renameColumn) - } - - if (node.setSchema) { - this.visitNode(node.setSchema) - } - - if (node.addColumn) { - this.visitNode(node.addColumn) - } - - if (node.dropColumn) { - this.visitNode(node.dropColumn) - } - - if (node.alterColumn) { - this.visitNode(node.alterColumn) - } - - if (node.modifyColumn) { - this.visitNode(node.modifyColumn) - } - } - - protected visitDropColumn(node: DropColumnNode): void { - this.visitNode(node.column) - } - - protected visitRenameColumn(node: RenameColumnNode): void { - this.visitNode(node.column) - this.visitNode(node.renameTo) - } - - protected visitAlterColumn(node: AlterColumnNode): void { - this.visitNode(node.column) - } - - protected visitModifyColumn(node: ModifyColumnNode): void { - this.visitNode(node.column) - } - - protected visitAddConstraint(node: AddConstraintNode): void { - this.visitNode(node.constraint) - } - - protected visitDropConstraint(node: DropConstraintNode): void { - this.visitNode(node.constraintName) - } - - protected visitForeignKeyConstraint(node: ForeignKeyConstraintNode): void { - if (node.name) { - this.visitNode(node.name) - } - - node.columns.forEach(this.visitNode) - this.visitNode(node.references) - } - - protected visitDataType(node: DataTypeNode): void {} - - protected visitSelectAll(_: SelectAllNode): void {} - - protected visitIdentifier(_: IdentifierNode): void {} - - protected visitValue(_: ValueNode): void {} - - protected visitPrimitiveValueList(_: PrimitiveValueListNode): void {} - - protected visitOperator(node: OperatorNode) {} + protected abstract visitSelectQuery(node: SelectQueryNode): void + protected abstract visitSelection(node: SelectionNode): void + protected abstract visitColumn(node: ColumnNode): void + protected abstract visitAlias(node: AliasNode): void + protected abstract visitTable(node: TableNode): void + protected abstract visitFrom(node: FromNode): void + protected abstract visitReference(node: ReferenceNode): void + protected abstract visitFilter(node: FilterNode): void + protected abstract visitAnd(node: AndNode): void + protected abstract visitOr(node: OrNode): void + protected abstract visitValueList(node: ValueListNode): void + protected abstract visitParens(node: ParensNode): void + protected abstract visitJoin(node: JoinNode): void + protected abstract visitRaw(node: RawNode): void + protected abstract visitWhere(node: WhereNode): void + protected abstract visitInsertQuery(node: InsertQueryNode): void + protected abstract visitDeleteQuery(node: DeleteQueryNode): void + protected abstract visitReturning(node: ReturningNode): void + protected abstract visitCreateTable(node: CreateTableNode): void + protected abstract visitAddColumn(node: AddColumnNode): void + protected abstract visitColumnDefinition(node: ColumnDefinitionNode): void + protected abstract visitDropTable(node: DropTableNode): void + protected abstract visitOrderBy(node: OrderByNode): void + protected abstract visitOrderByItem(node: OrderByItemNode): void + protected abstract visitGroupBy(node: GroupByNode): void + protected abstract visitGroupByItem(node: GroupByItemNode): void + protected abstract visitUpdateQuery(node: UpdateQueryNode): void + protected abstract visitColumnUpdate(node: ColumnUpdateNode): void + protected abstract visitLimit(node: LimitNode): void + protected abstract visitOffset(node: OffsetNode): void + protected abstract visitOnConflict(node: OnConflictNode): void + protected abstract visitOnDuplicateKey(node: OnDuplicateKeyNode): void + protected abstract visitCreateIndex(node: CreateIndexNode): void + protected abstract visitDropIndex(node: DropIndexNode): void + protected abstract visitList(node: ListNode): void + protected abstract visitPrimaryKeyConstraint( + node: PrimaryKeyConstraintNode + ): void + protected abstract visitUniqueConstraint(node: UniqueConstraintNode): void + protected abstract visitReferences(node: ReferencesNode): void + protected abstract visitCheckConstraint(node: CheckConstraintNode): void + protected abstract visitWith(node: WithNode): void + protected abstract visitCommonTableExpression( + node: CommonTableExpressionNode + ): void + protected abstract visitHaving(node: HavingNode): void + protected abstract visitCreateSchema(node: CreateSchemaNode): void + protected abstract visitDropSchema(node: DropSchemaNode): void + protected abstract visitAlterTable(node: AlterTableNode): void + protected abstract visitDropColumn(node: DropColumnNode): void + protected abstract visitRenameColumn(node: RenameColumnNode): void + protected abstract visitAlterColumn(node: AlterColumnNode): void + protected abstract visitModifyColumn(node: ModifyColumnNode): void + protected abstract visitAddConstraint(node: AddConstraintNode): void + protected abstract visitDropConstraint(node: DropConstraintNode): void + protected abstract visitForeignKeyConstraint( + node: ForeignKeyConstraintNode + ): void + protected abstract visitUnion(node: UnionNode): void + protected abstract visitDataType(node: DataTypeNode): void + protected abstract visitSelectAll(_: SelectAllNode): void + protected abstract visitIdentifier(_: IdentifierNode): void + protected abstract visitValue(_: ValueNode): void + protected abstract visitPrimitiveValueList(_: PrimitiveValueListNode): void + protected abstract visitOperator(node: OperatorNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index 9af6aaefb..5f7228bff 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -57,6 +57,7 @@ export type OperationNodeKind = | 'AlterColumnNode' | 'AddConstraintNode' | 'DropConstraintNode' + | 'UnionNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/operation-node/select-query-node.ts b/src/operation-node/select-query-node.ts index b8511f438..8aec03da6 100644 --- a/src/operation-node/select-query-node.ts +++ b/src/operation-node/select-query-node.ts @@ -13,6 +13,7 @@ import { OrderByNode } from './order-by-node.js' import { SelectionNode } from './selection-node.js' import { WhereNode } from './where-node.js' import { WithNode } from './with-node.js' +import { UnionNode } from './union-node.js' export type SelectModifier = | 'ForUpdate' @@ -37,6 +38,7 @@ export interface SelectQueryNode extends OperationNode { readonly offset?: OffsetNode readonly with?: WithNode readonly having?: HavingNode + readonly union?: ReadonlyArray } /** @@ -168,4 +170,16 @@ export const SelectQueryNode = freeze({ isDistinct: true, }) }, + + cloneWithUnion( + selectNode: SelectQueryNode, + union: UnionNode + ): SelectQueryNode { + return freeze({ + ...selectNode, + union: selectNode.union + ? freeze([...selectNode.union, union]) + : freeze([union]), + }) + }, }) diff --git a/src/operation-node/union-node.ts b/src/operation-node/union-node.ts new file mode 100644 index 000000000..2678aa624 --- /dev/null +++ b/src/operation-node/union-node.ts @@ -0,0 +1,29 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' +import { RawNode } from './raw-node.js' +import { SelectQueryNode } from './select-query-node.js' + +export type UnionExpressionNode = SelectQueryNode | RawNode + +export interface UnionNode extends OperationNode { + readonly kind: 'UnionNode' + readonly union: UnionExpressionNode + readonly all: boolean +} + +/** + * @internal + */ +export const UnionNode = freeze({ + is(node: OperationNode): node is UnionNode { + return node.kind === 'UnionNode' + }, + + create(union: UnionExpressionNode, all: boolean): UnionNode { + return freeze({ + kind: 'UnionNode', + union, + all, + }) + }, +}) diff --git a/src/parser/insert-values-parser.ts b/src/parser/insert-values-parser.ts index 623b08cce..852485c66 100644 --- a/src/parser/insert-values-parser.ts +++ b/src/parser/insert-values-parser.ts @@ -21,7 +21,7 @@ export type InsertObject = { export type InsertObjectOrList = | InsertObject - | InsertObject[] + | ReadonlyArray> type InsertValueExpression = | T diff --git a/src/parser/union-parser.ts b/src/parser/union-parser.ts new file mode 100644 index 000000000..68328684a --- /dev/null +++ b/src/parser/union-parser.ts @@ -0,0 +1,18 @@ +import { QueryNode } from '../operation-node/query-node.js' +import { UnionNode } from '../operation-node/union-node.js' +import { QueryBuilder } from '../query-builder/query-builder.js' +import { RawBuilder } from '../raw-builder/raw-builder.js' + +export type UnionExpression = QueryBuilder | RawBuilder + +export function parseUnion(union: UnionExpression, all: boolean) { + const node = union.toOperationNode() + + if (QueryNode.isMutating(node)) { + throw new Error( + "can't combine insert, update or delete queries using union" + ) + } + + return UnionNode.create(node, all) +} diff --git a/src/parser/value-parser.ts b/src/parser/value-parser.ts index ebe0a2e63..2522ae4b6 100644 --- a/src/parser/value-parser.ts +++ b/src/parser/value-parser.ts @@ -24,7 +24,7 @@ export type ValueExpression = export type ValueExpressionOrList = | ValueExpression - | ValueExpression[] + | ReadonlyArray> export function parseValueExpressionOrList( ctx: ParseContext, diff --git a/src/query-builder/query-builder.ts b/src/query-builder/query-builder.ts index e61f43a68..51008b3a1 100644 --- a/src/query-builder/query-builder.ts +++ b/src/query-builder/query-builder.ts @@ -81,6 +81,7 @@ import { ParseContext } from '../parser/parse-context.js' import { DeleteQueryNode } from '../operation-node/delete-query-node.js' import { OnDuplicateKeyNode } from '../operation-node/on-duplicate-key-node.js' import { parseGroupBy } from '../parser/group-by-parser.js' +import { parseUnion, UnionExpression } from '../parser/union-parser.js' /** * The main query builder class. @@ -1508,7 +1509,7 @@ export class QueryBuilder */ values(row: InsertObject): QueryBuilder - values(row: InsertObject[]): QueryBuilder + values(row: ReadonlyArray>): QueryBuilder values(args: InsertObjectOrList): any { assertCanHaveInsertValues(this.#props.queryNode) @@ -2243,6 +2244,56 @@ export class QueryBuilder }) } + /** + * Combines another select query or raw expression to this query using `union`. + * + * The output row type of the combined query must match `this` query. + * + * @xample + * ```ts + * db.selectFrom('person') + * .select(['id', 'first_name as name']) + * .union(db.selectFrom('pet').select(['id', 'name'])) + * .orderBy('name') + * ``` + */ + union(expression: UnionExpression): QueryBuilder { + assertCanHaveUnion(this.#props.queryNode) + + return new QueryBuilder({ + ...this.#props, + queryNode: SelectQueryNode.cloneWithUnion( + this.#props.queryNode, + parseUnion(expression, false) + ), + }) + } + + /** + * Combines another select query or raw expression to this query using `union all`. + * + * The output row type of the combined query must match `this` query. + * + * @xample + * ```ts + * db.selectFrom('person') + * .select(['id', 'first_name as name']) + * .unionAll(db.selectFrom('pet').select(['id', 'name'])) + * .orderBy('name') + * ``` + */ + unionAll(expression: UnionExpression): QueryBuilder { + assertCanHaveUnion(this.#props.queryNode) + + return new QueryBuilder({ + ...this.#props, + queryNode: SelectQueryNode.cloneWithUnion( + this.#props.queryNode, + parseUnion(expression, true) + ), + }) + } + /** * Gives an alias for the query. This method is only useful for sub queries. * @@ -2541,3 +2592,9 @@ function assertCanHaveOffset(node: QueryNode): asserts node is SelectQueryNode { throw new Error('only a select query can have an offset') } } + +function assertCanHaveUnion(node: QueryNode): asserts node is SelectQueryNode { + if (!SelectQueryNode.is(node)) { + throw new Error('only a select query can have an union') + } +} diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index e02685089..22789c225 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -74,6 +74,8 @@ import { ForeignKeyConstraintNode } from '../operation-node/foreign-key-constrai import { ColumnDefinitionNode } from '../operation-node/column-definition-node.js' import { ModifyColumnNode } from '../operation-node/modify-column-node.js' import { OnDuplicateKeyNode } from '../operation-node/on-duplicate-key-node.js' +import { ColumnNode } from '../operation-node/column-node.js' +import { UnionNode } from '../operation-node/union-node.js' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -94,7 +96,7 @@ export class DefaultQueryCompiler return freeze({ sql: this.getSql(), - parameters: this.#parameters, + parameters: [...this.#parameters], }) } @@ -154,6 +156,11 @@ export class DefaultQueryCompiler this.visitNode(node.having) } + if (node.union) { + this.append(' ') + this.compileList(node.union, ' ') + } + if (node.orderBy) { this.append(' ') this.visitNode(node.orderBy) @@ -186,6 +193,14 @@ export class DefaultQueryCompiler this.compileList(node.froms) } + protected override visitSelection(node: SelectionNode): void { + this.visitNode(node.selection) + } + + protected override visitColumn(node: ColumnNode): void { + this.visitNode(node.column) + } + protected compileDistinctOn(selections: ReadonlyArray): void { this.append('distinct on (') this.compileList(selections) @@ -883,6 +898,16 @@ export class DefaultQueryCompiler this.visitNode(node.constraintName) } + protected override visitUnion(node: UnionNode): void { + this.append('union ') + + if (node.all) { + this.append('all ') + } + + this.visitNode(node.union) + } + protected append(str: string): void { this.#sql += str } diff --git a/test/src/union.test.ts b/test/src/union.test.ts new file mode 100644 index 000000000..c9e6d4f33 --- /dev/null +++ b/test/src/union.test.ts @@ -0,0 +1,93 @@ +import { + BUILT_IN_DIALECTS, + clearDatabase, + destroyTest, + initTest, + TestContext, + testSql, + expect, + NOT_SUPPORTED, + TEST_INIT_TIMEOUT, + insertDefaultDataSet, +} from './test-setup.js' + +for (const dialect of BUILT_IN_DIALECTS) { + describe(`${dialect}: union`, () => { + let ctx: TestContext + + before(async function () { + this.timeout(TEST_INIT_TIMEOUT) + ctx = await initTest(dialect) + }) + + beforeEach(async () => { + await insertDefaultDataSet(ctx) + }) + + afterEach(async () => { + await clearDatabase(ctx) + }) + + after(async () => { + await destroyTest(ctx) + }) + + it('should combine two select queries using a union', async () => { + const query = ctx.db + .selectFrom('person') + .select(['id', 'first_name as name']) + .union(ctx.db.selectFrom('pet').select(['id', 'name'])) + .orderBy('name') + + testSql(query, dialect, { + postgres: { + sql: 'select "id", "first_name" as "name" from "person" union (select "id", "name" from "pet") order by "name"', + parameters: [], + }, + mysql: { + sql: 'select `id`, `first_name` as `name` from `person` union (select `id`, `name` from `pet`) order by `name`', + parameters: [], + }, + }) + + const result = await query.execute() + expect(result).to.containSubset([ + { name: 'Arnold' }, + { name: 'Catto' }, + { name: 'Doggo' }, + { name: 'Hammo' }, + { name: 'Jennifer' }, + { name: 'Sylvester' }, + ]) + }) + + it('should combine two select queries using a union all', async () => { + const query = ctx.db + .selectFrom('person') + .select(['id', 'first_name as name']) + .unionAll(ctx.db.selectFrom('pet').select(['id', 'name'])) + .orderBy('name') + + testSql(query, dialect, { + postgres: { + sql: 'select "id", "first_name" as "name" from "person" union all (select "id", "name" from "pet") order by "name"', + parameters: [], + }, + mysql: { + sql: 'select `id`, `first_name` as `name` from `person` union all (select `id`, `name` from `pet`) order by `name`', + parameters: [], + }, + }) + + const result = await query.execute() + expect(result).to.containSubset([ + { name: 'Arnold' }, + { name: 'Catto' }, + { name: 'Doggo' }, + { name: 'Hammo' }, + { name: 'Jennifer' }, + { name: 'Sylvester' }, + ]) + }) + }) +}