Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Set operations (union / intersect / except) support #1218

Merged
merged 36 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
26db589
(Mysql) Initial implementation of Set Operations support
Angelelz Sep 14, 2023
2899623
Initial implementation of Set Operations support for postgres
Angelelz Sep 14, 2023
943945a
Progress on type for only union in Mysql. More work needed
Angelelz Sep 14, 2023
06ed8de
Added generic types to mysql Set Operation to generate type errors if…
Angelelz Sep 15, 2023
273a884
Added support to pass multiple arguments to the standalone set operat…
Angelelz Sep 16, 2023
4c2f929
Added function support to the set operators methods in mysql
Angelelz Sep 17, 2023
c842f4d
(Mysql) fixed set operator function accepting only one parameter
Angelelz Sep 17, 2023
a21e386
(mysql) added type tests for the set operators
Angelelz Sep 17, 2023
8def9d6
[MySql] fixed type issue with the rest parameters passed to the set o…
Angelelz Sep 20, 2023
907605a
Added test for mysql implementation of set operators
Angelelz Sep 20, 2023
3893876
[Pg] Completed implementation of set operations
Angelelz Sep 21, 2023
87479a7
[Pg] Added type tests for the set operators
Angelelz Sep 21, 2023
4e371fb
[Pg] Added integration tests for the set operators
Angelelz Sep 21, 2023
35a525d
[SQLite] Completed implementation of set operators
Angelelz Sep 21, 2023
e555c09
[SQLite] Added type tests
Angelelz Sep 21, 2023
14f996b
[SQLite] Added integration tests for all cases
Angelelz Sep 21, 2023
8808a1f
[All] Completed implementation with requested changes
Angelelz Sep 23, 2023
9281dc2
[All] code cleanup and eslint rules fix
Angelelz Sep 24, 2023
7618db5
[All] Added offset
Angelelz Sep 24, 2023
85be035
[All] added `.as()` method to create subqueries from set operations. …
Angelelz Sep 24, 2023
256a292
[All] Moved the query construction logic to the each dialect
Angelelz Sep 24, 2023
1bcd418
Merge branch 'beta' into feat-union
Angelelz Oct 6, 2023
d38f3b4
[All] Resolved merge conflicts. Need to fix types
Angelelz Oct 6, 2023
152aa37
[SQLite] fixed types, addressed comments and added new type tests
Angelelz Oct 14, 2023
3c925d6
[SQLite] deleted commented code and improved error readability
Angelelz Oct 14, 2023
e763b29
[MySql] fixed types, addressed comments and added new type tests
Angelelz Oct 14, 2023
0b1d5ae
[Pg] fixed types, addressed comments and added new type tests, fixed …
Angelelz Oct 15, 2023
f4794c6
Merge branch 'beta' into feat-union
Angelelz Oct 15, 2023
57889e1
[All] fix typescript errors
Angelelz Oct 15, 2023
8fcd2d4
[MySql] Simplification by removing intermediary clases and making the…
Angelelz Oct 20, 2023
c8117b1
[Pg] Simplification by removing intermediary clases and making the qu…
Angelelz Oct 20, 2023
c7846b9
[SQLite] Simplification by removing intermediary clases and making th…
Angelelz Oct 20, 2023
1128c12
[MySql] Types simplification from review and fixed tests for western …
Angelelz Oct 31, 2023
99b2a3c
[Pg] Types simplification from review
Angelelz Oct 31, 2023
af5387f
[SQLite] Types simplification from review
Angelelz Oct 31, 2023
a83ea7d
[MySql] fixed failing tests based on query order
Angelelz Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions drizzle-orm/src/mysql-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Subquery, SubqueryConfig } from '~/subquery.ts';
import { getTableName, Table } from '~/table.ts';
import { orderSelectedFields, type UpdateSet } from '~/utils.ts';
import { View } from '~/view.ts';
import { DrizzleError, ViewBaseConfig } from '../index.ts';
import { DrizzleError, type Name, ViewBaseConfig } from '../index.ts';
import { MySqlColumn } from './columns/common.ts';
import type { MySqlDeleteConfig } from './query-builders/delete.ts';
import type { MySqlInsertConfig } from './query-builders/insert.ts';
Expand Down Expand Up @@ -204,6 +204,7 @@ export class MySqlDialect {
offset,
lockingClause,
distinct,
setOperators,
}: MySqlSelectConfig,
): SQL {
const fieldsList = fieldsFlat ?? orderSelectedFields<MySqlColumn>(fields);
Expand Down Expand Up @@ -331,7 +332,75 @@ export class MySqlDialect {
}
}

return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`;
const finalQuery =
sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`;

if (setOperators.length > 0) {
return this.buildSetOperations(finalQuery, setOperators);
}

return finalQuery;
}

