From 26db5896896f0f12e2176d2964171a69234a524e Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 13 Sep 2023 20:33:52 -0400 Subject: [PATCH 01/34] (Mysql) Initial implementation of Set Operations support Added a new file set-operators.ts with a SetOperatorQueryBuilder class and a SetOperator class The MySqlSelectBuilder now extends the SetOperatorQueryBuilder to inherit the set operator methods Added the export for the set operation functions --- .../src/mysql-core/query-builders/index.ts | 1 + .../src/mysql-core/query-builders/select.ts | 11 +- .../query-builders/set-operators.ts | 536 ++++++++++++++++++ 3 files changed, 544 insertions(+), 4 deletions(-) create mode 100644 drizzle-orm/src/mysql-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/mysql-core/query-builders/index.ts b/drizzle-orm/src/mysql-core/query-builders/index.ts index 16f0e1d4d..58d909c22 100644 --- a/drizzle-orm/src/mysql-core/query-builders/index.ts +++ b/drizzle-orm/src/mysql-core/query-builders/index.ts @@ -3,4 +3,5 @@ export * from './insert.ts'; export * from './query-builder.ts'; export * from './select.ts'; export * from './select.types.ts'; +export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index c93d97468..4e73b24d8 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -10,7 +10,6 @@ import type { import type { SubqueryWithSelection } from '~/mysql-core/subquery.ts'; import type { MySqlTable } from '~/mysql-core/table.ts'; import { MySqlViewBase } from '~/mysql-core/view.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -37,6 +36,7 @@ import type { MySqlSelectQueryBuilderHKT, SelectedFields, } from './select.types.ts'; +import { MySqlSetOperatorBuilder } from './set-operators.ts'; type CreateMySqlSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', @@ -128,9 +128,12 @@ export abstract class MySqlSelectQueryBuilder< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] +> extends MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + PreparedQueryHKTBase, + TNullabilityMap > { static readonly [entityKind]: string = 'MySqlSelectQueryBuilder'; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts new file mode 100644 index 000000000..317ff0651 --- /dev/null +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -0,0 +1,536 @@ +import { entityKind, is } from '~/entity.ts'; +import { + applyMixins, + orderSelectedFields, + type Placeholder, + type Query, + QueryPromise, + SelectionProxyHandler, + SQL, + sql, + type ValueOrArray, +} from '~/index.ts'; +import type { + MySqlSession, + PreparedQueryConfig, + PreparedQueryHKTBase, + PreparedQueryKind, +} from '~/mysql-core/session.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + BuildSubquerySelection, + JoinNullability, + SelectMode, + SelectResult, +} from '~/query-builders/select.types.ts'; +import { type ColumnsSelection } from '~/view.ts'; +import { MySqlColumn } from '../columns/common.ts'; +import type { MySqlDialect } from '../dialect.ts'; + +type SetOperator = 'union' | 'intersect' | 'except'; + +export interface MySqlSetOperatorBuilder< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends + TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] + >, + QueryPromise[]> +{} + +export abstract class MySqlSetOperatorBuilder< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] +> { + static readonly [entityKind]: string = 'MySqlSetOperatorBuilder'; + + protected abstract joinsNotNullableMap: Record; + protected abstract config: { + fields: Record; + limit?: number | Placeholder; + orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + }; + /* @internal */ + abstract readonly session: MySqlSession | undefined; + protected abstract dialect: MySqlDialect; + + /** @internal */ + getSetOperatorConfig() { + return { + session: this.session, + dialect: this.dialect, + joinsNotNullableMap: this.joinsNotNullableMap, + fields: this.config.fields, + }; + } + + union( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('union', false, this, rightSelect) as any; + } + + unionAll( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('union', true, this, rightSelect) as any; + } + + intersect( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('intersect', false, this, rightSelect) as any; + } + + intersectAll( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('intersect', true, this, rightSelect) as any; + } + + except( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('except', false, this, rightSelect) as any; + } + + exceptAll( + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT + > { + return new MySqlSetOperator('except', true, this, rightSelect) as any; + } + + abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; + abstract orderBy(...columns: (MySqlColumn | SQL | SQL.Aliased)[]): this; + + abstract limit(limit: number): this; +} + +export class MySqlSetOperator< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + static readonly [entityKind]: string = 'MySqlSetOperator'; + + protected joinsNotNullableMap: Record; + protected config: { + fields: Record; + limit?: number | Placeholder; + orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + }; + /* @internal */ + readonly session: MySqlSession | undefined; + protected dialect: MySqlDialect; + + constructor( + private operator: SetOperator, + private isAll: boolean, + private leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + private rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + ) { + super(); + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + this.session = session; + this.dialect = dialect; + this.joinsNotNullableMap = joinsNotNullableMap; + this.config = { + fields, + }; + } + + orderBy( + ...columns: + | [(aliases: TSelection) => ValueOrArray] + | (MySqlColumn | SQL | SQL.Aliased)[] + ) { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as TSelection, + ); + this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; + } else { + this.config.orderBy = columns as (MySqlColumn | SQL | SQL.Aliased)[]; + } + return this; + } + + limit(limit: number) { + this.config.limit = limit; + return this; + } + + override getSQL(): SQL { + const leftChunk = sql`(${this.leftSelect.getSQL()}) `; + const rightChunk = sql`(${this.rightSelect.getSQL()})`; + + let orderBySql; + if (this.config.orderBy && this.config.orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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 orderBy of this.config.orderBy) { + if (is(orderBy, MySqlColumn)) { + orderByValues.push(sql.raw(orderBy.name)); + } else if (is(orderBy, SQL)) { + for (let i = 0; i < orderBy.queryChunks.length; i++) { + const chunk = orderBy.queryChunks[i]; + + if (is(chunk, MySqlColumn)) { + orderBy.queryChunks[i] = sql.raw(chunk.name); + } + } + + orderByValues.push(sql`${orderBy}`); + } else { + orderByValues.push(sql`${orderBy}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; + } + + const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; + + const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + prepare() { + if (!this.session) { + throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); + } + const fieldsList = orderSelectedFields(this.config.fields); + const query = this.session.prepareQuery< + PreparedQueryConfig & { execute: SelectResult[] }, + TPreparedQueryHKT + >(this.dialect.sqlToQuery(this.getSQL()), fieldsList); + query.joinsNotNullableMap = this.joinsNotNullableMap; + return query as PreparedQueryKind< + TPreparedQueryHKT, + PreparedQueryConfig & { + execute: SelectResult[]; + iterator: SelectResult; + }, + true + >; + } + + execute = ((placeholderValues) => { + return this.prepare().execute(placeholderValues); + }) as ReturnType['execute']; + + private createIterator = (): ReturnType['iterator'] => { + const self = this; + return async function*(placeholderValues) { + yield* self.prepare().iterator(placeholderValues); + }; + }; + + iterator = this.createIterator(); +} + +applyMixins(MySqlSetOperatorBuilder, [QueryPromise]); + +export function union< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('union', false, leftSelect, rightSelect); +} + +export function unionAll< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('union', true, leftSelect, rightSelect); +} + +export function intersect< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); +} + +export function intersectAll< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); +} + +export function except< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('except', false, leftSelect, rightSelect); +} + +export function exceptAll< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +>( + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, + rightSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap + >, +): MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap +> { + return new MySqlSetOperator('except', true, leftSelect, rightSelect); +} From 2899623d49417eb3580ba8fa6bb1180da27f2935 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 13 Sep 2023 20:36:48 -0400 Subject: [PATCH 02/34] Initial implementation of Set Operations support for postgres This implementation will likely change with the same pattern used in the Mysql implementation --- .../src/pg-core/query-builders/index.ts | 1 + .../src/pg-core/query-builders/select.ts | 119 +++++++ .../pg-core/query-builders/set-operators.ts | 326 ++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 drizzle-orm/src/pg-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/pg-core/query-builders/index.ts b/drizzle-orm/src/pg-core/query-builders/index.ts index c4821e51d..fb3dd5931 100644 --- a/drizzle-orm/src/pg-core/query-builders/index.ts +++ b/drizzle-orm/src/pg-core/query-builders/index.ts @@ -4,4 +4,5 @@ export * from './query-builder.ts'; export * from './refresh-materialized-view.ts'; export * from './select.ts'; export * from './select.types.ts'; +export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 7179a43eb..734de066e 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -33,6 +33,7 @@ import type { PgSelectQueryBuilderHKT, SelectedFields, } from './select.types.ts'; +import { PgSetOperator } from './set-operators.ts'; type CreatePgSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', @@ -179,6 +180,16 @@ export abstract class PgSelectQueryBuilder< this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {}; } + /** @internal */ + getSetOperatorConfig() { + return { + session: this.session, + dialect: this.dialect, + joinsNotNullableMap: this.joinsNotNullableMap, + fields: this.config.fields, + }; + } + private createJoin( joinType: TJoinType, ): JoinFn { @@ -314,6 +325,114 @@ export abstract class PgSelectQueryBuilder< return this; } + union< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('union', false, this as any, rightSelect); + } + + unionAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('union', true, this as any, rightSelect); + } + + intersect< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('intersect', false, this as any, rightSelect); + } + + intersectAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('intersect', true, this as any, rightSelect); + } + + except< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('except', false, this as any, rightSelect); + } + + exceptAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + >( + rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode + > { + return new PgSetOperator('except', true, this as any, rightSelect); + } + /** * Sets the HAVING clause of this query, which often * used with GROUP BY and filters rows after they've been diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts new file mode 100644 index 000000000..667a11d85 --- /dev/null +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -0,0 +1,326 @@ +import { entityKind, is } from '~/entity.ts'; +import { + applyMixins, + orderSelectedFields, + type Placeholder, + type Query, + QueryPromise, + SelectionProxyHandler, + SQL, + sql, + type ValueOrArray, +} from '~/index.ts'; +import type { PgSession, PreparedQuery, PreparedQueryConfig } from '~/pg-core/session.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + BuildSubquerySelection, + JoinNullability, + SelectMode, + SelectResult, +} from '~/query-builders/select.types.ts'; +import { tracer } from '~/tracing.ts'; +import { type ColumnsSelection } from '~/view.ts'; +import { PgColumn } from '../columns/common.ts'; +import type { PgDialect } from '../dialect.ts'; +import type { PgSelect, PgSelectQueryBuilder } from './select.ts'; +import type { PgSelectHKTBase, PgSelectQueryBuilderHKT } from './select.types.ts'; + +type SetOperator = 'union' | 'intersect' | 'except'; + +export interface PgSetOperator< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends + TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] + >, + QueryPromise[]> +{} + +export class PgSetOperator< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] +> { + static readonly [entityKind]: string = 'PgSetOperator'; + + private session: PgSession | undefined; + private dialect: PgDialect; + private config: { + fields: Record; + joinsNotNullableMap: Record; + limit?: number | Placeholder; + orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + }; + + constructor( + private operator: SetOperator, + private isAll: boolean, + private leftSelect: + | PgSelect + | PgSelectQueryBuilder, + private rightSelect: + | PgSelect + | PgSelectQueryBuilder, + ) { + super(); + + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + this.session = session; + this.dialect = dialect; + this.config = { + fields, + joinsNotNullableMap, + }; + } + + orderBy(builder: (aliases: TSelection) => ValueOrArray): this; + orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; + orderBy( + ...columns: + | [(aliases: TSelection) => ValueOrArray] + | (PgColumn | SQL | SQL.Aliased)[] + ) { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as TSelection, + ); + this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; + } else { + this.config.orderBy = columns as (PgColumn | SQL | SQL.Aliased)[]; + } + return this; + } + + limit(limit: number) { + this.config.limit = limit; + return this; + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + override getSQL(): SQL { + const leftChunk = sql`(${this.leftSelect.getSQL()}) `; + const rightChunk = sql`(${this.rightSelect.getSQL()})`; + + let orderBySql; + if (this.config.orderBy && this.config.orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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 orderBy of this.config.orderBy) { + if (is(orderBy, PgColumn)) { + orderByValues.push(sql.raw(orderBy.name)); + } else if (is(orderBy, SQL)) { + for (let i = 0; i < orderBy.queryChunks.length; i++) { + const chunk = orderBy.queryChunks[i]; + + if (is(chunk, PgColumn)) { + orderBy.queryChunks[i] = sql.raw(chunk.name); + } + } + + orderByValues.push(sql`${orderBy}`); + } else { + orderByValues.push(sql`${orderBy}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; + } + + const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; + + const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + } + + private _prepare(name?: string): PreparedQuery< + PreparedQueryConfig & { + execute: SelectResult[]; + } + > { + const { session, config: { fields, joinsNotNullableMap }, dialect } = this; + if (!session) { + throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); + } + return tracer.startActiveSpan('drizzle.prepareQuery', () => { + const fieldsList = orderSelectedFields(fields); + const query = session.prepareQuery< + PreparedQueryConfig & { execute: SelectResult[] } + >(dialect.sqlToQuery(this.getSQL()), fieldsList, name); + query.joinsNotNullableMap = joinsNotNullableMap; + return query; + }); + } + + /** + * Create a prepared statement for this query. This allows + * the database to remember this query for the given session + * and call it by name, rather than specifying the full query. + * + * {@link https://www.postgresql.org/docs/current/sql-prepare.html|Postgres prepare documentation} + */ + prepare(name: string): PreparedQuery< + PreparedQueryConfig & { + execute: SelectResult[]; + } + > { + return this._prepare(name); + } + + execute: ReturnType['execute'] = (placeholderValues) => { + return tracer.startActiveSpan('drizzle.operation', () => { + return this._prepare().execute(placeholderValues); + }); + }; +} + +applyMixins(PgSetOperator, [QueryPromise]); + +export function union< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('union', false, leftSelect, rightSelect); +} + +export function unionAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('union', true, leftSelect, rightSelect); +} + +export function intersect< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('intersect', false, leftSelect, rightSelect); +} + +export function intersectAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('intersect', true, leftSelect, rightSelect); +} + +export function except< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('except', false, leftSelect, rightSelect); +} + +export function exceptAll< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, +>( + leftSelect: + | PgSelect + | PgSelectQueryBuilder, + rightSelect: + | PgSelect + | PgSelectQueryBuilder, +): PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode +> { + return new PgSetOperator('except', true, leftSelect, rightSelect); +} From 943945a1685ab8964a106cb34bbb4737bcfec1ea Mon Sep 17 00:00:00 2001 From: Angelelz Date: Thu, 14 Sep 2023 00:04:41 -0400 Subject: [PATCH 03/34] Progress on type for only union in Mysql. More work needed --- .../query-builders/set-operators.ts | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 317ff0651..3024adbc3 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -1,6 +1,7 @@ import { entityKind, is } from '~/entity.ts'; import { applyMixins, + type KnownKeysOnly, orderSelectedFields, type Placeholder, type Query, @@ -77,24 +78,40 @@ export abstract class MySqlSetOperatorBuilder< fields: this.config.fields, }; } - - union( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + union>( + rightSelect: TValue extends TypedQueryBuilder + ? R extends SelectResult[] + ? KnownKeysOnly> extends R[number] + ? TypedQueryBuilder[]> + : never + : never + : never, ): MySqlSetOperator< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('union', false, this, rightSelect) as any; + return new MySqlSetOperator('union', false, this, rightSelect as any); } + // union>( + // rightSelect: TValue extends TypedQueryBuilder + // ? R extends SelectResult[] + // ? TypedQueryBuilder[]> + // : 'first' + // : 'second', + // ): MySqlSetOperator< + // TTableName, + // TSelection, + // TSelectMode, + // TPreparedQueryHKT, + // TNullabilityMap + // > { + // return new MySqlSetOperator('union', false, this, rightSelect as any); + // } + unionAll( rightSelect: MySqlSetOperatorBuilder< TTableName, @@ -107,9 +124,10 @@ export abstract class MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('union', true, this, rightSelect) as any; + return new MySqlSetOperator('union', true, this, rightSelect); } intersect( @@ -124,9 +142,10 @@ export abstract class MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('intersect', false, this, rightSelect) as any; + return new MySqlSetOperator('intersect', false, this, rightSelect); } intersectAll( @@ -141,9 +160,10 @@ export abstract class MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('intersect', true, this, rightSelect) as any; + return new MySqlSetOperator('intersect', true, this, rightSelect); } except( @@ -158,9 +178,10 @@ export abstract class MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('except', false, this, rightSelect) as any; + return new MySqlSetOperator('except', false, this, rightSelect); } exceptAll( @@ -175,9 +196,10 @@ export abstract class MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, - TPreparedQueryHKT + TPreparedQueryHKT, + TNullabilityMap > { - return new MySqlSetOperator('except', true, this, rightSelect) as any; + return new MySqlSetOperator('except', true, this, rightSelect); } abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; From 06ed8de15251f0cbe0e7902f477961deaed4238e Mon Sep 17 00:00:00 2001 From: Angelelz Date: Thu, 14 Sep 2023 23:14:02 -0400 Subject: [PATCH 04/34] Added generic types to mysql Set Operation to generate type errors if the user attempts to select different types on both sides of the set operator --- .../query-builders/set-operators.ts | 281 +++++++----------- drizzle-orm/src/utils.ts | 6 + 2 files changed, 118 insertions(+), 169 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 3024adbc3..450d8e3c0 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -1,7 +1,6 @@ import { entityKind, is } from '~/entity.ts'; import { applyMixins, - type KnownKeysOnly, orderSelectedFields, type Placeholder, type Query, @@ -9,6 +8,7 @@ import { SelectionProxyHandler, SQL, sql, + type ValidateShape, type ValueOrArray, } from '~/index.ts'; import type { @@ -29,6 +29,7 @@ import { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; type SetOperator = 'union' | 'intersect' | 'except'; +// type NewValidate = T extends P ? P extends T ? TResult : 'one' : 'dos'; export interface MySqlSetOperatorBuilder< TTableName extends string | undefined, @@ -78,127 +79,75 @@ export abstract class MySqlSetOperatorBuilder< fields: this.config.fields, }; } - union>( - rightSelect: TValue extends TypedQueryBuilder - ? R extends SelectResult[] - ? KnownKeysOnly> extends R[number] - ? TypedQueryBuilder[]> - : never - : never - : never, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { - return new MySqlSetOperator('union', false, this, rightSelect as any); + union[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { + return new MySqlSetOperator('union', false, this, rightSelect); } - // union>( - // rightSelect: TValue extends TypedQueryBuilder - // ? R extends SelectResult[] - // ? TypedQueryBuilder[]> - // : 'first' - // : 'second', - // ): MySqlSetOperator< - // TTableName, - // TSelection, - // TSelectMode, - // TPreparedQueryHKT, - // TNullabilityMap - // > { - // return new MySqlSetOperator('union', false, this, rightSelect as any); - // } - - unionAll( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { + unionAll[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { return new MySqlSetOperator('union', true, this, rightSelect); } - intersect( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { + intersect[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { return new MySqlSetOperator('intersect', false, this, rightSelect); } - intersectAll( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { + intersectAll[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { return new MySqlSetOperator('intersect', true, this, rightSelect); } - except( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { + except[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { return new MySqlSetOperator('except', false, this, rightSelect); } - exceptAll( - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - ): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - > { + exceptAll[]>>( + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, + ): MySqlSetOperator { return new MySqlSetOperator('except', true, this, rightSelect); } @@ -244,13 +193,7 @@ export class MySqlSetOperator< TPreparedQueryHKT, TNullabilityMap >, - private rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + private rightSelect: TypedQueryBuilder[]>, ) { super(); const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); @@ -370,8 +313,8 @@ export function union< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -380,13 +323,13 @@ export function union< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, @@ -402,8 +345,8 @@ export function unionAll< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -412,13 +355,13 @@ export function unionAll< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, @@ -434,8 +377,8 @@ export function intersect< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -444,13 +387,13 @@ export function intersect< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, @@ -466,8 +409,8 @@ export function intersectAll< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -476,13 +419,13 @@ export function intersectAll< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, @@ -498,8 +441,8 @@ export function except< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -508,13 +451,13 @@ export function except< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, @@ -530,8 +473,8 @@ export function exceptAll< TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder< TTableName, @@ -540,13 +483,13 @@ export function exceptAll< TPreparedQueryHKT, TNullabilityMap >, - rightSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, + rightSelect: TValue extends + MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + MySqlSetOperatorBuilder + > + : TValue, ): MySqlSetOperator< TTableName, TSelection, diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index dc71112eb..16c92cfc1 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -194,6 +194,12 @@ export interface DrizzleConfig = Record< schema?: TSchema; } +export type ValidateShape = T extends ValidShape + ? Exclude extends never ? TResult + : Exclude extends string ? `Invalid key(s): ${Exclude}` + : 'uno' + : 'dos'; + export type KnownKeysOnly = { [K in keyof T]: K extends keyof U ? T[K] : never; }; From 273a88429bafce29850b8322b7c6d342ac4f56fa Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 15 Sep 2023 23:56:40 -0400 Subject: [PATCH 05/34] Added support to pass multiple arguments to the standalone set operator functions. The types might need some more work --- .../query-builders/set-operators.ts | 277 +++++++----------- 1 file changed, 102 insertions(+), 175 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 450d8e3c0..688225c22 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -29,7 +29,18 @@ import { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; type SetOperator = 'union' | 'intersect' | 'except'; -// type NewValidate = T extends P ? P extends T ? TResult : 'one' : 'dos'; + +type SetOperatorRightSelect< + TValue extends TypedQueryBuilder[]>, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, +> = TValue extends MySqlSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + TypedQueryBuilder[]> + > + : TValue; export interface MySqlSetOperatorBuilder< TTableName extends string | undefined, @@ -80,73 +91,37 @@ export abstract class MySqlSetOperatorBuilder< }; } union[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('union', false, this, rightSelect); } unionAll[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('union', true, this, rightSelect); } intersect[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('intersect', false, this, rightSelect); } intersectAll[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('intersect', true, this, rightSelect); } except[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('except', false, this, rightSelect); } exceptAll[]>>( - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, + rightSelect: SetOperatorRightSelect, ): MySqlSetOperator { return new MySqlSetOperator('except', true, this, rightSelect); } @@ -316,28 +291,20 @@ export function union< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('union', false, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('union', false, leftSelect, rightSelect); + } + + return union(new MySqlSetOperator('union', false, leftSelect, rightSelect!), ...rest); } export function unionAll< @@ -348,28 +315,20 @@ export function unionAll< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('union', true, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('union', true, leftSelect, rightSelect); + } + + return unionAll(new MySqlSetOperator('union', true, leftSelect, rightSelect!), ...rest); } export function intersect< @@ -380,28 +339,20 @@ export function intersect< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); + } + + return intersect(new MySqlSetOperator('intersect', false, leftSelect, rightSelect!), ...rest); } export function intersectAll< @@ -412,28 +363,20 @@ export function intersectAll< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); + } + + return intersectAll(new MySqlSetOperator('intersect', true, leftSelect, rightSelect!), ...rest); } export function except< @@ -444,28 +387,20 @@ export function except< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('except', false, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('except', false, leftSelect, rightSelect); + } + + return except(new MySqlSetOperator('except', false, leftSelect, rightSelect!), ...rest); } export function exceptAll< @@ -476,26 +411,18 @@ export function exceptAll< TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, >( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap - >, - rightSelect: TValue extends - MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - MySqlSetOperatorBuilder - > - : TValue, -): MySqlSetOperator< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap -> { - return new MySqlSetOperator('except', true, leftSelect, rightSelect); + leftSelect: MySqlSetOperatorBuilder, + ...rightSelects: SetOperatorRightSelect[] +): MySqlSetOperator { + if (rightSelects.length < 1) { + throw new Error('This operator requires at least two arguments'); + } + + const [rightSelect, ...rest] = rightSelects; + + if (rightSelect && rest.length === 0) { + return new MySqlSetOperator('except', false, leftSelect, rightSelect); + } + + return exceptAll(new MySqlSetOperator('except', false, leftSelect, rightSelect!), ...rest); } From 4c2f929418c80516efc1751a3a824e4dd7cd76a3 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sun, 17 Sep 2023 01:06:13 -0400 Subject: [PATCH 06/34] Added function support to the set operators methods in mysql --- .../query-builders/set-operators.ts | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 688225c22..af0b37152 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -30,6 +30,19 @@ import type { MySqlDialect } from '../dialect.ts'; type SetOperator = 'union' | 'intersect' | 'except'; +const getMySqlSetOperators = () => { + return { + union, + unionAll, + intersect, + intersectAll, + except, + exceptAll, + }; +}; + +type MySqlSetOperators = ReturnType; + type SetOperatorRightSelect< TValue extends TypedQueryBuilder[]>, TSelection extends ColumnsSelection, @@ -91,39 +104,63 @@ export abstract class MySqlSetOperatorBuilder< }; } union[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('union', false, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('union', false, this, rightSelectOrig); } unionAll[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('union', true, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('union', true, this, rightSelectOrig); } intersect[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('intersect', false, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('intersect', false, this, rightSelectOrig); } intersectAll[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('intersect', true, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('intersect', true, this, rightSelectOrig); } except[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('except', false, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('except', false, this, rightSelectOrig); } exceptAll[]>>( - rightSelect: SetOperatorRightSelect, + rightSelect: + | SetOperatorRightSelect + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), ): MySqlSetOperator { - return new MySqlSetOperator('except', true, this, rightSelect); + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + + return new MySqlSetOperator('except', true, this, rightSelectOrig); } abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; From c842f4d9ac8092f6bb16af7512f1e8ff7b7ede1c Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sun, 17 Sep 2023 02:43:43 -0400 Subject: [PATCH 07/34] (Mysql) fixed set operator function accepting only one parameter --- .../query-builders/set-operators.ts | 97 ++++++++----------- 1 file changed, 43 insertions(+), 54 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index af0b37152..f0ff107bf 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -311,6 +311,7 @@ export class MySqlSetOperator< private createIterator = (): ReturnType['iterator'] => { const self = this; return async function*(placeholderValues) { + console.log('placeholderValues', placeholderValues); yield* self.prepare().iterator(placeholderValues); }; }; @@ -329,19 +330,17 @@ export function union< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('union', false, leftSelect, rightSelect); } - return union(new MySqlSetOperator('union', false, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return union(new MySqlSetOperator('union', false, leftSelect, rightSelect), select, ...rest); } export function unionAll< @@ -353,19 +352,17 @@ export function unionAll< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('union', true, leftSelect, rightSelect); } - return unionAll(new MySqlSetOperator('union', true, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return unionAll(new MySqlSetOperator('union', true, leftSelect, rightSelect), select, ...rest); } export function intersect< @@ -377,19 +374,17 @@ export function intersect< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); } - return intersect(new MySqlSetOperator('intersect', false, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return intersect(new MySqlSetOperator('intersect', false, leftSelect, rightSelect!), select, ...rest); } export function intersectAll< @@ -401,19 +396,17 @@ export function intersectAll< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); } - return intersectAll(new MySqlSetOperator('intersect', true, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return intersectAll(new MySqlSetOperator('intersect', true, leftSelect, rightSelect!), select, ...rest); } export function except< @@ -425,19 +418,17 @@ export function except< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('except', false, leftSelect, rightSelect); } - return except(new MySqlSetOperator('except', false, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return except(new MySqlSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); } export function exceptAll< @@ -449,17 +440,15 @@ export function exceptAll< TValue extends TypedQueryBuilder[]>, >( leftSelect: MySqlSetOperatorBuilder, - ...rightSelects: SetOperatorRightSelect[] + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRightSelect[] ): MySqlSetOperator { - if (rightSelects.length < 1) { - throw new Error('This operator requires at least two arguments'); - } - - const [rightSelect, ...rest] = rightSelects; - - if (rightSelect && rest.length === 0) { + if (restSelects.length === 0) { return new MySqlSetOperator('except', false, leftSelect, rightSelect); } - return exceptAll(new MySqlSetOperator('except', false, leftSelect, rightSelect!), ...rest); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return exceptAll(new MySqlSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); } From a21e3861bb7a53f55856c362b4725faff9647a80 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sun, 17 Sep 2023 02:44:07 -0400 Subject: [PATCH 08/34] (mysql) added type tests for the set operators --- drizzle-orm/type-tests/mysql/set-operators.ts | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 drizzle-orm/type-tests/mysql/set-operators.ts diff --git a/drizzle-orm/type-tests/mysql/set-operators.ts b/drizzle-orm/type-tests/mysql/set-operators.ts new file mode 100644 index 000000000..4c95fa9fa --- /dev/null +++ b/drizzle-orm/type-tests/mysql/set-operators.ts @@ -0,0 +1,172 @@ +import { type Equal, Expect } from 'type-tests/utils.ts'; +import { eq } from '~/expressions.ts'; +import { except, exceptAll, intersect, intersectAll, union, unionAll } from '~/mysql-core/index.ts'; +import { desc, sql } from '~/sql/index.ts'; +import { db } from './db.ts'; +import { cities, classes, newYorkers, users } from './tables.ts'; + +const unionTest = await db + .select({ id: users.id }) + .from(users) + .union( + db + .select({ id: users.id }) + .from(users), + ); + +Expect>; + +const unionAllTest = await db + .select({ id: users.id, text: users.text }) + .from(users) + .unionAll( + db.select({ id: users.id, text: users.text }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const intersectTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .intersect(({ intersect }) => + intersect( + db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users), + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ) + ); + +Expect>; + +const intersectAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .intersectAll( + db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const exceptTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .except( + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ); + +Expect>; + +const exceptAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .exceptAll( + db + .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) + .from(users), + ); + +Expect>; + +const union2Test = await union(db.select().from(cities), db.select().from(cities)); + +Expect>; + +const unionAll2Test = await unionAll( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select().from(cities), +); + +Expect>; + +const intersect2Test = await intersect( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), +); + +Expect>; + +const intersectAll2Test = await intersectAll( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities), +).orderBy(desc(cities.id)).limit(23); + +Expect>; + +const except2Test = await except( + db.select({ + userId: newYorkers.userId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + }).from(newYorkers), +); + +Expect>; + +const exceptAll2Test = await exceptAll( + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }).from(newYorkers), +); + +Expect>; + +{ + const query = db + .select() + .from(users) + .union( + db.select() + .from(users), + ) + .prepare() + .iterator(); + for await (const row of query) { + Expect>(); + } +} + +// @ts-expect-error - The select on both sites must be the same shape +db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); + +// @ts-expect-error - The select on both sites must be the same shape +db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); From 8def9d61ae507270fd968d3e373e226e256bbfde Mon Sep 17 00:00:00 2001 From: Angelelz Date: Tue, 19 Sep 2023 21:46:25 -0400 Subject: [PATCH 09/34] [MySql] fixed type issue with the rest parameters passed to the set operator functions - Added a new type 'SetOperatorRestSelect' to properly handle the rest parameter - Added a new generic parameter TRest to properly handle the rest parameter - Added additional type tests for the rest parameters - deleted debigging strings on ValidateShape type --- .../query-builders/set-operators.ts | 31 ++++++++++++--- drizzle-orm/src/utils.ts | 7 ++-- drizzle-orm/type-tests/mysql/set-operators.ts | 39 ++++++++++++++++++- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index f0ff107bf..f0af2b79a 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -55,6 +55,19 @@ type SetOperatorRightSelect< > : TValue; +type SetOperatorRestSelect< + TValue extends readonly TypedQueryBuilder[], + Valid, +> = TValue extends [infer First, ...infer Rest] + ? First extends MySqlSetOperatorBuilder + ? Rest extends TypedQueryBuilder[] ? [ + ValidateShape, Valid, TValue[0]>, + ...SetOperatorRestSelect, + ] + : ValidateShape, Valid, TValue> + : never[] + : TValue; + export interface MySqlSetOperatorBuilder< TTableName extends string | undefined, TSelection extends ColumnsSelection, @@ -328,10 +341,11 @@ export function union< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('union', false, leftSelect, rightSelect); @@ -350,10 +364,11 @@ export function unionAll< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('union', true, leftSelect, rightSelect); @@ -372,10 +387,11 @@ export function intersect< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); @@ -394,10 +410,11 @@ export function intersectAll< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); @@ -416,10 +433,11 @@ export function except< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('except', false, leftSelect, rightSelect); @@ -438,10 +456,11 @@ export function exceptAll< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRightSelect[] + ...restSelects: SetOperatorRestSelect> ): MySqlSetOperator { if (restSelects.length === 0) { return new MySqlSetOperator('except', false, leftSelect, rightSelect); diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 16c92cfc1..713d88c84 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -196,9 +196,10 @@ export interface DrizzleConfig = Record< export type ValidateShape = T extends ValidShape ? Exclude extends never ? TResult - : Exclude extends string ? `Invalid key(s): ${Exclude}` - : 'uno' - : 'dos'; + : Exclude extends string + ? DrizzleTypeError<`Invalid key(s): ${Exclude}`> + : never + : never; export type KnownKeysOnly = { [K in keyof T]: K extends keyof U ? T[K] : never; diff --git a/drizzle-orm/type-tests/mysql/set-operators.ts b/drizzle-orm/type-tests/mysql/set-operators.ts index 4c95fa9fa..547d6cea6 100644 --- a/drizzle-orm/type-tests/mysql/set-operators.ts +++ b/drizzle-orm/type-tests/mysql/set-operators.ts @@ -77,7 +77,7 @@ const exceptAllTest = await db Expect>; -const union2Test = await union(db.select().from(cities), db.select().from(cities)); +const union2Test = await union(db.select().from(cities), db.select().from(cities), db.select().from(cities)); Expect>; @@ -170,3 +170,40 @@ db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); // @ts-expect-error - The select on both sites must be the same shape db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); + +union( + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities), + // @ts-expect-error - The select on rest parameter must be the same shape + db.select().from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: sql`${cities.id}` }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), +); From 907605a465d6e4578f64ad5f2294baf6a1865fa5 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 20 Sep 2023 00:32:41 -0400 Subject: [PATCH 10/34] Added test for mysql implementation of set operators - fixed wring import in pg that prevented test from running - added tests --- .../pg-core/query-builders/set-operators.ts | 4 +- integration-tests/tests/mysql.test.ts | 855 ++++++++++++++++++ 2 files changed, 857 insertions(+), 2 deletions(-) diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 667a11d85..8dc175780 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -1,10 +1,8 @@ import { entityKind, is } from '~/entity.ts'; import { - applyMixins, orderSelectedFields, type Placeholder, type Query, - QueryPromise, SelectionProxyHandler, SQL, sql, @@ -18,7 +16,9 @@ import type { SelectMode, SelectResult, } from '~/query-builders/select.types.ts'; +import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; +import { applyMixins } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 3f05ffe25..9032f2108 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -8,6 +8,7 @@ import { DefaultLogger, eq, gt, + gte, inArray, type InferModel, Name, @@ -20,9 +21,13 @@ import { boolean, date, datetime, + except, + exceptAll, getTableConfig, getViewConfig, int, + intersect, + intersectAll, json, mysqlEnum, mysqlTable, @@ -32,6 +37,8 @@ import { text, time, timestamp, + union, + unionAll, unique, uniqueIndex, uniqueKeyName, @@ -2016,3 +2023,851 @@ test.serial('utc config for datetime', async (t) => { await db.execute(sql`drop table if exists \`datestable\``); }); + +test.serial('set operations (union) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + ); + + t.assert(result.length === 11); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Peter' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 7, name: 'Mary' }, + { id: 8, name: 'Sally' }, + ]); +}); + +test.serial('set operations (union) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (union all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2).unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 4); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 2, name: 'London' }, + ]); +}); + +test.serial('set operations (union all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 3); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (intersect) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (intersect) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 0); + + t.deepEqual(result, []); +}); + +test.serial('set operations (intersect all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2).intersectAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + ]); +}); + +test.serial('set operations (intersect all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await intersectAll( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (except) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(citiesTable).except( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (except) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await except( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (except all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(citiesTable).exceptAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (except all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await exceptAll( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gt(users2Table.id, 7)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Peter' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 7, name: 'Mary' }, + ]); +}); + +test.serial('set operations (mixed) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(citiesTable).except( + ({ unionAll }) => + unionAll( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select().from(citiesTable).where(eq(citiesTable.id, 2)), + ), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (mixed all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select().from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 8, name: 'Sally' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); From 3893876dff4c97618cd5dd9d23d4b8d4f2efbcd3 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 20 Sep 2023 21:57:35 -0400 Subject: [PATCH 11/34] [Pg] Completed implementation of set operations - Added abstract class PgSetOperatorBuilder that extends the TypedQueryBuilder - Added type helpers for correct type inference including when passing multiple parameters to the function form - PgSelect now extends from PgSetOperatorBuilder instead of TypedQueryBuilder to inherit all its methods --- .../src/pg-core/query-builders/select.ts | 130 +------ .../pg-core/query-builders/set-operators.ts | 341 +++++++++++++----- 2 files changed, 251 insertions(+), 220 deletions(-) diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 734de066e..4ebb7b876 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -5,7 +5,6 @@ import type { PgSession, PreparedQuery, PreparedQueryConfig } from '~/pg-core/se import type { SubqueryWithSelection } from '~/pg-core/subquery.ts'; import type { PgTable } from '~/pg-core/table.ts'; import { PgViewBase } from '~/pg-core/view.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -33,7 +32,7 @@ import type { PgSelectQueryBuilderHKT, SelectedFields, } from './select.types.ts'; -import { PgSetOperator } from './set-operators.ts'; +import { PgSetOperatorBuilder } from './set-operators.ts'; type CreatePgSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', @@ -130,9 +129,12 @@ export abstract class PgSelectQueryBuilder< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] +> extends PgSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap > { static readonly [entityKind]: string = 'PgSelectQueryBuilder'; @@ -180,16 +182,6 @@ export abstract class PgSelectQueryBuilder< this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {}; } - /** @internal */ - getSetOperatorConfig() { - return { - session: this.session, - dialect: this.dialect, - joinsNotNullableMap: this.joinsNotNullableMap, - fields: this.config.fields, - }; - } - private createJoin( joinType: TJoinType, ): JoinFn { @@ -325,114 +317,6 @@ export abstract class PgSelectQueryBuilder< return this; } - union< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('union', false, this as any, rightSelect); - } - - unionAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('union', true, this as any, rightSelect); - } - - intersect< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('intersect', false, this as any, rightSelect); - } - - intersectAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('intersect', true, this as any, rightSelect); - } - - except< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('except', false, this as any, rightSelect); - } - - exceptAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - >( - rightSelect: - | PgSelect - | PgSelectQueryBuilder, - ): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode - > { - return new PgSetOperator('except', true, this as any, rightSelect); - } - /** * Sets the HAVING clause of this query, which often * used with GROUP BY and filters rows after they've been diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 8dc175780..05af888e1 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -18,15 +18,151 @@ import type { } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; -import { applyMixins } from '~/utils.ts'; +import { applyMixins, type ValidateShape } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; -import type { PgSelect, PgSelectQueryBuilder } from './select.ts'; -import type { PgSelectHKTBase, PgSelectQueryBuilderHKT } from './select.types.ts'; +import type { PgSelectHKTBase } from './select.types.ts'; type SetOperator = 'union' | 'intersect' | 'except'; +const getPgSetOperators = () => { + return { + union, + unionAll, + intersect, + intersectAll, + except, + exceptAll, + }; +}; + +type PgSetOperators = ReturnType; + +type SetOperatorRightSelect< + TValue extends TypedQueryBuilder[]>, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, +> = TValue extends PgSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + TypedQueryBuilder[]> + > + : TValue; + +type SetOperatorRestSelect< + TValue extends readonly TypedQueryBuilder[], + Valid, +> = TValue extends [infer First, ...infer Rest] + ? First extends PgSetOperatorBuilder + ? Rest extends TypedQueryBuilder[] ? [ + ValidateShape, Valid, TValue[0]>, + ...SetOperatorRestSelect, + ] + : ValidateShape, Valid, TValue> + : never[] + : TValue; + +export abstract class PgSetOperatorBuilder< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] +> { + static readonly [entityKind]: string = 'PgSetOperatorBuilder'; + + protected abstract joinsNotNullableMap: Record; + protected abstract config: { + fields: Record; + limit?: number | Placeholder; + orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + }; + /* @internal */ + protected abstract readonly session: PgSession | undefined; + protected abstract dialect: PgDialect; + + /** @internal */ + getSetOperatorConfig() { + return { + session: this.session, + dialect: this.dialect, + joinsNotNullableMap: this.joinsNotNullableMap, + fields: this.config.fields, + }; + } + union[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('union', false, this, rightSelectOrig); + } + + unionAll[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('union', true, this, rightSelectOrig); + } + + intersect[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('intersect', false, this, rightSelectOrig); + } + + intersectAll[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('intersect', true, this, rightSelectOrig); + } + + except[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('except', false, this, rightSelectOrig); + } + + exceptAll[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: PgSetOperators) => SetOperatorRightSelect), + ): PgSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + + return new PgSetOperator('except', true, this, rightSelectOrig); + } + + abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; + abstract orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; + + abstract limit(limit: number): this; +} + export interface PgSetOperator< // eslint-disable-next-line @typescript-eslint/no-unused-vars THKT extends PgSelectHKTBase, @@ -44,46 +180,45 @@ export interface PgSetOperator< {} export class PgSetOperator< - // eslint-disable-next-line @typescript-eslint/no-unused-vars THKT extends PgSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] +> extends PgSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap > { static readonly [entityKind]: string = 'PgSetOperator'; - private session: PgSession | undefined; - private dialect: PgDialect; - private config: { + protected joinsNotNullableMap: Record; + protected config: { fields: Record; - joinsNotNullableMap: Record; limit?: number | Placeholder; orderBy?: (PgColumn | SQL | SQL.Aliased)[]; }; + /* @internal */ + readonly session: PgSession | undefined; + protected dialect: PgDialect; constructor( private operator: SetOperator, private isAll: boolean, - private leftSelect: - | PgSelect - | PgSelectQueryBuilder, - private rightSelect: - | PgSelect - | PgSelectQueryBuilder, + private leftSelect: PgSetOperatorBuilder, + private rightSelect: TypedQueryBuilder[]>, ) { super(); const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); this.session = session; this.dialect = dialect; + this.joinsNotNullableMap = joinsNotNullableMap; this.config = { fields, - joinsNotNullableMap, }; } @@ -161,7 +296,7 @@ export class PgSetOperator< execute: SelectResult[]; } > { - const { session, config: { fields, joinsNotNullableMap }, dialect } = this; + const { session, joinsNotNullableMap, config: { fields }, dialect } = this; if (!session) { throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); } @@ -204,20 +339,22 @@ export function union< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('union', false, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('union', false, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return union(new PgSetOperator('union', false, leftSelect, rightSelect), select, ...rest); } export function unionAll< @@ -225,20 +362,22 @@ export function unionAll< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('union', true, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('union', true, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return unionAll(new PgSetOperator('union', true, leftSelect, rightSelect), select, ...rest); } export function intersect< @@ -246,20 +385,22 @@ export function intersect< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('intersect', false, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('intersect', false, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return intersect(new PgSetOperator('intersect', false, leftSelect, rightSelect!), select, ...rest); } export function intersectAll< @@ -267,20 +408,22 @@ export function intersectAll< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('intersect', true, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('intersect', true, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return intersectAll(new PgSetOperator('intersect', true, leftSelect, rightSelect!), select, ...rest); } export function except< @@ -288,20 +431,22 @@ export function except< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('except', false, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('except', false, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return except(new PgSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); } export function exceptAll< @@ -309,18 +454,20 @@ export function exceptAll< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], >( - leftSelect: - | PgSelect - | PgSelectQueryBuilder, - rightSelect: - | PgSelect - | PgSelectQueryBuilder, -): PgSetOperator< - THKT, - TTableName, - TSelection, - TSelectMode -> { - return new PgSetOperator('except', true, leftSelect, rightSelect); + leftSelect: PgSetOperatorBuilder, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): PgSetOperator { + if (restSelects.length === 0) { + return new PgSetOperator('except', false, leftSelect, rightSelect); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return exceptAll(new PgSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); } From 87479a7e55da1d0eb4a6508c4aadc68474ed7a2d Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 20 Sep 2023 21:58:43 -0400 Subject: [PATCH 12/34] [Pg] Added type tests for the set operators --- drizzle-orm/type-tests/pg/set-operators.ts | 194 +++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 drizzle-orm/type-tests/pg/set-operators.ts diff --git a/drizzle-orm/type-tests/pg/set-operators.ts b/drizzle-orm/type-tests/pg/set-operators.ts new file mode 100644 index 000000000..06c1c9bef --- /dev/null +++ b/drizzle-orm/type-tests/pg/set-operators.ts @@ -0,0 +1,194 @@ +import { type Equal, Expect } from 'type-tests/utils.ts'; +import { eq } from '~/expressions.ts'; +import { except, exceptAll, intersect, intersectAll, union, unionAll } from '~/pg-core/index.ts'; +import { desc, sql } from '~/sql/index.ts'; +import { db } from './db.ts'; +import { cities, classes, newYorkers, users } from './tables.ts'; + +const unionTest = await db + .select({ id: users.id }) + .from(users) + .union( + db + .select({ id: users.id }) + .from(users), + ); + +Expect>; + +const unionAllTest = await db + .select({ id: users.id, text: users.text }) + .from(users) + .unionAll( + db.select({ id: users.id, text: users.text }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const intersectTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .intersect(({ intersect }) => + intersect( + db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users), + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ) + ); + +Expect>; + +const intersectAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .intersectAll( + db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const exceptTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .except( + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ); + +Expect>; + +const exceptAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .exceptAll( + db + .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) + .from(users), + ); + +Expect>; + +const union2Test = await union(db.select().from(cities), db.select().from(cities), db.select().from(cities)); + +Expect>; + +const unionAll2Test = await unionAll( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select().from(cities), +); + +Expect>; + +const intersect2Test = await intersect( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), +); + +Expect>; + +const intersectAll2Test = await intersectAll( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities), +).orderBy(desc(cities.id)).limit(23); + +Expect>; + +const except2Test = await except( + db.select({ + userId: newYorkers.userId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + }).from(newYorkers), +); + +Expect>; + +const exceptAll2Test = await exceptAll( + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }).from(newYorkers), +); + +Expect>; + +// @ts-expect-error - The select on both sites must be the same shape +db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); + +// @ts-expect-error - The select on both sites must be the same shape +db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); + +union( + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities), + // @ts-expect-error - The select on rest parameter must be the same shape + db.select().from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: sql`${cities.id}` }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), +); From 4e371fb13cac28e468d620968d79e5f52dce8c5d Mon Sep 17 00:00:00 2001 From: Angelelz Date: Wed, 20 Sep 2023 21:59:08 -0400 Subject: [PATCH 13/34] [Pg] Added integration tests for the set operators --- integration-tests/tests/pg.test.ts | 869 ++++++++++++++++++++++++++++- 1 file changed, 857 insertions(+), 12 deletions(-) diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index fb2f058f6..d69f12555 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -5,6 +5,9 @@ import anyTest from 'ava'; import Docker from 'dockerode'; import { and, + arrayContained, + arrayContains, + arrayOverlaps, asc, eq, gt, @@ -17,9 +20,6 @@ import { sql, type SQLWrapper, TransactionRollbackError, - arrayContains, - arrayContained, - arrayOverlaps } from 'drizzle-orm'; import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; @@ -28,11 +28,15 @@ import { boolean, char, cidr, + except, + exceptAll, getMaterializedViewConfig, getTableConfig, getViewConfig, inet, integer, + intersect, + intersectAll, jsonb, macaddr, macaddr8, @@ -45,6 +49,8 @@ import { serial, text, timestamp, + union, + unionAll, unique, uniqueKeyName, uuid as pgUuid, @@ -73,6 +79,11 @@ const citiesTable = pgTable('cities', { state: char('state', { length: 2 }), }); +const cities2Table = pgTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + const users2Table = pgTable('users2', { id: serial('id').primaryKey(), name: text('name').notNull(), @@ -2465,7 +2476,7 @@ test.serial('array operators', async (t) => { const posts = pgTable('posts', { id: serial('id').primaryKey(), - tags: text('tags').array() + tags: text('tags').array(), }); await db.execute(sql`drop table if exists ${posts}`); @@ -2475,17 +2486,17 @@ test.serial('array operators', async (t) => { ); await db.insert(posts).values([{ - tags: ['ORM'] + tags: ['ORM'], }, { - tags: ['Typescript'] + tags: ['Typescript'], }, { - tags: ['Typescript', 'ORM'] + tags: ['Typescript', 'ORM'], }, { - tags: ['Typescript', 'Frontend', 'React'] + tags: ['Typescript', 'Frontend', 'React'], }, { - tags: ['Typescript', 'ORM', 'Database', 'Postgres'] + tags: ['Typescript', 'ORM', 'Database', 'Postgres'], }, { - tags: ['Java', 'Spring', 'OOP'] + tags: ['Java', 'Spring', 'OOP'], }]); const contains = await db.select({ id: posts.id }).from(posts) @@ -2497,11 +2508,845 @@ test.serial('array operators', async (t) => { const withSubQuery = await db.select({ id: posts.id }).from(posts) .where(arrayContains( posts.tags, - db.select({ tags: posts.tags }).from(posts).where(eq(posts.id, 1)) + db.select({ tags: posts.tags }).from(posts).where(eq(posts.id, 1)), )); t.deepEqual(contains, [{ id: 3 }, { id: 5 }]); t.deepEqual(contained, [{ id: 1 }, { id: 2 }, { id: 3 }]); t.deepEqual(overlaps, [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]); - t.deepEqual(withSubQuery, [{ id: 1 }, { id: 3 }, { id: 5 }]) + t.deepEqual(withSubQuery, [{ id: 1 }, { id: 3 }, { id: 5 }]); +}); + +test.serial('set operations (union) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 11); + + t.deepEqual(result, [ + { id: 5, name: 'Ben' }, + { id: 3, name: 'Jack' }, + { id: 2, name: 'Jane' }, + { id: 6, name: 'Jill' }, + { id: 1, name: 'John' }, + { id: 2, name: 'London' }, + { id: 7, name: 'Mary' }, + { id: 1, name: 'New York' }, + { id: 4, name: 'Peter' }, + { id: 8, name: 'Sally' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (union) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (union all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2).unionAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 4); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 2, name: 'London' }, + ]); +}); + +test.serial('set operations (union all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await unionAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 3); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (intersect) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).intersect( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (intersect) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await intersect( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 0); + + t.deepEqual(result, []); +}); + +test.serial('set operations (intersect all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2).intersectAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + ]); +}); + +test.serial('set operations (intersect all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await intersectAll( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (except) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(cities2Table).except( + db + .select() + .from(cities2Table).where(gt(citiesTable.id, 1)), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (except) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await except( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table), + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (except all) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(cities2Table).exceptAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (except all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await exceptAll( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gt(users2Table.id, 7)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Peter' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 7, name: 'Mary' }, + ]); +}); + +test.serial('set operations (mixed) from query builder', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(cities2Table).except( + ({ unionAll }) => + unionAll( + db + .select() + .from(cities2Table).where(gt(citiesTable.id, 1)), + db.select().from(cities2Table).where(eq(citiesTable.id, 2)), + ), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (mixed all) as function', async (t) => { + const { db } = t.context; + + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select().from(cities2Table).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 8, name: 'Sally' }, + ]); }); From 35a525de0e177a56d7b34ecbf2f97f43f5468450 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Thu, 21 Sep 2023 01:47:06 -0400 Subject: [PATCH 14/34] [SQLite] Completed implementation of set operators - added SQLiteSetOperatorBuilder and SQLiteSetOperator classes - SQLiteSelectBuilder now extends from SQLiteSetOperatorBuilder to inherit its methods - Exported funciton versions of union, unionAll, intersect and except - Had to add the last generic parameter as any to the in a type for libsql driver --- drizzle-orm/src/libsql/driver.ts | 9 +- .../src/sqlite-core/query-builders/index.ts | 1 + .../src/sqlite-core/query-builders/select.ts | 13 +- .../query-builders/set-operators.ts | 540 ++++++++++++++++++ 4 files changed, 552 insertions(+), 11 deletions(-) create mode 100644 drizzle-orm/src/sqlite-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/libsql/driver.ts b/drizzle-orm/src/libsql/driver.ts index 2f4bc5c58..9a9f33cf8 100644 --- a/drizzle-orm/src/libsql/driver.ts +++ b/drizzle-orm/src/libsql/driver.ts @@ -10,12 +10,7 @@ import { } from '~/relations.ts'; import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts'; import { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts'; -import type { - SQLiteDelete, - SQLiteInsert, - SQLiteSelect, - SQLiteUpdate, -} from '~/sqlite-core/index.ts'; +import type { SQLiteDelete, SQLiteInsert, SQLiteSelect, SQLiteUpdate } from '~/sqlite-core/index.ts'; import type { SQLiteRelationalQuery } from '~/sqlite-core/query-builders/query.ts'; import type { SQLiteRaw } from '~/sqlite-core/query-builders/raw.ts'; import { type DrizzleConfig } from '~/utils.ts'; @@ -23,7 +18,7 @@ import { LibSQLSession } from './session.ts'; export type BatchParameters = | SQLiteUpdate - | SQLiteSelect + | SQLiteSelect | SQLiteDelete | Omit, 'where'> | Omit, 'where'> diff --git a/drizzle-orm/src/sqlite-core/query-builders/index.ts b/drizzle-orm/src/sqlite-core/query-builders/index.ts index 16f0e1d4d..58d909c22 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/index.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/index.ts @@ -3,4 +3,5 @@ export * from './insert.ts'; export * from './query-builder.ts'; export * from './select.ts'; export * from './select.types.ts'; +export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 9baa3dcd4..d226c5366 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -1,5 +1,4 @@ import { entityKind, is } from '~/entity.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -36,6 +35,7 @@ import type { SQLiteSelectHKTBase, SQLiteSelectQueryBuilderHKT, } from './select.types.ts'; +import { SQLiteSetOperatorBuilder } from './set-operators.ts'; type CreateSQLiteSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', @@ -128,9 +128,14 @@ export abstract class SQLiteSelectQueryBuilder< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] +> extends SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap > { static readonly [entityKind]: string = 'SQLiteSelectQueryBuilder'; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts new file mode 100644 index 000000000..cb14005b4 --- /dev/null +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -0,0 +1,540 @@ +import { entityKind, is } from '~/entity.ts'; +import { + orderSelectedFields, + type Placeholder, + type Query, + SelectionProxyHandler, + SQL, + sql, + type ValueOrArray, +} from '~/index.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { + BuildSubquerySelection, + JoinNullability, + SelectMode, + SelectResult, +} from '~/query-builders/select.types.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { PreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; +import { applyMixins, type PromiseOf, type ValidateShape } from '~/utils.ts'; +import { type ColumnsSelection } from '~/view.ts'; +import { SQLiteColumn } from '../columns/common.ts'; +import type { SQLiteDialect } from '../dialect.ts'; +import type { SQLiteSelectHKTBase } from './select.types.ts'; + +type SetOperator = 'union' | 'intersect' | 'except'; + +const getSQLiteSetOperators = () => { + return { + union, + unionAll, + intersect, + except, + }; +}; + +type SQLiteSetOperators = ReturnType; + +type SetOperatorRightSelect< + TValue extends TypedQueryBuilder[]>, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, +> = TValue extends SQLiteSetOperatorBuilder ? ValidateShape< + SelectResult, + SelectResult, + TypedQueryBuilder[]> + > + : TValue; + +type SetOperatorRestSelect< + TValue extends readonly TypedQueryBuilder[], + Valid, +> = TValue extends [infer First, ...infer Rest] + ? First extends SQLiteSetOperatorBuilder + ? Rest extends TypedQueryBuilder[] ? [ + ValidateShape, Valid, TValue[0]>, + ...SetOperatorRestSelect, + ] + : ValidateShape, Valid, TValue> + : never[] + : TValue; + +export abstract class SQLiteSetOperatorBuilder< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] +> { + static readonly [entityKind]: string = 'SQLiteSetOperatorBuilder'; + + protected abstract joinsNotNullableMap: Record; + protected abstract config: { + fields: Record; + limit?: number | Placeholder; + orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + }; + /* @internal */ + protected abstract readonly session: SQLiteSession | undefined; + protected abstract dialect: SQLiteDialect; + + /** @internal */ + getSetOperatorConfig() { + return { + session: this.session, + dialect: this.dialect, + joinsNotNullableMap: this.joinsNotNullableMap, + fields: this.config.fields, + }; + } + union[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), + ): SQLiteSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; + + return new SQLiteSetOperator( + 'union', + false, + this, + rightSelectOrig, + ); + } + + unionAll[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), + ): SQLiteSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; + + return new SQLiteSetOperator( + 'union', + true, + this, + rightSelectOrig, + ); + } + + intersect[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), + ): SQLiteSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; + + return new SQLiteSetOperator( + 'intersect', + false, + this, + rightSelectOrig, + ); + } + + except[]>>( + rightSelect: + | SetOperatorRightSelect + | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), + ): SQLiteSetOperator { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; + + return new SQLiteSetOperator( + 'except', + false, + this, + rightSelectOrig, + ); + } + + abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; + abstract orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; + + abstract limit(limit: number): this; +} + +export interface SQLiteSetOperator< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TResultType extends 'sync' | 'async', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends + TypedQueryBuilder< + BuildSubquerySelection, + SelectResult[] + >, + QueryPromise[]> +{} + +export class SQLiteSetOperator< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, +> extends SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap +> { + static readonly [entityKind]: string = 'SQLiteSetOperator'; + + protected joinsNotNullableMap: Record; + protected config: { + fields: Record; + limit?: number | Placeholder; + orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + }; + /* @internal */ + readonly session: SQLiteSession | undefined; + protected dialect: SQLiteDialect; + + constructor( + private operator: SetOperator, + private isAll: boolean, + private leftSelect: SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap + >, + private rightSelect: TypedQueryBuilder[]>, + ) { + super(); + + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + this.session = session; + this.dialect = dialect; + this.joinsNotNullableMap = joinsNotNullableMap; + this.config = { + fields, + }; + } + + orderBy(builder: (aliases: TSelection) => ValueOrArray): this; + orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; + orderBy( + ...columns: + | [(aliases: TSelection) => ValueOrArray] + | (SQLiteColumn | SQL | SQL.Aliased)[] + ): this { + if (typeof columns[0] === 'function') { + const orderBy = columns[0]( + new Proxy( + this.config.fields, + new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + ) as TSelection, + ); + this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; + } else { + this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]; + } + return this; + } + + limit(limit: number) { + this.config.limit = limit; + return this; + } + + toSQL(): Query { + const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; + } + + override getSQL(): SQL { + const leftChunk = sql`${this.leftSelect.getSQL()} `; + const rightChunk = sql`${this.rightSelect.getSQL()}`; + + let orderBySql; + if (this.config.orderBy && this.config.orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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 orderBy of this.config.orderBy) { + if (is(orderBy, SQLiteColumn)) { + orderByValues.push(sql.raw(orderBy.name)); + } else if (is(orderBy, SQL)) { + for (let i = 0; i < orderBy.queryChunks.length; i++) { + const chunk = orderBy.queryChunks[i]; + + if (is(chunk, SQLiteColumn)) { + orderBy.queryChunks[i] = sql.raw(chunk.name); + } + } + + orderByValues.push(sql`${orderBy}`); + } else { + orderByValues.push(sql`${orderBy}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)}`; + } + + const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; + + const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + } + + prepare(isOneTimeQuery?: boolean): PreparedQuery< + { + type: TResultType; + run: TRunResult; + all: SelectResult[]; + get: SelectResult | undefined; + values: any[][]; + execute: SelectResult[]; + } + > { + if (!this.session) { + throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); + } + const fieldsList = orderSelectedFields(this.config.fields); + const query = this.session[isOneTimeQuery ? 'prepareOneTimeQuery' : 'prepareQuery']( + this.dialect.sqlToQuery(this.getSQL()), + fieldsList, + 'all', + ); + query.joinsNotNullableMap = this.joinsNotNullableMap; + return query as ReturnType; + } + + run: ReturnType['run'] = (placeholderValues) => { + return this.prepare(true).run(placeholderValues); + }; + + all: ReturnType['all'] = (placeholderValues) => { + return this.prepare(true).all(placeholderValues); + }; + + get: ReturnType['get'] = (placeholderValues) => { + return this.prepare(true).get(placeholderValues); + }; + + values: ReturnType['values'] = (placeholderValues) => { + return this.prepare(true).values(placeholderValues); + }; + + async execute(): Promise[]> { + return this.all() as PromiseOf>; + } +} + +applyMixins(SQLiteSetOperator, [QueryPromise]); + +export function union< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], +>( + leftSelect: SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): SQLiteSetOperator { + if (restSelects.length === 0) { + return new SQLiteSetOperator( + 'union', + false, + leftSelect, + rightSelect, + ); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return union( + new SQLiteSetOperator( + 'union', + false, + leftSelect, + rightSelect, + ), + select, + ...rest, + ); +} + +export function unionAll< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], +>( + leftSelect: SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): SQLiteSetOperator { + if (restSelects.length === 0) { + return new SQLiteSetOperator( + 'union', + true, + leftSelect, + rightSelect, + ); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return unionAll( + new SQLiteSetOperator( + 'union', + true, + leftSelect, + rightSelect, + ), + select, + ...rest, + ); +} + +export function intersect< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], +>( + leftSelect: SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): SQLiteSetOperator { + if (restSelects.length === 0) { + return new SQLiteSetOperator( + 'intersect', + false, + leftSelect, + rightSelect, + ); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return intersect( + new SQLiteSetOperator( + 'intersect', + false, + leftSelect, + rightSelect!, + ), + select, + ...rest, + ); +} + +export function except< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends TypedQueryBuilder[]>, + TRest extends TypedQueryBuilder[]>[], +>( + leftSelect: SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect> +): SQLiteSetOperator { + if (restSelects.length === 0) { + return new SQLiteSetOperator( + 'except', + false, + leftSelect, + rightSelect, + ); + } + + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); + + return except( + new SQLiteSetOperator( + 'except', + false, + leftSelect, + rightSelect!, + ), + select, + ...rest, + ); +} From e555c093f4e4e990d870167cbebf7be960b9eca7 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Thu, 21 Sep 2023 01:47:49 -0400 Subject: [PATCH 15/34] [SQLite] Added type tests --- .../type-tests/sqlite/set-operators.ts | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 drizzle-orm/type-tests/sqlite/set-operators.ts diff --git a/drizzle-orm/type-tests/sqlite/set-operators.ts b/drizzle-orm/type-tests/sqlite/set-operators.ts new file mode 100644 index 000000000..c37c857d7 --- /dev/null +++ b/drizzle-orm/type-tests/sqlite/set-operators.ts @@ -0,0 +1,194 @@ +import { type Equal, Expect } from 'type-tests/utils.ts'; +import { eq } from '~/expressions.ts'; +import { desc, sql } from '~/sql/index.ts'; +import { except, intersect, union, unionAll } from '~/sqlite-core/index.ts'; +import { db } from './db.ts'; +import { cities, classes, newYorkers, users } from './tables.ts'; + +const unionTest = await db + .select({ id: users.id }) + .from(users) + .union( + db + .select({ id: users.id }) + .from(users), + ); + +Expect>; + +const unionAllTest = await db + .select({ id: users.id, text: users.name }) + .from(users) + .unionAll( + db.select({ id: users.id, text: users.name }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const intersectTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .intersect(({ intersect }) => + intersect( + db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users), + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ) + ); + +Expect>; + +const intersectAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .intersect( + db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .leftJoin(cities, eq(users.id, cities.id)), + ); + +Expect>; + +const exceptTest = await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + .except( + db + .select({ id: users.id, homeCity: sql`${users.homeCity}`.mapWith(Number) }) + .from(users), + ); + +Expect>; + +const exceptAllTest = await db + .select({ id: users.id, homeCity: users.class }) + .from(users) + .except( + db + .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) + .from(users), + ); + +Expect>; + +const union2Test = await union(db.select().from(cities), db.select().from(cities), db.select().from(cities)); + +Expect>; + +const unionAll2Test = await unionAll( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select().from(cities), +); + +Expect>; + +const intersect2Test = await intersect( + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), + db.select({ + id: cities.id, + name: cities.name, + population: cities.population, + }).from(cities), +); + +Expect>; + +const intersectAll2Test = await intersect( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities), +).orderBy(desc(cities.id)).limit(23); + +Expect>; + +const except2Test = await except( + db.select({ + userId: newYorkers.userId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + }).from(newYorkers), +); + +Expect>; + +const exceptAll2Test = await except( + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }) + .from(newYorkers), + db.select({ + userId: newYorkers.userId, + cityId: newYorkers.cityId, + }).from(newYorkers), +); + +Expect>; + +// @ts-expect-error - The select on both sites must be the same shape +db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); + +// @ts-expect-error - The select on both sites must be the same shape +db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); + +union( + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities), + // @ts-expect-error - The select on rest parameter must be the same shape + db.select().from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), +); + +union( + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities), + db.select({ id: sql`${cities.id}` }).from(cities), + db.select({ id: cities.id }).from(cities), + // @ts-expect-error - The select on any part of the rest parameter must be the same shape + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), +); From 14f996be3d0a0ecbe02db253c03f3858677bf606 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Thu, 21 Sep 2023 01:48:12 -0400 Subject: [PATCH 16/34] [SQLite] Added integration tests for all cases --- integration-tests/tests/libsql.test.ts | 613 ++++++++++++++++++++++++- 1 file changed, 607 insertions(+), 6 deletions(-) diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index 575f20c75..5229eb574 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -7,6 +7,7 @@ import { asc, eq, gt, + gte, inArray, type InferModel, Name, @@ -20,13 +21,17 @@ import { migrate } from 'drizzle-orm/libsql/migrator'; import { alias, blob, + except, getViewConfig, integer, + intersect, primaryKey, sqliteTable, sqliteTableCreator, sqliteView, text, + union, + unionAll, } from 'drizzle-orm/sqlite-core'; import { type Equal, Expect } from './utils.ts'; @@ -1756,16 +1761,16 @@ test.serial('insert with onConflict do update where', async (t) => { await db .insert(usersTable) - .values([{ id: 1, name: "John", verified: false }]) + .values([{ id: 1, name: 'John', verified: false }]) .run(); await db .insert(usersTable) - .values({ id: 1, name: "John1", verified: true }) + .values({ id: 1, name: 'John1', verified: true }) .onConflictDoUpdate({ target: usersTable.id, - set: { name: "John1", verified: true }, - where: eq(usersTable.verified, false) + set: { name: 'John1', verified: true }, + where: eq(usersTable.verified, false), }) .run(); @@ -1775,8 +1780,8 @@ test.serial('insert with onConflict do update where', async (t) => { .where(eq(usersTable.id, 1)) .all(); - t.deepEqual(res, [{ id: 1, name: "John1", verified: true }]); -}) + t.deepEqual(res, [{ id: 1, name: 'John1', verified: true }]); +}); test.serial('insert with onConflict do update using composite pk', async (t) => { const { db } = t.context; @@ -1970,3 +1975,599 @@ test.serial('select + .get() for empty result', async (t) => { await db.run(sql`drop table ${users}`); }); + +test.serial('set operations (union) from query builder', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run(sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `); + + await db.run(sql` + create table \`users2\` ( + id integer primary key, + name text not null, + city_id integer references ${citiesTable}(${name(citiesTable.id.name)}) + ) + `); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 11); + + t.deepEqual(result, [ + { id: 5, name: 'Ben' }, + { id: 3, name: 'Jack' }, + { id: 2, name: 'Jane' }, + { id: 6, name: 'Jill' }, + { id: 1, name: 'John' }, + { id: 2, name: 'London' }, + { id: 7, name: 'Mary' }, + { id: 1, name: 'New York' }, + { id: 4, name: 'Peter' }, + { id: 8, name: 'Sally' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (union) as function', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run(sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `); + + await db.run(sql` + create table \`users2\` ( + id integer primary key, + name text not null, + city_id integer references ${citiesTable}(${name(citiesTable.id.name)}) + ) + `); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (union all) from query builder', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable), + ).orderBy(asc(citiesTable.id)); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (union all) as function', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 3); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 1, name: 'John' }, + { id: 1, name: 'John' }, + ]); +}); + +test.serial('set operations (intersect) from query builder', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`name`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (intersect) as function', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + + t.assert(result.length === 0); + + t.deepEqual(result, []); +}); + +test.serial('set operations (except) from query builder', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(citiesTable).except( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + + t.assert(result.length === 1); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + ]); +}); + +test.serial('set operations (except) as function', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await except( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); +}); + +test.serial('set operations (mixed) from query builder', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table users2 ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await db + .select() + .from(citiesTable).except( + ({ unionAll }) => + unionAll( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select().from(citiesTable).where(eq(citiesTable.id, 2)), + ), + ); + + t.assert(result.length === 2); + + t.deepEqual(result, [ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + ]); +}); + +test.serial('set operations (mixed all) as function', async (t) => { + const { db } = t.context; + + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run( + sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `, + ); + await db.run( + sql` + create table \`users2\` ( + id integer primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); + + const result = await union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select().from(citiesTable).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + + t.assert(result.length === 6); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + { id: 5, name: 'Ben' }, + { id: 6, name: 'Jill' }, + { id: 8, name: 'Sally' }, + ]); +}); From 8808a1f05a0cbb9e17943098b9b4bb557f2c6353 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 22 Sep 2023 21:03:20 -0400 Subject: [PATCH 17/34] [All] Completed implementation with requested changes - Added coments for type tests - Added runtime check that throws an error if the select have keys in different order - Added tests for the new error throwing behavior --- .../query-builders/set-operators.ts | 16 + .../pg-core/query-builders/set-operators.ts | 16 +- .../query-builders/set-operators.ts | 16 +- drizzle-orm/src/utils.ts | 17 + drizzle-orm/type-tests/mysql/set-operators.ts | 24 +- drizzle-orm/type-tests/pg/set-operators.ts | 24 +- .../type-tests/sqlite/set-operators.ts | 24 +- integration-tests/tests/libsql.test.ts | 530 +++++-------- integration-tests/tests/mysql.test.ts | 739 ++++++------------ integration-tests/tests/pg.test.ts | 735 ++++++----------- 10 files changed, 745 insertions(+), 1396 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index f0af2b79a..5c7c3cf0a 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -1,6 +1,7 @@ import { entityKind, is } from '~/entity.ts'; import { applyMixins, + haveSameKeys, orderSelectedFields, type Placeholder, type Query, @@ -221,7 +222,22 @@ export class MySqlSetOperator< private rightSelect: TypedQueryBuilder[]>, ) { super(); + + const leftSelectedFields = leftSelect.getSelectedFields(); + const rightSelectedFields = rightSelect.getSelectedFields(); + + if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { + throw new Error( + 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', + ); + } + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + + this._ = { + selectedFields: fields, + } as this['_']; + this.session = session; this.dialect = dialect; this.joinsNotNullableMap = joinsNotNullableMap; diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 05af888e1..03293cbf9 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -18,7 +18,7 @@ import type { } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; -import { applyMixins, type ValidateShape } from '~/utils.ts'; +import { applyMixins, haveSameKeys, type ValidateShape } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; @@ -213,7 +213,21 @@ export class PgSetOperator< ) { super(); + const leftSelectedFields = leftSelect.getSelectedFields(); + const rightSelectedFields = rightSelect.getSelectedFields(); + + if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { + throw new Error( + 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', + ); + } + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + + this._ = { + selectedFields: fields as BuildSubquerySelection, + } as this['_']; + this.session = session; this.dialect = dialect; this.joinsNotNullableMap = joinsNotNullableMap; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index cb14005b4..635249786 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -17,7 +17,7 @@ import type { } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import type { PreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; -import { applyMixins, type PromiseOf, type ValidateShape } from '~/utils.ts'; +import { applyMixins, haveSameKeys, type PromiseOf, type ValidateShape } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; @@ -227,7 +227,21 @@ export class SQLiteSetOperator< ) { super(); + const leftSelectedFields = leftSelect.getSelectedFields(); + const rightSelectedFields = rightSelect.getSelectedFields(); + + if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { + throw new Error( + 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', + ); + } + const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); + + this._ = { + selectedFields: fields as BuildSubquerySelection, + } as this['_']; + this.session = session; this.dialect = dialect; this.joinsNotNullableMap = joinsNotNullableMap; diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 713d88c84..4a1d40939 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -91,6 +91,23 @@ export function orderSelectedFields( }, []) as SelectedFieldsOrdered; } +export function haveSameKeys(left: Record, right: Record) { + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); + + if (leftKeys.length !== rightKeys.length) { + return false; + } + + for (let i = 0; i < leftKeys.length; i++) { + if (leftKeys[i] !== rightKeys[i]) { + return false; + } + } + + return true; +} + /** @internal */ export function mapUpdateSet(table: Table, values: Record): UpdateSet { const entries: [string, UpdateSet[string]][] = Object.entries(values) diff --git a/drizzle-orm/type-tests/mysql/set-operators.ts b/drizzle-orm/type-tests/mysql/set-operators.ts index 547d6cea6..bc869bf88 100644 --- a/drizzle-orm/type-tests/mysql/set-operators.ts +++ b/drizzle-orm/type-tests/mysql/set-operators.ts @@ -165,16 +165,22 @@ Expect } } -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id, name: cities.name }).from(cities), - // @ts-expect-error - The select on rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select().from(cities), ); @@ -182,7 +188,9 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -191,7 +199,9 @@ union( union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -204,6 +214,8 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), ); diff --git a/drizzle-orm/type-tests/pg/set-operators.ts b/drizzle-orm/type-tests/pg/set-operators.ts index 06c1c9bef..4394fa43d 100644 --- a/drizzle-orm/type-tests/pg/set-operators.ts +++ b/drizzle-orm/type-tests/pg/set-operators.ts @@ -150,16 +150,22 @@ const exceptAll2Test = await exceptAll( Expect>; -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id, name: cities.name }).from(cities), - // @ts-expect-error - The select on rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select().from(cities), ); @@ -167,7 +173,9 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -176,7 +184,9 @@ union( union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -189,6 +199,8 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), ); diff --git a/drizzle-orm/type-tests/sqlite/set-operators.ts b/drizzle-orm/type-tests/sqlite/set-operators.ts index c37c857d7..ed98dac9e 100644 --- a/drizzle-orm/type-tests/sqlite/set-operators.ts +++ b/drizzle-orm/type-tests/sqlite/set-operators.ts @@ -150,16 +150,22 @@ const exceptAll2Test = await except( Expect>; -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); -// @ts-expect-error - The select on both sites must be the same shape +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id, name: cities.name }).from(cities), - // @ts-expect-error - The select on rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select().from(cities), ); @@ -167,7 +173,9 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -176,7 +184,9 @@ union( union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -189,6 +199,8 @@ union( db.select({ id: cities.id }).from(cities), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), - // @ts-expect-error - The select on any part of the rest parameter must be the same shape + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), ); diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index 5229eb574..1414a96ce 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -216,6 +216,42 @@ test.beforeEach(async (t) => { `); }); +async function setupSetOperationTest(db: LibSQLDatabase>) { + await db.run(sql`drop table if exists users2`); + await db.run(sql`drop table if exists cities`); + await db.run(sql` + create table \`cities\` ( + id integer primary key, + name text not null + ) + `); + + await db.run(sql` + create table \`users2\` ( + id integer primary key, + name text not null, + city_id integer references ${citiesTable}(${name(citiesTable.id.name)}) + ) + `); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); +} + test.serial('insert bigint values', async (t) => { const { db } = t.context; @@ -1979,39 +2015,7 @@ test.serial('select + .get() for empty result', async (t) => { test.serial('set operations (union) from query builder', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run(sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `); - - await db.run(sql` - create table \`users2\` ( - id integer primary key, - name text not null, - city_id integer references ${citiesTable}(${name(citiesTable.id.name)}) - ) - `); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2036,44 +2040,22 @@ test.serial('set operations (union) from query builder', async (t) => { { id: 8, name: 'Sally' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (union) as function', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run(sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `); - - await db.run(sql` - create table \`users2\` ( - id integer primary key, - name text not null, - city_id integer references ${citiesTable}(${name(citiesTable.id.name)}) - ) - `); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -2093,47 +2075,26 @@ test.serial('set operations (union) as function', async (t) => { { id: 1, name: 'John' }, { id: 1, name: 'New York' }, ]); + + t.throws(() => { + union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (union all) from query builder', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2153,47 +2114,22 @@ test.serial('set operations (union all) from query builder', async (t) => { { id: 3, name: 'Tampa' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).unionAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable), + ).orderBy(asc(citiesTable.id)); + }); }); test.serial('set operations (union all) as function', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await unionAll( db @@ -2214,47 +2150,26 @@ test.serial('set operations (union all) as function', async (t) => { { id: 1, name: 'John' }, { id: 1, name: 'John' }, ]); + + t.throws(() => { + unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (intersect) from query builder', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2270,47 +2185,22 @@ test.serial('set operations (intersect) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (intersect) as function', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await intersect( db @@ -2327,47 +2217,26 @@ test.serial('set operations (intersect) as function', async (t) => { t.assert(result.length === 0); t.deepEqual(result, []); + + t.throws(() => { + intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (except) from query builder', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -2382,47 +2251,22 @@ test.serial('set operations (except) from query builder', async (t) => { t.deepEqual(result, [ { id: 1, name: 'New York' }, ]); + + t.throws(() => { + db + .select() + .from(citiesTable).except( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + }); }); test.serial('set operations (except) as function', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await except( db @@ -2442,47 +2286,26 @@ test.serial('set operations (except) as function', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + except( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (mixed) from query builder', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table users2 ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -2502,47 +2325,27 @@ test.serial('set operations (mixed) from query builder', async (t) => { { id: 1, name: 'New York' }, { id: 2, name: 'London' }, ]); + + t.throws(() => { + db + .select() + .from(citiesTable).except( + ({ unionAll }) => + unionAll( + db + .select() + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).where(eq(citiesTable.id, 2)), + ), + ); + }); }); test.serial('set operations (mixed all) as function', async (t) => { const { db } = t.context; - await db.run(sql`drop table if exists users2`); - await db.run(sql`drop table if exists cities`); - await db.run( - sql` - create table \`cities\` ( - id integer primary key, - name text not null - ) - `, - ); - await db.run( - sql` - create table \`users2\` ( - id integer primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -2570,4 +2373,23 @@ test.serial('set operations (mixed all) as function', async (t) => { { id: 6, name: 'Jill' }, { id: 8, name: 'Sally' }, ]); + + t.throws(() => { + union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 9032f2108..f25a55328 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -222,6 +222,46 @@ test.beforeEach(async (t) => { ); }); +async function setupSetOperationTest(db: MySql2Database) { + await db.execute(sql`drop table if exists \`users2\``); + await db.execute(sql`drop table if exists \`cities\``); + await db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); + + await db.insert(citiesTable).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); +} + test.serial('table configs: unique third param', async (t) => { const cities1Table = mysqlTable('cities1', { id: serial('id').primaryKey(), @@ -2027,43 +2067,7 @@ test.serial('utc config for datetime', async (t) => { test.serial('set operations (union) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2088,48 +2092,23 @@ test.serial('set operations (union) from query builder', async (t) => { { id: 7, name: 'Mary' }, { id: 8, name: 'Sally' }, ]); -}); - -test.serial('set operations (union) as function', async (t) => { - const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null + // union should throw if selected fields are not in the same order + t.throws(() => + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).union( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table), ) - `, ); +}); - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); +test.serial('set operations (union) as function', async (t) => { + const { db } = t.context; - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -2149,48 +2128,26 @@ test.serial('set operations (union) as function', async (t) => { { id: 1, name: 'New York' }, { id: 1, name: 'John' }, ]); + + t.throws(() => { + union( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (union all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2208,48 +2165,22 @@ test.serial('set operations (union all) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 2, name: 'London' }, ]); + + t.throws(() => { + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2).unionAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).limit(2), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (union all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await unionAll( db @@ -2270,48 +2201,26 @@ test.serial('set operations (union all) as function', async (t) => { { id: 1, name: 'John' }, { id: 1, name: 'John' }, ]); + + t.throws(() => { + unionAll( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (intersect) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2327,48 +2236,22 @@ test.serial('set operations (intersect) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + ); + }); }); test.serial('set operations (intersect) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await intersect( db @@ -2385,48 +2268,26 @@ test.serial('set operations (intersect) as function', async (t) => { t.assert(result.length === 0); t.deepEqual(result, []); + + t.throws(() => { + intersect( + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (intersect all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) @@ -2442,48 +2303,22 @@ test.serial('set operations (intersect all) from query builder', async (t) => { { id: 1, name: 'New York' }, { id: 2, name: 'London' }, ]); + + t.throws(() => { + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).limit(2).intersectAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).limit(2), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (intersect all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await intersectAll( db @@ -2502,48 +2337,26 @@ test.serial('set operations (intersect all) as function', async (t) => { t.deepEqual(result, [ { id: 1, name: 'John' }, ]); + + t.throws(() => { + intersectAll( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (except) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -2563,43 +2376,7 @@ test.serial('set operations (except) from query builder', async (t) => { test.serial('set operations (except) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await except( db @@ -2619,48 +2396,26 @@ test.serial('set operations (except) as function', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + except( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable), + db + .select({ id: citiesTable.id, name: citiesTable.name }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (except all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -2676,48 +2431,22 @@ test.serial('set operations (except all) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select() + .from(citiesTable).exceptAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).where(eq(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (except all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await exceptAll( db @@ -2741,48 +2470,26 @@ test.serial('set operations (except all) as function', async (t) => { { id: 6, name: 'Jill' }, { id: 7, name: 'Mary' }, ]); + + t.throws(() => { + exceptAll( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gt(users2Table.id, 7)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (mixed) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -2801,48 +2508,26 @@ test.serial('set operations (mixed) from query builder', async (t) => { t.deepEqual(result, [ { id: 1, name: 'New York' }, ]); + + t.throws(() => { + db + .select() + .from(citiesTable).except( + ({ unionAll }) => + unionAll( + db + .select({ name: citiesTable.name, id: citiesTable.id }) + .from(citiesTable).where(gt(citiesTable.id, 1)), + db.select().from(citiesTable).where(eq(citiesTable.id, 2)), + ), + ); + }); }); test.serial('set operations (mixed all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -2870,4 +2555,22 @@ test.serial('set operations (mixed all) as function', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select().from(citiesTable).where(gt(citiesTable.id, 1)), + ); + }); }); diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index d69f12555..eaad3c4d6 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -292,6 +292,45 @@ test.beforeEach(async (t) => { ); }); +async function setupSetOperationTest(db: NodePgDatabase) { + await db.execute(sql`drop table if exists users2`); + await db.execute(sql`drop table if exists cities`); + await db.execute( + sql` + create table cities ( + id serial primary key, + name text not null + ) + `, + ); + await db.execute( + sql` + create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + ) + `, + ); + + await db.insert(cities2Table).values([ + { id: 1, name: 'New York' }, + { id: 2, name: 'London' }, + { id: 3, name: 'Tampa' }, + ]); + + await db.insert(users2Table).values([ + { id: 1, name: 'John', cityId: 1 }, + { id: 2, name: 'Jane', cityId: 2 }, + { id: 3, name: 'Jack', cityId: 3 }, + { id: 4, name: 'Peter', cityId: 3 }, + { id: 5, name: 'Ben', cityId: 2 }, + { id: 6, name: 'Jill', cityId: 1 }, + { id: 7, name: 'Mary', cityId: 2 }, + { id: 8, name: 'Sally', cityId: 1 }, + ]); +} + test.serial('table configs: unique third param', async (t) => { const cities1Table = pgTable('cities1', { id: serial('id').primaryKey(), @@ -2520,42 +2559,7 @@ test.serial('array operators', async (t) => { test.serial('set operations (union) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: cities2Table.id, name: citiesTable.name }) @@ -2580,47 +2584,23 @@ test.serial('set operations (union) from query builder', async (t) => { { id: 8, name: 'Sally' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ id: cities2Table.id, name: citiesTable.name, name2: users2Table.name }) + .from(cities2Table).union( + // @ts-expect-error + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (union) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -2640,47 +2620,26 @@ test.serial('set operations (union) as function', async (t) => { { id: 1, name: 'John' }, { id: 1, name: 'New York' }, ]); + + t.throws(() => { + union( + db + .select({ name: citiesTable.name, id: cities2Table.id }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (union all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: cities2Table.id, name: citiesTable.name }) @@ -2698,47 +2657,22 @@ test.serial('set operations (union all) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 2, name: 'London' }, ]); + + t.throws(() => { + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2).unionAll( + db + .select({ name: citiesTable.name, id: cities2Table.id }) + .from(cities2Table).limit(2), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (union all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await unionAll( db @@ -2759,47 +2693,26 @@ test.serial('set operations (union all) as function', async (t) => { { id: 1, name: 'John' }, { id: 1, name: 'John' }, ]); + + t.throws(() => { + unionAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (intersect) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: cities2Table.id, name: citiesTable.name }) @@ -2815,47 +2728,23 @@ test.serial('set operations (intersect) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).intersect( + // @ts-expect-error + db + .select({ id: cities2Table.id, name: citiesTable.name, id2: cities2Table.id }) + .from(cities2Table).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`name`)); + }); }); test.serial('set operations (intersect) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await intersect( db @@ -2872,47 +2761,26 @@ test.serial('set operations (intersect) as function', async (t) => { t.assert(result.length === 0); t.deepEqual(result, []); + + t.throws(() => { + intersect( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (intersect all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select({ id: cities2Table.id, name: citiesTable.name }) @@ -2928,47 +2796,22 @@ test.serial('set operations (intersect all) from query builder', async (t) => { { id: 1, name: 'New York' }, { id: 2, name: 'London' }, ]); + + t.throws(() => { + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).limit(2).intersectAll( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(cities2Table).limit(2), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (intersect all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await intersectAll( db @@ -2987,47 +2830,26 @@ test.serial('set operations (intersect all) as function', async (t) => { t.deepEqual(result, [ { id: 1, name: 'John' }, ]); + + t.throws(() => { + intersectAll( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ); + }); }); test.serial('set operations (except) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -3042,47 +2864,22 @@ test.serial('set operations (except) from query builder', async (t) => { t.deepEqual(result, [ { id: 1, name: 'New York' }, ]); + + t.throws(() => { + db + .select() + .from(cities2Table).except( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(cities2Table).where(gt(citiesTable.id, 1)), + ); + }); }); test.serial('set operations (except) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await except( db @@ -3102,47 +2899,26 @@ test.serial('set operations (except) as function', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + except( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (except all) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -3158,47 +2934,22 @@ test.serial('set operations (except all) from query builder', async (t) => { { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); + + t.throws(() => { + db + .select({ name: cities2Table.name, id: cities2Table.id }) + .from(cities2Table).exceptAll( + db + .select({ id: cities2Table.id, name: citiesTable.name }) + .from(cities2Table).where(eq(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (except all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await exceptAll( db @@ -3222,47 +2973,26 @@ test.serial('set operations (except all) as function', async (t) => { { id: 6, name: 'Jill' }, { id: 7, name: 'Mary' }, ]); + + t.throws(() => { + exceptAll( + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gt(users2Table.id, 7)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); test.serial('set operations (mixed) from query builder', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await db .select() @@ -3281,47 +3011,26 @@ test.serial('set operations (mixed) from query builder', async (t) => { t.deepEqual(result, [ { id: 1, name: 'New York' }, ]); + + t.throws(() => { + db + .select() + .from(cities2Table).except( + ({ unionAll }) => + unionAll( + db + .select({ name: cities2Table.name, id: cities2Table.id }) + .from(cities2Table).where(gt(citiesTable.id, 1)), + db.select().from(cities2Table).where(eq(citiesTable.id, 2)), + ), + ); + }); }); test.serial('set operations (mixed all) as function', async (t) => { const { db } = t.context; - await db.execute(sql`drop table if exists users2`); - await db.execute(sql`drop table if exists cities`); - await db.execute( - sql` - create table cities ( - id serial primary key, - name text not null - ) - `, - ); - await db.execute( - sql` - create table users2 ( - id serial primary key, - name text not null, - city_id integer references cities(id) - ) - `, - ); - - await db.insert(cities2Table).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); + await setupSetOperationTest(db); const result = await union( db @@ -3349,4 +3058,22 @@ test.serial('set operations (mixed all) as function', async (t) => { { id: 6, name: 'Jill' }, { id: 8, name: 'Sally' }, ]); + + t.throws(() => { + union( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 1)), + except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ name: users2Table.name, id: users2Table.id }) + .from(users2Table).where(eq(users2Table.id, 7)), + ), + db + .select().from(cities2Table).where(gt(citiesTable.id, 1)), + ).orderBy(asc(sql`id`)); + }); }); From 9281dc292ddff0ee14675f4fda512916a157333d Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 23 Sep 2023 20:16:36 -0400 Subject: [PATCH 18/34] [All] code cleanup and eslint rules fix --- .../query-builders/set-operators.ts | 196 +++----------- .../pg-core/query-builders/set-operators.ts | 195 +++----------- .../query-builders/set-operators.ts | 246 +++--------------- drizzle-orm/src/utils.ts | 4 +- 4 files changed, 99 insertions(+), 542 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 5c7c3cf0a..a1d7c95dc 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -117,65 +117,32 @@ export abstract class MySqlSetOperatorBuilder< fields: this.config.fields, }; } - union[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; - return new MySqlSetOperator('union', false, this, rightSelectOrig); - } - - unionAll[]>>( + setOperator( + type: SetOperator, + isAll: boolean, + ): []>>( rightSelect: | SetOperatorRightSelect | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; - - return new MySqlSetOperator('union', true, this, rightSelectOrig); - } - - intersect[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; - - return new MySqlSetOperator('intersect', false, this, rightSelectOrig); + ) => MySqlSetOperator { + return (rightSelect) => { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + return new MySqlSetOperator(type, isAll, this, rightSelectOrig); + }; } - intersectAll[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + union = this.setOperator('union', false); - return new MySqlSetOperator('intersect', true, this, rightSelectOrig); - } + unionAll = this.setOperator('union', true); - except[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + intersect = this.setOperator('intersect', false); - return new MySqlSetOperator('except', false, this, rightSelectOrig); - } + intersectAll = this.setOperator('intersect', true); - exceptAll[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ): MySqlSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; + except = this.setOperator('except', false); - return new MySqlSetOperator('except', true, this, rightSelectOrig); - } + exceptAll = this.setOperator('except', true); abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; abstract orderBy(...columns: (MySqlColumn | SQL | SQL.Aliased)[]): this; @@ -350,53 +317,7 @@ export class MySqlSetOperator< applyMixins(MySqlSetOperatorBuilder, [QueryPromise]); -export function union< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('union', false, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return union(new MySqlSetOperator('union', false, leftSelect, rightSelect), select, ...rest); -} - -export function unionAll< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('union', true, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return unionAll(new MySqlSetOperator('union', true, leftSelect, rightSelect), select, ...rest); -} - -export function intersect< +function setOperator(type: SetOperator, isAll: boolean): < TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -408,82 +329,27 @@ export function intersect< leftSelect: MySqlSetOperatorBuilder, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('intersect', false, leftSelect, rightSelect); - } +) => MySqlSetOperator { + return (leftSelect, rightSelect, ...restSelects) => { + if (restSelects.length === 0) { + return new MySqlSetOperator(type, isAll, leftSelect, rightSelect); + } - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); - return intersect(new MySqlSetOperator('intersect', false, leftSelect, rightSelect!), select, ...rest); + return setOperator(type, isAll)(new MySqlSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + }; } -export function intersectAll< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('intersect', true, leftSelect, rightSelect); - } +export const union = setOperator('union', false); - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); +export const unionAll = setOperator('union', true); - return intersectAll(new MySqlSetOperator('intersect', true, leftSelect, rightSelect!), select, ...rest); -} +export const intersect = setOperator('intersect', false); -export function except< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('except', false, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); +export const intersectAll = setOperator('intersect', true); - return except(new MySqlSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); -} +export const except = setOperator('except', false); -export function exceptAll< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): MySqlSetOperator { - if (restSelects.length === 0) { - return new MySqlSetOperator('except', false, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return exceptAll(new MySqlSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); -} +export const exceptAll = setOperator('except', true); diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 03293cbf9..e925ebad1 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -97,65 +97,33 @@ export abstract class PgSetOperatorBuilder< fields: this.config.fields, }; } - union[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - return new PgSetOperator('union', false, this, rightSelectOrig); - } - - unionAll[]>>( + setOperator( + type: SetOperator, + isAll: boolean, + ): []>>( rightSelect: | SetOperatorRightSelect | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - - return new PgSetOperator('union', true, this, rightSelectOrig); - } + ) => PgSetOperator { + return (rightSelect) => { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - intersect[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - - return new PgSetOperator('intersect', false, this, rightSelectOrig); + return new PgSetOperator(type, isAll, this, rightSelectOrig); + }; } - intersectAll[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + union = this.setOperator('union', false); - return new PgSetOperator('intersect', true, this, rightSelectOrig); - } + unionAll = this.setOperator('union', true); - except[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + intersect = this.setOperator('intersect', false); - return new PgSetOperator('except', false, this, rightSelectOrig); - } + intersectAll = this.setOperator('intersect', true); - exceptAll[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ): PgSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; + except = this.setOperator('except', false); - return new PgSetOperator('except', true, this, rightSelectOrig); - } + exceptAll = this.setOperator('except', true); abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; abstract orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; @@ -348,7 +316,7 @@ export class PgSetOperator< applyMixins(PgSetOperator, [QueryPromise]); -export function union< +function setOperator(type: SetOperator, isAll: boolean): < THKT extends PgSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, @@ -360,128 +328,27 @@ export function union< leftSelect: PgSetOperatorBuilder, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('union', false, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return union(new PgSetOperator('union', false, leftSelect, rightSelect), select, ...rest); -} - -export function unionAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('union', true, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return unionAll(new PgSetOperator('union', true, leftSelect, rightSelect), select, ...rest); -} - -export function intersect< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('intersect', false, leftSelect, rightSelect); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return intersect(new PgSetOperator('intersect', false, leftSelect, rightSelect!), select, ...rest); -} - -export function intersectAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('intersect', true, leftSelect, rightSelect); - } +) => PgSetOperator { + return (leftSelect, rightSelect, ...restSelects) => { + if (restSelects.length === 0) { + return new PgSetOperator(type, isAll, leftSelect, rightSelect); + } - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); - return intersectAll(new PgSetOperator('intersect', true, leftSelect, rightSelect!), select, ...rest); + return setOperator(type, isAll)(new PgSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + }; } -export function except< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('except', false, leftSelect, rightSelect); - } +export const union = setOperator('union', false); - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); +export const unionAll = setOperator('union', true); - return except(new PgSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); -} +export const intersect = setOperator('intersect', false); -export function exceptAll< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): PgSetOperator { - if (restSelects.length === 0) { - return new PgSetOperator('except', false, leftSelect, rightSelect); - } +export const intersectAll = setOperator('intersect', true); - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); +export const except = setOperator('except', false); - return exceptAll(new PgSetOperator('except', false, leftSelect, rightSelect!), select, ...rest); -} +export const exceptAll = setOperator('except', true); diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 635249786..239721209 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -95,65 +95,34 @@ export abstract class SQLiteSetOperatorBuilder< fields: this.config.fields, }; } - union[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ): SQLiteSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - - return new SQLiteSetOperator( - 'union', - false, - this, - rightSelectOrig, - ); - } - unionAll[]>>( + setOperator( + type: SetOperator, + isAll: boolean, + ): []>>( rightSelect: | SetOperatorRightSelect | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ): SQLiteSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - - return new SQLiteSetOperator( - 'union', - true, - this, - rightSelectOrig, - ); + ) => SQLiteSetOperator { + return (rightSelect) => { + const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; + + return new SQLiteSetOperator( + type, + isAll, + this, + rightSelectOrig, + ); + }; } - intersect[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ): SQLiteSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - - return new SQLiteSetOperator( - 'intersect', - false, - this, - rightSelectOrig, - ); - } + union = this.setOperator('union', false); - except[]>>( - rightSelect: - | SetOperatorRightSelect - | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ): SQLiteSetOperator { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - - return new SQLiteSetOperator( - 'except', - false, - this, - rightSelectOrig, - ); - } + unionAll = this.setOperator('union', true); + + intersect = this.setOperator('intersect', false); + + except = this.setOperator('except', false); abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; abstract orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; @@ -365,7 +334,7 @@ export class SQLiteSetOperator< applyMixins(SQLiteSetOperator, [QueryPromise]); -export function union< +function setOperator(type: SetOperator, isAll: boolean): < THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, TResultType extends 'sync' | 'async', @@ -387,168 +356,23 @@ export function union< >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect> -): SQLiteSetOperator { - if (restSelects.length === 0) { - return new SQLiteSetOperator( - 'union', - false, - leftSelect, - rightSelect, - ); - } +) => SQLiteSetOperator { + return (leftSelect, rightSelect, ...restSelects) => { + if (restSelects.length === 0) { + return new SQLiteSetOperator(type, isAll, leftSelect, rightSelect); + } - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return union( - new SQLiteSetOperator( - 'union', - false, - leftSelect, - rightSelect, - ), - select, - ...rest, - ); -} + const [select, ...rest] = restSelects; + if (!select) throw new Error('Cannot pass undefined values to any set operator'); -export function unionAll< - THKT extends SQLiteSelectHKTBase, - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: SQLiteSetOperatorBuilder< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap - >, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): SQLiteSetOperator { - if (restSelects.length === 0) { - return new SQLiteSetOperator( - 'union', - true, - leftSelect, - rightSelect, - ); - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return unionAll( - new SQLiteSetOperator( - 'union', - true, - leftSelect, - rightSelect, - ), - select, - ...rest, - ); + return setOperator(type, isAll)(new SQLiteSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + }; } -export function intersect< - THKT extends SQLiteSelectHKTBase, - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: SQLiteSetOperatorBuilder< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap - >, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): SQLiteSetOperator { - if (restSelects.length === 0) { - return new SQLiteSetOperator( - 'intersect', - false, - leftSelect, - rightSelect, - ); - } +export const union = setOperator('union', false); - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return intersect( - new SQLiteSetOperator( - 'intersect', - false, - leftSelect, - rightSelect!, - ), - select, - ...rest, - ); -} +export const unionAll = setOperator('union', true); -export function except< - THKT extends SQLiteSelectHKTBase, - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: SQLiteSetOperatorBuilder< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap - >, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -): SQLiteSetOperator { - if (restSelects.length === 0) { - return new SQLiteSetOperator( - 'except', - false, - leftSelect, - rightSelect, - ); - } +export const intersect = setOperator('intersect', false); - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return except( - new SQLiteSetOperator( - 'except', - false, - leftSelect, - rightSelect!, - ), - select, - ...rest, - ); -} +export const except = setOperator('except', false); diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 4a1d40939..02420d261 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -99,8 +99,8 @@ export function haveSameKeys(left: Record, right: Record Date: Sun, 24 Sep 2023 01:26:56 -0400 Subject: [PATCH 19/34] [All] Added offset --- .../query-builders/set-operators.ts | 15 ++++++- .../pg-core/query-builders/set-operators.ts | 15 ++++++- .../query-builders/set-operators.ts | 15 ++++++- integration-tests/tests/libsql.test.ts | 23 +++------- integration-tests/tests/mysql.test.ts | 45 ++++++++----------- integration-tests/tests/pg.test.ts | 24 +++------- 6 files changed, 71 insertions(+), 66 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index a1d7c95dc..7c42d6db9 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -103,6 +103,7 @@ export abstract class MySqlSetOperatorBuilder< fields: Record; limit?: number | Placeholder; orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ abstract readonly session: MySqlSession | undefined; @@ -118,7 +119,7 @@ export abstract class MySqlSetOperatorBuilder< }; } - setOperator( + private setOperator( type: SetOperator, isAll: boolean, ): []>>( @@ -148,6 +149,8 @@ export abstract class MySqlSetOperatorBuilder< abstract orderBy(...columns: (MySqlColumn | SQL | SQL.Aliased)[]): this; abstract limit(limit: number): this; + + abstract offset(offset: number | Placeholder): this; } export class MySqlSetOperator< @@ -171,6 +174,7 @@ export class MySqlSetOperator< fields: Record; limit?: number | Placeholder; orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ readonly session: MySqlSession | undefined; @@ -237,6 +241,11 @@ export class MySqlSetOperator< return this; } + offset(offset: number | Placeholder) { + this.config.offset = offset; + return this; + } + override getSQL(): SQL { const leftChunk = sql`(${this.leftSelect.getSQL()}) `; const rightChunk = sql`(${this.rightSelect.getSQL()})`; @@ -272,7 +281,9 @@ export class MySqlSetOperator< const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; } toSQL(): Query { diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index e925ebad1..0157e4982 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -83,6 +83,7 @@ export abstract class PgSetOperatorBuilder< fields: Record; limit?: number | Placeholder; orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ protected abstract readonly session: PgSession | undefined; @@ -98,7 +99,7 @@ export abstract class PgSetOperatorBuilder< }; } - setOperator( + private setOperator( type: SetOperator, isAll: boolean, ): []>>( @@ -129,6 +130,8 @@ export abstract class PgSetOperatorBuilder< abstract orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; abstract limit(limit: number): this; + + abstract offset(offset: number | Placeholder): this; } export interface PgSetOperator< @@ -168,6 +171,7 @@ export class PgSetOperator< fields: Record; limit?: number | Placeholder; orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ readonly session: PgSession | undefined; @@ -230,6 +234,11 @@ export class PgSetOperator< return this; } + offset(offset: number | Placeholder) { + this.config.offset = offset; + return this; + } + toSQL(): Query { const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); return rest; @@ -270,7 +279,9 @@ export class PgSetOperator< const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; } private _prepare(name?: string): PreparedQuery< diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 239721209..ce56ae5d6 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -81,6 +81,7 @@ export abstract class SQLiteSetOperatorBuilder< fields: Record; limit?: number | Placeholder; orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ protected abstract readonly session: SQLiteSession | undefined; @@ -96,7 +97,7 @@ export abstract class SQLiteSetOperatorBuilder< }; } - setOperator( + private setOperator( type: SetOperator, isAll: boolean, ): []>>( @@ -128,6 +129,8 @@ export abstract class SQLiteSetOperatorBuilder< abstract orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; abstract limit(limit: number): this; + + abstract offset(offset: number | Placeholder): this; } export interface SQLiteSetOperator< @@ -175,6 +178,7 @@ export class SQLiteSetOperator< fields: Record; limit?: number | Placeholder; orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; }; /* @internal */ readonly session: SQLiteSession | undefined; @@ -245,6 +249,11 @@ export class SQLiteSetOperator< return this; } + offset(offset: number | Placeholder) { + this.config.offset = offset; + return this; + } + toSQL(): Query { const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); return rest; @@ -285,7 +294,9 @@ export class SQLiteSetOperator< const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}`; + const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; } prepare(isOneTimeQuery?: boolean): PreparedQuery< diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index 1414a96ce..9c4748b80 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -2023,22 +2023,16 @@ test.serial('set operations (union) from query builder', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table), - ).orderBy(asc(sql`name`)); + ).orderBy(asc(sql`name`)).limit(5).offset(5); - t.assert(result.length === 11); + t.assert(result.length === 5); t.deepEqual(result, [ - { id: 5, name: 'Ben' }, - { id: 3, name: 'Jack' }, - { id: 2, name: 'Jane' }, - { id: 6, name: 'Jill' }, - { id: 1, name: 'John' }, { id: 2, name: 'London' }, { id: 7, name: 'Mary' }, { id: 1, name: 'New York' }, { id: 4, name: 'Peter' }, { id: 8, name: 'Sally' }, - { id: 3, name: 'Tampa' }, ]); t.throws(() => { @@ -2102,12 +2096,11 @@ test.serial('set operations (union all) from query builder', async (t) => { db .select({ id: citiesTable.id, name: citiesTable.name }) .from(citiesTable), - ).orderBy(asc(citiesTable.id)); + ).orderBy(asc(citiesTable.id)).limit(5).offset(1); - t.assert(result.length === 6); + t.assert(result.length === 5); t.deepEqual(result, [ - { id: 1, name: 'New York' }, { id: 1, name: 'New York' }, { id: 2, name: 'London' }, { id: 2, name: 'London' }, @@ -2122,7 +2115,7 @@ test.serial('set operations (union all) from query builder', async (t) => { db .select({ name: citiesTable.name, id: citiesTable.id }) .from(citiesTable), - ).orderBy(asc(citiesTable.id)); + ).orderBy(asc(citiesTable.id)).limit(5).offset(1); }); }); @@ -2361,17 +2354,15 @@ test.serial('set operations (mixed all) as function', async (t) => { ), db .select().from(citiesTable).where(gt(citiesTable.id, 1)), - ).orderBy(asc(sql`id`)); + ).orderBy(asc(sql`id`)).limit(4).offset(1); - t.assert(result.length === 6); + t.assert(result.length === 4); t.deepEqual(result, [ - { id: 1, name: 'John' }, { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, { id: 5, name: 'Ben' }, { id: 6, name: 'Jill' }, - { id: 8, name: 'Sally' }, ]); t.throws(() => { diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index f25a55328..d7eab61b1 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -2075,9 +2075,9 @@ test.serial('set operations (union) from query builder', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table), - ); + ).limit(8); - t.assert(result.length === 11); + t.assert(result.length === 8); t.deepEqual(result, [ { id: 1, name: 'New York' }, @@ -2088,9 +2088,6 @@ test.serial('set operations (union) from query builder', async (t) => { { id: 3, name: 'Jack' }, { id: 4, name: 'Peter' }, { id: 5, name: 'Ben' }, - { id: 6, name: 'Jill' }, - { id: 7, name: 'Mary' }, - { id: 8, name: 'Sally' }, ]); // union should throw if selected fields are not in the same order @@ -2155,15 +2152,14 @@ test.serial('set operations (union all) from query builder', async (t) => { db .select({ id: citiesTable.id, name: citiesTable.name }) .from(citiesTable).limit(2), - ).orderBy(asc(sql`id`)); + ).orderBy(asc(sql`id`)).limit(3); - t.assert(result.length === 4); + t.assert(result.length === 3); t.deepEqual(result, [ { id: 1, name: 'New York' }, { id: 1, name: 'New York' }, { id: 2, name: 'London' }, - { id: 2, name: 'London' }, ]); t.throws(() => { @@ -2192,14 +2188,12 @@ test.serial('set operations (union all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(1); - t.assert(result.length === 3); + t.assert(result.length === 1); t.deepEqual(result, [ { id: 1, name: 'New York' }, - { id: 1, name: 'John' }, - { id: 1, name: 'John' }, ]); t.throws(() => { @@ -2213,7 +2207,7 @@ test.serial('set operations (union all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(1); }); }); @@ -2263,7 +2257,7 @@ test.serial('set operations (intersect) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(1); t.assert(result.length === 0); @@ -2280,7 +2274,7 @@ test.serial('set operations (intersect) as function', async (t) => { db .select({ name: users2Table.name, id: users2Table.id }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(1); }); }); @@ -2388,7 +2382,7 @@ test.serial('set operations (except) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(3); t.assert(result.length === 2); @@ -2408,7 +2402,7 @@ test.serial('set operations (except) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(3); }); }); @@ -2458,7 +2452,7 @@ test.serial('set operations (except all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(6); t.assert(result.length === 6); @@ -2482,7 +2476,7 @@ test.serial('set operations (except all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ); + ).limit(6); }); }); @@ -2500,13 +2494,14 @@ test.serial('set operations (mixed) from query builder', async (t) => { .select() .from(citiesTable).where(gt(citiesTable.id, 1)), db.select().from(citiesTable).where(eq(citiesTable.id, 2)), - ), + ).orderBy(asc(citiesTable.id)).limit(1).offset(1), ); - t.assert(result.length === 1); + t.assert(result.length === 2); t.deepEqual(result, [ { id: 1, name: 'New York' }, + { id: 3, name: 'Tampa' }, ]); t.throws(() => { @@ -2540,18 +2535,16 @@ test.serial('set operations (mixed all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 7)), - ), + ).limit(1), db .select().from(citiesTable).where(gt(citiesTable.id, 1)), ); - t.assert(result.length === 6); + t.assert(result.length === 4); t.deepEqual(result, [ { id: 1, name: 'John' }, { id: 5, name: 'Ben' }, - { id: 6, name: 'Jill' }, - { id: 8, name: 'Sally' }, { id: 2, name: 'London' }, { id: 3, name: 'Tampa' }, ]); @@ -2568,7 +2561,7 @@ test.serial('set operations (mixed all) as function', async (t) => { db .select({ name: users2Table.name, id: users2Table.id }) .from(users2Table).where(eq(users2Table.id, 7)), - ), + ).limit(1), db .select().from(citiesTable).where(gt(citiesTable.id, 1)), ); diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index eaad3c4d6..d27b42ee9 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -2567,22 +2567,13 @@ test.serial('set operations (union) from query builder', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table), - ).orderBy(asc(sql`name`)); + ).orderBy(asc(sql`name`)).limit(2).offset(1); - t.assert(result.length === 11); + t.assert(result.length === 2); t.deepEqual(result, [ - { id: 5, name: 'Ben' }, { id: 3, name: 'Jack' }, { id: 2, name: 'Jane' }, - { id: 6, name: 'Jill' }, - { id: 1, name: 'John' }, - { id: 2, name: 'London' }, - { id: 7, name: 'Mary' }, - { id: 1, name: 'New York' }, - { id: 4, name: 'Peter' }, - { id: 8, name: 'Sally' }, - { id: 3, name: 'Tampa' }, ]); t.throws(() => { @@ -2612,12 +2603,11 @@ test.serial('set operations (union) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ).orderBy(asc(sql`name`)); + ).orderBy(asc(sql`name`)).limit(1).offset(1); - t.assert(result.length === 2); + t.assert(result.length === 1); t.deepEqual(result, [ - { id: 1, name: 'John' }, { id: 1, name: 'New York' }, ]); @@ -2961,13 +2951,11 @@ test.serial('set operations (except all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ).orderBy(asc(sql`id`)); + ).orderBy(asc(sql`id`)).limit(5).offset(2); - t.assert(result.length === 6); + t.assert(result.length === 4); t.deepEqual(result, [ - { id: 2, name: 'Jane' }, - { id: 3, name: 'Jack' }, { id: 4, name: 'Peter' }, { id: 5, name: 'Ben' }, { id: 6, name: 'Jill' }, From 85be035c8589a44bf9982e1526d9b1a3e1db5eaf Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sun, 24 Sep 2023 01:54:46 -0400 Subject: [PATCH 20/34] [All] added `.as()` method to create subqueries from set operations. Added tests --- .../query-builders/set-operators.ts | 11 +++++++ .../pg-core/query-builders/set-operators.ts | 11 +++++++ .../query-builders/set-operators.ts | 11 +++++++ drizzle-orm/type-tests/mysql/subquery.ts | 12 ++++++++ drizzle-orm/type-tests/pg/subquery.ts | 12 ++++++++ drizzle-orm/type-tests/sqlite/subquery.ts | 12 ++++++++ integration-tests/tests/libsql.test.ts | 16 ++++++---- integration-tests/tests/mysql.test.ts | 29 ++++++++++--------- integration-tests/tests/pg.test.ts | 19 +++++++----- 9 files changed, 106 insertions(+), 27 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 7c42d6db9..aa50ed0f9 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -9,6 +9,7 @@ import { SelectionProxyHandler, SQL, sql, + Subquery, type ValidateShape, type ValueOrArray, } from '~/index.ts'; @@ -28,6 +29,7 @@ import type { import { type ColumnsSelection } from '~/view.ts'; import { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; +import type { SubqueryWithSelection } from '../subquery.ts'; type SetOperator = 'union' | 'intersect' | 'except'; @@ -324,6 +326,15 @@ export class MySqlSetOperator< }; iterator = this.createIterator(); + + as( + alias: TAlias, + ): SubqueryWithSelection, TAlias, 'mysql'> { + return new Proxy( + new Subquery(this.getSQL(), this.config.fields, alias), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as SubqueryWithSelection, TAlias, 'mysql'>; + } } applyMixins(MySqlSetOperatorBuilder, [QueryPromise]); diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 0157e4982..e56b782f4 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -6,6 +6,7 @@ import { SelectionProxyHandler, SQL, sql, + Subquery, type ValueOrArray, } from '~/index.ts'; import type { PgSession, PreparedQuery, PreparedQueryConfig } from '~/pg-core/session.ts'; @@ -22,6 +23,7 @@ import { applyMixins, haveSameKeys, type ValidateShape } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; +import type { SubqueryWithSelection } from '../subquery.ts'; import type { PgSelectHKTBase } from './select.types.ts'; type SetOperator = 'union' | 'intersect' | 'except'; @@ -323,6 +325,15 @@ export class PgSetOperator< return this._prepare().execute(placeholderValues); }); }; + + as( + alias: TAlias, + ): SubqueryWithSelection, TAlias> { + return new Proxy( + new Subquery(this.getSQL(), this.config.fields, alias), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as SubqueryWithSelection, TAlias>; + } } applyMixins(PgSetOperator, [QueryPromise]); diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index ce56ae5d6..85d6e1589 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -6,6 +6,7 @@ import { SelectionProxyHandler, SQL, sql, + Subquery, type ValueOrArray, } from '~/index.ts'; import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; @@ -21,6 +22,7 @@ import { applyMixins, haveSameKeys, type PromiseOf, type ValidateShape } from '~ import { type ColumnsSelection } from '~/view.ts'; import { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; +import type { SubqueryWithSelection } from '../subquery.ts'; import type { SQLiteSelectHKTBase } from './select.types.ts'; type SetOperator = 'union' | 'intersect' | 'except'; @@ -341,6 +343,15 @@ export class SQLiteSetOperator< async execute(): Promise[]> { return this.all() as PromiseOf>; } + + as( + alias: TAlias, + ): SubqueryWithSelection, TAlias> { + return new Proxy( + new Subquery(this.getSQL(), this.config.fields, alias), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), + ) as SubqueryWithSelection, TAlias>; + } } applyMixins(SQLiteSetOperator, [QueryPromise]); diff --git a/drizzle-orm/type-tests/mysql/subquery.ts b/drizzle-orm/type-tests/mysql/subquery.ts index 920520e45..3408fbc73 100644 --- a/drizzle-orm/type-tests/mysql/subquery.ts +++ b/drizzle-orm/type-tests/mysql/subquery.ts @@ -83,3 +83,15 @@ Expect< const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); Expect ? true : false>; } + +const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); + +const resUnion = await db.select().from(sqUnion); + +Expect< + Equal<{ + id: number; + name: string | null; + authorId: number | null; + }[], typeof resUnion> +>; diff --git a/drizzle-orm/type-tests/pg/subquery.ts b/drizzle-orm/type-tests/pg/subquery.ts index 0af17ccf8..47193c5a4 100644 --- a/drizzle-orm/type-tests/pg/subquery.ts +++ b/drizzle-orm/type-tests/pg/subquery.ts @@ -83,3 +83,15 @@ Expect< const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); Expect ? true : false>; } + +const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); + +const resUnion = await db.select().from(sqUnion); + +Expect< + Equal<{ + id: number; + name: string | null; + authorId: number | null; + }[], typeof resUnion> +>; diff --git a/drizzle-orm/type-tests/sqlite/subquery.ts b/drizzle-orm/type-tests/sqlite/subquery.ts index 87926864e..f67a948cb 100644 --- a/drizzle-orm/type-tests/sqlite/subquery.ts +++ b/drizzle-orm/type-tests/sqlite/subquery.ts @@ -84,3 +84,15 @@ Expect< const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); Expect ? true : false>; } + +const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); + +const resUnion = await db.select().from(sqUnion); + +Expect< + Equal<{ + id: number; + name: string | null; + authorId: number | null; + }[], typeof resUnion> +>; diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index 9c4748b80..8d87193a6 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -2012,18 +2012,20 @@ test.serial('select + .get() for empty result', async (t) => { await db.run(sql`drop table ${users}`); }); -test.serial('set operations (union) from query builder', async (t) => { +test.serial('set operations (union) from query builder with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); - const result = await db + const sq = db .select({ id: citiesTable.id, name: citiesTable.name }) .from(citiesTable).union( db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table), - ).orderBy(asc(sql`name`)).limit(5).offset(5); + ).orderBy(asc(sql`name`)).as('sq'); + + const result = await db.select().from(sq).limit(5).offset(5); t.assert(result.length === 5); @@ -2335,12 +2337,12 @@ test.serial('set operations (mixed) from query builder', async (t) => { }); }); -test.serial('set operations (mixed all) as function', async (t) => { +test.serial('set operations (mixed all) as function with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); - const result = await union( + const sq = union( db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), @@ -2354,7 +2356,9 @@ test.serial('set operations (mixed all) as function', async (t) => { ), db .select().from(citiesTable).where(gt(citiesTable.id, 1)), - ).orderBy(asc(sql`id`)).limit(4).offset(1); + ).orderBy(asc(sql`id`)).as('sq'); + + const result = await db.select().from(sq).limit(4).offset(1); t.assert(result.length === 4); diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index d7eab61b1..62f41d92a 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -2064,17 +2064,18 @@ test.serial('utc config for datetime', async (t) => { await db.execute(sql`drop table if exists \`datestable\``); }); -test.serial('set operations (union) from query builder', async (t) => { +test.serial('set operations (union) from query builder with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); + const sq = db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).as('sq'); const result = await db .select({ id: citiesTable.id, name: citiesTable.name }) .from(citiesTable).union( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table), + db.select().from(sq), ).limit(8); t.assert(result.length === 8); @@ -2519,23 +2520,25 @@ test.serial('set operations (mixed) from query builder', async (t) => { }); }); -test.serial('set operations (mixed all) as function', async (t) => { +test.serial('set operations (mixed all) as function with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); + const sq = except( + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(gte(users2Table.id, 5)), + db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).where(eq(users2Table.id, 7)), + ).as('sq'); + const result = await union( db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - except( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(gte(users2Table.id, 5)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 7)), - ).limit(1), + db.select().from(sq).limit(1), db .select().from(citiesTable).where(gt(citiesTable.id, 1)), ); diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index d27b42ee9..a545d4c99 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -2556,17 +2556,19 @@ test.serial('array operators', async (t) => { t.deepEqual(withSubQuery, [{ id: 1 }, { id: 3 }, { id: 5 }]); }); -test.serial('set operations (union) from query builder', async (t) => { +test.serial('set operations (union) from query builder with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); + const sq = db + .select({ id: users2Table.id, name: users2Table.name }) + .from(users2Table).as('sq'); + const result = await db .select({ id: cities2Table.id, name: citiesTable.name }) .from(cities2Table).union( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table), + db.select().from(sq), ).orderBy(asc(sql`name`)).limit(2).offset(1); t.assert(result.length === 2); @@ -2977,19 +2979,20 @@ test.serial('set operations (except all) as function', async (t) => { }); }); -test.serial('set operations (mixed) from query builder', async (t) => { +test.serial('set operations (mixed) from query builder with subquery', async (t) => { const { db } = t.context; await setupSetOperationTest(db); + const sq = db + .select() + .from(cities2Table).where(gt(citiesTable.id, 1)).as('sq'); const result = await db .select() .from(cities2Table).except( ({ unionAll }) => unionAll( - db - .select() - .from(cities2Table).where(gt(citiesTable.id, 1)), + db.select().from(sq), db.select().from(cities2Table).where(eq(citiesTable.id, 2)), ), ); From 256a292167236f4d1bec73e98337866f0b1cef19 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sun, 24 Sep 2023 12:43:14 -0400 Subject: [PATCH 21/34] [All] Moved the query construction logic to the each dialect --- drizzle-orm/src/mysql-core/dialect.ts | 49 ++++++++++++ .../query-builders/set-operators.ts | 76 ++++++------------ drizzle-orm/src/pg-core/dialect.ts | 55 ++++++++++++- .../pg-core/query-builders/set-operators.ts | 75 ++++++------------ drizzle-orm/src/sqlite-core/dialect.ts | 55 ++++++++++++- .../query-builders/set-operators.ts | 77 ++++++------------- 6 files changed, 231 insertions(+), 156 deletions(-) diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index ea6f4ceb9..640d30ddf 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -24,6 +24,7 @@ import { MySqlColumn } from './columns/common.ts'; import type { MySqlDeleteConfig } from './query-builders/delete.ts'; import type { MySqlInsertConfig } from './query-builders/insert.ts'; import type { Join, MySqlSelectConfig, SelectedFieldsOrdered } from './query-builders/select.types.ts'; +import type { MySqlSetOperatorConfig } from './query-builders/set-operators.ts'; import type { MySqlUpdateConfig } from './query-builders/update.ts'; import type { MySqlSession } from './session.ts'; import { MySqlTable } from './table.ts'; @@ -334,6 +335,54 @@ export class MySqlDialect { return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`; } + buildSetOperationQuery({ + operator, + isAll, + leftSelect, + rightSelect, + limit, + orderBy, + offset, + }: MySqlSetOperatorConfig): SQL { + const leftChunk = sql`(${leftSelect.getSQL()}) `; + const rightChunk = sql`(${rightSelect.getSQL()})`; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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.raw(orderByUnit.name)); + } else if (is(orderByUnit, SQL)) { + for (let i = 0; i < orderByUnit.queryChunks.length; i++) { + const chunk = orderByUnit.queryChunks[i]; + + if (is(chunk, MySqlColumn)) { + orderByUnit.queryChunks[i] = sql.raw(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(`${operator} ${isAll ? 'all ' : ''}`); + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + } + buildInsertQuery({ table, values, ignore, onConflict }: MySqlInsertConfig): SQL { // const isSingleValue = values.length === 1; const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index aa50ed0f9..ce76a3963 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -1,4 +1,4 @@ -import { entityKind, is } from '~/entity.ts'; +import { entityKind } from '~/entity.ts'; import { applyMixins, haveSameKeys, @@ -7,8 +7,7 @@ import { type Query, QueryPromise, SelectionProxyHandler, - SQL, - sql, + type SQL, Subquery, type ValidateShape, type ValueOrArray, @@ -27,7 +26,7 @@ import type { SelectResult, } from '~/query-builders/select.types.ts'; import { type ColumnsSelection } from '~/view.ts'; -import { MySqlColumn } from '../columns/common.ts'; +import type { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; @@ -71,6 +70,17 @@ type SetOperatorRestSelect< : never[] : TValue; +export interface MySqlSetOperatorConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: MySqlSetOperatorBuilder; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + export interface MySqlSetOperatorBuilder< TTableName extends string | undefined, TSelection extends ColumnsSelection, @@ -172,27 +182,22 @@ export class MySqlSetOperator< static readonly [entityKind]: string = 'MySqlSetOperator'; protected joinsNotNullableMap: Record; - protected config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; + protected config: MySqlSetOperatorConfig; /* @internal */ readonly session: MySqlSession | undefined; protected dialect: MySqlDialect; constructor( - private operator: SetOperator, - private isAll: boolean, - private leftSelect: MySqlSetOperatorBuilder< + operator: SetOperator, + isAll: boolean, + leftSelect: MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, TPreparedQueryHKT, TNullabilityMap >, - private rightSelect: TypedQueryBuilder[]>, + rightSelect: TypedQueryBuilder[]>, ) { super(); @@ -216,6 +221,10 @@ export class MySqlSetOperator< this.joinsNotNullableMap = joinsNotNullableMap; this.config = { fields, + operator, + isAll, + leftSelect, + rightSelect, }; } @@ -248,44 +257,9 @@ export class MySqlSetOperator< return this; } + /** @internal */ override getSQL(): SQL { - const leftChunk = sql`(${this.leftSelect.getSQL()}) `; - const rightChunk = sql`(${this.rightSelect.getSQL()})`; - - let orderBySql; - if (this.config.orderBy && this.config.orderBy.length > 0) { - const orderByValues: SQL[] = []; - - // 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 orderBy of this.config.orderBy) { - if (is(orderBy, MySqlColumn)) { - orderByValues.push(sql.raw(orderBy.name)); - } else if (is(orderBy, SQL)) { - for (let i = 0; i < orderBy.queryChunks.length; i++) { - const chunk = orderBy.queryChunks[i]; - - if (is(chunk, MySqlColumn)) { - orderBy.queryChunks[i] = sql.raw(chunk.name); - } - } - - orderByValues.push(sql`${orderBy}`); - } else { - orderByValues.push(sql`${orderBy}`); - } - } - - orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; - } - - const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; - - const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - - const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; - - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + return this.dialect.buildSetOperationQuery(this.config); } toSQL(): Query { diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 5b255b2bd..0a3bf35e9 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -4,7 +4,12 @@ import { entityKind, is } from '~/entity.ts'; import { DrizzleError } from '~/errors.ts'; import type { MigrationMeta } from '~/migrator.ts'; import { PgColumn, PgDate, PgJson, PgJsonb, PgNumeric, PgTime, PgTimestamp, PgUUID } from '~/pg-core/columns/index.ts'; -import type { PgDeleteConfig, PgInsertConfig, PgUpdateConfig } from '~/pg-core/query-builders/index.ts'; +import type { + PgDeleteConfig, + PgInsertConfig, + PgSetOperationConfig, + PgUpdateConfig, +} from '~/pg-core/query-builders/index.ts'; import type { Join, PgSelectConfig, SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts'; import { PgTable } from '~/pg-core/table.ts'; import { @@ -343,6 +348,54 @@ export class PgDialect { return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`; } + buildSetOperationQuery({ + operator, + isAll, + leftSelect, + rightSelect, + limit, + orderBy, + offset, + }: PgSetOperationConfig): SQL { + const leftChunk = sql`(${leftSelect.getSQL()}) `; + const rightChunk = sql`(${rightSelect.getSQL()})`; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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 singleOrderBy of orderBy) { + if (is(singleOrderBy, PgColumn)) { + orderByValues.push(sql.raw(singleOrderBy.name)); + } else if (is(singleOrderBy, SQL)) { + for (let i = 0; i < singleOrderBy.queryChunks.length; i++) { + const chunk = singleOrderBy.queryChunks[i]; + + if (is(chunk, PgColumn)) { + singleOrderBy.queryChunks[i] = sql.raw(chunk.name); + } + } + + orderByValues.push(sql`${singleOrderBy}`); + } else { + orderByValues.push(sql`${singleOrderBy}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; + } + + const limitSql = limit ? sql` limit ${limit}` : undefined; + + const operatorChunk = sql.raw(`${operator} ${isAll ? 'all ' : ''}`); + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + } + buildInsertQuery({ table, values, onConflict, returning }: PgInsertConfig): SQL { const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index e56b782f4..c4ddccfa1 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -1,11 +1,10 @@ -import { entityKind, is } from '~/entity.ts'; +import { entityKind } from '~/entity.ts'; import { orderSelectedFields, type Placeholder, type Query, SelectionProxyHandler, - SQL, - sql, + type SQL, Subquery, type ValueOrArray, } from '~/index.ts'; @@ -21,7 +20,7 @@ import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; import { applyMixins, haveSameKeys, type ValidateShape } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; -import { PgColumn } from '../columns/common.ts'; +import { type PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; import type { PgSelectHKTBase } from './select.types.ts'; @@ -66,6 +65,17 @@ type SetOperatorRestSelect< : never[] : TValue; +export interface PgSetOperationConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: PgSetOperatorBuilder; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + export abstract class PgSetOperatorBuilder< // eslint-disable-next-line @typescript-eslint/no-unused-vars THKT extends PgSelectHKTBase, @@ -169,21 +179,16 @@ export class PgSetOperator< static readonly [entityKind]: string = 'PgSetOperator'; protected joinsNotNullableMap: Record; - protected config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (PgColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; + protected config: PgSetOperationConfig; /* @internal */ readonly session: PgSession | undefined; protected dialect: PgDialect; constructor( - private operator: SetOperator, - private isAll: boolean, - private leftSelect: PgSetOperatorBuilder, - private rightSelect: TypedQueryBuilder[]>, + operator: SetOperator, + isAll: boolean, + leftSelect: PgSetOperatorBuilder, + rightSelect: TypedQueryBuilder[]>, ) { super(); @@ -207,6 +212,10 @@ export class PgSetOperator< this.joinsNotNullableMap = joinsNotNullableMap; this.config = { fields, + operator, + isAll, + leftSelect, + rightSelect, }; } @@ -247,43 +256,7 @@ export class PgSetOperator< } override getSQL(): SQL { - const leftChunk = sql`(${this.leftSelect.getSQL()}) `; - const rightChunk = sql`(${this.rightSelect.getSQL()})`; - - let orderBySql; - if (this.config.orderBy && this.config.orderBy.length > 0) { - const orderByValues: SQL[] = []; - - // 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 orderBy of this.config.orderBy) { - if (is(orderBy, PgColumn)) { - orderByValues.push(sql.raw(orderBy.name)); - } else if (is(orderBy, SQL)) { - for (let i = 0; i < orderBy.queryChunks.length; i++) { - const chunk = orderBy.queryChunks[i]; - - if (is(chunk, PgColumn)) { - orderBy.queryChunks[i] = sql.raw(chunk.name); - } - } - - orderByValues.push(sql`${orderBy}`); - } else { - orderByValues.push(sql`${orderBy}`); - } - } - - orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)} `; - } - - const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; - - const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - - const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; - - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + return this.dialect.buildSetOperationQuery(this.config); } private _prepare(name?: string): PreparedQuery< diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index cc59b3a2b..6bbefcdc3 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -18,7 +18,12 @@ import { } from '~/relations.ts'; import { and, eq, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; import { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; -import type { SQLiteDeleteConfig, SQLiteInsertConfig, SQLiteUpdateConfig } from '~/sqlite-core/query-builders/index.ts'; +import type { + SQLiteDeleteConfig, + SQLiteInsertConfig, + SQLiteSetOperationConfig, + SQLiteUpdateConfig, +} from '~/sqlite-core/query-builders/index.ts'; import { SQLiteTable } from '~/sqlite-core/table.ts'; import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; @@ -272,6 +277,54 @@ export abstract class SQLiteDialect { return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}`; } + buildSetOperationQuery({ + operator, + isAll, + leftSelect, + rightSelect, + limit, + orderBy, + offset, + }: SQLiteSetOperationConfig): SQL { + const leftChunk = sql`${leftSelect.getSQL()} `; + const rightChunk = sql`${rightSelect.getSQL()}`; + + let orderBySql; + if (orderBy && orderBy.length > 0) { + const orderByValues: SQL[] = []; + + // 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 singleOrderBy of orderBy) { + if (is(singleOrderBy, SQLiteColumn)) { + orderByValues.push(sql.raw(singleOrderBy.name)); + } else if (is(singleOrderBy, SQL)) { + for (let i = 0; i < singleOrderBy.queryChunks.length; i++) { + const chunk = singleOrderBy.queryChunks[i]; + + if (is(chunk, SQLiteColumn)) { + singleOrderBy.queryChunks[i] = sql.raw(chunk.name); + } + } + + orderByValues.push(sql`${singleOrderBy}`); + } else { + orderByValues.push(sql`${singleOrderBy}`); + } + } + + orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)}`; + } + + const limitSql = limit ? sql` limit ${limit}` : undefined; + + const operatorChunk = sql.raw(`${operator} ${isAll ? 'all ' : ''}`); + + const offsetSql = offset ? sql` offset ${offset}` : undefined; + + return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + } + buildInsertQuery({ table, values, onConflict, returning }: SQLiteInsertConfig): SQL { // const isSingleValue = values.length === 1; const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 85d6e1589..91e85bb36 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -1,11 +1,10 @@ -import { entityKind, is } from '~/entity.ts'; +import { entityKind } from '~/entity.ts'; import { orderSelectedFields, type Placeholder, type Query, SelectionProxyHandler, - SQL, - sql, + type SQL, Subquery, type ValueOrArray, } from '~/index.ts'; @@ -19,8 +18,8 @@ import type { import { QueryPromise } from '~/query-promise.ts'; import type { PreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; import { applyMixins, haveSameKeys, type PromiseOf, type ValidateShape } from '~/utils.ts'; -import { type ColumnsSelection } from '~/view.ts'; -import { SQLiteColumn } from '../columns/common.ts'; +import type { ColumnsSelection } from '~/view.ts'; +import type { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; import type { SQLiteSelectHKTBase } from './select.types.ts'; @@ -63,6 +62,17 @@ type SetOperatorRestSelect< : never[] : TValue; +export interface SQLiteSetOperationConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: SQLiteSetOperatorBuilder; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + export abstract class SQLiteSetOperatorBuilder< THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, @@ -176,20 +186,15 @@ export class SQLiteSetOperator< static readonly [entityKind]: string = 'SQLiteSetOperator'; protected joinsNotNullableMap: Record; - protected config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; + protected config: SQLiteSetOperationConfig; /* @internal */ readonly session: SQLiteSession | undefined; protected dialect: SQLiteDialect; constructor( - private operator: SetOperator, - private isAll: boolean, - private leftSelect: SQLiteSetOperatorBuilder< + operator: SetOperator, + isAll: boolean, + leftSelect: SQLiteSetOperatorBuilder< THKT, TTableName, TResultType, @@ -198,7 +203,7 @@ export class SQLiteSetOperator< TSelectMode, TNullabilityMap >, - private rightSelect: TypedQueryBuilder[]>, + rightSelect: TypedQueryBuilder[]>, ) { super(); @@ -222,6 +227,10 @@ export class SQLiteSetOperator< this.joinsNotNullableMap = joinsNotNullableMap; this.config = { fields, + operator, + isAll, + leftSelect, + rightSelect, }; } @@ -262,43 +271,7 @@ export class SQLiteSetOperator< } override getSQL(): SQL { - const leftChunk = sql`${this.leftSelect.getSQL()} `; - const rightChunk = sql`${this.rightSelect.getSQL()}`; - - let orderBySql; - if (this.config.orderBy && this.config.orderBy.length > 0) { - const orderByValues: SQL[] = []; - - // 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 orderBy of this.config.orderBy) { - if (is(orderBy, SQLiteColumn)) { - orderByValues.push(sql.raw(orderBy.name)); - } else if (is(orderBy, SQL)) { - for (let i = 0; i < orderBy.queryChunks.length; i++) { - const chunk = orderBy.queryChunks[i]; - - if (is(chunk, SQLiteColumn)) { - orderBy.queryChunks[i] = sql.raw(chunk.name); - } - } - - orderByValues.push(sql`${orderBy}`); - } else { - orderByValues.push(sql`${orderBy}`); - } - } - - orderBySql = sql` order by ${sql.join(orderByValues, sql`, `)}`; - } - - const limitSql = this.config.limit ? sql` limit ${this.config.limit}` : undefined; - - const operatorChunk = sql.raw(`${this.operator} ${this.isAll ? 'all ' : ''}`); - - const offsetSql = this.config.offset ? sql` offset ${this.config.offset}` : undefined; - - return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; + return this.dialect.buildSetOperationQuery(this.config); } prepare(isOneTimeQuery?: boolean): PreparedQuery< From d38f3b42e47af5c86cf56b6497846e894b82fd9e Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 6 Oct 2023 00:10:01 -0400 Subject: [PATCH 22/34] [All] Resolved merge conflicts. Need to fix types --- .../mysql-core/query-builders/select.types.ts | 4 +- .../query-builders/set-operators.ts | 78 ++++++++++------ .../pg-core/query-builders/set-operators.ts | 79 +++++++++++----- .../query-builders/select.types.ts | 4 +- .../query-builders/set-operators.ts | 93 +++++++++++++------ 5 files changed, 175 insertions(+), 83 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 529ce0e5d..b8e295f32 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -239,12 +239,12 @@ export type CreateMySqlSelectFromBuilderMode< export type MySqlSelectQueryBuilder< THKT extends MySqlSelectHKTBase = MySqlSelectQueryBuilderHKT, TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = ColumnsSelection, + TSelection extends ColumnsSelection = Record, TSelectMode extends SelectMode = SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, TNullabilityMap extends Record = Record, TResult = unknown, - TSelectedFields extends ColumnsSelection = ColumnsSelection, + TSelectedFields extends ColumnsSelection = Record, > = MySqlSelectQueryBuilderBase< THKT, TTableName, diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index ce76a3963..fb7916b47 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -74,7 +74,7 @@ export interface MySqlSetOperatorConfig { fields: Record; operator: SetOperator; isAll: boolean; - leftSelect: MySqlSetOperatorBuilder; + leftSelect: MySqlSetOperatorBuilder; rightSelect: TypedQueryBuilder; limit?: number | Placeholder; orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; @@ -89,13 +89,13 @@ export interface MySqlSetOperatorBuilder< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends - TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] - >, - QueryPromise[]> -{} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder, QueryPromise {} export abstract class MySqlSetOperatorBuilder< TTableName extends string | undefined, @@ -104,10 +104,11 @@ export abstract class MySqlSetOperatorBuilder< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] -> { + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder { static readonly [entityKind]: string = 'MySqlSetOperatorBuilder'; protected abstract joinsNotNullableMap: Record; @@ -138,7 +139,17 @@ export abstract class MySqlSetOperatorBuilder< rightSelect: | SetOperatorRightSelect | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ) => MySqlSetOperator { + ) => MySqlSetOperator< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; return new MySqlSetOperator(type, isAll, this, rightSelectOrig); @@ -156,13 +167,6 @@ export abstract class MySqlSetOperatorBuilder< except = this.setOperator('except', false); exceptAll = this.setOperator('except', true); - - abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; - abstract orderBy(...columns: (MySqlColumn | SQL | SQL.Aliased)[]): this; - - abstract limit(limit: number): this; - - abstract offset(offset: number | Placeholder): this; } export class MySqlSetOperator< @@ -172,12 +176,20 @@ export class MySqlSetOperator< TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record = TTableName extends string ? Record : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends MySqlSetOperatorBuilder< TTableName, TSelection, TSelectMode, TPreparedQueryHKT, - TNullabilityMap + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields > { static readonly [entityKind]: string = 'MySqlSetOperator'; @@ -195,7 +207,11 @@ export class MySqlSetOperator< TSelection, TSelectMode, TPreparedQueryHKT, - TNullabilityMap + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields >, rightSelect: TypedQueryBuilder[]>, ) { @@ -273,14 +289,14 @@ export class MySqlSetOperator< } const fieldsList = orderSelectedFields(this.config.fields); const query = this.session.prepareQuery< - PreparedQueryConfig & { execute: SelectResult[] }, + PreparedQueryConfig & { execute: TResult }, TPreparedQueryHKT >(this.dialect.sqlToQuery(this.getSQL()), fieldsList); query.joinsNotNullableMap = this.joinsNotNullableMap; return query as PreparedQueryKind< TPreparedQueryHKT, PreparedQueryConfig & { - execute: SelectResult[]; + execute: TResult; iterator: SelectResult; }, true @@ -303,11 +319,11 @@ export class MySqlSetOperator< as( alias: TAlias, - ): SubqueryWithSelection, TAlias, 'mysql'> { + ): SubqueryWithSelection { return new Proxy( new Subquery(this.getSQL(), this.config.fields, alias), new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), - ) as SubqueryWithSelection, TAlias, 'mysql'>; + ) as SubqueryWithSelection; } } @@ -322,7 +338,15 @@ function setOperator(type: SetOperator, isAll: boolean): < TValue extends TypedQueryBuilder[]>, TRest extends TypedQueryBuilder[]>[], >( - leftSelect: MySqlSetOperatorBuilder, + leftSelect: MySqlSetOperatorBuilder< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + any, + any + >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect> ) => MySqlSetOperator { diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index c4ddccfa1..f40893c87 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -69,7 +69,7 @@ export interface PgSetOperationConfig { fields: Record; operator: SetOperator; isAll: boolean; - leftSelect: PgSetOperatorBuilder; + leftSelect: PgSetOperatorBuilder; rightSelect: TypedQueryBuilder; limit?: number | Placeholder; orderBy?: (PgColumn | SQL | SQL.Aliased)[]; @@ -84,10 +84,11 @@ export abstract class PgSetOperatorBuilder< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] -> { + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder { static readonly [entityKind]: string = 'PgSetOperatorBuilder'; protected abstract joinsNotNullableMap: Record; @@ -118,7 +119,17 @@ export abstract class PgSetOperatorBuilder< rightSelect: | SetOperatorRightSelect | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ) => PgSetOperator { + ) => PgSetOperator< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; @@ -137,13 +148,6 @@ export abstract class PgSetOperatorBuilder< except = this.setOperator('except', false); exceptAll = this.setOperator('except', true); - - abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; - abstract orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; - - abstract limit(limit: number): this; - - abstract offset(offset: number | Placeholder): this; } export interface PgSetOperator< @@ -154,12 +158,23 @@ export interface PgSetOperator< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] + PgSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields >, - QueryPromise[]> + QueryPromise {} export class PgSetOperator< @@ -169,12 +184,20 @@ export class PgSetOperator< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends PgSetOperatorBuilder< THKT, TTableName, TSelection, TSelectMode, - TNullabilityMap + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields > { static readonly [entityKind]: string = 'PgSetOperator'; @@ -187,7 +210,17 @@ export class PgSetOperator< constructor( operator: SetOperator, isAll: boolean, - leftSelect: PgSetOperatorBuilder, + leftSelect: PgSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, rightSelect: TypedQueryBuilder[]>, ) { super(); @@ -204,7 +237,7 @@ export class PgSetOperator< const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); this._ = { - selectedFields: fields as BuildSubquerySelection, + selectedFields: fields as TSelectedFields, } as this['_']; this.session = session; @@ -261,7 +294,7 @@ export class PgSetOperator< private _prepare(name?: string): PreparedQuery< PreparedQueryConfig & { - execute: SelectResult[]; + execute: TResult; } > { const { session, joinsNotNullableMap, config: { fields }, dialect } = this; @@ -271,7 +304,7 @@ export class PgSetOperator< return tracer.startActiveSpan('drizzle.prepareQuery', () => { const fieldsList = orderSelectedFields(fields); const query = session.prepareQuery< - PreparedQueryConfig & { execute: SelectResult[] } + PreparedQueryConfig & { execute: TResult } >(dialect.sqlToQuery(this.getSQL()), fieldsList, name); query.joinsNotNullableMap = joinsNotNullableMap; return query; @@ -287,7 +320,7 @@ export class PgSetOperator< */ prepare(name: string): PreparedQuery< PreparedQueryConfig & { - execute: SelectResult[]; + execute: TResult; } > { return this._prepare(name); diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 62260b8e4..b7e7dd656 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -258,11 +258,11 @@ export type SQLiteSelectQueryBuilder< TTableName extends string | undefined = string | undefined, TResultType extends 'sync' | 'async' = 'sync' | 'async', TRunResult = unknown, - TSelection extends ColumnsSelection = ColumnsSelection, + TSelection extends ColumnsSelection = Record, TSelectMode extends SelectMode = SelectMode, TNullabilityMap extends Record = Record, TResult = unknown, - TSelectedFields extends ColumnsSelection = ColumnsSelection, + TSelectedFields extends ColumnsSelection = Record, > = SQLiteSelectQueryBuilderBase< THKT, TTableName, diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 91e85bb36..2b81c9bf5 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -16,7 +16,7 @@ import type { SelectResult, } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; -import type { PreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; +import type { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; import { applyMixins, haveSameKeys, type PromiseOf, type ValidateShape } from '~/utils.ts'; import type { ColumnsSelection } from '~/view.ts'; import type { SQLiteColumn } from '../columns/common.ts'; @@ -66,7 +66,7 @@ export interface SQLiteSetOperationConfig { fields: Record; operator: SetOperator; isAll: boolean; - leftSelect: SQLiteSetOperatorBuilder; + leftSelect: SQLiteSetOperatorBuilder; rightSelect: TypedQueryBuilder; limit?: number | Placeholder; orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; @@ -82,9 +82,13 @@ export abstract class SQLiteSetOperatorBuilder< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] + TSelectedFields, + TResult > { static readonly [entityKind]: string = 'SQLiteSetOperatorBuilder'; @@ -116,11 +120,35 @@ export abstract class SQLiteSetOperatorBuilder< rightSelect: | SetOperatorRightSelect | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ) => SQLiteSetOperator { + ) => SQLiteSetOperator< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - return new SQLiteSetOperator( + return new SQLiteSetOperator< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >( type, isAll, this, @@ -136,13 +164,6 @@ export abstract class SQLiteSetOperatorBuilder< intersect = this.setOperator('intersect', false); except = this.setOperator('except', false); - - abstract orderBy(builder: (aliases: TSelection) => ValueOrArray): this; - abstract orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; - - abstract limit(limit: number): this; - - abstract offset(offset: number | Placeholder): this; } export interface SQLiteSetOperator< @@ -157,13 +178,13 @@ export interface SQLiteSetOperator< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, -> extends - TypedQueryBuilder< - BuildSubquerySelection, - SelectResult[] - >, - QueryPromise[]> -{} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder, QueryPromise {} export class SQLiteSetOperator< THKT extends SQLiteSelectHKTBase, @@ -174,6 +195,10 @@ export class SQLiteSetOperator< TSelectMode extends SelectMode, TNullabilityMap extends Record = TTableName extends string ? Record : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends SQLiteSetOperatorBuilder< THKT, TTableName, @@ -181,7 +206,11 @@ export class SQLiteSetOperator< TRunResult, TSelection, TSelectMode, - TNullabilityMap + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields > { static readonly [entityKind]: string = 'SQLiteSetOperator'; @@ -201,7 +230,11 @@ export class SQLiteSetOperator< TRunResult, TSelection, TSelectMode, - TNullabilityMap + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields >, rightSelect: TypedQueryBuilder[]>, ) { @@ -219,7 +252,7 @@ export class SQLiteSetOperator< const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); this._ = { - selectedFields: fields as BuildSubquerySelection, + selectedFields: fields as TSelectedFields, } as this['_']; this.session = session; @@ -274,14 +307,14 @@ export class SQLiteSetOperator< return this.dialect.buildSetOperationQuery(this.config); } - prepare(isOneTimeQuery?: boolean): PreparedQuery< + prepare(isOneTimeQuery?: boolean): SQLitePreparedQuery< { type: TResultType; run: TRunResult; - all: SelectResult[]; + all: TResult; get: SelectResult | undefined; values: any[][]; - execute: SelectResult[]; + execute: TResult; } > { if (!this.session) { @@ -313,7 +346,7 @@ export class SQLiteSetOperator< return this.prepare(true).values(placeholderValues); }; - async execute(): Promise[]> { + async execute(): Promise { return this.all() as PromiseOf>; } @@ -347,11 +380,13 @@ function setOperator(type: SetOperator, isAll: boolean): < TRunResult, TSelection, TSelectMode, - TNullabilityMap + TNullabilityMap, + any, + any >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect> -) => SQLiteSetOperator { +) => SQLiteSetOperator { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { return new SQLiteSetOperator(type, isAll, leftSelect, rightSelect); From 152aa37911b9897e933a4cb84701d268458e830e Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 14 Oct 2023 11:03:02 -0400 Subject: [PATCH 23/34] [SQLite] fixed types, addressed comments and added new type tests --- .../src/query-builders/select.types.ts | 2 + drizzle-orm/src/sqlite-core/dialect.ts | 10 +- .../src/sqlite-core/query-builders/select.ts | 4 +- .../query-builders/select.types.ts | 274 ++++++++++++++++-- .../query-builders/set-operators.ts | 209 ++++++------- drizzle-orm/src/utils.ts | 9 +- .../type-tests/sqlite/set-operators.ts | 101 ++++++- 7 files changed, 448 insertions(+), 161 deletions(-) diff --git a/drizzle-orm/src/query-builders/select.types.ts b/drizzle-orm/src/query-builders/select.types.ts index 2477ba0ba..73fe41b40 100644 --- a/drizzle-orm/src/query-builders/select.types.ts +++ b/drizzle-orm/src/query-builders/select.types.ts @@ -165,3 +165,5 @@ export type SelectResultFields = [Key in keyof TSelectedFields & string]: SelectResultField; } >; + +export type SetOperator = 'union' | 'intersect' | 'except'; diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 91b726572..5070f701b 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -16,7 +16,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations.ts'; -import { and, eq, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; +import { and, eq, Name, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; import { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; import type { SQLiteDeleteConfig, @@ -295,19 +295,19 @@ export abstract class SQLiteDialect { let orderBySql; if (orderBy && orderBy.length > 0) { - const orderByValues: SQL[] = []; + const orderByValues: (SQL | 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 + // which is invalid Sql syntax, Table from one of the SELECTs cannot be used in global ORDER clause for (const singleOrderBy of orderBy) { if (is(singleOrderBy, SQLiteColumn)) { - orderByValues.push(sql.raw(singleOrderBy.name)); + orderByValues.push(sql.identifier(singleOrderBy.name)); } else if (is(singleOrderBy, SQL)) { for (let i = 0; i < singleOrderBy.queryChunks.length; i++) { const chunk = singleOrderBy.queryChunks[i]; if (is(chunk, SQLiteColumn)) { - singleOrderBy.queryChunks[i] = sql.raw(chunk.name); + singleOrderBy.queryChunks[i] = sql.identifier(chunk.name); } } diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 382515d07..05359102a 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -118,7 +118,7 @@ export abstract class SQLiteSelectQueryBuilderBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends SQLiteSetOperatorBuilder< THKT, @@ -399,7 +399,7 @@ export interface SQLiteSelectBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends SQLiteSelectQueryBuilderBase< diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index b7e7dd656..402dfb6b2 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -1,5 +1,5 @@ import type { Placeholder, SQL } from '~/sql/index.ts'; -import type { Assume } from '~/utils.ts'; +import type { Assume, ValidateShape } from '~/utils.ts'; import type { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; import type { SQLiteTable, SQLiteTableWithColumns } from '~/sqlite-core/table.ts'; @@ -9,6 +9,7 @@ import type { SelectedFieldsFlat as SelectFieldsFlatBase, SelectedFieldsOrdered as SelectFieldsOrderedBase, } from '~/operations.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { AppendToNullabilityMap, AppendToResult, @@ -19,13 +20,16 @@ import type { MapColumnsToTableAlias, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import type { Subquery } from '~/subquery.ts'; import type { Table, UpdateTableConfig } from '~/table.ts'; import { type ColumnsSelection, type View } from '~/view.ts'; -import type { SQLitePreparedQuery } from '../session.ts'; +import type { SQLiteDialect } from '../dialect.ts'; +import type { SQLitePreparedQuery, SQLiteSession } from '../session.ts'; import type { SQLiteViewBase, SQLiteViewWithSelection } from '../view.ts'; import type { SQLiteSelectBase, SQLiteSelectQueryBuilderBase } from './select.ts'; +import type { SQLiteSetOperatorBase, SQLiteSetOperatorBuilder } from './set-operators.ts'; export interface SQLiteSelectJoinConfig { on: SQL | undefined; @@ -162,7 +166,7 @@ export interface SQLiteSelectQueryBuilderHKT extends SQLiteSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -177,7 +181,7 @@ export interface SQLiteSelectHKT extends SQLiteSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -209,22 +213,43 @@ export type SQLiteSelectWithout< T extends AnySQLiteSelectQueryBuilder, TDynamic extends boolean, K extends keyof T & string, -> = TDynamic extends true ? T : Omit< - SQLiteSelectKind< - T['_']['hkt'], - T['_']['tableName'], - T['_']['resultType'], - T['_']['runResult'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - TDynamic, - T['_']['excludedMethods'] | K, - T['_']['result'], - T['_']['selectedFields'] - >, - T['_']['excludedMethods'] | K ->; +> = TDynamic extends true ? T + : Omit< + SQLiteSelectBase< + T['_']['tableName'], + T['_']['resultType'], + T['_']['runResult'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + TDynamic, + T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + T['_']['excludedMethods'] | K + >; + +// export type SQLiteSelectWithout< +// T extends AnySQLiteSelectQueryBuilder, +// TDynamic extends boolean, +// K extends keyof T & string, +// > = TDynamic extends true ? T : Omit< +// SQLiteSelectKind< +// T['_']['hkt'], +// T['_']['tableName'], +// T['_']['resultType'], +// T['_']['runResult'], +// T['_']['selection'], +// T['_']['selectMode'], +// T['_']['nullabilityMap'], +// TDynamic, +// T['_']['excludedMethods'] | K, +// T['_']['result'], +// T['_']['selectedFields'] +// >, +// T['_']['excludedMethods'] | K +// >; export type SQLiteSelectExecute = T['_']['result']; @@ -261,7 +286,7 @@ export type SQLiteSelectQueryBuilder< TSelection extends ColumnsSelection = Record, TSelectMode extends SelectMode = SelectMode, TNullabilityMap extends Record = Record, - TResult = unknown, + TResult extends any[] = unknown[], TSelectedFields extends ColumnsSelection = Record, > = SQLiteSelectQueryBuilderBase< THKT, @@ -301,3 +326,210 @@ export type SQLiteSelect< > = SQLiteSelectBase; export type AnySQLiteSelect = SQLiteSelectBase; + +export type SetOperatorRightSelect< + TValue extends AnySQLiteSetOperatorBase, + TResult extends any[], +> = TValue extends SQLiteSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly AnySQLiteSetOperatorBase[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends SQLiteSetOperatorInterface + ? TValueResult extends Array + ? Rest extends SQLiteSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : TValue + : TValue; + +export interface SQLiteSetOperationConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: AnySQLiteSetOperatorBase; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + +export interface SQLiteSetOperatorInterface< + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends + TypedQueryBuilder< + TSelectedFields, + TResult + > +{ + _: { + dialect: 'sqlite'; + readonly hkt: THKT; + readonly tableName: TTableName; + readonly resultType: TResultType; + readonly runResult: TRunResult; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + getSelectedFields: () => TSelectedFields; + getSetOperatorConfig: () => { + session: SQLiteSession | undefined; + dialect: SQLiteDialect; + joinsNotNullableMap: Record; + fields: Record; + }; +} + +export type AnySQLiteSetOperatorBase = SQLiteSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type CreateSetOperatorFn = < + THKT extends SQLiteSelectHKTBase, + TTableName extends string | undefined, + TResultType extends 'sync' | 'async', + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends AnySQLiteSetOperatorBase, + TRest extends AnySQLiteSetOperatorBase[], + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +>( + leftSelect: SQLiteSetOperatorInterface< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect +) => SQLiteSetOperatorBase< + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields +>; + +export type AnySQLiteSetOperator = SQLiteSetOperatorBase< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type SQLiteSetOperator< + TTableName extends string | undefined = string | undefined, + TResultType extends 'sync' | 'async' = 'sync' | 'async', + TRunResult = unknown, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = SQLiteSetOperatorBase; + +export type AnySQLiteSetOperatorBuilder = SQLiteSetOperatorBuilder< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type SQLiteSetOperatorWithout< + T extends AnySQLiteSetOperator, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + SQLiteSetOperatorBase< + T['_']['tableName'], + T['_']['resultType'], + T['_']['runResult'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + TDynamic, + T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + T['_']['excludedMethods'] | K + >; + +export type SQLiteSetOperatorDynamic = SQLiteSetOperatorBase< + T['_']['tableName'], + T['_']['resultType'], + T['_']['runResult'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + true, + never, + T['_']['result'], + T['_']['selectedFields'] +>; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 2b81c9bf5..bcf45717e 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -14,17 +14,26 @@ import type { JoinNullability, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; +import type { RunnableQuery } from '~/runnable-query.ts'; import type { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; -import { applyMixins, haveSameKeys, type PromiseOf, type ValidateShape } from '~/utils.ts'; +import { applyMixins, haveSameKeys, type PromiseOf } from '~/utils.ts'; import type { ColumnsSelection } from '~/view.ts'; import type { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; -import type { SQLiteSelectHKTBase } from './select.types.ts'; - -type SetOperator = 'union' | 'intersect' | 'except'; +import type { + AnySQLiteSetOperatorBase, + CreateSetOperatorFn, + SetOperatorRightSelect, + SQLiteSelectHKTBase, + SQLiteSetOperationConfig, + SQLiteSetOperatorDynamic, + SQLiteSetOperatorInterface, + SQLiteSetOperatorWithout, +} from './select.types.ts'; const getSQLiteSetOperators = () => { return { @@ -37,42 +46,6 @@ const getSQLiteSetOperators = () => { type SQLiteSetOperators = ReturnType; -type SetOperatorRightSelect< - TValue extends TypedQueryBuilder[]>, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, -> = TValue extends SQLiteSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - TypedQueryBuilder[]> - > - : TValue; - -type SetOperatorRestSelect< - TValue extends readonly TypedQueryBuilder[], - Valid, -> = TValue extends [infer First, ...infer Rest] - ? First extends SQLiteSetOperatorBuilder - ? Rest extends TypedQueryBuilder[] ? [ - ValidateShape, Valid, TValue[0]>, - ...SetOperatorRestSelect, - ] - : ValidateShape, Valid, TValue> - : never[] - : TValue; - -export interface SQLiteSetOperationConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: SQLiteSetOperatorBuilder; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} - export abstract class SQLiteSetOperatorBuilder< THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, @@ -84,7 +57,7 @@ export abstract class SQLiteSetOperatorBuilder< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends TypedQueryBuilder< TSelectedFields, @@ -92,6 +65,21 @@ export abstract class SQLiteSetOperatorBuilder< > { static readonly [entityKind]: string = 'SQLiteSetOperatorBuilder'; + abstract override readonly _: { + dialect: 'sqlite'; + readonly hkt: THKT; + readonly tableName: TTableName; + readonly resultType: TResultType; + readonly runResult: TRunResult; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected abstract joinsNotNullableMap: Record; protected abstract config: { fields: Record; @@ -113,15 +101,14 @@ export abstract class SQLiteSetOperatorBuilder< }; } - private setOperator( + private createSetOperator( type: SetOperator, isAll: boolean, - ): []>>( + ): ( rightSelect: - | SetOperatorRightSelect - | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect), - ) => SQLiteSetOperator< - THKT, + | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => SQLiteSetOperatorBase< TTableName, TResultType, TRunResult, @@ -136,39 +123,25 @@ export abstract class SQLiteSetOperatorBuilder< return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - return new SQLiteSetOperator< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >( + return new SQLiteSetOperatorBase( type, isAll, this, - rightSelectOrig, + rightSelectOrig as any, ); }; } - union = this.setOperator('union', false); + union = this.createSetOperator('union', false); - unionAll = this.setOperator('union', true); + unionAll = this.createSetOperator('union', true); - intersect = this.setOperator('intersect', false); + intersect = this.createSetOperator('intersect', false); - except = this.setOperator('except', false); + except = this.createSetOperator('except', false); } -export interface SQLiteSetOperator< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - THKT extends SQLiteSelectHKTBase, +export interface SQLiteSetOperatorBase< TTableName extends string | undefined, // eslint-disable-next-line @typescript-eslint/no-unused-vars TResultType extends 'sync' | 'async', @@ -182,12 +155,11 @@ export interface SQLiteSetOperator< TDynamic extends boolean = false, // eslint-disable-next-line @typescript-eslint/no-unused-vars TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder, QueryPromise {} +> extends TypedQueryBuilder, QueryPromise, RunnableQuery {} -export class SQLiteSetOperator< - THKT extends SQLiteSelectHKTBase, +export class SQLiteSetOperatorBase< TTableName extends string | undefined, TResultType extends 'sync' | 'async', TRunResult, @@ -197,10 +169,10 @@ export class SQLiteSetOperator< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends SQLiteSetOperatorBuilder< - THKT, + SQLiteSelectHKTBase, TTableName, TResultType, TRunResult, @@ -214,6 +186,21 @@ export class SQLiteSetOperator< > { static readonly [entityKind]: string = 'SQLiteSetOperator'; + override readonly _: { + dialect: 'sqlite'; + readonly hkt: SQLiteSelectHKTBase; + readonly tableName: TTableName; + readonly resultType: TResultType; + readonly runResult: TRunResult; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected joinsNotNullableMap: Record; protected config: SQLiteSetOperationConfig; /* @internal */ @@ -223,8 +210,8 @@ export class SQLiteSetOperator< constructor( operator: SetOperator, isAll: boolean, - leftSelect: SQLiteSetOperatorBuilder< - THKT, + leftSelect: SQLiteSetOperatorInterface< + SQLiteSelectHKTBase, TTableName, TResultType, TRunResult, @@ -236,7 +223,7 @@ export class SQLiteSetOperator< TResult, TSelectedFields >, - rightSelect: TypedQueryBuilder[]>, + rightSelect: TypedQueryBuilder, ) { super(); @@ -264,16 +251,18 @@ export class SQLiteSetOperator< isAll, leftSelect, rightSelect, - }; + } as SQLiteSetOperationConfig; } - orderBy(builder: (aliases: TSelection) => ValueOrArray): this; - orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): this; + orderBy( + builder: (aliases: TSelection) => ValueOrArray, + ): SQLiteSetOperatorWithout; + orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteSetOperatorWithout; orderBy( ...columns: | [(aliases: TSelection) => ValueOrArray] | (SQLiteColumn | SQL | SQL.Aliased)[] - ): this { + ): SQLiteSetOperatorWithout { if (typeof columns[0] === 'function') { const orderBy = columns[0]( new Proxy( @@ -285,17 +274,17 @@ export class SQLiteSetOperator< } else { this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]; } - return this; + return this as any; } - limit(limit: number) { + limit(limit: number): SQLiteSetOperatorWithout { this.config.limit = limit; - return this; + return this as any; } - offset(offset: number | Placeholder) { + offset(offset: number | Placeholder): SQLiteSetOperatorWithout { this.config.offset = offset; - return this; + return this as any; } toSQL(): Query { @@ -358,51 +347,35 @@ export class SQLiteSetOperator< new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection, TAlias>; } + + $dynamic(): SQLiteSetOperatorDynamic { + return this as any; + } } -applyMixins(SQLiteSetOperator, [QueryPromise]); +applyMixins(SQLiteSetOperatorBase, [QueryPromise]); -function setOperator(type: SetOperator, isAll: boolean): < - THKT extends SQLiteSelectHKTBase, - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: SQLiteSetOperatorBuilder< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - any, - any - >, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -) => SQLiteSetOperator { +function createSetOperator(type: SetOperator, isAll: boolean): CreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { - return new SQLiteSetOperator(type, isAll, leftSelect, rightSelect); + return new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any); } const [select, ...rest] = restSelects; if (!select) throw new Error('Cannot pass undefined values to any set operator'); - return setOperator(type, isAll)(new SQLiteSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + return createSetOperator(type, isAll)( + new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any), + select as any, + ...rest, + ); }; } -export const union = setOperator('union', false); +export const union = createSetOperator('union', false); -export const unionAll = setOperator('union', true); +export const unionAll = createSetOperator('union', true); -export const intersect = setOperator('intersect', false); +export const intersect = createSetOperator('intersect', false); -export const except = setOperator('except', false); +export const except = createSetOperator('except', false); diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 02420d261..c910816f1 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -210,13 +210,12 @@ export interface DrizzleConfig = Record< logger?: boolean | Logger; schema?: TSchema; } - export type ValidateShape = T extends ValidShape ? Exclude extends never ? TResult - : Exclude extends string - ? DrizzleTypeError<`Invalid key(s): ${Exclude}`> - : never - : never; + : DrizzleTypeError< + `Invalid key(s): ${Exclude<(keyof T) & (string | number | bigint | boolean | null | undefined), keyof ValidShape>}` + > + : DrizzleTypeError<`Fields need to be the same and produce the same types`>; export type KnownKeysOnly = { [K in keyof T]: K extends keyof U ? T[K] : never; diff --git a/drizzle-orm/type-tests/sqlite/set-operators.ts b/drizzle-orm/type-tests/sqlite/set-operators.ts index ed98dac9e..86734c2bb 100644 --- a/drizzle-orm/type-tests/sqlite/set-operators.ts +++ b/drizzle-orm/type-tests/sqlite/set-operators.ts @@ -1,7 +1,7 @@ import { type Equal, Expect } from 'type-tests/utils.ts'; import { eq } from '~/expressions.ts'; import { desc, sql } from '~/sql/index.ts'; -import { except, intersect, union, unionAll } from '~/sqlite-core/index.ts'; +import { except, intersect, type SQLiteSetOperator, union, unionAll } from '~/sqlite-core/index.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -113,9 +113,15 @@ const intersect2Test = await intersect( Expect>; const intersectAll2Test = await intersect( - db.select({ - id: cities.id, - }).from(cities), + union( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities).where(sql``), + ), db.select({ id: cities.id, }) @@ -141,27 +147,92 @@ const exceptAll2Test = await except( userId: newYorkers.userId, cityId: newYorkers.cityId, }) - .from(newYorkers), + .from(newYorkers).where(sql``), db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, - }).from(newYorkers), + }).from(newYorkers).leftJoin(newYorkers, sql``), ); Expect>; +const unionfull = await union(db.select().from(users), db.select().from(users)).orderBy(sql``).limit(1).offset(2); + +Expect< + Equal<{ + id: number; + name: string | null; + homeCity: number; + currentCity: number | null; + serialNullable: number | null; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + }[], typeof unionfull> +>; + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + +union(db.select().from(users), db.select().from(users)) + .offset(1) + // @ts-expect-error - method was already called + .offset(2); + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + +{ + function dynamic(qb: T) { + return qb.orderBy(sql``).limit(1).offset(2); + } + + const qb = union(db.select().from(users), db.select().from(users)).$dynamic(); + const result = await dynamic(qb); + Expect>; +} + +await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + .intersect(({ intersect }) => + intersect( + db + .select() + .from(users), + db + .select() + .from(users), + ) + ); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error +db.select({ id: classes.id }).from(classes).union(db.select().from(classes).where(sql``)); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( - db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), db.select({ id: cities.id, name: cities.name }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type @@ -169,6 +240,16 @@ union( db.select().from(cities), ); +union( + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``).limit(3).$dynamic(), + db.select({ id: cities.id, name: cities.name }).from(cities), +); + union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -189,18 +270,18 @@ union( // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: newYorkers.userId }).from(newYorkers), db.select({ id: cities.id }).from(cities), ); union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities).where(sql``), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error - db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities).where(sql``), ); From 3c925d61eaa240548f757c8fdf58649979cc787a Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 14 Oct 2023 15:32:35 -0400 Subject: [PATCH 24/34] [SQLite] deleted commented code and improved error readability --- .../query-builders/select.types.ts | 45 ++++++++----------- .../query-builders/set-operators.ts | 4 +- drizzle-orm/src/utils.ts | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 402dfb6b2..a53b9abab 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -230,27 +230,6 @@ export type SQLiteSelectWithout< T['_']['excludedMethods'] | K >; -// export type SQLiteSelectWithout< -// T extends AnySQLiteSelectQueryBuilder, -// TDynamic extends boolean, -// K extends keyof T & string, -// > = TDynamic extends true ? T : Omit< -// SQLiteSelectKind< -// T['_']['hkt'], -// T['_']['tableName'], -// T['_']['resultType'], -// T['_']['runResult'], -// T['_']['selection'], -// T['_']['selectMode'], -// T['_']['nullabilityMap'], -// TDynamic, -// T['_']['excludedMethods'] | K, -// T['_']['result'], -// T['_']['selectedFields'] -// >, -// T['_']['excludedMethods'] | K -// >; - export type SQLiteSelectExecute = T['_']['result']; export type SQLiteSelectPrepare = SQLitePreparedQuery< @@ -316,6 +295,20 @@ export type AnySQLiteSelectQueryBuilder = SQLiteSelectQueryBuilderBase< any >; +export type SQLiteSetOperatorBaseWithResult = SQLiteSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + any, + T, + any +>; + export type SQLiteSelect< TTableName extends string | undefined = string | undefined, TResultType extends 'sync' | 'async' = 'sync' | 'async', @@ -328,7 +321,7 @@ export type SQLiteSelect< export type AnySQLiteSelect = SQLiteSelectBase; export type SetOperatorRightSelect< - TValue extends AnySQLiteSetOperatorBase, + TValue extends SQLiteSetOperatorBaseWithResult, TResult extends any[], > = TValue extends SQLiteSetOperatorInterface ? TValueResult extends Array ? ValidateShape< @@ -340,7 +333,7 @@ export type SetOperatorRightSelect< : TValue; export type SetOperatorRestSelect< - TValue extends readonly AnySQLiteSetOperatorBase[], + TValue extends readonly SQLiteSetOperatorBaseWithResult[], TResult extends any[], > = TValue extends [infer First, ...infer Rest] ? First extends SQLiteSetOperatorInterface @@ -351,7 +344,7 @@ export type SetOperatorRestSelect< ] : ValidateShape[]> : never - : TValue + : never : TValue; export interface SQLiteSetOperationConfig { @@ -429,8 +422,8 @@ export type CreateSetOperatorFn = < TSelection extends ColumnsSelection, TSelectMode extends SelectMode, TNullabilityMap extends Record, - TValue extends AnySQLiteSetOperatorBase, - TRest extends AnySQLiteSetOperatorBase[], + TValue extends SQLiteSetOperatorBaseWithResult, + TRest extends SQLiteSetOperatorBaseWithResult[], TDynamic extends boolean = false, TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index bcf45717e..daa353e84 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -25,11 +25,11 @@ import type { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; import type { - AnySQLiteSetOperatorBase, CreateSetOperatorFn, SetOperatorRightSelect, SQLiteSelectHKTBase, SQLiteSetOperationConfig, + SQLiteSetOperatorBaseWithResult, SQLiteSetOperatorDynamic, SQLiteSetOperatorInterface, SQLiteSetOperatorWithout, @@ -104,7 +104,7 @@ export abstract class SQLiteSetOperatorBuilder< private createSetOperator( type: SetOperator, isAll: boolean, - ): ( + ): >( rightSelect: | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect) | SetOperatorRightSelect, diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index c910816f1..8c52a1a75 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -215,7 +215,7 @@ export type ValidateShape = T extends ValidShape : DrizzleTypeError< `Invalid key(s): ${Exclude<(keyof T) & (string | number | bigint | boolean | null | undefined), keyof ValidShape>}` > - : DrizzleTypeError<`Fields need to be the same and produce the same types`>; + : never; export type KnownKeysOnly = { [K in keyof T]: K extends keyof U ? T[K] : never; From e763b299ec75950db0c907d150841fb8019b51de Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 14 Oct 2023 19:33:49 -0400 Subject: [PATCH 25/34] [MySql] fixed types, addressed comments and added new type tests --- drizzle-orm/src/mysql-core/dialect.ts | 18 +- .../src/mysql-core/query-builders/select.ts | 5 +- .../mysql-core/query-builders/select.types.ts | 237 +++++++++++++++++- .../query-builders/set-operators.ts | 204 +++++++-------- .../query-builders/select.types.ts | 2 +- .../query-builders/set-operators.ts | 4 +- drizzle-orm/type-tests/mysql/set-operators.ts | 130 +++++++--- 7 files changed, 441 insertions(+), 159 deletions(-) diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index ac82e8ffa..138c78a3f 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -19,12 +19,16 @@ import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; import { View, ViewBaseConfig } from '~/view.ts'; -import { DrizzleError } from '../index.ts'; +import { DrizzleError, Name } 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'; -import type { MySqlSelectConfig, MySqlSelectJoinConfig, SelectedFieldsOrdered } from './query-builders/select.types.ts'; -import type { MySqlSetOperatorConfig } from './query-builders/set-operators.ts'; +import type { + MySqlSelectConfig, + MySqlSelectJoinConfig, + MySqlSetOperationConfig, + SelectedFieldsOrdered, +} from './query-builders/select.types.ts'; import type { MySqlUpdateConfig } from './query-builders/update.ts'; import type { MySqlSession } from './session.ts'; import { MySqlTable } from './table.ts'; @@ -343,25 +347,25 @@ export class MySqlDialect { limit, orderBy, offset, - }: MySqlSetOperatorConfig): SQL { + }: MySqlSetOperationConfig): SQL { const leftChunk = sql`(${leftSelect.getSQL()}) `; const rightChunk = sql`(${rightSelect.getSQL()})`; let orderBySql; if (orderBy && orderBy.length > 0) { - const orderByValues: SQL[] = []; + const orderByValues: (SQL | 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.raw(orderByUnit.name)); + orderByValues.push(sql.identifier(orderByUnit.name)); } else if (is(orderByUnit, SQL)) { for (let i = 0; i < orderByUnit.queryChunks.length; i++) { const chunk = orderByUnit.queryChunks[i]; if (is(chunk, MySqlColumn)) { - orderByUnit.queryChunks[i] = sql.raw(chunk.name); + orderByUnit.queryChunks[i] = sql.identifier(chunk.name); } } diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index 640575289..0d499881c 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -120,9 +120,10 @@ export abstract class MySqlSelectQueryBuilderBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends MySqlSetOperatorBuilder< + THKT, TTableName, TSelection, TSelectMode, @@ -401,7 +402,7 @@ export interface MySqlSelectBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends MySqlSelectQueryBuilderBase< diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index b8e295f32..3492d3607 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -6,6 +6,7 @@ import type { SelectedFieldsFlat as SelectedFieldsFlatBase, SelectedFieldsOrdered as SelectedFieldsOrderedBase, } from '~/operations.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { AppendToNullabilityMap, AppendToResult, @@ -16,14 +17,17 @@ import type { MapColumnsToTableAlias, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import type { Placeholder, SQL } from '~/sql/index.ts'; import type { Subquery } from '~/subquery.ts'; import type { Table, UpdateTableConfig } from '~/table.ts'; -import type { Assume } from '~/utils.ts'; +import type { Assume, ValidateShape } from '~/utils.ts'; import type { ColumnsSelection, View } from '~/view.ts'; -import type { PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; +import type { MySqlDialect } from '../dialect.ts'; +import type { MySqlSession, PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; import type { MySqlSelectBase, MySqlSelectQueryBuilderBase } from './select.ts'; +import type { MySqlSetOperatorBase, MySqlSetOperatorBuilder } from './set-operators.ts'; export interface MySqlSelectJoinConfig { on: SQL | undefined; @@ -81,13 +85,14 @@ export type MySqlJoin< T['_']['selection'], TJoinedName, TJoinedTable extends MySqlTable ? TJoinedTable['_']['columns'] - : TJoinedTable extends Subquery ? Assume + : TJoinedTable extends Subquery | View ? Assume : never, T['_']['selectMode'] >, T['_']['selectMode'] extends 'partial' ? T['_']['selectMode'] : 'multiple', + T['_']['preparedQueryHKT'], AppendToNullabilityMap, - TDynamic, + T['_']['dynamic'], T['_']['excludedMethods'] >, TDynamic, @@ -144,6 +149,7 @@ export type MySqlSelectKind< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record, TDynamic extends boolean, TExcludedMethods extends string, @@ -153,6 +159,7 @@ export type MySqlSelectKind< tableName: TTableName; selection: TSelection; selectMode: TSelectMode; + preparedQueryHKT: TPreparedQueryHKT; nullabilityMap: TNullabilityMap; dynamic: TDynamic; excludedMethods: TExcludedMethods; @@ -170,7 +177,7 @@ export interface MySqlSelectQueryBuilderHKT extends MySqlSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -184,7 +191,7 @@ export interface MySqlSelectHKT extends MySqlSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -194,14 +201,16 @@ export type MySqlSelectWithout< TDynamic extends boolean, K extends keyof T & string, > = TDynamic extends true ? T : Omit< - MySqlSelectKind< - T['_']['hkt'], + MySqlSelectBase< T['_']['tableName'], T['_']['selection'], T['_']['selectMode'], + T['_']['preparedQueryHKT'], T['_']['nullabilityMap'], TDynamic, - T['_']['excludedMethods'] | K + T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] >, T['_']['excludedMethods'] | K >; @@ -220,6 +229,7 @@ export type MySqlSelectDynamic = MySqlSele T['_']['tableName'], T['_']['selection'], T['_']['selectMode'], + T['_']['preparedQueryHKT'], T['_']['nullabilityMap'], true, never, @@ -243,7 +253,7 @@ export type MySqlSelectQueryBuilder< TSelectMode extends SelectMode = SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, TNullabilityMap extends Record = Record, - TResult = unknown, + TResult extends any[] = unknown[], TSelectedFields extends ColumnsSelection = Record, > = MySqlSelectQueryBuilderBase< THKT, @@ -258,7 +268,7 @@ export type MySqlSelectQueryBuilder< TSelectedFields >; -export type AnyMySqlSelectQueryBuilder = MySqlSelectQueryBuilderBase; +export type AnyMySqlSelectQueryBuilder = MySqlSelectQueryBuilderBase; export type MySqlSelect< TTableName extends string | undefined = string | undefined, @@ -268,3 +278,208 @@ export type MySqlSelect< > = MySqlSelectBase; export type AnyMySqlSelect = MySqlSelectBase; + +export type MySqlSetOperatorBaseWithResult = MySqlSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + T, + any +>; + +export type SetOperatorRightSelect< + TValue extends MySqlSetOperatorBaseWithResult, + TResult extends any[], +> = TValue extends MySqlSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly MySqlSetOperatorBaseWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends MySqlSetOperatorInterface + ? TValueResult extends Array + ? Rest extends MySqlSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : never + : TValue; + +export interface MySqlSetOperationConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: AnyMySqlSetOperatorBase; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + +export interface MySqlSetOperatorInterface< + THKT extends MySqlSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends + TypedQueryBuilder< + TSelectedFields, + TResult + > +{ + _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + getSelectedFields: () => TSelectedFields; + getSetOperatorConfig: () => { + session: MySqlSession | undefined; + dialect: MySqlDialect; + joinsNotNullableMap: Record; + fields: Record; + }; +} + +export type AnyMySqlSetOperatorBase = MySqlSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type MySqlCreateSetOperatorFn = < + THKT extends MySqlSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record, + TValue extends MySqlSetOperatorBaseWithResult, + TRest extends MySqlSetOperatorBaseWithResult[], + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +>( + leftSelect: MySqlSetOperatorInterface< + THKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect +) => MySqlSetOperatorBase< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields +>; + +export type AnyMySqlSetOperator = MySqlSetOperatorBase< + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type MySqlSetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = Record, +> = MySqlSetOperatorBase; + +export type AnyMySqlSetOperatorBuilder = MySqlSetOperatorBuilder< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type MySqlSetOperatorWithout< + T extends AnyMySqlSetOperator, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + MySqlSetOperatorBase< + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + T['_']['preparedQueryHKT'], + TDynamic, + T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + T['_']['excludedMethods'] | K + >; + +export type MySqlSetOperatorDynamic = MySqlSetOperatorBase< + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['preparedQueryHKT'], + T['_']['nullabilityMap'], + true, + never, + T['_']['result'], + T['_']['selectedFields'] +>; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index fb7916b47..160b55123 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -9,7 +9,6 @@ import { SelectionProxyHandler, type SQL, Subquery, - type ValidateShape, type ValueOrArray, } from '~/index.ts'; import type { @@ -24,13 +23,22 @@ import type { JoinNullability, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import { type ColumnsSelection } from '~/view.ts'; import type { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; - -type SetOperator = 'union' | 'intersect' | 'except'; +import type { + MySqlCreateSetOperatorFn, + MySqlSelectHKTBase, + MySqlSetOperationConfig, + MySqlSetOperatorBaseWithResult, + MySqlSetOperatorDynamic, + MySqlSetOperatorInterface, + MySqlSetOperatorWithout, + SetOperatorRightSelect, +} from './select.types.ts'; const getMySqlSetOperators = () => { return { @@ -45,59 +53,8 @@ const getMySqlSetOperators = () => { type MySqlSetOperators = ReturnType; -type SetOperatorRightSelect< - TValue extends TypedQueryBuilder[]>, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, -> = TValue extends MySqlSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - TypedQueryBuilder[]> - > - : TValue; - -type SetOperatorRestSelect< - TValue extends readonly TypedQueryBuilder[], - Valid, -> = TValue extends [infer First, ...infer Rest] - ? First extends MySqlSetOperatorBuilder - ? Rest extends TypedQueryBuilder[] ? [ - ValidateShape, Valid, TValue[0]>, - ...SetOperatorRestSelect, - ] - : ValidateShape, Valid, TValue> - : never[] - : TValue; - -export interface MySqlSetOperatorConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: MySqlSetOperatorBuilder; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} - -export interface MySqlSetOperatorBuilder< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TDynamic extends boolean = false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TExcludedMethods extends string = never, - TResult = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder, QueryPromise {} - export abstract class MySqlSetOperatorBuilder< + THKT extends MySqlSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -106,11 +63,24 @@ export abstract class MySqlSetOperatorBuilder< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends TypedQueryBuilder { static readonly [entityKind]: string = 'MySqlSetOperatorBuilder'; + abstract override readonly _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected abstract joinsNotNullableMap: Record; protected abstract config: { fields: Record; @@ -132,14 +102,14 @@ export abstract class MySqlSetOperatorBuilder< }; } - private setOperator( + private createSetOperator( type: SetOperator, isAll: boolean, - ): []>>( + ): >( rightSelect: - | SetOperatorRightSelect - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect), - ) => MySqlSetOperator< + | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => MySqlSetOperatorBase< TTableName, TSelection, TSelectMode, @@ -152,24 +122,39 @@ export abstract class MySqlSetOperatorBuilder< > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; - return new MySqlSetOperator(type, isAll, this, rightSelectOrig); + return new MySqlSetOperatorBase(type, isAll, this as any, rightSelectOrig as any); }; } - union = this.setOperator('union', false); + union = this.createSetOperator('union', false); - unionAll = this.setOperator('union', true); + unionAll = this.createSetOperator('union', true); - intersect = this.setOperator('intersect', false); + intersect = this.createSetOperator('intersect', false); - intersectAll = this.setOperator('intersect', true); + intersectAll = this.createSetOperator('intersect', true); - except = this.setOperator('except', false); + except = this.createSetOperator('except', false); - exceptAll = this.setOperator('except', true); + exceptAll = this.createSetOperator('except', true); } -export class MySqlSetOperator< +export interface MySqlSetOperatorBase< + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TDynamic extends boolean = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends TypedQueryBuilder, QueryPromise {} + +export class MySqlSetOperatorBase< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -181,6 +166,7 @@ export class MySqlSetOperator< TResult = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends MySqlSetOperatorBuilder< + MySqlSelectHKTBase, TTableName, TSelection, TSelectMode, @@ -193,8 +179,21 @@ export class MySqlSetOperator< > { static readonly [entityKind]: string = 'MySqlSetOperator'; + readonly _: { + readonly hkt: MySqlSelectHKTBase; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly preparedQueryHKT: TPreparedQueryHKT; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected joinsNotNullableMap: Record; - protected config: MySqlSetOperatorConfig; + protected config: MySqlSetOperationConfig; /* @internal */ readonly session: MySqlSession | undefined; protected dialect: MySqlDialect; @@ -202,7 +201,8 @@ export class MySqlSetOperator< constructor( operator: SetOperator, isAll: boolean, - leftSelect: MySqlSetOperatorBuilder< + leftSelect: MySqlSetOperatorInterface< + MySqlSelectHKTBase, TTableName, TSelection, TSelectMode, @@ -213,7 +213,7 @@ export class MySqlSetOperator< TResult, TSelectedFields >, - rightSelect: TypedQueryBuilder[]>, + rightSelect: TypedQueryBuilder, ) { super(); @@ -248,7 +248,7 @@ export class MySqlSetOperator< ...columns: | [(aliases: TSelection) => ValueOrArray] | (MySqlColumn | SQL | SQL.Aliased)[] - ) { + ): MySqlSetOperatorWithout { if (typeof columns[0] === 'function') { const orderBy = columns[0]( new Proxy( @@ -260,17 +260,17 @@ export class MySqlSetOperator< } else { this.config.orderBy = columns as (MySqlColumn | SQL | SQL.Aliased)[]; } - return this; + return this as any; } - limit(limit: number) { + limit(limit: number): MySqlSetOperatorWithout { this.config.limit = limit; - return this; + return this as any; } - offset(offset: number | Placeholder) { + offset(offset: number | Placeholder): MySqlSetOperatorWithout { this.config.offset = offset; - return this; + return this as any; } /** @internal */ @@ -325,51 +325,39 @@ export class MySqlSetOperator< new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection; } + + $dynamic(): MySqlSetOperatorDynamic { + return this as any; + } } -applyMixins(MySqlSetOperatorBuilder, [QueryPromise]); +applyMixins(MySqlSetOperatorBase, [QueryPromise]); -function setOperator(type: SetOperator, isAll: boolean): < - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: MySqlSetOperatorBuilder< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, - any, - any - >, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -) => MySqlSetOperator { +function createSetOperator(type: SetOperator, isAll: boolean): MySqlCreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { - return new MySqlSetOperator(type, isAll, leftSelect, rightSelect); + return new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any); } const [select, ...rest] = restSelects; if (!select) throw new Error('Cannot pass undefined values to any set operator'); - return setOperator(type, isAll)(new MySqlSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + return createSetOperator(type, isAll)( + new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any), + select as any, + ...rest, + ); }; } -export const union = setOperator('union', false); +export const union = createSetOperator('union', false); -export const unionAll = setOperator('union', true); +export const unionAll = createSetOperator('union', true); -export const intersect = setOperator('intersect', false); +export const intersect = createSetOperator('intersect', false); -export const intersectAll = setOperator('intersect', true); +export const intersectAll = createSetOperator('intersect', true); -export const except = setOperator('except', false); +export const except = createSetOperator('except', false); -export const exceptAll = setOperator('except', true); +export const exceptAll = createSetOperator('except', true); diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index a53b9abab..9f8634be0 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -414,7 +414,7 @@ export type AnySQLiteSetOperatorBase = SQLiteSetOperatorInterface< any >; -export type CreateSetOperatorFn = < +export type SQLiteCreateSetOperatorFn = < THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, TResultType extends 'sync' | 'async', diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index daa353e84..930a13abf 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -25,8 +25,8 @@ import type { SQLiteColumn } from '../columns/common.ts'; import type { SQLiteDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; import type { - CreateSetOperatorFn, SetOperatorRightSelect, + SQLiteCreateSetOperatorFn, SQLiteSelectHKTBase, SQLiteSetOperationConfig, SQLiteSetOperatorBaseWithResult, @@ -355,7 +355,7 @@ export class SQLiteSetOperatorBase< applyMixins(SQLiteSetOperatorBase, [QueryPromise]); -function createSetOperator(type: SetOperator, isAll: boolean): CreateSetOperatorFn { +function createSetOperator(type: SetOperator, isAll: boolean): SQLiteCreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { return new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any); diff --git a/drizzle-orm/type-tests/mysql/set-operators.ts b/drizzle-orm/type-tests/mysql/set-operators.ts index bc869bf88..a936868ed 100644 --- a/drizzle-orm/type-tests/mysql/set-operators.ts +++ b/drizzle-orm/type-tests/mysql/set-operators.ts @@ -1,6 +1,14 @@ import { type Equal, Expect } from 'type-tests/utils.ts'; import { eq } from '~/expressions.ts'; -import { except, exceptAll, intersect, intersectAll, union, unionAll } from '~/mysql-core/index.ts'; +import { + except, + exceptAll, + intersect, + intersectAll, + type MySqlSetOperator, + union, + unionAll, +} from '~/mysql-core/index.ts'; import { desc, sql } from '~/sql/index.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -17,15 +25,15 @@ const unionTest = await db Expect>; const unionAllTest = await db - .select({ id: users.id, text: users.text }) + .select({ id: users.id, age: users.age1 }) .from(users) .unionAll( - db.select({ id: users.id, text: users.text }) + db.select({ id: users.id, age: users.age1 }) .from(users) .leftJoin(cities, eq(users.id, cities.id)), ); -Expect>; +Expect>; const intersectTest = await db .select({ id: users.id, homeCity: users.homeCity }) @@ -46,7 +54,7 @@ Expect>; const intersectAllTest = await db .select({ id: users.id, homeCity: users.class }) .from(users) - .intersectAll( + .intersect( db .select({ id: users.id, homeCity: users.class }) .from(users) @@ -69,7 +77,7 @@ Expect>; const exceptAllTest = await db .select({ id: users.id, homeCity: users.class }) .from(users) - .exceptAll( + .except( db .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) .from(users), @@ -112,10 +120,16 @@ const intersect2Test = await intersect( Expect>; -const intersectAll2Test = await intersectAll( - db.select({ - id: cities.id, - }).from(cities), +const intersectAll2Test = await intersect( + union( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities).where(sql``), + ), db.select({ id: cities.id, }) @@ -136,47 +150,97 @@ const except2Test = await except( Expect>; -const exceptAll2Test = await exceptAll( +const exceptAll2Test = await except( db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, }) - .from(newYorkers), + .from(newYorkers).where(sql``), db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, - }).from(newYorkers), + }).from(newYorkers).leftJoin(newYorkers, sql``), ); Expect>; +const unionfull = await union(db.select().from(users), db.select().from(users)).orderBy(sql``).limit(1).offset(2); + +Expect< + Equal<{ + id: number; + text: string | null; + homeCity: number; + currentCity: number | null; + serialNullable: number; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + }[], typeof unionfull> +>; + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + +union(db.select().from(users), db.select().from(users)) + .offset(1) + // @ts-expect-error - method was already called + .offset(2); + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + { - const query = db - .select() - .from(users) - .union( - db.select() - .from(users), - ) - .prepare() - .iterator(); - for await (const row of query) { - Expect>(); + function dynamic(qb: T) { + return qb.orderBy(sql``).limit(1).offset(2); } + + const qb = union(db.select().from(users), db.select().from(users)).$dynamic(); + const result = await dynamic(qb); + Expect>; } +await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + .intersect(({ intersect }) => + intersect( + db + .select() + .from(users), + db + .select() + .from(users), + ) + ); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error +db.select({ id: classes.id }).from(classes).union(db.select().from(classes).where(sql``)); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( - db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), db.select({ id: cities.id, name: cities.name }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type @@ -184,6 +248,16 @@ union( db.select().from(cities), ); +union( + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``).limit(3).$dynamic(), + db.select({ id: cities.id, name: cities.name }).from(cities), +); + union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -204,18 +278,18 @@ union( // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: newYorkers.userId }).from(newYorkers), db.select({ id: cities.id }).from(cities), ); union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities).where(sql``), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error - db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities).where(sql``), ); From 0b1d5ae8848b0fe03f27a8c1b3e926339300be08 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 14 Oct 2023 21:17:46 -0400 Subject: [PATCH 26/34] [Pg] fixed types, addressed comments and added new type tests, fixed eslint errors --- drizzle-orm/src/mysql-core/dialect.ts | 2 +- .../mysql-core/query-builders/select.types.ts | 31 +-- .../query-builders/set-operators.ts | 7 +- drizzle-orm/src/pg-core/dialect.ts | 7 +- .../src/pg-core/query-builders/select.ts | 4 +- .../pg-core/query-builders/select.types.ts | 210 +++++++++++++++++- .../pg-core/query-builders/set-operators.ts | 159 ++++++------- drizzle-orm/src/sqlite-core/dialect.ts | 2 +- .../query-builders/select.types.ts | 32 +-- .../query-builders/set-operators.ts | 13 +- drizzle-orm/type-tests/mysql/set-operators.ts | 15 +- drizzle-orm/type-tests/pg/set-operators.ts | 104 +++++++-- .../type-tests/sqlite/set-operators.ts | 13 +- 13 files changed, 425 insertions(+), 174 deletions(-) diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 138c78a3f..fe936d97f 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -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, ViewBaseConfig } from '~/view.ts'; -import { DrizzleError, Name } from '../index.ts'; +import { DrizzleError, type Name } 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'; diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 3492d3607..e70475dd4 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -24,8 +24,7 @@ import type { Subquery } from '~/subquery.ts'; import type { Table, UpdateTableConfig } from '~/table.ts'; import type { Assume, ValidateShape } from '~/utils.ts'; import type { ColumnsSelection, View } from '~/view.ts'; -import type { MySqlDialect } from '../dialect.ts'; -import type { MySqlSession, PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; +import type { PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; import type { MySqlSelectBase, MySqlSelectQueryBuilderBase } from './select.ts'; import type { MySqlSetOperatorBase, MySqlSetOperatorBuilder } from './set-operators.ts'; @@ -343,9 +342,20 @@ export interface MySqlSetOperatorInterface< TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - TypedQueryBuilder< - TSelectedFields, - TResult + Omit< + MySqlSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' > { _: { @@ -360,13 +370,6 @@ export interface MySqlSetOperatorInterface< readonly result: TResult; readonly selectedFields: TSelectedFields; }; - getSelectedFields: () => TSelectedFields; - getSetOperatorConfig: () => { - session: MySqlSession | undefined; - dialect: MySqlDialect; - joinsNotNullableMap: Record; - fields: Record; - }; } export type AnyMySqlSetOperatorBase = MySqlSetOperatorInterface< @@ -415,8 +418,8 @@ export type MySqlCreateSetOperatorFn = < TSelectMode, TPreparedQueryHKT, TNullabilityMap, - TDynamic, - TExcludedMethods, + false, + never, TResult, TSelectedFields >; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 160b55123..7b1b742ce 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -115,8 +115,8 @@ export abstract class MySqlSetOperatorBuilder< TSelectMode, TPreparedQueryHKT, TNullabilityMap, - TDynamic, - TExcludedMethods, + false, + never, TResult, TSelectedFields > { @@ -143,6 +143,7 @@ export interface MySqlSetOperatorBase< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, + // eslint-disable-next-line @typescript-eslint/no-unused-vars TPreparedQueryHKT extends PreparedQueryHKTBase, TNullabilityMap extends Record = TTableName extends string ? Record : {}, @@ -336,7 +337,7 @@ applyMixins(MySqlSetOperatorBase, [QueryPromise]); function createSetOperator(type: SetOperator, isAll: boolean): MySqlCreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { - return new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any); + return new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; } const [select, ...rest] = restSelects; diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 330ef45be..f86923a71 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -29,6 +29,7 @@ import { and, type DriverValueEncoder, eq, + type Name, Param, type QueryTypingsValue, type QueryWithTypings, @@ -361,19 +362,19 @@ export class PgDialect { let orderBySql; if (orderBy && orderBy.length > 0) { - const orderByValues: SQL[] = []; + const orderByValues: (SQL | 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 singleOrderBy of orderBy) { if (is(singleOrderBy, PgColumn)) { - orderByValues.push(sql.raw(singleOrderBy.name)); + orderByValues.push(sql.identifier(singleOrderBy.name)); } else if (is(singleOrderBy, SQL)) { for (let i = 0; i < singleOrderBy.queryChunks.length; i++) { const chunk = singleOrderBy.queryChunks[i]; if (is(chunk, PgColumn)) { - singleOrderBy.queryChunks[i] = sql.raw(chunk.name); + singleOrderBy.queryChunks[i] = sql.identifier(chunk.name); } } diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 6894f03b7..351163aaf 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -126,7 +126,7 @@ export abstract class PgSelectQueryBuilderBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends PgSetOperatorBuilder< THKT, @@ -514,7 +514,7 @@ export interface PgSelectBase< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends PgSelectQueryBuilderBase< diff --git a/drizzle-orm/src/pg-core/query-builders/select.types.ts b/drizzle-orm/src/pg-core/query-builders/select.types.ts index 949736a3a..832f2c37a 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.types.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.types.ts @@ -6,6 +6,7 @@ import type { import type { PgColumn } from '~/pg-core/columns/index.ts'; import type { PgTable, PgTableWithColumns } from '~/pg-core/table.ts'; import type { PgViewBase, PgViewWithSelection } from '~/pg-core/view.ts'; +import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { AppendToNullabilityMap, AppendToResult, @@ -16,14 +17,16 @@ import type { MapColumnsToTableAlias, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import type { Placeholder, SQL, SQLWrapper } from '~/sql/index.ts'; import type { Subquery } from '~/subquery.ts'; import type { Table, UpdateTableConfig } from '~/table.ts'; -import type { Assume, ValueOrArray } from '~/utils.ts'; +import type { Assume, ValidateShape, ValueOrArray } from '~/utils.ts'; import type { ColumnsSelection, View } from '~/view.ts'; import type { PreparedQuery, PreparedQueryConfig } from '../session.ts'; import type { PgSelectBase, PgSelectQueryBuilderBase } from './select.ts'; +import type { PgSetOperatorBase, PgSetOperatorBuilder } from './set-operators.ts'; export interface PgSelectJoinConfig { on: SQL | undefined; @@ -175,7 +178,7 @@ export interface PgSelectQueryBuilderHKT extends PgSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -188,7 +191,7 @@ export interface PgSelectHKT extends PgSelectHKTBase { Assume>, this['dynamic'], this['excludedMethods'], - this['result'], + Assume, Assume >; } @@ -244,7 +247,7 @@ export type PgSelectQueryBuilder< TSelection extends ColumnsSelection = ColumnsSelection, TSelectMode extends SelectMode = SelectMode, TNullabilityMap extends Record = Record, - TResult = unknown, + TResult extends any[] = unknown[], TSelectedFields extends ColumnsSelection = ColumnsSelection, > = PgSelectQueryBuilderBase< THKT, @@ -268,3 +271,202 @@ export type PgSelect< > = PgSelectBase; export type AnyPgSelect = PgSelectBase; + +export type PgSetOperatorBaseWithResult = PgSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + T, + any +>; + +export type SetOperatorRightSelect< + TValue extends PgSetOperatorBaseWithResult, + TResult extends any[], +> = TValue extends PgSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly PgSetOperatorBaseWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends PgSetOperatorInterface + ? TValueResult extends Array + ? Rest extends PgSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : never + : TValue; + +export interface PgSetOperationConfig { + fields: Record; + operator: SetOperator; + isAll: boolean; + leftSelect: AnyPgSetOperatorBase; + rightSelect: TypedQueryBuilder; + limit?: number | Placeholder; + orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + offset?: number | Placeholder; +} + +export interface PgSetOperatorInterface< + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +> extends + Omit< + PgSetOperatorBuilder< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' + > +{ + _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; +} + +export type AnyPgSetOperatorBase = PgSetOperatorInterface< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type PgCreateSetOperatorFn = < + THKT extends PgSelectHKTBase, + TTableName extends string | undefined, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode, + TNullabilityMap extends Record, + TValue extends PgSetOperatorBaseWithResult, + TRest extends PgSetOperatorBaseWithResult[], + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, +>( + leftSelect: PgSetOperatorInterface< + THKT, + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + rightSelect: SetOperatorRightSelect, + ...restSelects: SetOperatorRestSelect +) => PgSetOperatorBase< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + false, + never, + TResult, + TSelectedFields +>; + +export type AnyPgSetOperator = PgSetOperatorBase< + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type PgSetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = PgSetOperatorBase; + +export type AnyPgSetOperatorBuilder = PgSetOperatorBuilder< + any, + any, + any, + any, + any, + any, + any, + any, + any +>; + +export type PgSetOperatorWithout< + T extends AnyPgSetOperator, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true ? T + : Omit< + PgSetOperatorBase< + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + TDynamic, + T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + T['_']['excludedMethods'] | K + >; + +export type PgSetOperatorDynamic = PgSetOperatorBase< + T['_']['tableName'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + true, + never, + T['_']['result'], + T['_']['selectedFields'] +>; diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index f40893c87..31ee78c0e 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -15,17 +15,25 @@ import type { JoinNullability, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; -import { applyMixins, haveSameKeys, type ValidateShape } from '~/utils.ts'; +import { applyMixins, haveSameKeys } from '~/utils.ts'; import { type ColumnsSelection } from '~/view.ts'; import { type PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; -import type { PgSelectHKTBase } from './select.types.ts'; - -type SetOperator = 'union' | 'intersect' | 'except'; +import type { + PgCreateSetOperatorFn, + PgSelectHKTBase, + PgSetOperationConfig, + PgSetOperatorBaseWithResult, + PgSetOperatorDynamic, + PgSetOperatorInterface, + PgSetOperatorWithout, + SetOperatorRightSelect, +} from './select.types.ts'; const getPgSetOperators = () => { return { @@ -40,44 +48,7 @@ const getPgSetOperators = () => { type PgSetOperators = ReturnType; -type SetOperatorRightSelect< - TValue extends TypedQueryBuilder[]>, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, -> = TValue extends PgSetOperatorBuilder ? ValidateShape< - SelectResult, - SelectResult, - TypedQueryBuilder[]> - > - : TValue; - -type SetOperatorRestSelect< - TValue extends readonly TypedQueryBuilder[], - Valid, -> = TValue extends [infer First, ...infer Rest] - ? First extends PgSetOperatorBuilder - ? Rest extends TypedQueryBuilder[] ? [ - ValidateShape, Valid, TValue[0]>, - ...SetOperatorRestSelect, - ] - : ValidateShape, Valid, TValue> - : never[] - : TValue; - -export interface PgSetOperationConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: PgSetOperatorBuilder; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (PgColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} - export abstract class PgSetOperatorBuilder< - // eslint-disable-next-line @typescript-eslint/no-unused-vars THKT extends PgSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, @@ -86,11 +57,23 @@ export abstract class PgSetOperatorBuilder< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends TypedQueryBuilder { static readonly [entityKind]: string = 'PgSetOperatorBuilder'; + abstract override readonly _: { + readonly hkt: THKT; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected abstract joinsNotNullableMap: Record; protected abstract config: { fields: Record; @@ -115,25 +98,24 @@ export abstract class PgSetOperatorBuilder< private setOperator( type: SetOperator, isAll: boolean, - ): []>>( + ): >( rightSelect: - | SetOperatorRightSelect - | ((setOperator: PgSetOperators) => SetOperatorRightSelect), - ) => PgSetOperator< - THKT, + | ((setOperator: PgSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => PgSetOperatorBase< TTableName, TSelection, TSelectMode, TNullabilityMap, - TDynamic, - TExcludedMethods, + false, + never, TResult, TSelectedFields > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - return new PgSetOperator(type, isAll, this, rightSelectOrig); + return new PgSetOperatorBase(type, isAll, this, rightSelectOrig as any) as any; }; } @@ -150,9 +132,7 @@ export abstract class PgSetOperatorBuilder< exceptAll = this.setOperator('except', true); } -export interface PgSetOperator< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - THKT extends PgSelectHKTBase, +export interface PgSetOperatorBase< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -160,11 +140,11 @@ export interface PgSetOperator< : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, - TResult = SelectResult[], + TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends PgSetOperatorBuilder< - THKT, + PgSelectHKTBase, TTableName, TSelection, TSelectMode, @@ -177,8 +157,7 @@ export interface PgSetOperator< QueryPromise {} -export class PgSetOperator< - THKT extends PgSelectHKTBase, +export class PgSetOperatorBase< TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -189,7 +168,7 @@ export class PgSetOperator< TResult = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends PgSetOperatorBuilder< - THKT, + PgSelectHKTBase, TTableName, TSelection, TSelectMode, @@ -201,6 +180,18 @@ export class PgSetOperator< > { static readonly [entityKind]: string = 'PgSetOperator'; + readonly _: { + readonly hkt: PgSelectHKTBase; + readonly tableName: TTableName; + readonly selection: TSelection; + readonly selectMode: TSelectMode; + readonly nullabilityMap: TNullabilityMap; + readonly dynamic: TDynamic; + readonly excludedMethods: TExcludedMethods; + readonly result: TResult; + readonly selectedFields: TSelectedFields; + }; + protected joinsNotNullableMap: Record; protected config: PgSetOperationConfig; /* @internal */ @@ -210,8 +201,8 @@ export class PgSetOperator< constructor( operator: SetOperator, isAll: boolean, - leftSelect: PgSetOperatorBuilder< - THKT, + leftSelect: PgSetOperatorInterface< + PgSelectHKTBase, TTableName, TSelection, TSelectMode, @@ -221,7 +212,7 @@ export class PgSetOperator< TResult, TSelectedFields >, - rightSelect: TypedQueryBuilder[]>, + rightSelect: TypedQueryBuilder, ) { super(); @@ -252,13 +243,15 @@ export class PgSetOperator< }; } - orderBy(builder: (aliases: TSelection) => ValueOrArray): this; - orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): this; + orderBy( + builder: (aliases: TSelection) => ValueOrArray, + ): PgSetOperatorWithout; + orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): PgSetOperatorWithout; orderBy( ...columns: | [(aliases: TSelection) => ValueOrArray] | (PgColumn | SQL | SQL.Aliased)[] - ) { + ): PgSetOperatorWithout { if (typeof columns[0] === 'function') { const orderBy = columns[0]( new Proxy( @@ -270,17 +263,17 @@ export class PgSetOperator< } else { this.config.orderBy = columns as (PgColumn | SQL | SQL.Aliased)[]; } - return this; + return this as any; } - limit(limit: number) { + limit(limit: number): PgSetOperatorWithout { this.config.limit = limit; - return this; + return this as any; } - offset(offset: number | Placeholder) { + offset(offset: number | Placeholder): PgSetOperatorWithout { this.config.offset = offset; - return this; + return this as any; } toSQL(): Query { @@ -340,32 +333,28 @@ export class PgSetOperator< new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection, TAlias>; } + + $dynamic(): PgSetOperatorDynamic { + return this as any; + } } -applyMixins(PgSetOperator, [QueryPromise]); +applyMixins(PgSetOperatorBase, [QueryPromise]); -function setOperator(type: SetOperator, isAll: boolean): < - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends TypedQueryBuilder[]>, - TRest extends TypedQueryBuilder[]>[], ->( - leftSelect: PgSetOperatorBuilder, - rightSelect: SetOperatorRightSelect, - ...restSelects: SetOperatorRestSelect> -) => PgSetOperator { +function setOperator(type: SetOperator, isAll: boolean): PgCreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { - return new PgSetOperator(type, isAll, leftSelect, rightSelect); + return new PgSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; } const [select, ...rest] = restSelects; if (!select) throw new Error('Cannot pass undefined values to any set operator'); - return setOperator(type, isAll)(new PgSetOperator(type, isAll, leftSelect, rightSelect), select, ...rest); + return setOperator(type, isAll)( + new PgSetOperatorBase(type, isAll, leftSelect, rightSelect as any), + select as any, + ...rest, + ); }; } diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 5070f701b..925ec9758 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -16,7 +16,7 @@ import { type TableRelationalConfig, type TablesRelationalConfig, } from '~/relations.ts'; -import { and, eq, Name, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; +import { and, eq, type Name, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; import { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; import type { SQLiteDeleteConfig, diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 9f8634be0..3e4c7fe3f 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -25,8 +25,7 @@ import type { import type { Subquery } from '~/subquery.ts'; import type { Table, UpdateTableConfig } from '~/table.ts'; import { type ColumnsSelection, type View } from '~/view.ts'; -import type { SQLiteDialect } from '../dialect.ts'; -import type { SQLitePreparedQuery, SQLiteSession } from '../session.ts'; +import type { SQLitePreparedQuery } from '../session.ts'; import type { SQLiteViewBase, SQLiteViewWithSelection } from '../view.ts'; import type { SQLiteSelectBase, SQLiteSelectQueryBuilderBase } from './select.ts'; import type { SQLiteSetOperatorBase, SQLiteSetOperatorBuilder } from './set-operators.ts'; @@ -372,9 +371,21 @@ export interface SQLiteSetOperatorInterface< TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - TypedQueryBuilder< - TSelectedFields, - TResult + Omit< + SQLiteSetOperatorBuilder< + THKT, + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' > { _: { @@ -391,13 +402,6 @@ export interface SQLiteSetOperatorInterface< readonly result: TResult; readonly selectedFields: TSelectedFields; }; - getSelectedFields: () => TSelectedFields; - getSetOperatorConfig: () => { - session: SQLiteSession | undefined; - dialect: SQLiteDialect; - joinsNotNullableMap: Record; - fields: Record; - }; } export type AnySQLiteSetOperatorBase = SQLiteSetOperatorInterface< @@ -451,8 +455,8 @@ export type SQLiteCreateSetOperatorFn = < TSelection, TSelectMode, TNullabilityMap, - TDynamic, - TExcludedMethods, + false, + never, TResult, TSelectedFields >; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts index 930a13abf..1538a1b0b 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts @@ -115,20 +115,15 @@ export abstract class SQLiteSetOperatorBuilder< TSelection, TSelectMode, TNullabilityMap, - TDynamic, - TExcludedMethods, + false, + never, TResult, TSelectedFields > { return (rightSelect) => { const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - return new SQLiteSetOperatorBase( - type, - isAll, - this, - rightSelectOrig as any, - ); + return new SQLiteSetOperatorBase(type, isAll, this, rightSelectOrig as any) as any; }; } @@ -358,7 +353,7 @@ applyMixins(SQLiteSetOperatorBase, [QueryPromise]); function createSetOperator(type: SetOperator, isAll: boolean): SQLiteCreateSetOperatorFn { return (leftSelect, rightSelect, ...restSelects) => { if (restSelects.length === 0) { - return new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any); + return new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; } const [select, ...rest] = restSelects; diff --git a/drizzle-orm/type-tests/mysql/set-operators.ts b/drizzle-orm/type-tests/mysql/set-operators.ts index a936868ed..9afac2346 100644 --- a/drizzle-orm/type-tests/mysql/set-operators.ts +++ b/drizzle-orm/type-tests/mysql/set-operators.ts @@ -120,7 +120,7 @@ const intersect2Test = await intersect( Expect>; -const intersectAll2Test = await intersect( +const intersectAll2Test = await intersectAll( union( db.select({ id: cities.id, @@ -150,7 +150,7 @@ const except2Test = await except( Expect>; -const exceptAll2Test = await except( +const exceptAll2Test = await exceptAll( db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, @@ -213,16 +213,7 @@ await db // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error - .intersect(({ intersect }) => - intersect( - db - .select() - .from(users), - db - .select() - .from(users), - ) - ); + .intersect(({ intersect }) => intersect(db.select().from(users), db.select().from(users))); // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type diff --git a/drizzle-orm/type-tests/pg/set-operators.ts b/drizzle-orm/type-tests/pg/set-operators.ts index 4394fa43d..3d53c4043 100644 --- a/drizzle-orm/type-tests/pg/set-operators.ts +++ b/drizzle-orm/type-tests/pg/set-operators.ts @@ -1,6 +1,6 @@ import { type Equal, Expect } from 'type-tests/utils.ts'; import { eq } from '~/expressions.ts'; -import { except, exceptAll, intersect, intersectAll, union, unionAll } from '~/pg-core/index.ts'; +import { except, exceptAll, intersect, intersectAll, type PgSetOperator, union, unionAll } from '~/pg-core/index.ts'; import { desc, sql } from '~/sql/index.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -17,15 +17,15 @@ const unionTest = await db Expect>; const unionAllTest = await db - .select({ id: users.id, text: users.text }) + .select({ id: users.id, age: users.age1 }) .from(users) .unionAll( - db.select({ id: users.id, text: users.text }) + db.select({ id: users.id, age: users.age1 }) .from(users) .leftJoin(cities, eq(users.id, cities.id)), ); -Expect>; +Expect>; const intersectTest = await db .select({ id: users.id, homeCity: users.homeCity }) @@ -46,7 +46,7 @@ Expect>; const intersectAllTest = await db .select({ id: users.id, homeCity: users.class }) .from(users) - .intersectAll( + .intersect( db .select({ id: users.id, homeCity: users.class }) .from(users) @@ -69,7 +69,7 @@ Expect>; const exceptAllTest = await db .select({ id: users.id, homeCity: users.class }) .from(users) - .exceptAll( + .except( db .select({ id: users.id, homeCity: sql<'A' | 'C'>`${users.class}` }) .from(users), @@ -113,9 +113,15 @@ const intersect2Test = await intersect( Expect>; const intersectAll2Test = await intersectAll( - db.select({ - id: cities.id, - }).from(cities), + union( + db.select({ + id: cities.id, + }).from(cities), + db.select({ + id: cities.id, + }) + .from(cities).where(sql``), + ), db.select({ id: cities.id, }) @@ -141,27 +147,85 @@ const exceptAll2Test = await exceptAll( userId: newYorkers.userId, cityId: newYorkers.cityId, }) - .from(newYorkers), + .from(newYorkers).where(sql``), db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, - }).from(newYorkers), + }).from(newYorkers).leftJoin(newYorkers, sql``), ); Expect>; +const unionfull = await union(db.select().from(users), db.select().from(users)).orderBy(sql``).limit(1).offset(2); + +Expect< + Equal<{ + id: number; + uuid: string; + homeCity: number; + currentCity: number | null; + serialNullable: number; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + text: string | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + arrayCol: string[]; + }[], typeof unionfull> +>; + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + +union(db.select().from(users), db.select().from(users)) + .offset(1) + // @ts-expect-error - method was already called + .offset(2); + +union(db.select().from(users), db.select().from(users)) + .orderBy(sql``) + // @ts-expect-error - method was already called + .orderBy(sql``); + +{ + function dynamic(qb: T) { + return qb.orderBy(sql``).limit(1).offset(2); + } + + const qb = union(db.select().from(users), db.select().from(users)).$dynamic(); + const result = await dynamic(qb); + Expect>; +} + +await db + .select({ id: users.id, homeCity: users.homeCity }) + .from(users) + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + .intersect(({ intersect }) => intersect(db.select().from(users), db.select().from(users))); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select().from(classes).union(db.select({ id: classes.id }).from(classes)); +// All queries in combining statements should return the same number of columns +// and the corresponding columns should have compatible data type +// @ts-expect-error +db.select({ id: classes.id }).from(classes).union(db.select().from(classes).where(sql``)); + // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error db.select({ id: classes.id }).from(classes).union(db.select().from(classes)); union( - db.select({ id: cities.id, name: cities.name }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), db.select({ id: cities.id, name: cities.name }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type @@ -169,6 +233,16 @@ union( db.select().from(cities), ); +union( + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``), + // All queries in combining statements should return the same number of columns + // and the corresponding columns should have compatible data type + // @ts-expect-error + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name }).from(cities).where(sql``).limit(3).$dynamic(), + db.select({ id: cities.id, name: cities.name }).from(cities), +); + union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), @@ -189,18 +263,18 @@ union( // @ts-expect-error db.select({ id: cities.id, name: cities.name }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: newYorkers.userId }).from(newYorkers), db.select({ id: cities.id }).from(cities), ); union( db.select({ id: cities.id }).from(cities), db.select({ id: cities.id }).from(cities), - db.select({ id: cities.id }).from(cities), + db.select({ id: cities.id }).from(cities).where(sql``), db.select({ id: sql`${cities.id}` }).from(cities), db.select({ id: cities.id }).from(cities), // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error - db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities), + db.select({ id: cities.id, name: cities.name, population: cities.population }).from(cities).where(sql``), ); diff --git a/drizzle-orm/type-tests/sqlite/set-operators.ts b/drizzle-orm/type-tests/sqlite/set-operators.ts index 86734c2bb..e0239ba24 100644 --- a/drizzle-orm/type-tests/sqlite/set-operators.ts +++ b/drizzle-orm/type-tests/sqlite/set-operators.ts @@ -147,7 +147,7 @@ const exceptAll2Test = await except( userId: newYorkers.userId, cityId: newYorkers.cityId, }) - .from(newYorkers).where(sql``), + .from(newYorkers).where(eq(newYorkers.cityId, 2)), db.select({ userId: newYorkers.userId, cityId: newYorkers.cityId, @@ -205,16 +205,7 @@ await db // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type // @ts-expect-error - .intersect(({ intersect }) => - intersect( - db - .select() - .from(users), - db - .select() - .from(users), - ) - ); + .intersect(({ intersect }) => intersect(db.select().from(users), db.select().from(users))); // All queries in combining statements should return the same number of columns // and the corresponding columns should have compatible data type From 57889e17d89cf62a2b36057cf0394e686a868f2b Mon Sep 17 00:00:00 2001 From: Angelelz Date: Sat, 14 Oct 2023 21:54:36 -0400 Subject: [PATCH 27/34] [All] fix typescript errors --- drizzle-orm/src/mysql-core/dialect.ts | 4 ++-- drizzle-orm/src/mysql-core/query-builders/set-operators.ts | 2 +- drizzle-orm/src/pg-core/query-builders/set-operators.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index fe936d97f..4d3edfeca 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -18,8 +18,8 @@ import { and, eq, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from ' import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; -import { View, ViewBaseConfig } from '~/view.ts'; -import { DrizzleError, type Name } from '../index.ts'; +import { View } from '~/view.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'; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts index 7b1b742ce..92b5d3680 100644 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts @@ -25,7 +25,7 @@ import type { SelectResult, SetOperator, } from '~/query-builders/select.types.ts'; -import { type ColumnsSelection } from '~/view.ts'; +import type { ColumnsSelection } from '~/view.ts'; import type { MySqlColumn } from '../columns/common.ts'; import type { MySqlDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts index 31ee78c0e..27c024f1c 100644 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ b/drizzle-orm/src/pg-core/query-builders/set-operators.ts @@ -20,8 +20,8 @@ import type { import { QueryPromise } from '~/query-promise.ts'; import { tracer } from '~/tracing.ts'; import { applyMixins, haveSameKeys } from '~/utils.ts'; -import { type ColumnsSelection } from '~/view.ts'; -import { type PgColumn } from '../columns/common.ts'; +import type { ColumnsSelection } from '~/view.ts'; +import type { PgColumn } from '../columns/common.ts'; import type { PgDialect } from '../dialect.ts'; import type { SubqueryWithSelection } from '../subquery.ts'; import type { From 8fcd2d41d498116589c401d0bd50fb114628959e Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 20 Oct 2023 08:09:54 -0400 Subject: [PATCH 28/34] [MySql] Simplification by removing intermediary clases and making the queries mutable --- drizzle-orm/src/mysql-core/dialect.ts | 51 ++- .../src/mysql-core/query-builders/index.ts | 1 - .../src/mysql-core/query-builders/select.ts | 150 +++++++- .../mysql-core/query-builders/select.types.ts | 287 +++++++------- .../query-builders/set-operators.ts | 364 ------------------ 5 files changed, 309 insertions(+), 544 deletions(-) delete mode 100644 drizzle-orm/src/mysql-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 4d3edfeca..a41dc6169 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -23,12 +23,7 @@ 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'; -import type { - MySqlSelectConfig, - MySqlSelectJoinConfig, - MySqlSetOperationConfig, - SelectedFieldsOrdered, -} from './query-builders/select.types.ts'; +import type { MySqlSelectConfig, MySqlSelectJoinConfig, SelectedFieldsOrdered } from './query-builders/select.types.ts'; import type { MySqlUpdateConfig } from './query-builders/update.ts'; import type { MySqlSession } from './session.ts'; import { MySqlTable } from './table.ts'; @@ -209,6 +204,7 @@ export class MySqlDialect { offset, lockingClause, distinct, + setOperators, }: MySqlSelectConfig, ): SQL { const fieldsList = fieldsFlat ?? orderSelectedFields(fields); @@ -336,18 +332,37 @@ 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 }); + } + + return this.buildSetOperations( + this.buildSetOperationQuery({ leftSelect, setOperator }), + rest, + ); } buildSetOperationQuery({ - operator, - isAll, leftSelect, - rightSelect, - limit, - orderBy, - offset, - }: MySqlSetOperationConfig): SQL { + setOperator: { type, isAll, rightSelect, limit, orderBy, offset }, + }: { leftSelect: SQL; setOperator: MySqlSelectConfig['setOperators'][number] }): SQL { const leftChunk = sql`(${leftSelect.getSQL()}) `; const rightChunk = sql`(${rightSelect.getSQL()})`; @@ -380,7 +395,7 @@ export class MySqlDialect { const limitSql = limit ? sql` limit ${limit}` : undefined; - const operatorChunk = sql.raw(`${operator} ${isAll ? 'all ' : ''}`); + const operatorChunk = sql.raw(`${type} ${isAll ? 'all ' : ''}`); const offsetSql = offset ? sql` offset ${offset}` : undefined; @@ -684,6 +699,7 @@ export class MySqlDialect { where, limit, offset, + setOperators: [], }); where = undefined; @@ -706,6 +722,7 @@ export class MySqlDialect { limit, offset, orderBy, + setOperators: [], }); } else { result = this.buildSelectQuery({ @@ -720,6 +737,7 @@ export class MySqlDialect { limit, offset, orderBy, + setOperators: [], }); } @@ -974,6 +992,7 @@ export class MySqlDialect { where, limit, offset, + setOperators: [], }); where = undefined; @@ -995,6 +1014,7 @@ export class MySqlDialect { limit, offset, orderBy, + setOperators: [], }); } else { result = this.buildSelectQuery({ @@ -1008,6 +1028,7 @@ export class MySqlDialect { limit, offset, orderBy, + setOperators: [], }); } diff --git a/drizzle-orm/src/mysql-core/query-builders/index.ts b/drizzle-orm/src/mysql-core/query-builders/index.ts index 58d909c22..16f0e1d4d 100644 --- a/drizzle-orm/src/mysql-core/query-builders/index.ts +++ b/drizzle-orm/src/mysql-core/query-builders/index.ts @@ -3,5 +3,4 @@ export * from './insert.ts'; export * from './query-builder.ts'; export * from './select.ts'; export * from './select.types.ts'; -export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index 53e51a2ee..aac36dc04 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -5,6 +5,7 @@ import type { MySqlSession, PreparedQueryConfig, PreparedQueryHKTBase } from '~/ import type { SubqueryWithSelection } from '~/mysql-core/subquery.ts'; import type { MySqlTable } from '~/mysql-core/table.ts'; import { MySqlViewBase } from '~/mysql-core/view.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -13,19 +14,23 @@ 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, @@ -33,9 +38,11 @@ import type { MySqlSelectHKTBase, MySqlSelectPrepare, MySqlSelectWithout, + MySqlSetOperatorExcludedMethods, + MySqlSetOperatorWithResult, SelectedFields, + SetOperatorRightSelect, } from './select.types.ts'; -import { MySqlSetOperatorBuilder } from './set-operators.ts'; export class MySqlSelectBuilder< TSelection extends SelectedFields | undefined, @@ -123,18 +130,7 @@ export abstract class MySqlSelectQueryBuilderBase< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends MySqlSetOperatorBuilder< - THKT, - TTableName, - TSelection, - TSelectMode, - PreparedQueryHKTBase, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { +> extends TypedQueryBuilder { static readonly [entityKind]: string = 'MySqlSelectQueryBuilder'; override readonly _: { @@ -175,6 +171,7 @@ export abstract class MySqlSelectQueryBuilderBase< table, fields: { ...fields }, distinct, + setOperators: [], }; this.isPartialSelect = isPartialSelect; this.session = session; @@ -271,6 +268,61 @@ export abstract class MySqlSelectQueryBuilderBase< fullJoin = this.createJoin('full'); + private createSetOperator( + type: SetOperator, + isAll: boolean, + ): >( + rightSelection: + | ((setOperators: GetMySqlSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => 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( + '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 { @@ -340,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.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.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; } @@ -474,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); diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index e70475dd4..5fc35df99 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -26,7 +26,6 @@ import type { Assume, ValidateShape } from '~/utils.ts'; import type { ColumnsSelection, View } from '~/view.ts'; import type { PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; import type { MySqlSelectBase, MySqlSelectQueryBuilderBase } from './select.ts'; -import type { MySqlSetOperatorBase, MySqlSetOperatorBuilder } from './set-operators.ts'; export interface MySqlSelectJoinConfig { on: SQL | undefined; @@ -67,6 +66,14 @@ export interface MySqlSelectConfig { config: LockConfig; }; distinct?: boolean; + setOperators: { + rightSelect: TypedQueryBuilder; + type: SetOperator; + isAll: boolean; + orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; + limit?: number | Placeholder; + offset?: number | Placeholder; + }[]; } export type MySqlJoin< @@ -195,10 +202,32 @@ export interface MySqlSelectHKT extends MySqlSelectHKTBase { >; } +export type MySqlSetOperatorExcludedMethods = Exclude< + keyof AnyMySqlSelectQueryBuilder, + // Only add the methods that a SetOperator is supposed to have + | 'orderBy' + | 'limit' + | 'offset' + | 'union' + | 'unionAll' + | 'intersect' + | 'intersectAll' + | 'except' + | 'exceptAll' + | '_' + | 'getSQL' + | 'as' + | 'addSetOperators' + | 'toSQL' + | '$dynamic' + | 'getSelectedFields' +>; + export type MySqlSelectWithout< T extends AnyMySqlSelectQueryBuilder, TDynamic extends boolean, K extends keyof T & string, + TResetExcluded extends boolean = false, > = TDynamic extends true ? T : Omit< MySqlSelectBase< T['_']['tableName'], @@ -207,11 +236,11 @@ export type MySqlSelectWithout< T['_']['preparedQueryHKT'], T['_']['nullabilityMap'], TDynamic, - T['_']['excludedMethods'] | K, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K, T['_']['result'], T['_']['selectedFields'] >, - T['_']['excludedMethods'] | K + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K >; export type MySqlSelectPrepare = PreparedQueryKind< @@ -269,68 +298,9 @@ export type MySqlSelectQueryBuilder< export type AnyMySqlSelectQueryBuilder = MySqlSelectQueryBuilderBase; -export type MySqlSelect< - TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TNullabilityMap extends Record = Record, -> = MySqlSelectBase; - -export type AnyMySqlSelect = MySqlSelectBase; - -export type MySqlSetOperatorBaseWithResult = MySqlSetOperatorInterface< - any, - any, - any, - any, - any, - any, - any, - any, - T, - any ->; - -export type SetOperatorRightSelect< - TValue extends MySqlSetOperatorBaseWithResult, - TResult extends any[], -> = TValue extends MySqlSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never - : TValue; - -export type SetOperatorRestSelect< - TValue extends readonly MySqlSetOperatorBaseWithResult[], - TResult extends any[], -> = TValue extends [infer First, ...infer Rest] - ? First extends MySqlSetOperatorInterface - ? TValueResult extends Array - ? Rest extends MySqlSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never - : never - : TValue; - -export interface MySqlSetOperationConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: AnyMySqlSetOperatorBase; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} +export type AnyMySqlSetOperatorInterface = MySqlSetOperatorInterface; export interface MySqlSetOperatorInterface< - THKT extends MySqlSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -342,9 +312,8 @@ export interface MySqlSetOperatorInterface< TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - Omit< - MySqlSetOperatorBuilder< - THKT, + Pick< + MySqlSelectBase< TTableName, TSelection, TSelectMode, @@ -355,11 +324,11 @@ export interface MySqlSetOperatorInterface< TResult, TSelectedFields >, - 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' + never > { _: { - readonly hkt: THKT; + readonly hkt: MySqlSelectHKT; readonly tableName: TTableName; readonly selection: TSelection; readonly selectMode: TSelectMode; @@ -372,8 +341,32 @@ export interface MySqlSetOperatorInterface< }; } -export type AnyMySqlSetOperatorBase = MySqlSetOperatorInterface< +export type MySqlSelectBuilderWithResult = MySqlSelectQueryBuilderBase< + any, + any, + any, + any, + any, + any, + any, + any, + TResult, + any +>; + +export type MySqlSelectWithResult = MySqlSelectBase< + any, + any, + any, + any, + any, any, + any, + TResult, + any +>; + +export type MySqlSetOperatorWithResult = MySqlSetOperatorInterface< any, any, any, @@ -381,25 +374,76 @@ export type AnyMySqlSetOperatorBase = MySqlSetOperatorInterface< any, any, any, + TResult, any >; +export type MySqlSelect< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = MySqlSelectBase; + +export type AnyMySqlSelect = MySqlSelectBase; + +export type MySqlSetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = Record, +> = MySqlSelectBase< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + true, + MySqlSetOperatorExcludedMethods +>; + +export type SetOperatorRightSelect< + TValue extends MySqlSetOperatorWithResult, + TResult extends any[], +> = TValue extends MySqlSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly MySqlSetOperatorWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends MySqlSetOperatorInterface + ? TValueResult extends Array ? Rest extends AnyMySqlSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : never + : TValue; + export type MySqlCreateSetOperatorFn = < - THKT extends MySqlSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record, - TValue extends MySqlSetOperatorBaseWithResult, - TRest extends MySqlSetOperatorBaseWithResult[], + TValue extends MySqlSetOperatorWithResult, + TRest extends MySqlSetOperatorWithResult[], + TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, >( leftSelect: MySqlSetOperatorInterface< - THKT, TTableName, TSelection, TSelectMode, @@ -412,77 +456,28 @@ export type MySqlCreateSetOperatorFn = < >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect -) => MySqlSetOperatorBase< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, +) => MySqlSelectWithout< + MySqlSelectBase< + TTableName, + TSelection, + TSelectMode, + TPreparedQueryHKT, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, false, - never, - TResult, - TSelectedFields ->; - -export type AnyMySqlSetOperator = MySqlSetOperatorBase< - any, - any, - any, - any, - any, - any, - any, - any ->; - -export type MySqlSetOperator< - TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, - TNullabilityMap extends Record = Record, -> = MySqlSetOperatorBase; - -export type AnyMySqlSetOperatorBuilder = MySqlSetOperatorBuilder< - any, - any, - any, - any, - any, - any, - any, - any, - any + MySqlSetOperatorExcludedMethods, + true >; -export type MySqlSetOperatorWithout< - T extends AnyMySqlSetOperator, - TDynamic extends boolean, - K extends keyof T & string, -> = TDynamic extends true ? T - : Omit< - MySqlSetOperatorBase< - T['_']['tableName'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - T['_']['preparedQueryHKT'], - TDynamic, - T['_']['excludedMethods'] | K, - T['_']['result'], - T['_']['selectedFields'] - >, - T['_']['excludedMethods'] | K - >; - -export type MySqlSetOperatorDynamic = MySqlSetOperatorBase< - T['_']['tableName'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['preparedQueryHKT'], - T['_']['nullabilityMap'], - true, - never, - T['_']['result'], - T['_']['selectedFields'] ->; +export type GetMySqlSetOperators = { + union: MySqlCreateSetOperatorFn; + intersect: MySqlCreateSetOperatorFn; + except: MySqlCreateSetOperatorFn; + unionAll: MySqlCreateSetOperatorFn; + intersectAll: MySqlCreateSetOperatorFn; + exceptAll: MySqlCreateSetOperatorFn; +}; diff --git a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts b/drizzle-orm/src/mysql-core/query-builders/set-operators.ts deleted file mode 100644 index 92b5d3680..000000000 --- a/drizzle-orm/src/mysql-core/query-builders/set-operators.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { entityKind } from '~/entity.ts'; -import { - applyMixins, - haveSameKeys, - orderSelectedFields, - type Placeholder, - type Query, - QueryPromise, - SelectionProxyHandler, - type SQL, - Subquery, - type ValueOrArray, -} from '~/index.ts'; -import type { - MySqlSession, - PreparedQueryConfig, - PreparedQueryHKTBase, - PreparedQueryKind, -} from '~/mysql-core/session.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; -import type { - BuildSubquerySelection, - JoinNullability, - SelectMode, - SelectResult, - SetOperator, -} from '~/query-builders/select.types.ts'; -import type { ColumnsSelection } from '~/view.ts'; -import type { MySqlColumn } from '../columns/common.ts'; -import type { MySqlDialect } from '../dialect.ts'; -import type { SubqueryWithSelection } from '../subquery.ts'; -import type { - MySqlCreateSetOperatorFn, - MySqlSelectHKTBase, - MySqlSetOperationConfig, - MySqlSetOperatorBaseWithResult, - MySqlSetOperatorDynamic, - MySqlSetOperatorInterface, - MySqlSetOperatorWithout, - SetOperatorRightSelect, -} from './select.types.ts'; - -const getMySqlSetOperators = () => { - return { - union, - unionAll, - intersect, - intersectAll, - except, - exceptAll, - }; -}; - -type MySqlSetOperators = ReturnType; - -export abstract class MySqlSetOperatorBuilder< - THKT extends MySqlSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder { - static readonly [entityKind]: string = 'MySqlSetOperatorBuilder'; - - abstract override readonly _: { - readonly hkt: THKT; - readonly tableName: TTableName; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly preparedQueryHKT: TPreparedQueryHKT; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected abstract joinsNotNullableMap: Record; - protected abstract config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (MySqlColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; - /* @internal */ - abstract readonly session: MySqlSession | undefined; - protected abstract dialect: MySqlDialect; - - /** @internal */ - getSetOperatorConfig() { - return { - session: this.session, - dialect: this.dialect, - joinsNotNullableMap: this.joinsNotNullableMap, - fields: this.config.fields, - }; - } - - private createSetOperator( - type: SetOperator, - isAll: boolean, - ): >( - rightSelect: - | ((setOperator: MySqlSetOperators) => SetOperatorRightSelect) - | SetOperatorRightSelect, - ) => MySqlSetOperatorBase< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, - false, - never, - TResult, - TSelectedFields - > { - return (rightSelect) => { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getMySqlSetOperators()) : rightSelect; - return new MySqlSetOperatorBase(type, isAll, this as any, rightSelectOrig 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); -} - -export interface MySqlSetOperatorBase< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TDynamic extends boolean = false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder, QueryPromise {} - -export class MySqlSetOperatorBase< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TPreparedQueryHKT extends PreparedQueryHKTBase, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends MySqlSetOperatorBuilder< - MySqlSelectHKTBase, - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { - static readonly [entityKind]: string = 'MySqlSetOperator'; - - readonly _: { - readonly hkt: MySqlSelectHKTBase; - readonly tableName: TTableName; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly preparedQueryHKT: TPreparedQueryHKT; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected joinsNotNullableMap: Record; - protected config: MySqlSetOperationConfig; - /* @internal */ - readonly session: MySqlSession | undefined; - protected dialect: MySqlDialect; - - constructor( - operator: SetOperator, - isAll: boolean, - leftSelect: MySqlSetOperatorInterface< - MySqlSelectHKTBase, - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - rightSelect: TypedQueryBuilder, - ) { - super(); - - const leftSelectedFields = leftSelect.getSelectedFields(); - const rightSelectedFields = rightSelect.getSelectedFields(); - - if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { - throw new Error( - 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', - ); - } - - const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); - - this._ = { - selectedFields: fields, - } as this['_']; - - this.session = session; - this.dialect = dialect; - this.joinsNotNullableMap = joinsNotNullableMap; - this.config = { - fields, - operator, - isAll, - leftSelect, - rightSelect, - }; - } - - orderBy( - ...columns: - | [(aliases: TSelection) => ValueOrArray] - | (MySqlColumn | SQL | SQL.Aliased)[] - ): MySqlSetOperatorWithout { - if (typeof columns[0] === 'function') { - const orderBy = columns[0]( - new Proxy( - this.config.fields, - new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), - ) as TSelection, - ); - this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; - } else { - this.config.orderBy = columns as (MySqlColumn | SQL | SQL.Aliased)[]; - } - return this as any; - } - - limit(limit: number): MySqlSetOperatorWithout { - this.config.limit = limit; - return this as any; - } - - offset(offset: number | Placeholder): MySqlSetOperatorWithout { - this.config.offset = offset; - return this as any; - } - - /** @internal */ - override getSQL(): SQL { - return this.dialect.buildSetOperationQuery(this.config); - } - - toSQL(): Query { - const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); - return rest; - } - - prepare() { - if (!this.session) { - throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); - } - const fieldsList = orderSelectedFields(this.config.fields); - const query = this.session.prepareQuery< - PreparedQueryConfig & { execute: TResult }, - TPreparedQueryHKT - >(this.dialect.sqlToQuery(this.getSQL()), fieldsList); - query.joinsNotNullableMap = this.joinsNotNullableMap; - return query as PreparedQueryKind< - TPreparedQueryHKT, - PreparedQueryConfig & { - execute: TResult; - iterator: SelectResult; - }, - true - >; - } - - execute = ((placeholderValues) => { - return this.prepare().execute(placeholderValues); - }) as ReturnType['execute']; - - private createIterator = (): ReturnType['iterator'] => { - const self = this; - return async function*(placeholderValues) { - console.log('placeholderValues', placeholderValues); - yield* self.prepare().iterator(placeholderValues); - }; - }; - - iterator = this.createIterator(); - - as( - alias: TAlias, - ): SubqueryWithSelection { - return new Proxy( - new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), - ) as SubqueryWithSelection; - } - - $dynamic(): MySqlSetOperatorDynamic { - return this as any; - } -} - -applyMixins(MySqlSetOperatorBase, [QueryPromise]); - -function createSetOperator(type: SetOperator, isAll: boolean): MySqlCreateSetOperatorFn { - return (leftSelect, rightSelect, ...restSelects) => { - if (restSelects.length === 0) { - return new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return createSetOperator(type, isAll)( - new MySqlSetOperatorBase(type, isAll, leftSelect, rightSelect as any), - select as any, - ...rest, - ); - }; -} - -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); From c8117b1a7ff66a10a594bf81a77fff530ec88c98 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 20 Oct 2023 18:27:04 -0400 Subject: [PATCH 29/34] [Pg] Simplification by removing intermediary clases and making the queries mutable --- drizzle-orm/src/mysql-core/dialect.ts | 1 + .../mysql-core/query-builders/select.types.ts | 38 +- drizzle-orm/src/pg-core/dialect.ts | 44 ++- .../src/pg-core/query-builders/index.ts | 1 - .../src/pg-core/query-builders/select.ts | 149 ++++++- .../pg-core/query-builders/select.types.ts | 265 ++++++------- .../pg-core/query-builders/set-operators.ts | 371 ------------------ 7 files changed, 291 insertions(+), 578 deletions(-) delete mode 100644 drizzle-orm/src/pg-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index a41dc6169..057565e0f 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -353,6 +353,7 @@ export class MySqlDialect { return this.buildSetOperationQuery({ leftSelect, setOperator }); } + // Some recursive magic here return this.buildSetOperations( this.buildSetOperationQuery({ leftSelect, setOperator }), rest, diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 5fc35df99..1acb4a5e5 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -91,14 +91,14 @@ export type MySqlJoin< T['_']['selection'], TJoinedName, TJoinedTable extends MySqlTable ? TJoinedTable['_']['columns'] - : TJoinedTable extends Subquery | View ? Assume + : TJoinedTable extends Subquery ? Assume : never, T['_']['selectMode'] >, T['_']['selectMode'] extends 'partial' ? T['_']['selectMode'] : 'multiple', T['_']['preparedQueryHKT'], AppendToNullabilityMap, - T['_']['dynamic'], + TDynamic, T['_']['excludedMethods'] >, TDynamic, @@ -229,7 +229,8 @@ export type MySqlSelectWithout< K extends keyof T & string, TResetExcluded extends boolean = false, > = TDynamic extends true ? T : Omit< - MySqlSelectBase< + MySqlSelectKind< + T['_']['hkt'], T['_']['tableName'], T['_']['selection'], T['_']['selectMode'], @@ -277,12 +278,12 @@ export type CreateMySqlSelectFromBuilderMode< export type MySqlSelectQueryBuilder< THKT extends MySqlSelectHKTBase = MySqlSelectQueryBuilderHKT, TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = Record, + TSelection extends ColumnsSelection = ColumnsSelection, TSelectMode extends SelectMode = SelectMode, TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, TNullabilityMap extends Record = Record, TResult extends any[] = unknown[], - TSelectedFields extends ColumnsSelection = Record, + TSelectedFields extends ColumnsSelection = ColumnsSelection, > = MySqlSelectQueryBuilderBase< THKT, TTableName, @@ -296,7 +297,7 @@ export type MySqlSelectQueryBuilder< TSelectedFields >; -export type AnyMySqlSelectQueryBuilder = MySqlSelectQueryBuilderBase; +export type AnyMySqlSelectQueryBuilder = MySqlSelectQueryBuilderBase; export type AnyMySqlSetOperatorInterface = MySqlSetOperatorInterface; @@ -341,31 +342,6 @@ export interface MySqlSetOperatorInterface< }; } -export type MySqlSelectBuilderWithResult = MySqlSelectQueryBuilderBase< - any, - any, - any, - any, - any, - any, - any, - any, - TResult, - any ->; - -export type MySqlSelectWithResult = MySqlSelectBase< - any, - any, - any, - any, - any, - any, - any, - TResult, - any ->; - export type MySqlSetOperatorWithResult = MySqlSetOperatorInterface< any, any, diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 851dff453..64d2eface 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -8,7 +8,6 @@ import type { PgDeleteConfig, PgInsertConfig, PgSelectJoinConfig, - PgSetOperationConfig, PgUpdateConfig, } from '~/pg-core/query-builders/index.ts'; import type { PgSelectConfig, SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts'; @@ -209,6 +208,7 @@ export class PgDialect { offset, lockingClause, distinct, + setOperators, }: PgSelectConfig, ): SQL { const fieldsList = fieldsFlat ?? orderSelectedFields(fields); @@ -352,19 +352,38 @@ export class PgDialect { } lockingClauseSql.append(clauseSql); } + const finalQuery = + sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClauseSql}`; - return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClauseSql}`; + if (setOperators.length > 0) { + return this.buildSetOperations(finalQuery, setOperators); + } + + return finalQuery; + } + + buildSetOperations(leftSelect: SQL, setOperators: PgSelectConfig['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({ - operator, - isAll, leftSelect, - rightSelect, - limit, - orderBy, - offset, - }: PgSetOperationConfig): SQL { + setOperator: { type, isAll, rightSelect, limit, orderBy, offset }, + }: { leftSelect: SQL; setOperator: PgSelectConfig['setOperators'][number] }): SQL { const leftChunk = sql`(${leftSelect.getSQL()}) `; const rightChunk = sql`(${rightSelect.getSQL()})`; @@ -373,7 +392,7 @@ export class PgDialect { const orderByValues: (SQL | 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 + // which is invalid Sql syntax, Table from one of the SELECTs cannot be used in global ORDER clause for (const singleOrderBy of orderBy) { if (is(singleOrderBy, PgColumn)) { orderByValues.push(sql.identifier(singleOrderBy.name)); @@ -397,7 +416,7 @@ export class PgDialect { const limitSql = limit ? sql` limit ${limit}` : undefined; - const operatorChunk = sql.raw(`${operator} ${isAll ? 'all ' : ''}`); + const operatorChunk = sql.raw(`${type} ${isAll ? 'all ' : ''}`); const offsetSql = offset ? sql` offset ${offset}` : undefined; @@ -1260,6 +1279,7 @@ export class PgDialect { limit, offset, orderBy, + setOperators: [], }); where = undefined; @@ -1282,6 +1302,7 @@ export class PgDialect { limit, offset, orderBy, + setOperators: [], }); } else { result = this.buildSelectQuery({ @@ -1296,6 +1317,7 @@ export class PgDialect { limit, offset, orderBy, + setOperators: [], }); } diff --git a/drizzle-orm/src/pg-core/query-builders/index.ts b/drizzle-orm/src/pg-core/query-builders/index.ts index fb3dd5931..c4821e51d 100644 --- a/drizzle-orm/src/pg-core/query-builders/index.ts +++ b/drizzle-orm/src/pg-core/query-builders/index.ts @@ -4,5 +4,4 @@ export * from './query-builder.ts'; export * from './refresh-materialized-view.ts'; export * from './select.ts'; export * from './select.types.ts'; -export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 0874ef774..f9e4b41b7 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -5,6 +5,7 @@ import type { PgSession, PreparedQueryConfig } from '~/pg-core/session.ts'; import type { SubqueryWithSelection } from '~/pg-core/subquery.ts'; import type { PgTable } from '~/pg-core/table.ts'; import { PgViewBase } from '~/pg-core/view.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -13,20 +14,24 @@ import type { JoinType, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import { type Placeholder, type Query, SQL, type SQLWrapper } from '~/sql/index.ts'; import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery.ts'; import { Table } from '~/table.ts'; import { tracer } from '~/tracing.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 { + AnyPgSelect, CreatePgSelectFromBuilderMode, + GetPgSetOperators, LockConfig, LockStrength, + PgCreateSetOperatorFn, PgJoinFn, PgSelectConfig, PgSelectDynamic, @@ -34,9 +39,11 @@ import type { PgSelectHKTBase, PgSelectPrepare, PgSelectWithout, + PgSetOperatorExcludedMethods, + PgSetOperatorWithResult, SelectedFields, + SetOperatorRightSelect, } from './select.types.ts'; -import { PgSetOperatorBuilder } from './set-operators.ts'; export class PgSelectBuilder< TSelection extends SelectedFields | undefined, @@ -129,17 +136,7 @@ export abstract class PgSelectQueryBuilderBase< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends PgSetOperatorBuilder< - THKT, - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { +> extends TypedQueryBuilder { static readonly [entityKind]: string = 'PgSelectQueryBuilder'; override readonly _: { @@ -180,6 +177,7 @@ export abstract class PgSelectQueryBuilderBase< table, fields: { ...fields }, distinct, + setOperators: [], }; this.isPartialSelect = isPartialSelect; this.session = session; @@ -301,6 +299,61 @@ export abstract class PgSelectQueryBuilderBase< */ fullJoin = this.createJoin('full'); + private createSetOperator( + type: SetOperator, + isAll: boolean, + ): >( + rightSelection: + | ((setOperators: GetPgSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => PgSelectWithout< + this, + TDynamic, + PgSetOperatorExcludedMethods, + true + > { + return (rightSelection) => { + const rightSelect = (typeof rightSelection === 'function' + ? rightSelection(getPgSetOperators()) + : rightSelection) as TypedQueryBuilder< + any, + TResult + >; + + if (!haveSameKeys(this.getSelectedFields(), rightSelect.getSelectedFields())) { + throw new Error( + '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: PgSelectConfig['setOperators']): PgSelectWithout< + this, + TDynamic, + PgSetOperatorExcludedMethods, + true + > { + this.config.setOperators.push(...setOperators); + return this as any; + } + /** * Specify a condition to narrow the result set. Multiple * conditions can be combined with the `and` and `or` @@ -422,9 +475,22 @@ export abstract class PgSelectQueryBuilderBase< 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 (PgColumn | SQL | SQL.Aliased)[]; + const orderByArray = columns as (PgColumn | 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; } @@ -443,7 +509,11 @@ export abstract class PgSelectQueryBuilderBase< * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-LIMIT | Postgres LIMIT documentation} */ limit(limit: number | Placeholder): PgSelectWithout { - 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; } @@ -459,7 +529,11 @@ export abstract class PgSelectQueryBuilderBase< * ``` */ offset(offset: number | Placeholder): PgSelectWithout { - 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; } @@ -589,3 +663,44 @@ export class PgSelectBase< } applyMixins(PgSelectBase, [QueryPromise]); + +function createSetOperator(type: SetOperator, isAll: boolean): PgCreateSetOperatorFn { + return (leftSelect, rightSelect, ...restSelects) => { + const setOperators = [rightSelect, ...restSelects].map((select) => ({ + type, + isAll, + rightSelect: select as AnyPgSelect, + })); + + 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 AnyPgSelect).addSetOperators(setOperators) as any; + }; +} + +const getPgSetOperators = () => ({ + 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); diff --git a/drizzle-orm/src/pg-core/query-builders/select.types.ts b/drizzle-orm/src/pg-core/query-builders/select.types.ts index 832f2c37a..4d8ba939f 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.types.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.types.ts @@ -26,7 +26,6 @@ import type { Assume, ValidateShape, ValueOrArray } from '~/utils.ts'; import type { ColumnsSelection, View } from '~/view.ts'; import type { PreparedQuery, PreparedQueryConfig } from '../session.ts'; import type { PgSelectBase, PgSelectQueryBuilderBase } from './select.ts'; -import type { PgSetOperatorBase, PgSetOperatorBuilder } from './set-operators.ts'; export interface PgSelectJoinConfig { on: SQL | undefined; @@ -70,10 +69,18 @@ export interface PgSelectConfig { distinct?: boolean | { on: (PgColumn | SQLWrapper)[]; }; + setOperators: { + rightSelect: TypedQueryBuilder; + type: SetOperator; + isAll: boolean; + orderBy?: (PgColumn | SQL | SQL.Aliased)[]; + limit?: number | Placeholder; + offset?: number | Placeholder; + }[]; } export type PgJoin< - T extends AnyPgSelectQueryBuilderBase, + T extends AnyPgSelectQueryBuilder, TDynamic extends boolean, TJoinType extends JoinType, TJoinedTable extends PgTable | Subquery | PgViewBase | SQL, @@ -102,7 +109,7 @@ export type PgJoin< : never; export type PgJoinFn< - T extends AnyPgSelectQueryBuilderBase, + T extends AnyPgSelectQueryBuilder, TDynamic extends boolean, TJoinType extends JoinType, > = < @@ -204,10 +211,32 @@ export type CreatePgSelectFromBuilderMode< > = TBuilderMode extends 'db' ? PgSelectBase : PgSelectQueryBuilderBase; +export type PgSetOperatorExcludedMethods = Exclude< + keyof AnyPgSelectQueryBuilder, + // Only add the methods that a SetOperator is supposed to have + | 'orderBy' + | 'limit' + | 'offset' + | 'union' + | 'unionAll' + | 'intersect' + | 'intersectAll' + | 'except' + | 'exceptAll' + | '_' + | 'getSQL' + | 'as' + | 'addSetOperators' + | 'toSQL' + | '$dynamic' + | 'getSelectedFields' +>; + export type PgSelectWithout< - T extends AnyPgSelectQueryBuilderBase, + T extends AnyPgSelectQueryBuilder, TDynamic extends boolean, K extends keyof T & string, + TResetExcluded extends boolean = false, > = TDynamic extends true ? T : Omit< PgSelectKind< T['_']['hkt'], @@ -216,11 +245,11 @@ export type PgSelectWithout< T['_']['selectMode'], T['_']['nullabilityMap'], TDynamic, - T['_']['excludedMethods'] | K, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K, T['_']['result'], T['_']['selectedFields'] >, - T['_']['excludedMethods'] | K + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K >; export type PgSelectPrepare = PreparedQuery< @@ -229,7 +258,7 @@ export type PgSelectPrepare = PreparedQuery< } >; -export type PgSelectDynamic = PgSelectKind< +export type PgSelectDynamic = PgSelectKind< T['_']['hkt'], T['_']['tableName'], T['_']['selection'], @@ -261,69 +290,11 @@ export type PgSelectQueryBuilder< TSelectedFields >; -export type AnyPgSelectQueryBuilderBase = PgSelectQueryBuilderBase; - -export type PgSelect< - TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TNullabilityMap extends Record = Record, -> = PgSelectBase; - -export type AnyPgSelect = PgSelectBase; +export type AnyPgSelectQueryBuilder = PgSelectQueryBuilderBase; -export type PgSetOperatorBaseWithResult = PgSetOperatorInterface< - any, - any, - any, - any, - any, - any, - any, - T, - any ->; - -export type SetOperatorRightSelect< - TValue extends PgSetOperatorBaseWithResult, - TResult extends any[], -> = TValue extends PgSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never - : TValue; - -export type SetOperatorRestSelect< - TValue extends readonly PgSetOperatorBaseWithResult[], - TResult extends any[], -> = TValue extends [infer First, ...infer Rest] - ? First extends PgSetOperatorInterface - ? TValueResult extends Array - ? Rest extends PgSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never - : never - : TValue; - -export interface PgSetOperationConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: AnyPgSetOperatorBase; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (PgColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} +export type AnyPgSetOperatorInterface = PgSetOperatorInterface; export interface PgSetOperatorInterface< - THKT extends PgSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, @@ -334,9 +305,8 @@ export interface PgSetOperatorInterface< TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - Omit< - PgSetOperatorBuilder< - THKT, + Pick< + PgSelectBase< TTableName, TSelection, TSelectMode, @@ -346,11 +316,11 @@ export interface PgSetOperatorInterface< TResult, TSelectedFields >, - 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' + never > { _: { - readonly hkt: THKT; + readonly hkt: PgSelectHKT; readonly tableName: TTableName; readonly selection: TSelection; readonly selectMode: TSelectMode; @@ -362,33 +332,80 @@ export interface PgSetOperatorInterface< }; } -export type AnyPgSetOperatorBase = PgSetOperatorInterface< - any, - any, +export type PgSetOperatorWithResult = PgSetOperatorInterface< any, any, any, any, any, any, + TResult, any >; +export type PgSelect< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = PgSelectBase; + +export type AnyPgSelect = PgSelectBase; + +export type PgSetOperator< + TTableName extends string | undefined = string | undefined, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = PgSelectBase< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + true, + PgSetOperatorExcludedMethods +>; + +export type SetOperatorRightSelect< + TValue extends PgSetOperatorWithResult, + TResult extends any[], +> = TValue extends PgSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly PgSetOperatorWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends PgSetOperatorInterface + ? TValueResult extends Array ? Rest extends AnyPgSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : never + : TValue; + export type PgCreateSetOperatorFn = < - THKT extends PgSelectHKTBase, TTableName extends string | undefined, TSelection extends ColumnsSelection, TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends PgSetOperatorBaseWithResult, - TRest extends PgSetOperatorBaseWithResult[], + TValue extends PgSetOperatorWithResult, + TRest extends PgSetOperatorWithResult[], + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, >( leftSelect: PgSetOperatorInterface< - THKT, TTableName, TSelection, TSelectMode, @@ -400,73 +417,27 @@ export type PgCreateSetOperatorFn = < >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect -) => PgSetOperatorBase< - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, +) => PgSelectWithout< + PgSelectBase< + TTableName, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, false, - never, - TResult, - TSelectedFields + PgSetOperatorExcludedMethods, + true >; -export type AnyPgSetOperator = PgSetOperatorBase< - any, - any, - any, - any, - any, - any, - any, - any ->; - -export type PgSetOperator< - TTableName extends string | undefined = string | undefined, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TNullabilityMap extends Record = Record, -> = PgSetOperatorBase; - -export type AnyPgSetOperatorBuilder = PgSetOperatorBuilder< - any, - any, - any, - any, - any, - any, - any, - any, - any ->; - -export type PgSetOperatorWithout< - T extends AnyPgSetOperator, - TDynamic extends boolean, - K extends keyof T & string, -> = TDynamic extends true ? T - : Omit< - PgSetOperatorBase< - T['_']['tableName'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - TDynamic, - T['_']['excludedMethods'] | K, - T['_']['result'], - T['_']['selectedFields'] - >, - T['_']['excludedMethods'] | K - >; - -export type PgSetOperatorDynamic = PgSetOperatorBase< - T['_']['tableName'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - true, - never, - T['_']['result'], - T['_']['selectedFields'] ->; +export type GetPgSetOperators = { + union: PgCreateSetOperatorFn; + intersect: PgCreateSetOperatorFn; + except: PgCreateSetOperatorFn; + unionAll: PgCreateSetOperatorFn; + intersectAll: PgCreateSetOperatorFn; + exceptAll: PgCreateSetOperatorFn; +}; diff --git a/drizzle-orm/src/pg-core/query-builders/set-operators.ts b/drizzle-orm/src/pg-core/query-builders/set-operators.ts deleted file mode 100644 index 27c024f1c..000000000 --- a/drizzle-orm/src/pg-core/query-builders/set-operators.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { entityKind } from '~/entity.ts'; -import { - orderSelectedFields, - type Placeholder, - type Query, - SelectionProxyHandler, - type SQL, - Subquery, - type ValueOrArray, -} from '~/index.ts'; -import type { PgSession, PreparedQuery, PreparedQueryConfig } from '~/pg-core/session.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; -import type { - BuildSubquerySelection, - JoinNullability, - SelectMode, - SelectResult, - SetOperator, -} from '~/query-builders/select.types.ts'; -import { QueryPromise } from '~/query-promise.ts'; -import { tracer } from '~/tracing.ts'; -import { applyMixins, haveSameKeys } from '~/utils.ts'; -import type { ColumnsSelection } from '~/view.ts'; -import type { PgColumn } from '../columns/common.ts'; -import type { PgDialect } from '../dialect.ts'; -import type { SubqueryWithSelection } from '../subquery.ts'; -import type { - PgCreateSetOperatorFn, - PgSelectHKTBase, - PgSetOperationConfig, - PgSetOperatorBaseWithResult, - PgSetOperatorDynamic, - PgSetOperatorInterface, - PgSetOperatorWithout, - SetOperatorRightSelect, -} from './select.types.ts'; - -const getPgSetOperators = () => { - return { - union, - unionAll, - intersect, - intersectAll, - except, - exceptAll, - }; -}; - -type PgSetOperators = ReturnType; - -export abstract class PgSetOperatorBuilder< - THKT extends PgSelectHKTBase, - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder { - static readonly [entityKind]: string = 'PgSetOperatorBuilder'; - - abstract override readonly _: { - readonly hkt: THKT; - readonly tableName: TTableName; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected abstract joinsNotNullableMap: Record; - protected abstract config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (PgColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; - /* @internal */ - protected abstract readonly session: PgSession | undefined; - protected abstract dialect: PgDialect; - - /** @internal */ - getSetOperatorConfig() { - return { - session: this.session, - dialect: this.dialect, - joinsNotNullableMap: this.joinsNotNullableMap, - fields: this.config.fields, - }; - } - - private setOperator( - type: SetOperator, - isAll: boolean, - ): >( - rightSelect: - | ((setOperator: PgSetOperators) => SetOperatorRightSelect) - | SetOperatorRightSelect, - ) => PgSetOperatorBase< - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - false, - never, - TResult, - TSelectedFields - > { - return (rightSelect) => { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getPgSetOperators()) : rightSelect; - - return new PgSetOperatorBase(type, isAll, this, rightSelectOrig as any) as any; - }; - } - - union = this.setOperator('union', false); - - unionAll = this.setOperator('union', true); - - intersect = this.setOperator('intersect', false); - - intersectAll = this.setOperator('intersect', true); - - except = this.setOperator('except', false); - - exceptAll = this.setOperator('except', true); -} - -export interface PgSetOperatorBase< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends - PgSetOperatorBuilder< - PgSelectHKTBase, - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - QueryPromise -{} - -export class PgSetOperatorBase< - TTableName extends string | undefined, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends PgSetOperatorBuilder< - PgSelectHKTBase, - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { - static readonly [entityKind]: string = 'PgSetOperator'; - - readonly _: { - readonly hkt: PgSelectHKTBase; - readonly tableName: TTableName; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected joinsNotNullableMap: Record; - protected config: PgSetOperationConfig; - /* @internal */ - readonly session: PgSession | undefined; - protected dialect: PgDialect; - - constructor( - operator: SetOperator, - isAll: boolean, - leftSelect: PgSetOperatorInterface< - PgSelectHKTBase, - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - rightSelect: TypedQueryBuilder, - ) { - super(); - - const leftSelectedFields = leftSelect.getSelectedFields(); - const rightSelectedFields = rightSelect.getSelectedFields(); - - if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { - throw new Error( - 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', - ); - } - - const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); - - this._ = { - selectedFields: fields as TSelectedFields, - } as this['_']; - - this.session = session; - this.dialect = dialect; - this.joinsNotNullableMap = joinsNotNullableMap; - this.config = { - fields, - operator, - isAll, - leftSelect, - rightSelect, - }; - } - - orderBy( - builder: (aliases: TSelection) => ValueOrArray, - ): PgSetOperatorWithout; - orderBy(...columns: (PgColumn | SQL | SQL.Aliased)[]): PgSetOperatorWithout; - orderBy( - ...columns: - | [(aliases: TSelection) => ValueOrArray] - | (PgColumn | SQL | SQL.Aliased)[] - ): PgSetOperatorWithout { - if (typeof columns[0] === 'function') { - const orderBy = columns[0]( - new Proxy( - this.config.fields, - new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), - ) as TSelection, - ); - this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; - } else { - this.config.orderBy = columns as (PgColumn | SQL | SQL.Aliased)[]; - } - return this as any; - } - - limit(limit: number): PgSetOperatorWithout { - this.config.limit = limit; - return this as any; - } - - offset(offset: number | Placeholder): PgSetOperatorWithout { - this.config.offset = offset; - return this as any; - } - - toSQL(): Query { - const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); - return rest; - } - - override getSQL(): SQL { - return this.dialect.buildSetOperationQuery(this.config); - } - - private _prepare(name?: string): PreparedQuery< - PreparedQueryConfig & { - execute: TResult; - } - > { - const { session, joinsNotNullableMap, config: { fields }, dialect } = this; - if (!session) { - throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); - } - return tracer.startActiveSpan('drizzle.prepareQuery', () => { - const fieldsList = orderSelectedFields(fields); - const query = session.prepareQuery< - PreparedQueryConfig & { execute: TResult } - >(dialect.sqlToQuery(this.getSQL()), fieldsList, name); - query.joinsNotNullableMap = joinsNotNullableMap; - return query; - }); - } - - /** - * Create a prepared statement for this query. This allows - * the database to remember this query for the given session - * and call it by name, rather than specifying the full query. - * - * {@link https://www.postgresql.org/docs/current/sql-prepare.html|Postgres prepare documentation} - */ - prepare(name: string): PreparedQuery< - PreparedQueryConfig & { - execute: TResult; - } - > { - return this._prepare(name); - } - - execute: ReturnType['execute'] = (placeholderValues) => { - return tracer.startActiveSpan('drizzle.operation', () => { - return this._prepare().execute(placeholderValues); - }); - }; - - as( - alias: TAlias, - ): SubqueryWithSelection, TAlias> { - return new Proxy( - new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), - ) as SubqueryWithSelection, TAlias>; - } - - $dynamic(): PgSetOperatorDynamic { - return this as any; - } -} - -applyMixins(PgSetOperatorBase, [QueryPromise]); - -function setOperator(type: SetOperator, isAll: boolean): PgCreateSetOperatorFn { - return (leftSelect, rightSelect, ...restSelects) => { - if (restSelects.length === 0) { - return new PgSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return setOperator(type, isAll)( - new PgSetOperatorBase(type, isAll, leftSelect, rightSelect as any), - select as any, - ...rest, - ); - }; -} - -export const union = setOperator('union', false); - -export const unionAll = setOperator('union', true); - -export const intersect = setOperator('intersect', false); - -export const intersectAll = setOperator('intersect', true); - -export const except = setOperator('except', false); - -export const exceptAll = setOperator('except', true); From c7846b97f71a40dd528d1068ba1cd1068e8c15de Mon Sep 17 00:00:00 2001 From: Angelelz Date: Fri, 20 Oct 2023 19:12:42 -0400 Subject: [PATCH 30/34] [SQLite] Simplification by removing intermediary clases and making the queries mutable --- drizzle-orm/src/mysql-core/dialect.ts | 2 +- drizzle-orm/src/sqlite-core/dialect.ts | 66 ++- .../src/sqlite-core/query-builders/index.ts | 1 - .../src/sqlite-core/query-builders/select.ts | 148 ++++++- .../query-builders/select.types.ts | 314 +++++++-------- .../query-builders/set-operators.ts | 376 ------------------ 6 files changed, 316 insertions(+), 591 deletions(-) delete mode 100644 drizzle-orm/src/sqlite-core/query-builders/set-operators.ts diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 057565e0f..26521bdaf 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -353,7 +353,7 @@ export class MySqlDialect { return this.buildSetOperationQuery({ leftSelect, setOperator }); } - // Some recursive magic here + // Some recursive magic here return this.buildSetOperations( this.buildSetOperationQuery({ leftSelect, setOperator }), rest, diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 1a65cbc90..f4251309a 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -18,12 +18,7 @@ import { } from '~/relations.ts'; import { and, eq, type Name, Param, type QueryWithTypings, SQL, sql, type SQLChunk } from '~/sql/index.ts'; import { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; -import type { - SQLiteDeleteConfig, - SQLiteInsertConfig, - SQLiteSetOperationConfig, - SQLiteUpdateConfig, -} from '~/sqlite-core/query-builders/index.ts'; +import type { SQLiteDeleteConfig, SQLiteInsertConfig, SQLiteUpdateConfig } from '~/sqlite-core/query-builders/index.ts'; import { SQLiteTable } from '~/sqlite-core/table.ts'; import { Subquery, SubqueryConfig } from '~/subquery.ts'; import { getTableName, Table } from '~/table.ts'; @@ -156,8 +151,21 @@ export abstract class SQLiteDialect { } buildSelectQuery( - { withList, fields, fieldsFlat, where, having, table, joins, orderBy, groupBy, limit, offset, distinct }: - SQLiteSelectConfig, + { + withList, + fields, + fieldsFlat, + where, + having, + table, + joins, + orderBy, + groupBy, + limit, + offset, + distinct, + setOperators, + }: SQLiteSelectConfig, ): SQL { const fieldsList = fieldsFlat ?? orderSelectedFields(fields); for (const f of fieldsList) { @@ -278,18 +286,39 @@ export abstract class SQLiteDialect { const offsetSql = offset ? sql` offset ${offset}` : undefined; - return sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}`; + const finalQuery = + sql`${withSql}select${distinctSql} ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}`; + + if (setOperators.length > 0) { + return this.buildSetOperations(finalQuery, setOperators); + } + + return finalQuery; + } + + buildSetOperations(leftSelect: SQL, setOperators: SQLiteSelectConfig['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({ - operator, - isAll, leftSelect, - rightSelect, - limit, - orderBy, - offset, - }: SQLiteSetOperationConfig): SQL { + setOperator: { type, isAll, rightSelect, limit, orderBy, offset }, + }: { leftSelect: SQL; setOperator: SQLiteSelectConfig['setOperators'][number] }): SQL { + // SQLite doesn't support parenthesis in set operations const leftChunk = sql`${leftSelect.getSQL()} `; const rightChunk = sql`${rightSelect.getSQL()}`; @@ -322,7 +351,7 @@ export abstract class SQLiteDialect { const limitSql = limit ? sql` limit ${limit}` : undefined; - const operatorChunk = sql.raw(`${operator} ${isAll ? 'all ' : ''}`); + const operatorChunk = sql.raw(`${type} ${isAll ? 'all ' : ''}`); const offsetSql = offset ? sql` offset ${offset}` : undefined; @@ -623,6 +652,7 @@ export abstract class SQLiteDialect { limit, offset, orderBy, + setOperators: [], }); where = undefined; @@ -645,6 +675,7 @@ export abstract class SQLiteDialect { limit, offset, orderBy, + setOperators: [], }); } else { result = this.buildSelectQuery({ @@ -659,6 +690,7 @@ export abstract class SQLiteDialect { limit, offset, orderBy, + setOperators: [], }); } diff --git a/drizzle-orm/src/sqlite-core/query-builders/index.ts b/drizzle-orm/src/sqlite-core/query-builders/index.ts index 58d909c22..16f0e1d4d 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/index.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/index.ts @@ -3,5 +3,4 @@ export * from './insert.ts'; export * from './query-builder.ts'; export * from './select.ts'; export * from './select.types.ts'; -export * from './set-operators.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index e8443744d..54a1feb5a 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -1,4 +1,5 @@ import { entityKind, is } from '~/entity.ts'; +import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { BuildSubquerySelection, GetSelectTableName, @@ -7,6 +8,7 @@ import type { JoinType, SelectMode, SelectResult, + SetOperator, } from '~/query-builders/select.types.ts'; import { QueryPromise } from '~/query-promise.ts'; import type { RunnableQuery } from '~/runnable-query.ts'; @@ -18,13 +20,24 @@ import type { SubqueryWithSelection } from '~/sqlite-core/subquery.ts'; import type { SQLiteTable } from '~/sqlite-core/table.ts'; import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery.ts'; import { Table } from '~/table.ts'; -import { applyMixins, getTableColumns, getTableLikeName, orderSelectedFields, type ValueOrArray } from '~/utils.ts'; +import { + applyMixins, + getTableColumns, + getTableLikeName, + haveSameKeys, + orderSelectedFields, + type ValueOrArray, +} from '~/utils.ts'; import { ViewBaseConfig } from '~/view-common.ts'; import { type ColumnsSelection, View } from '~/view.ts'; import { SQLiteViewBase } from '../view.ts'; import type { + AnySQLiteSelect, CreateSQLiteSelectFromBuilderMode, + GetSQLiteSetOperators, SelectedFields, + SetOperatorRightSelect, + SQLiteCreateSetOperatorFn, SQLiteJoinFn, SQLiteSelectConfig, SQLiteSelectDynamic, @@ -33,8 +46,9 @@ import type { SQLiteSelectHKTBase, SQLiteSelectPrepare, SQLiteSelectWithout, + SQLiteSetOperatorExcludedMethods, + SQLiteSetOperatorWithResult, } from './select.types.ts'; -import { SQLiteSetOperatorBuilder } from './set-operators.ts'; export class SQLiteSelectBuilder< TSelection extends SelectedFields | undefined, @@ -121,19 +135,7 @@ export abstract class SQLiteSelectQueryBuilderBase< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends SQLiteSetOperatorBuilder< - THKT, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { +> extends TypedQueryBuilder { static readonly [entityKind]: string = 'SQLiteSelectQueryBuilder'; override readonly _: { @@ -176,6 +178,7 @@ export abstract class SQLiteSelectQueryBuilderBase< table, fields: { ...fields }, distinct, + setOperators: [], }; this.isPartialSelect = isPartialSelect; this.session = session; @@ -271,6 +274,57 @@ export abstract class SQLiteSelectQueryBuilderBase< fullJoin = this.createJoin('full'); + private createSetOperator( + type: SetOperator, + isAll: boolean, + ): >( + rightSelection: + | ((setOperators: GetSQLiteSetOperators) => SetOperatorRightSelect) + | SetOperatorRightSelect, + ) => SQLiteSelectWithout< + this, + TDynamic, + SQLiteSetOperatorExcludedMethods, + true + > { + return (rightSelection) => { + const rightSelect = (typeof rightSelection === 'function' + ? rightSelection(getSQLiteSetOperators()) + : rightSelection) as TypedQueryBuilder< + any, + TResult + >; + + if (!haveSameKeys(this.getSelectedFields(), rightSelect.getSelectedFields())) { + throw new Error( + '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); + + except = this.createSetOperator('except', false); + + /** @internal */ + addSetOperators(setOperators: SQLiteSelectConfig['setOperators']): SQLiteSelectWithout< + this, + TDynamic, + SQLiteSetOperatorExcludedMethods, + true + > { + this.config.setOperators.push(...setOperators); + return this as any; + } + where( where: ((aliases: TSelection) => SQL | undefined) | SQL | undefined, ): SQLiteSelectWithout { @@ -340,20 +394,41 @@ export abstract class SQLiteSelectQueryBuilderBase< 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 (SQLiteColumn | SQL | SQL.Aliased)[]; + const orderByArray = columns as (SQLiteColumn | 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 | Placeholder): SQLiteSelectWithout { - 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 | Placeholder): SQLiteSelectWithout { - 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; } @@ -482,3 +557,38 @@ export class SQLiteSelectBase< } applyMixins(SQLiteSelectBase, [QueryPromise]); + +function createSetOperator(type: SetOperator, isAll: boolean): SQLiteCreateSetOperatorFn { + return (leftSelect, rightSelect, ...restSelects) => { + const setOperators = [rightSelect, ...restSelects].map((select) => ({ + type, + isAll, + rightSelect: select as AnySQLiteSelect, + })); + + 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 AnySQLiteSelect).addSetOperators(setOperators) as any; + }; +} + +const getSQLiteSetOperators = () => ({ + union, + unionAll, + intersect, + except, +}); + +export const union = createSetOperator('union', false); + +export const unionAll = createSetOperator('union', true); + +export const intersect = createSetOperator('intersect', false); + +export const except = createSetOperator('except', false); diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 90cfdef58..32b47486a 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -28,7 +28,6 @@ import type { ColumnsSelection, View } from '~/view.ts'; import type { SQLitePreparedQuery } from '../session.ts'; import type { SQLiteViewBase, SQLiteViewWithSelection } from '../view.ts'; import type { SQLiteSelectBase, SQLiteSelectQueryBuilderBase } from './select.ts'; -import type { SQLiteSetOperatorBase, SQLiteSetOperatorBuilder } from './set-operators.ts'; export interface SQLiteSelectJoinConfig { on: SQL | undefined; @@ -64,6 +63,14 @@ export interface SQLiteSelectConfig { orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; groupBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; distinct?: boolean; + setOperators: { + rightSelect: TypedQueryBuilder; + type: SetOperator; + isAll: boolean; + orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; + limit?: number | Placeholder; + offset?: number | Placeholder; + }[]; } export type SQLiteJoin< @@ -185,6 +192,27 @@ export interface SQLiteSelectHKT extends SQLiteSelectHKTBase { >; } +export type SQLiteSetOperatorExcludedMethods = Exclude< + keyof AnySQLiteSelectQueryBuilder, + // Only add the methods that a SetOperator is supposed to have + | 'orderBy' + | 'limit' + | 'offset' + | 'union' + | 'unionAll' + | 'intersect' + | 'intersectAll' + | 'except' + | 'exceptAll' + | '_' + | 'getSQL' + | 'as' + | 'addSetOperators' + | 'toSQL' + | '$dynamic' + | 'getSelectedFields' +>; + export type CreateSQLiteSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', TTableName extends string | undefined, @@ -212,22 +240,23 @@ export type SQLiteSelectWithout< T extends AnySQLiteSelectQueryBuilder, TDynamic extends boolean, K extends keyof T & string, -> = TDynamic extends true ? T - : Omit< - SQLiteSelectBase< - T['_']['tableName'], - T['_']['resultType'], - T['_']['runResult'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - TDynamic, - T['_']['excludedMethods'] | K, - T['_']['result'], - T['_']['selectedFields'] - >, - T['_']['excludedMethods'] | K - >; + TResetExcluded extends boolean = false, +> = TDynamic extends true ? T : Omit< + SQLiteSelectKind< + T['_']['hkt'], + T['_']['tableName'], + T['_']['resultType'], + T['_']['runResult'], + T['_']['selection'], + T['_']['selectMode'], + T['_']['nullabilityMap'], + TDynamic, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K, + T['_']['result'], + T['_']['selectedFields'] + >, + TResetExcluded extends true ? K : T['_']['excludedMethods'] | K +>; export type SQLiteSelectExecute = T['_']['result']; @@ -261,11 +290,11 @@ export type SQLiteSelectQueryBuilder< TTableName extends string | undefined = string | undefined, TResultType extends 'sync' | 'async' = 'sync' | 'async', TRunResult = unknown, - TSelection extends ColumnsSelection = Record, + TSelection extends ColumnsSelection = ColumnsSelection, TSelectMode extends SelectMode = SelectMode, TNullabilityMap extends Record = Record, TResult extends any[] = unknown[], - TSelectedFields extends ColumnsSelection = Record, + TSelectedFields extends ColumnsSelection = ColumnsSelection, > = SQLiteSelectQueryBuilderBase< THKT, TTableName, @@ -294,76 +323,14 @@ export type AnySQLiteSelectQueryBuilder = SQLiteSelectQueryBuilderBase< any >; -export type SQLiteSetOperatorBaseWithResult = SQLiteSetOperatorInterface< - any, - any, - any, - any, - any, - any, - any, - any, - any, - T, - any ->; - -export type SQLiteSelect< - TTableName extends string | undefined = string | undefined, - TResultType extends 'sync' | 'async' = 'sync' | 'async', - TRunResult = unknown, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TNullabilityMap extends Record = Record, -> = SQLiteSelectBase; - -export type AnySQLiteSelect = SQLiteSelectBase; - -export type SetOperatorRightSelect< - TValue extends SQLiteSetOperatorBaseWithResult, - TResult extends any[], -> = TValue extends SQLiteSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never - : TValue; - -export type SetOperatorRestSelect< - TValue extends readonly SQLiteSetOperatorBaseWithResult[], - TResult extends any[], -> = TValue extends [infer First, ...infer Rest] - ? First extends SQLiteSetOperatorInterface - ? TValueResult extends Array - ? Rest extends SQLiteSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never - : never - : TValue; - -export interface SQLiteSetOperationConfig { - fields: Record; - operator: SetOperator; - isAll: boolean; - leftSelect: AnySQLiteSetOperatorBase; - rightSelect: TypedQueryBuilder; - limit?: number | Placeholder; - orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; -} +export type AnySQLiteSetOperatorInterface = SQLiteSetOperatorInterface; export interface SQLiteSetOperatorInterface< - THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, TResultType extends 'sync' | 'async', TRunResult, TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, + TSelectMode extends SelectMode = 'single', TNullabilityMap extends Record = TTableName extends string ? Record : {}, TDynamic extends boolean = false, @@ -371,9 +338,8 @@ export interface SQLiteSetOperatorInterface< TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, > extends - Omit< - SQLiteSetOperatorBuilder< - THKT, + Pick< + SQLiteSelectBase< TTableName, TResultType, TRunResult, @@ -385,12 +351,11 @@ export interface SQLiteSetOperatorInterface< TResult, TSelectedFields >, - 'joinsNotNullableMap' | 'session' | 'dialect' | 'createSetOperator' + never > { _: { - dialect: 'sqlite'; - readonly hkt: THKT; + readonly hkt: SQLiteSelectHKTBase; readonly tableName: TTableName; readonly resultType: TResultType; readonly runResult: TRunResult; @@ -404,9 +369,7 @@ export interface SQLiteSetOperatorInterface< }; } -export type AnySQLiteSetOperatorBase = SQLiteSetOperatorInterface< - any, - any, +export type SQLiteSetOperatorWithResult = SQLiteSetOperatorInterface< any, any, any, @@ -415,26 +378,81 @@ export type AnySQLiteSetOperatorBase = SQLiteSetOperatorInterface< any, any, any, + TResult, any >; +export type SQLiteSelect< + TTableName extends string | undefined = string | undefined, + TResultType extends 'sync' | 'async' = 'sync' | 'async', + TRunResult = unknown, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = SQLiteSelectBase; + +export type AnySQLiteSelect = SQLiteSelectBase; + +export type SQLiteSetOperator< + TTableName extends string | undefined = string | undefined, + TResultType extends 'sync' | 'async' = 'sync' | 'async', + TRunResult = unknown, + TSelection extends ColumnsSelection = Record, + TSelectMode extends SelectMode = SelectMode, + TNullabilityMap extends Record = Record, +> = SQLiteSelectBase< + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + true, + SQLiteSetOperatorExcludedMethods +>; + +export type SetOperatorRightSelect< + TValue extends SQLiteSetOperatorWithResult, + TResult extends any[], +> = TValue extends SQLiteSetOperatorInterface + ? TValueResult extends Array ? ValidateShape< + TValueObj, + TResult[number], + TypedQueryBuilder + > + : never + : TValue; + +export type SetOperatorRestSelect< + TValue extends readonly SQLiteSetOperatorWithResult[], + TResult extends any[], +> = TValue extends [infer First, ...infer Rest] + ? First extends SQLiteSetOperatorInterface + ? TValueResult extends Array ? Rest extends AnySQLiteSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> + : never + : never + : TValue; + export type SQLiteCreateSetOperatorFn = < - THKT extends SQLiteSelectHKTBase, TTableName extends string | undefined, TResultType extends 'sync' | 'async', TRunResult, TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record, - TValue extends SQLiteSetOperatorBaseWithResult, - TRest extends SQLiteSetOperatorBaseWithResult[], + TValue extends SQLiteSetOperatorWithResult, + TRest extends SQLiteSetOperatorWithResult[], + TSelectMode extends SelectMode = 'single', + TNullabilityMap extends Record = TTableName extends string ? Record + : {}, TDynamic extends boolean = false, TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, >( leftSelect: SQLiteSetOperatorInterface< - THKT, TTableName, TResultType, TRunResult, @@ -448,85 +466,27 @@ export type SQLiteCreateSetOperatorFn = < >, rightSelect: SetOperatorRightSelect, ...restSelects: SetOperatorRestSelect -) => SQLiteSetOperatorBase< - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, +) => SQLiteSelectWithout< + SQLiteSelectBase< + TTableName, + TResultType, + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, false, - never, - TResult, - TSelectedFields ->; - -export type AnySQLiteSetOperator = SQLiteSetOperatorBase< - any, - any, - any, - any, - any, - any, - any, - any, - any, - any ->; - -export type SQLiteSetOperator< - TTableName extends string | undefined = string | undefined, - TResultType extends 'sync' | 'async' = 'sync' | 'async', - TRunResult = unknown, - TSelection extends ColumnsSelection = Record, - TSelectMode extends SelectMode = SelectMode, - TNullabilityMap extends Record = Record, -> = SQLiteSetOperatorBase; - -export type AnySQLiteSetOperatorBuilder = SQLiteSetOperatorBuilder< - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any + SQLiteSetOperatorExcludedMethods, + true >; -export type SQLiteSetOperatorWithout< - T extends AnySQLiteSetOperator, - TDynamic extends boolean, - K extends keyof T & string, -> = TDynamic extends true ? T - : Omit< - SQLiteSetOperatorBase< - T['_']['tableName'], - T['_']['resultType'], - T['_']['runResult'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - TDynamic, - T['_']['excludedMethods'] | K, - T['_']['result'], - T['_']['selectedFields'] - >, - T['_']['excludedMethods'] | K - >; - -export type SQLiteSetOperatorDynamic = SQLiteSetOperatorBase< - T['_']['tableName'], - T['_']['resultType'], - T['_']['runResult'], - T['_']['selection'], - T['_']['selectMode'], - T['_']['nullabilityMap'], - true, - never, - T['_']['result'], - T['_']['selectedFields'] ->; +export type GetSQLiteSetOperators = { + union: SQLiteCreateSetOperatorFn; + intersect: SQLiteCreateSetOperatorFn; + except: SQLiteCreateSetOperatorFn; + unionAll: SQLiteCreateSetOperatorFn; +}; diff --git a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts b/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts deleted file mode 100644 index 1538a1b0b..000000000 --- a/drizzle-orm/src/sqlite-core/query-builders/set-operators.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { entityKind } from '~/entity.ts'; -import { - orderSelectedFields, - type Placeholder, - type Query, - SelectionProxyHandler, - type SQL, - Subquery, - type ValueOrArray, -} from '~/index.ts'; -import { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; -import type { - BuildSubquerySelection, - JoinNullability, - SelectMode, - SelectResult, - SetOperator, -} from '~/query-builders/select.types.ts'; -import { QueryPromise } from '~/query-promise.ts'; -import type { RunnableQuery } from '~/runnable-query.ts'; -import type { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts'; -import { applyMixins, haveSameKeys, type PromiseOf } from '~/utils.ts'; -import type { ColumnsSelection } from '~/view.ts'; -import type { SQLiteColumn } from '../columns/common.ts'; -import type { SQLiteDialect } from '../dialect.ts'; -import type { SubqueryWithSelection } from '../subquery.ts'; -import type { - SetOperatorRightSelect, - SQLiteCreateSetOperatorFn, - SQLiteSelectHKTBase, - SQLiteSetOperationConfig, - SQLiteSetOperatorBaseWithResult, - SQLiteSetOperatorDynamic, - SQLiteSetOperatorInterface, - SQLiteSetOperatorWithout, -} from './select.types.ts'; - -const getSQLiteSetOperators = () => { - return { - union, - unionAll, - intersect, - except, - }; -}; - -type SQLiteSetOperators = ReturnType; - -export abstract class SQLiteSetOperatorBuilder< - THKT extends SQLiteSelectHKTBase, - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder< - TSelectedFields, - TResult -> { - static readonly [entityKind]: string = 'SQLiteSetOperatorBuilder'; - - abstract override readonly _: { - dialect: 'sqlite'; - readonly hkt: THKT; - readonly tableName: TTableName; - readonly resultType: TResultType; - readonly runResult: TRunResult; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected abstract joinsNotNullableMap: Record; - protected abstract config: { - fields: Record; - limit?: number | Placeholder; - orderBy?: (SQLiteColumn | SQL | SQL.Aliased)[]; - offset?: number | Placeholder; - }; - /* @internal */ - protected abstract readonly session: SQLiteSession | undefined; - protected abstract dialect: SQLiteDialect; - - /** @internal */ - getSetOperatorConfig() { - return { - session: this.session, - dialect: this.dialect, - joinsNotNullableMap: this.joinsNotNullableMap, - fields: this.config.fields, - }; - } - - private createSetOperator( - type: SetOperator, - isAll: boolean, - ): >( - rightSelect: - | ((setOperator: SQLiteSetOperators) => SetOperatorRightSelect) - | SetOperatorRightSelect, - ) => SQLiteSetOperatorBase< - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - false, - never, - TResult, - TSelectedFields - > { - return (rightSelect) => { - const rightSelectOrig = typeof rightSelect === 'function' ? rightSelect(getSQLiteSetOperators()) : rightSelect; - - return new SQLiteSetOperatorBase(type, isAll, this, rightSelectOrig as any) as any; - }; - } - - union = this.createSetOperator('union', false); - - unionAll = this.createSetOperator('union', true); - - intersect = this.createSetOperator('intersect', false); - - except = this.createSetOperator('except', false); -} - -export interface SQLiteSetOperatorBase< - TTableName extends string | undefined, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TResultType extends 'sync' | 'async', - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TDynamic extends boolean = false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends TypedQueryBuilder, QueryPromise, RunnableQuery {} - -export class SQLiteSetOperatorBase< - TTableName extends string | undefined, - TResultType extends 'sync' | 'async', - TRunResult, - TSelection extends ColumnsSelection, - TSelectMode extends SelectMode, - TNullabilityMap extends Record = TTableName extends string ? Record - : {}, - TDynamic extends boolean = false, - TExcludedMethods extends string = never, - TResult extends any[] = SelectResult[], - TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends SQLiteSetOperatorBuilder< - SQLiteSelectHKTBase, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields -> { - static readonly [entityKind]: string = 'SQLiteSetOperator'; - - override readonly _: { - dialect: 'sqlite'; - readonly hkt: SQLiteSelectHKTBase; - readonly tableName: TTableName; - readonly resultType: TResultType; - readonly runResult: TRunResult; - readonly selection: TSelection; - readonly selectMode: TSelectMode; - readonly nullabilityMap: TNullabilityMap; - readonly dynamic: TDynamic; - readonly excludedMethods: TExcludedMethods; - readonly result: TResult; - readonly selectedFields: TSelectedFields; - }; - - protected joinsNotNullableMap: Record; - protected config: SQLiteSetOperationConfig; - /* @internal */ - readonly session: SQLiteSession | undefined; - protected dialect: SQLiteDialect; - - constructor( - operator: SetOperator, - isAll: boolean, - leftSelect: SQLiteSetOperatorInterface< - SQLiteSelectHKTBase, - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - rightSelect: TypedQueryBuilder, - ) { - super(); - - const leftSelectedFields = leftSelect.getSelectedFields(); - const rightSelectedFields = rightSelect.getSelectedFields(); - - if (!haveSameKeys(leftSelectedFields, rightSelectedFields)) { - throw new Error( - 'Set operator error (union / intersect / except): selected fields are not the same or are in a different order', - ); - } - - const { session, dialect, joinsNotNullableMap, fields } = leftSelect.getSetOperatorConfig(); - - this._ = { - selectedFields: fields as TSelectedFields, - } as this['_']; - - this.session = session; - this.dialect = dialect; - this.joinsNotNullableMap = joinsNotNullableMap; - this.config = { - fields, - operator, - isAll, - leftSelect, - rightSelect, - } as SQLiteSetOperationConfig; - } - - orderBy( - builder: (aliases: TSelection) => ValueOrArray, - ): SQLiteSetOperatorWithout; - orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteSetOperatorWithout; - orderBy( - ...columns: - | [(aliases: TSelection) => ValueOrArray] - | (SQLiteColumn | SQL | SQL.Aliased)[] - ): SQLiteSetOperatorWithout { - if (typeof columns[0] === 'function') { - const orderBy = columns[0]( - new Proxy( - this.config.fields, - new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), - ) as TSelection, - ); - this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]; - } else { - this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]; - } - return this as any; - } - - limit(limit: number): SQLiteSetOperatorWithout { - this.config.limit = limit; - return this as any; - } - - offset(offset: number | Placeholder): SQLiteSetOperatorWithout { - this.config.offset = offset; - return this as any; - } - - toSQL(): Query { - const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); - return rest; - } - - override getSQL(): SQL { - return this.dialect.buildSetOperationQuery(this.config); - } - - prepare(isOneTimeQuery?: boolean): SQLitePreparedQuery< - { - type: TResultType; - run: TRunResult; - all: TResult; - get: SelectResult | undefined; - values: any[][]; - execute: TResult; - } - > { - if (!this.session) { - throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.'); - } - const fieldsList = orderSelectedFields(this.config.fields); - const query = this.session[isOneTimeQuery ? 'prepareOneTimeQuery' : 'prepareQuery']( - this.dialect.sqlToQuery(this.getSQL()), - fieldsList, - 'all', - ); - query.joinsNotNullableMap = this.joinsNotNullableMap; - return query as ReturnType; - } - - run: ReturnType['run'] = (placeholderValues) => { - return this.prepare(true).run(placeholderValues); - }; - - all: ReturnType['all'] = (placeholderValues) => { - return this.prepare(true).all(placeholderValues); - }; - - get: ReturnType['get'] = (placeholderValues) => { - return this.prepare(true).get(placeholderValues); - }; - - values: ReturnType['values'] = (placeholderValues) => { - return this.prepare(true).values(placeholderValues); - }; - - async execute(): Promise { - return this.all() as PromiseOf>; - } - - as( - alias: TAlias, - ): SubqueryWithSelection, TAlias> { - return new Proxy( - new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), - ) as SubqueryWithSelection, TAlias>; - } - - $dynamic(): SQLiteSetOperatorDynamic { - return this as any; - } -} - -applyMixins(SQLiteSetOperatorBase, [QueryPromise]); - -function createSetOperator(type: SetOperator, isAll: boolean): SQLiteCreateSetOperatorFn { - return (leftSelect, rightSelect, ...restSelects) => { - if (restSelects.length === 0) { - return new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any) as any; - } - - const [select, ...rest] = restSelects; - if (!select) throw new Error('Cannot pass undefined values to any set operator'); - - return createSetOperator(type, isAll)( - new SQLiteSetOperatorBase(type, isAll, leftSelect, rightSelect as any), - select as any, - ...rest, - ); - }; -} - -export const union = createSetOperator('union', false); - -export const unionAll = createSetOperator('union', true); - -export const intersect = createSetOperator('intersect', false); - -export const except = createSetOperator('except', false); From 1128c12f4536e3d682012f51bc530695c4293fb0 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Mon, 30 Oct 2023 23:15:41 -0400 Subject: [PATCH 31/34] [MySql] Types simplification from review and fixed tests for western hemisphere --- .../mysql-core/query-builders/select.types.ts | 71 ++++++------------- integration-tests/tests/mysql-proxy.test.ts | 4 +- integration-tests/tests/mysql-schema.test.ts | 6 +- integration-tests/tests/mysql.custom.test.ts | 6 +- .../tests/mysql.prefixed.test.ts | 4 +- integration-tests/tests/mysql.test.ts | 6 +- integration-tests/tests/utils.ts | 6 ++ 7 files changed, 43 insertions(+), 60 deletions(-) diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 1acb4a5e5..022219522 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -202,26 +202,16 @@ export interface MySqlSelectHKT extends MySqlSelectHKTBase { >; } -export type MySqlSetOperatorExcludedMethods = Exclude< - keyof AnyMySqlSelectQueryBuilder, - // Only add the methods that a SetOperator is supposed to have - | 'orderBy' - | 'limit' - | 'offset' - | 'union' - | 'unionAll' - | 'intersect' - | 'intersectAll' - | 'except' - | 'exceptAll' - | '_' - | 'getSQL' - | 'as' - | 'addSetOperators' - | 'toSQL' - | '$dynamic' - | 'getSelectedFields' ->; +export type MySqlSetOperatorExcludedMethods = + | 'where' + | 'having' + | 'groupBy' + | 'session' + | 'leftJoin' + | 'rightJoin' + | 'innerJoin' + | 'fullJoin' + | 'for'; export type MySqlSelectWithout< T extends AnyMySqlSelectQueryBuilder, @@ -230,7 +220,7 @@ export type MySqlSelectWithout< TResetExcluded extends boolean = false, > = TDynamic extends true ? T : Omit< MySqlSelectKind< - T['_']['hkt'], + T['_']['hkt'], T['_']['tableName'], T['_']['selection'], T['_']['selectMode'], @@ -312,22 +302,7 @@ export interface MySqlSetOperatorInterface< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends - Pick< - MySqlSelectBase< - TTableName, - TSelection, - TSelectMode, - TPreparedQueryHKT, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - never - > -{ +> { _: { readonly hkt: MySqlSelectHKT; readonly tableName: TTableName; @@ -383,12 +358,11 @@ export type SetOperatorRightSelect< TValue extends MySqlSetOperatorWithResult, TResult extends any[], > = TValue extends MySqlSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never + ? ValidateShape< + TValueResult[number], + TResult[number], + TypedQueryBuilder + > : TValue; export type SetOperatorRestSelect< @@ -396,12 +370,11 @@ export type SetOperatorRestSelect< TResult extends any[], > = TValue extends [infer First, ...infer Rest] ? First extends MySqlSetOperatorInterface - ? TValueResult extends Array ? Rest extends AnyMySqlSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never + ? Rest extends AnyMySqlSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> : never : TValue; diff --git a/integration-tests/tests/mysql-proxy.test.ts b/integration-tests/tests/mysql-proxy.test.ts index c18f4fb84..861d6811e 100644 --- a/integration-tests/tests/mysql-proxy.test.ts +++ b/integration-tests/tests/mysql-proxy.test.ts @@ -32,7 +32,7 @@ import { migrate } from 'drizzle-orm/mysql-proxy/migrator'; import getPort from 'get-port'; import * as mysql from 'mysql2/promise'; import { v4 as uuid } from 'uuid'; -import { type Equal, Expect } from './utils.ts'; +import { type Equal, Expect, toLocalDate } from './utils.ts'; const ENABLE_LOGGING = false; @@ -1079,7 +1079,7 @@ test.serial('insert + select all possible dates', async (t) => { t.assert(typeof res[0]?.datetimeAsString === 'string'); t.deepEqual(res, [{ - date: new Date('2022-11-11'), + date: toLocalDate(new Date('2022-11-11')), dateAsString: '2022-11-11', time: '12:12:12', datetime: new Date('2022-11-11'), diff --git a/integration-tests/tests/mysql-schema.test.ts b/integration-tests/tests/mysql-schema.test.ts index 08a62bd16..f82d47533 100644 --- a/integration-tests/tests/mysql-schema.test.ts +++ b/integration-tests/tests/mysql-schema.test.ts @@ -27,6 +27,7 @@ import { drizzle } from 'drizzle-orm/mysql2'; import getPort from 'get-port'; import * as mysql from 'mysql2/promise'; import { v4 as uuid } from 'uuid'; +import { toLocalDate } from './utils'; const mySchema = mysqlSchema('mySchema'); @@ -471,7 +472,8 @@ test.serial('build query insert with onDuplicate', async (t) => { .toSQL(); t.deepEqual(query, { - sql: 'insert into `mySchema`.`userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', + sql: + 'insert into `mySchema`.`userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', params: ['John', '["foo","bar"]', 'John1'], }); }); @@ -740,7 +742,7 @@ test.serial('insert + select all possible dates', async (t) => { t.assert(typeof res[0]?.datetimeAsString === 'string'); t.deepEqual(res, [{ - date: new Date('2022-11-11'), + date: toLocalDate(new Date('2022-11-11')), dateAsString: '2022-11-11', time: '12:12:12', datetime: new Date('2022-11-11'), diff --git a/integration-tests/tests/mysql.custom.test.ts b/integration-tests/tests/mysql.custom.test.ts index e0ad8df25..c60b88e47 100644 --- a/integration-tests/tests/mysql.custom.test.ts +++ b/integration-tests/tests/mysql.custom.test.ts @@ -25,6 +25,7 @@ import { migrate } from 'drizzle-orm/mysql2/migrator'; import getPort from 'get-port'; import * as mysql from 'mysql2/promise'; import { v4 as uuid } from 'uuid'; +import { toLocalDate } from './utils'; const customSerial = customType<{ data: number; notNull: true; default: true }>({ dataType() { @@ -486,7 +487,8 @@ test.serial('build query insert with onDuplicate', async (t) => { .toSQL(); t.deepEqual(query, { - sql: 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', + sql: + 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', params: ['John', '["foo","bar"]', 'John1'], }); }); @@ -777,7 +779,7 @@ test.serial('insert + select all possible dates', async (t) => { t.assert(typeof res[0]?.datetimeAsString === 'string'); t.deepEqual(res, [{ - date: new Date('2022-11-11'), + date: toLocalDate(new Date('2022-11-11')), dateAsString: '2022-11-11', time: '12:12:12', datetime: new Date('2022-11-11'), diff --git a/integration-tests/tests/mysql.prefixed.test.ts b/integration-tests/tests/mysql.prefixed.test.ts index c4b4d25f7..324dced00 100644 --- a/integration-tests/tests/mysql.prefixed.test.ts +++ b/integration-tests/tests/mysql.prefixed.test.ts @@ -41,7 +41,7 @@ import { migrate } from 'drizzle-orm/mysql2/migrator'; import getPort from 'get-port'; import * as mysql from 'mysql2/promise'; import { v4 as uuid } from 'uuid'; -import { type Equal, Expect } from './utils.ts'; +import { type Equal, Expect, toLocalDate } from './utils.ts'; const ENABLE_LOGGING = false; @@ -793,7 +793,7 @@ test.serial('insert + select all possible dates', async (t) => { t.assert(typeof res[0]?.datetimeAsString === 'string'); t.deepEqual(res, [{ - date: new Date('2022-11-11'), + date: toLocalDate(new Date('2022-11-11')), dateAsString: '2022-11-11', time: '12:12:12', datetime: new Date('2022-11-11'), diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 4da40a326..6028a299a 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -42,9 +42,9 @@ import { text, time, timestamp, + tinyint, union, unionAll, - tinyint, unique, uniqueIndex, uniqueKeyName, @@ -56,7 +56,7 @@ import { migrate } from 'drizzle-orm/mysql2/migrator'; import getPort from 'get-port'; import * as mysql from 'mysql2/promise'; import { v4 as uuid } from 'uuid'; -import { type Equal, Expect } from './utils.ts'; +import { type Equal, Expect, toLocalDate } from './utils.ts'; const ENABLE_LOGGING = false; @@ -1132,7 +1132,7 @@ test.serial('insert + select all possible dates', async (t) => { t.assert(typeof res[0]?.datetimeAsString === 'string'); t.deepEqual(res, [{ - date: new Date('2022-11-11'), + date: toLocalDate(new Date('2022-11-11')), dateAsString: '2022-11-11', time: '12:12:12', datetime: new Date('2022-11-11'), diff --git a/integration-tests/tests/utils.ts b/integration-tests/tests/utils.ts index 199ed1a57..2d021a2e3 100644 --- a/integration-tests/tests/utils.ts +++ b/integration-tests/tests/utils.ts @@ -3,3 +3,9 @@ export function Expect() {} export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; + +export function toLocalDate(date: Date) { + const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + localTime.setUTCHours(0); + return localTime; +} From 99b2a3c5d3684fac73da2c7230f04e15a0601c80 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Mon, 30 Oct 2023 23:22:49 -0400 Subject: [PATCH 32/34] [Pg] Types simplification from review --- .../pg-core/query-builders/select.types.ts | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/drizzle-orm/src/pg-core/query-builders/select.types.ts b/drizzle-orm/src/pg-core/query-builders/select.types.ts index 4d8ba939f..d07784b06 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.types.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.types.ts @@ -211,26 +211,15 @@ export type CreatePgSelectFromBuilderMode< > = TBuilderMode extends 'db' ? PgSelectBase : PgSelectQueryBuilderBase; -export type PgSetOperatorExcludedMethods = Exclude< - keyof AnyPgSelectQueryBuilder, - // Only add the methods that a SetOperator is supposed to have - | 'orderBy' - | 'limit' - | 'offset' - | 'union' - | 'unionAll' - | 'intersect' - | 'intersectAll' - | 'except' - | 'exceptAll' - | '_' - | 'getSQL' - | 'as' - | 'addSetOperators' - | 'toSQL' - | '$dynamic' - | 'getSelectedFields' ->; +export type PgSetOperatorExcludedMethods = + | 'leftJoin' + | 'rightJoin' + | 'innerJoin' + | 'fullJoin' + | 'where' + | 'having' + | 'groupBy' + | 'for'; export type PgSelectWithout< T extends AnyPgSelectQueryBuilder, @@ -304,21 +293,7 @@ export interface PgSetOperatorInterface< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends - Pick< - PgSelectBase< - TTableName, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - never - > -{ +> { _: { readonly hkt: PgSelectHKT; readonly tableName: TTableName; @@ -369,13 +344,11 @@ export type PgSetOperator< export type SetOperatorRightSelect< TValue extends PgSetOperatorWithResult, TResult extends any[], -> = TValue extends PgSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never +> = TValue extends PgSetOperatorInterface ? ValidateShape< + TValueResult[number], + TResult[number], + TypedQueryBuilder + > : TValue; export type SetOperatorRestSelect< @@ -383,12 +356,11 @@ export type SetOperatorRestSelect< TResult extends any[], > = TValue extends [infer First, ...infer Rest] ? First extends PgSetOperatorInterface - ? TValueResult extends Array ? Rest extends AnyPgSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never + ? Rest extends AnyPgSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> : never : TValue; From af5387f7892de63157436a8b3c56f57b570e6516 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Mon, 30 Oct 2023 23:29:12 -0400 Subject: [PATCH 33/34] [SQLite] Types simplification from review --- .../query-builders/select.types.ts | 69 ++++++------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 32b47486a..3e8b4359f 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -192,26 +192,15 @@ export interface SQLiteSelectHKT extends SQLiteSelectHKTBase { >; } -export type SQLiteSetOperatorExcludedMethods = Exclude< - keyof AnySQLiteSelectQueryBuilder, - // Only add the methods that a SetOperator is supposed to have - | 'orderBy' - | 'limit' - | 'offset' - | 'union' - | 'unionAll' - | 'intersect' - | 'intersectAll' - | 'except' - | 'exceptAll' - | '_' - | 'getSQL' - | 'as' - | 'addSetOperators' - | 'toSQL' - | '$dynamic' - | 'getSelectedFields' ->; +export type SQLiteSetOperatorExcludedMethods = + | 'config' + | 'leftJoin' + | 'rightJoin' + | 'innerJoin' + | 'fullJoin' + | 'where' + | 'having' + | 'groupBy'; export type CreateSQLiteSelectFromBuilderMode< TBuilderMode extends 'db' | 'qb', @@ -337,23 +326,7 @@ export interface SQLiteSetOperatorInterface< TExcludedMethods extends string = never, TResult extends any[] = SelectResult[], TSelectedFields extends ColumnsSelection = BuildSubquerySelection, -> extends - Pick< - SQLiteSelectBase< - TTableName, - TResultType, - TRunResult, - TSelection, - TSelectMode, - TNullabilityMap, - TDynamic, - TExcludedMethods, - TResult, - TSelectedFields - >, - never - > -{ +> { _: { readonly hkt: SQLiteSelectHKTBase; readonly tableName: TTableName; @@ -415,12 +388,11 @@ export type SetOperatorRightSelect< TValue extends SQLiteSetOperatorWithResult, TResult extends any[], > = TValue extends SQLiteSetOperatorInterface - ? TValueResult extends Array ? ValidateShape< - TValueObj, - TResult[number], - TypedQueryBuilder - > - : never + ? ValidateShape< + TValueResult[number], + TResult[number], + TypedQueryBuilder + > : TValue; export type SetOperatorRestSelect< @@ -428,12 +400,11 @@ export type SetOperatorRestSelect< TResult extends any[], > = TValue extends [infer First, ...infer Rest] ? First extends SQLiteSetOperatorInterface - ? TValueResult extends Array ? Rest extends AnySQLiteSetOperatorInterface[] ? [ - ValidateShape>, - ...SetOperatorRestSelect, - ] - : ValidateShape[]> - : never + ? Rest extends AnySQLiteSetOperatorInterface[] ? [ + ValidateShape>, + ...SetOperatorRestSelect, + ] + : ValidateShape[]> : never : TValue; From a83ea7dadf147d89d6d35c4128bea0aed10e01a5 Mon Sep 17 00:00:00 2001 From: Angelelz Date: Mon, 30 Oct 2023 23:40:34 -0400 Subject: [PATCH 34/34] [MySql] fixed failing tests based on query order --- integration-tests/tests/mysql.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 6028a299a..3b545fcd8 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -2537,7 +2537,7 @@ test.serial('set operations (except all) as function', async (t) => { db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(6); + ).limit(6).orderBy(asc(sql.identifier('id'))); t.assert(result.length === 6); @@ -2616,7 +2616,7 @@ test.serial('set operations (mixed all) as function with subquery', async (t) => db .select({ id: users2Table.id, name: users2Table.name }) .from(users2Table).where(eq(users2Table.id, 7)), - ).as('sq'); + ).orderBy(asc(sql.identifier('id'))).as('sq'); const result = await union( db