buildSetOperations(leftSelect: SQL, setOperators: MySqlSelectConfig['setOperators']): SQL {
const [setOperator, ...rest] = setOperators;

if (!setOperator) {
throw new Error('Cannot pass undefined values to any set operator');
}

if (rest.length === 0) {
return this.buildSetOperationQuery({ leftSelect, setOperator });
}

// Some recursive magic here
return this.buildSetOperations(
this.buildSetOperationQuery({ leftSelect, setOperator }),
rest,
);
}

buildSetOperationQuery({
leftSelect,
setOperator: { type, isAll, rightSelect, limit, orderBy, offset },
}: { leftSelect: SQL; setOperator: MySqlSelectConfig['setOperators'][number] }): SQL {
const leftChunk = sql`(${leftSelect.getSQL()}) `;
const rightChunk = sql`(${rightSelect.getSQL()})`;

let orderBySql;
if (orderBy && orderBy.length > 0) {
const orderByValues: (SQL<unknown> | Name)[] = [];

// The next bit is necessary because the sql operator replaces ${table.column} with `table`.`column`
// which is invalid MySql syntax, Table from one of the SELECTs cannot be used in global ORDER clause
for (const orderByUnit of orderBy) {
if (is(orderByUnit, MySqlColumn)) {
orderByValues.push(sql.identifier(orderByUnit.name));
} else if (is(orderByUnit, SQL)) {
for (let i = 0; i < orderByUnit.queryChunks.length; i++) {
Angelelz marked this conversation as resolved.
Show resolved Hide resolved
const chunk = orderByUnit.queryChunks[i];

if (is(chunk, MySqlColumn)) {
orderByUnit.queryChunks[i] = sql.identifier(chunk.name);
}
}

orderByValues.push(sql`${orderByUnit}`);
} else {
orderByValues.push(sql`${orderByUnit}`);
}
}

orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `;
}

const limitSql = limit ? sql` limit ${limit}` : undefined;

const operatorChunk = sql.raw(`${type} ${isAll ? 'all ' : ''}`);

const offsetSql = offset ? sql` offset ${offset}` : undefined;

return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`;
}

buildInsertQuery({ table, values, ignore, onConflict }: MySqlInsertConfig): SQL {
Expand Down Expand Up @@ -631,6 +700,7 @@ export class MySqlDialect {
where,
limit,
offset,
setOperators: [],
});

where = undefined;
Expand All @@ -653,6 +723,7 @@ export class MySqlDialect {
limit,
offset,
orderBy,
setOperators: [],
});
} else {
result = this.buildSelectQuery({
Expand All @@ -667,6 +738,7 @@ export class MySqlDialect {
limit,
offset,
orderBy,
setOperators: [],
});
}

Expand Down Expand Up @@ -921,6 +993,7 @@ export class MySqlDialect {
where,
limit,
offset,
setOperators: [],
});

where = undefined;
Expand All @@ -942,6 +1015,7 @@ export class MySqlDialect {
limit,
offset,
orderBy,
setOperators: [],
});
} else {
result = this.buildSelectQuery({
Expand All @@ -955,6 +1029,7 @@ export class MySqlDialect {
limit,
offset,
orderBy,
setOperators: [],
});
}

Expand Down
139 changes: 132 additions & 7 deletions drizzle-orm/src/mysql-core/query-builders/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,34 @@ import type {
JoinType,
SelectMode,
SelectResult,
SetOperator,
} from '~/query-builders/select.types.ts';
import { QueryPromise } from '~/query-promise.ts';
import { type Query, SQL } from '~/sql/index.ts';
import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery.ts';
import { Table } from '~/table.ts';
import { applyMixins, getTableColumns, getTableLikeName, type ValueOrArray } from '~/utils.ts';
import { applyMixins, getTableColumns, getTableLikeName, haveSameKeys, type ValueOrArray } from '~/utils.ts';
import { orderSelectedFields } from '~/utils.ts';
import { ViewBaseConfig } from '~/view-common.ts';
import { type ColumnsSelection, View } from '~/view.ts';
import type {
AnyMySqlSelect,
CreateMySqlSelectFromBuilderMode,
GetMySqlSetOperators,
LockConfig,
LockStrength,
MySqlCreateSetOperatorFn,
MySqlJoinFn,
MySqlSelectConfig,
MySqlSelectDynamic,
MySqlSelectHKT,
MySqlSelectHKTBase,
MySqlSelectPrepare,
MySqlSelectWithout,
MySqlSetOperatorExcludedMethods,
MySqlSetOperatorWithResult,
SelectedFields,
SetOperatorRightSelect,
} from './select.types.ts';

export class MySqlSelectBuilder<
Expand Down Expand Up @@ -121,7 +128,7 @@ export abstract class MySqlSelectQueryBuilderBase<
: {},
TDynamic extends boolean = false,
TExcludedMethods extends string = never,
TResult = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
> extends TypedQueryBuilder<TSelectedFields, TResult> {
static readonly [entityKind]: string = 'MySqlSelectQueryBuilder';
Expand Down Expand Up @@ -164,6 +171,7 @@ export abstract class MySqlSelectQueryBuilderBase<
table,
fields: { ...fields },
distinct,
setOperators: [],
};
this.isPartialSelect = isPartialSelect;
this.session = session;
Expand Down Expand Up @@ -260,6 +268,61 @@ export abstract class MySqlSelectQueryBuilderBase<

fullJoin = this.createJoin('full');

private createSetOperator(
type: SetOperator,
isAll: boolean,
): <TValue extends MySqlSetOperatorWithResult<TResult>>(
rightSelection:
| ((setOperators: GetMySqlSetOperators) => SetOperatorRightSelect<TValue, TResult>)
| SetOperatorRightSelect<TValue, TResult>,
) => MySqlSelectWithout<
this,
TDynamic,
MySqlSetOperatorExcludedMethods,
true
> {
return (rightSelection) => {
const rightSelect = (typeof rightSelection === 'function'
? rightSelection(getMySqlSetOperators())
: rightSelection) as TypedQueryBuilder<
any,
TResult
>;

if (!haveSameKeys(this.getSelectedFields(), rightSelect.getSelectedFields())) {
throw new Error(
dankochetov marked this conversation as resolved.
Show resolved Hide resolved
'Set operator error (union / intersect / except): selected fields are not the same or are in a different order',
);
}

this.config.setOperators.push({ type, isAll, rightSelect });
return this as any;
};
}

union = this.createSetOperator('union', false);

unionAll = this.createSetOperator('union', true);

intersect = this.createSetOperator('intersect', false);

intersectAll = this.createSetOperator('intersect', true);

except = this.createSetOperator('except', false);

exceptAll = this.createSetOperator('except', true);

/** @internal */
addSetOperators(setOperators: MySqlSelectConfig['setOperators']): MySqlSelectWithout<
this,
TDynamic,
MySqlSetOperatorExcludedMethods,
true
> {
this.config.setOperators.push(...setOperators);
return this as any;
}

where(
where: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined,
): MySqlSelectWithout<this, TDynamic, 'where'> {
Expand Down Expand Up @@ -329,20 +392,41 @@ export abstract class MySqlSelectQueryBuilderBase<
new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }),
) as TSelection,
);
this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];

const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];

if (this.config.setOperators.length > 0) {
this.config.setOperators.at(-1)!.orderBy = orderByArray;
} else {
this.config.orderBy = orderByArray;
}
} else {
this.config.orderBy = columns as (MySqlColumn | SQL | SQL.Aliased)[];
const orderByArray = columns as (MySqlColumn | SQL | SQL.Aliased)[];

if (this.config.setOperators.length > 0) {
this.config.setOperators.at(-1)!.orderBy = orderByArray;
} else {
this.config.orderBy = orderByArray;
}
}
return this as any;
}

limit(limit: number): MySqlSelectWithout<this, TDynamic, 'limit'> {
this.config.limit = limit;
if (this.config.setOperators.length > 0) {
this.config.setOperators.at(-1)!.limit = limit;
} else {
this.config.limit = limit;
}
return this as any;
}

offset(offset: number): MySqlSelectWithout<this, TDynamic, 'offset'> {
this.config.offset = offset;
if (this.config.setOperators.length > 0) {
this.config.setOperators.at(-1)!.offset = offset;
} else {
this.config.offset = offset;
}
return this as any;
}

Expand Down Expand Up @@ -392,7 +476,7 @@ export interface MySqlSelectBase<
: {},
TDynamic extends boolean = false,
TExcludedMethods extends string = never,
TResult = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
> extends
MySqlSelectQueryBuilderBase<
Expand Down Expand Up @@ -463,3 +547,44 @@ export class MySqlSelectBase<
}

applyMixins(MySqlSelectBase, [QueryPromise]);

function createSetOperator(type: SetOperator, isAll: boolean): MySqlCreateSetOperatorFn {
return (leftSelect, rightSelect, ...restSelects) => {
const setOperators = [rightSelect, ...restSelects].map((select) => ({
type,
isAll,
rightSelect: select as AnyMySqlSelect,
}));

for (const setOperator of setOperators) {
if (!haveSameKeys((leftSelect as any).getSelectedFields(), setOperator.rightSelect.getSelectedFields())) {
throw new Error(
'Set operator error (union / intersect / except): selected fields are not the same or are in a different order',
);
}
}

return (leftSelect as AnyMySqlSelect).addSetOperators(setOperators) as any;
};
}

const getMySqlSetOperators = () => ({
union,
unionAll,
intersect,
intersectAll,
except,
exceptAll,
});

export const union = createSetOperator('union', false);

export const unionAll = createSetOperator('union', true);

export const intersect = createSetOperator('intersect', false);

export const intersectAll = createSetOperator('intersect', true);

export const except = createSetOperator('except', false);

export const exceptAll = createSetOperator('except', true);
Loading