Skip to content

Commit 6e1aa47

Browse files
authored
Merge pull request #452 from drizzle-team/join-subquery-with-join
2 parents 4645e6f + c1bb977 commit 6e1aa47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1387
-231
lines changed

changelogs/drizzle-orm/0.23.11.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
- 🐛 Fix migrator function for PostgreSQL
22

3-
> Would suggest to upgrade to this version anyone who is using postgres dialect. `0.23.9` and `0.23.10` are broken for postgresql migrations
3+
> Would suggest to upgrade to this version anyone who is using postgres dialect. `0.23.9` and `0.23.10` are broken for postgresql migrations

changelogs/drizzle-orm/0.23.12.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- 🐛 Fixed multi-level join results (e.g. joining a subquery with a nested join)

drizzle-orm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-orm",
3-
"version": "0.23.11",
3+
"version": "0.23.12",
44
"description": "Drizzle ORM package for SQL databases",
55
"scripts": {
66
"build": "tsc && resolve-tspaths && cp ../README.md package.json dist/",

drizzle-orm/src/alias.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
11
import type { AnyColumn } from './column';
22
import { Column } from './column';
33
import { Table } from './table';
4+
import { type View, ViewBaseConfig } from './view';
45

56
export class ColumnAliasProxyHandler<TColumn extends AnyColumn> implements ProxyHandler<TColumn> {
6-
constructor(private table: Table) {}
7+
constructor(private table: Table | View) {}
78

89
get(columnObj: TColumn, prop: string | symbol, receiver: any): any {
910
if (prop === 'table') {
1011
return this.table;
1112
}
13+
1214
return columnObj[prop as keyof TColumn];
1315
}
1416
}
1517

16-
export class TableAliasProxyHandler implements ProxyHandler<Table> {
17-
constructor(private alias: string) {}
18+
export class TableAliasProxyHandler<T extends Table | View> implements ProxyHandler<T> {
19+
constructor(private alias: string, private replaceOriginalName: boolean) {}
1820

19-
get(tableObj: Table, prop: string | symbol, receiver: any): any {
21+
get(tableObj: T, prop: string | symbol, receiver: any): any {
2022
if (prop === Table.Symbol.Name) {
2123
return this.alias;
2224
}
25+
26+
if (this.replaceOriginalName && prop === Table.Symbol.OriginalName) {
27+
return this.alias;
28+
}
29+
30+
if (prop === ViewBaseConfig) {
31+
return {
32+
...tableObj[ViewBaseConfig as keyof typeof tableObj],
33+
name: this.alias,
34+
};
35+
}
36+
2337
if (prop === Table.Symbol.Columns) {
24-
const columns = tableObj[Table.Symbol.Columns];
38+
const columns = (tableObj as Table)[Table.Symbol.Columns];
2539
if (!columns) {
2640
return columns;
2741
}
@@ -38,7 +52,7 @@ export class TableAliasProxyHandler implements ProxyHandler<Table> {
3852
return proxiedColumns;
3953
}
4054

41-
const value = tableObj[prop as keyof Table];
55+
const value = tableObj[prop as keyof typeof tableObj];
4256
if (value instanceof Column) {
4357
return new Proxy(value, new ColumnAliasProxyHandler(new Proxy(tableObj, this)));
4458
}

drizzle-orm/src/mysql-core/alias.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export function alias<TTable extends AnyMySqlTable, TAlias extends string>(
77
table: TTable,
88
alias: TAlias,
99
): BuildAliasTable<TTable, TAlias> {
10-
return new Proxy(table, new TableAliasProxyHandler(alias)) as any;
10+
return new Proxy(table, new TableAliasProxyHandler(alias, false)) as any;
1111
}

drizzle-orm/src/mysql-core/db.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ResultSetHeader } from 'mysql2/promise';
22
import type { QueryBuilder } from '~/query-builders/query-builder';
33
import type { SQLWrapper } from '~/sql';
44
import { SelectionProxyHandler, WithSubquery } from '~/subquery';
5+
import { type ColumnsSelection } from '~/view';
56
import type { MySqlDialect } from './dialect';
67
import type { QueryBuilderInstance } from './query-builders';
78
import {
@@ -32,7 +33,7 @@ export class MySqlDatabase<TQueryResult extends QueryResultHKT> {
3233

3334
$with<TAlias extends string>(alias: TAlias) {
3435
return {
35-
as<TSelection>(
36+
as<TSelection extends ColumnsSelection>(
3637
qb: QueryBuilder<TSelection> | ((qb: QueryBuilderInstance) => QueryBuilder<TSelection>),
3738
): WithSubqueryWithSelection<TSelection, TAlias> {
3839
if (typeof qb === 'function') {

drizzle-orm/src/mysql-core/dialect.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { AnyColumn } from '~/column';
22
import { Column } from '~/column';
33
import type { MigrationConfig, MigrationMeta } from '~/migrator';
4-
import { Name, name, type Query, SQL, sql, type SQLChunk } from '~/sql';
4+
import { name, type Query, SQL, sql, type SQLChunk } from '~/sql';
55
import { Subquery, SubqueryConfig } from '~/subquery';
66
import { getTableName, Table } from '~/table';
7-
import type { UpdateSet } from '~/utils';
8-
import { ViewBaseConfig } from '~/view';
7+
import { orderSelectedFields, type UpdateSet } from '~/utils';
8+
import { View, ViewBaseConfig } from '~/view';
99
import type { AnyMySqlColumn } from './columns/common';
1010
import { MySqlColumn } from './columns/common';
1111
import type { MySqlDeleteConfig } from './query-builders/delete';
@@ -90,7 +90,7 @@ export class MySqlDialect {
9090
setEntries
9191
.map(([colName, value], i): SQL[] => {
9292
const col: AnyMySqlColumn = table[Table.Symbol.Columns][colName]!;
93-
const res = sql`${new Name(col.name)} = ${value}`;
93+
const res = sql`${name(col.name)} = ${value}`;
9494
if (i < setSize - 1) {
9595
return [res, sql.raw(', ')];
9696
}
@@ -134,7 +134,7 @@ export class MySqlDialect {
134134
const chunk: SQLChunk[] = [];
135135

136136
if (field instanceof SQL.Aliased && field.isSelectionField) {
137-
chunk.push(new Name(field.fieldAlias));
137+
chunk.push(name(field.fieldAlias));
138138
} else if (field instanceof SQL.Aliased || field instanceof SQL) {
139139
const query = field instanceof SQL.Aliased ? field.sql : field;
140140

@@ -143,7 +143,7 @@ export class MySqlDialect {
143143
new SQL(
144144
query.queryChunks.map((c) => {
145145
if (c instanceof MySqlColumn) {
146-
return new Name(c.name);
146+
return name(c.name);
147147
}
148148
return c;
149149
}),
@@ -154,11 +154,11 @@ export class MySqlDialect {
154154
}
155155

156156
if (field instanceof SQL.Aliased) {
157-
chunk.push(sql` as ${new Name(field.fieldAlias)}`);
157+
chunk.push(sql` as ${name(field.fieldAlias)}`);
158158
}
159159
} else if (field instanceof Column) {
160160
if (isSingleTable) {
161-
chunk.push(new Name(field.name));
161+
chunk.push(name(field.name));
162162
} else {
163163
chunk.push(field);
164164
}
@@ -176,9 +176,10 @@ export class MySqlDialect {
176176
}
177177

178178
buildSelectQuery(
179-
{ withList, fieldsList, where, having, table, joins, orderBy, groupBy, limit, offset, lockingClause }:
179+
{ withList, fields, where, having, table, joins, orderBy, groupBy, limit, offset, lockingClause }:
180180
MySqlSelectConfig,
181181
): SQL {
182+
const fieldsList = orderSelectedFields<AnyMySqlColumn>(fields);
182183
fieldsList.forEach((f) => {
183184
if (
184185
f.field instanceof Column
@@ -207,7 +208,7 @@ export class MySqlDialect {
207208
if (withList.length) {
208209
const withSqlChunks = [sql`with `];
209210
withList.forEach((w, i) => {
210-
withSqlChunks.push(sql`${new Name(w[SubqueryConfig].alias)} as (${w[SubqueryConfig].sql})`);
211+
withSqlChunks.push(sql`${name(w[SubqueryConfig].alias)} as (${w[SubqueryConfig].sql})`);
211212
if (i < withList.length - 1) {
212213
withSqlChunks.push(sql`, `);
213214
}
@@ -232,10 +233,11 @@ export class MySqlDialect {
232233
const origTableName = table[MySqlTable.Symbol.OriginalName];
233234
const alias = tableName === origTableName ? undefined : joinMeta.alias;
234235
joinsArray.push(
235-
sql`${sql.raw(joinMeta.joinType)} join ${tableSchema ? sql`${new Name(tableSchema)}.` : undefined}${new Name(
236-
origTableName,
237-
)} ${alias && new Name(alias)} on ${joinMeta.on}`,
236+
sql`${sql.raw(joinMeta.joinType)} join ${tableSchema ? sql`${name(tableSchema)}.` : undefined}${
237+
name(origTableName)
238+
}${alias && sql` ${name(alias)}`} on ${joinMeta.on}`,
238239
);
240+
} else if (table instanceof View) {
239241
} else {
240242
joinsArray.push(
241243
sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`,
@@ -299,7 +301,7 @@ export class MySqlDialect {
299301
const colEntries: [string, AnyMySqlColumn][] = isSingleValue
300302
? Object.keys(values[0]!).map((fieldName) => [fieldName, columns[fieldName]!])
301303
: Object.entries(columns);
302-
const insertOrder = colEntries.map(([, column]) => new Name(column.name));
304+
const insertOrder = colEntries.map(([, column]) => name(column.name));
303305

304306
values.forEach((value, valueIndex) => {
305307
const valueList: (SQLChunk | SQL)[] = [];

drizzle-orm/src/mysql-core/query-builders/query-builder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { MySqlDialect } from '~/mysql-core/dialect';
22
import type { WithSubqueryWithSelection } from '~/mysql-core/subquery';
33
import type { QueryBuilder } from '~/query-builders/query-builder';
44
import { SelectionProxyHandler, WithSubquery } from '~/subquery';
5+
import { type ColumnsSelection } from '~/view';
56
import type { MySqlSelectBuilder } from './select';
67
import type { SelectedFields } from './select.types';
78

@@ -18,7 +19,7 @@ export class QueryBuilderInstance {
1819
const queryBuilder = this;
1920

2021
return {
21-
as<TSelection>(
22+
as<TSelection extends ColumnsSelection>(
2223
qb: QueryBuilder<TSelection> | ((qb: QueryBuilderInstance) => QueryBuilder<TSelection>),
2324
): WithSubqueryWithSelection<TSelection, TAlias> {
2425
if (typeof qb === 'function') {

drizzle-orm/src/mysql-core/query-builders/select.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { SelectionProxyHandler, Subquery, SubqueryConfig } from '~/subquery';
2020
import { Table } from '~/table';
2121
import { applyMixins, getTableColumns, type Simplify, type ValueOrArray } from '~/utils';
2222
import { orderSelectedFields } from '~/utils';
23-
import { ViewBaseConfig } from '~/view';
23+
import { type ColumnsSelection, View, ViewBaseConfig } from '~/view';
2424
import type {
2525
JoinFn,
2626
LockConfig,
@@ -35,7 +35,7 @@ import type {
3535
type CreateMySqlSelectFromBuilderMode<
3636
TBuilderMode extends 'db' | 'qb',
3737
TTableName extends string | undefined,
38-
TSelection,
38+
TSelection extends ColumnsSelection,
3939
TSelectMode extends SelectMode,
4040
> = TBuilderMode extends 'db' ? MySqlSelect<TTableName, TSelection, TSelectMode>
4141
: MySqlSelectQueryBuilder<MySqlSelectQueryBuilderHKT, TTableName, TSelection, TSelectMode>;
@@ -79,11 +79,9 @@ export class MySqlSelectBuilder<
7979
fields = getTableColumns<AnyMySqlTable>(source);
8080
}
8181

82-
const fieldsList = orderSelectedFields<AnyMySqlColumn>(fields);
8382
return new MySqlSelect(
8483
source,
8584
fields,
86-
fieldsList,
8785
isPartialSelect,
8886
this.session,
8987
this.dialect,
@@ -95,7 +93,7 @@ export class MySqlSelectBuilder<
9593
export abstract class MySqlSelectQueryBuilder<
9694
THKT extends MySqlSelectHKTBase,
9795
TTableName extends string | undefined,
98-
TSelection,
96+
TSelection extends ColumnsSelection,
9997
TSelectMode extends SelectMode,
10098
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string ? Record<TTableName, 'not-null'>
10199
: {},
@@ -113,7 +111,6 @@ export abstract class MySqlSelectQueryBuilder<
113111
constructor(
114112
table: MySqlSelectConfig['table'],
115113
fields: MySqlSelectConfig['fields'],
116-
fieldsList: MySqlSelectConfig['fieldsList'],
117114
private isPartialSelect: boolean,
118115
protected session: MySqlSession | undefined,
119116
protected dialect: MySqlDialect,
@@ -123,8 +120,7 @@ export abstract class MySqlSelectQueryBuilder<
123120
this.config = {
124121
withList,
125122
table,
126-
fields,
127-
fieldsList,
123+
fields: { ...fields },
128124
joins: [],
129125
orderBy: [],
130126
groupBy: [],
@@ -146,12 +142,14 @@ export abstract class MySqlSelectQueryBuilder<
146142
joinType: TJoinType,
147143
): JoinFn<THKT, TTableName, TSelectMode, TJoinType, TSelection, TNullabilityMap> {
148144
return (
149-
table: AnyMySqlTable | Subquery | SQL,
145+
table: AnyMySqlTable | Subquery | MySqlViewBase | SQL,
150146
on: ((aliases: TSelection) => SQL | undefined) | SQL | undefined,
151147
) => {
152148
const baseTableName = this.tableName;
153149
const tableName = table instanceof Subquery
154150
? table[SubqueryConfig].alias
151+
: table instanceof View
152+
? table[ViewBaseConfig].name
155153
: table instanceof SQL
156154
? undefined
157155
: table[Table.Symbol.Name];
@@ -163,18 +161,17 @@ export abstract class MySqlSelectQueryBuilder<
163161
if (!this.isPartialSelect) {
164162
// If this is the first join and this is not a partial select and we're not selecting from raw SQL, "move" the fields from the main table to the nested object
165163
if (Object.keys(this.joinsNotNullableMap).length === 1 && typeof baseTableName === 'string') {
166-
this.config.fieldsList = this.config.fieldsList.map((field) => ({
167-
...field,
168-
path: [baseTableName, ...field.path],
169-
}));
164+
this.config.fields = {
165+
[baseTableName]: this.config.fields,
166+
};
170167
}
171168
if (typeof tableName === 'string' && !(table instanceof SQL)) {
172-
this.config.fieldsList.push(
173-
...orderSelectedFields<AnyMySqlColumn>(
174-
table instanceof Subquery ? table[SubqueryConfig].selection : table[Table.Symbol.Columns],
175-
[tableName],
176-
),
177-
);
169+
const selection = table instanceof Subquery
170+
? table[SubqueryConfig].selection
171+
: table instanceof View
172+
? table[ViewBaseConfig].selectedFields
173+
: table[Table.Symbol.Columns];
174+
this.config.fields[tableName] = selection;
178175
}
179176
}
180177

@@ -329,7 +326,7 @@ export abstract class MySqlSelectQueryBuilder<
329326

330327
export interface MySqlSelect<
331328
TTableName extends string | undefined,
332-
TSelection,
329+
TSelection extends ColumnsSelection,
333330
TSelectMode extends SelectMode,
334331
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string ? Record<TTableName, 'not-null'>
335332
: {},
@@ -353,9 +350,10 @@ export class MySqlSelect<
353350
if (!this.session) {
354351
throw new Error('Cannot execute a query on a query builder. Please use a database instance instead.');
355352
}
353+
const fieldsList = orderSelectedFields<AnyMySqlColumn>(this.config.fields);
356354
const query = this.session.prepareQuery<
357355
PreparedQueryConfig & { execute: SelectResult<TSelection, TSelectMode, TNullabilityMap>[] }
358-
>(this.dialect.sqlToQuery(this.getSQL()), this.config.fieldsList, name);
356+
>(this.dialect.sqlToQuery(this.getSQL()), fieldsList, name);
359357
query.joinsNotNullableMap = this.joinsNotNullableMap;
360358
return query;
361359
}

drizzle-orm/src/mysql-core/query-builders/select.types.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import type { Placeholder, SQL } from '~/sql';
1919
import type { Subquery } from '~/subquery';
2020
import type { AnyTable, UpdateTableConfig } from '~/table';
2121
import type { Assume } from '~/utils';
22+
import { type ColumnsSelection } from '~/view';
2223
import type { MySqlSelect, MySqlSelectQueryBuilder } from './select';
2324

2425
export interface JoinsValue {
2526
on: SQL | undefined;
26-
table: AnyMySqlTable | Subquery | SQL;
27+
table: AnyMySqlTable | Subquery | MySqlViewBase | SQL;
2728
alias: string | undefined;
2829
joinType: JoinType;
2930
}
@@ -42,8 +43,7 @@ export type BuildAliasTable<TTable extends AnyTable, TAlias extends string> = My
4243

4344
export interface MySqlSelectConfig {
4445
withList: Subquery[];
45-
fields: SelectedFields;
46-
fieldsList: SelectedFieldsOrdered;
46+
fields: Record<string, unknown>;
4747
where?: SQL;
4848
having?: SQL;
4949
table: AnyMySqlTable | Subquery | MySqlViewBase | SQL;
@@ -66,7 +66,7 @@ export type JoinFn<
6666
TSelection,
6767
TNullabilityMap extends Record<string, JoinNullability>,
6868
> = <
69-
TJoinedTable extends AnyMySqlTable | Subquery | SQL,
69+
TJoinedTable extends AnyMySqlTable | Subquery | MySqlViewBase | SQL,
7070
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
7171
>(table: TJoinedTable, on: ((aliases: TSelection) => SQL | undefined) | SQL | undefined) => MySqlSelectKind<
7272
THKT,
@@ -76,7 +76,7 @@ export type JoinFn<
7676
TSelection,
7777
TJoinedName,
7878
TJoinedTable extends AnyMySqlTable ? TJoinedTable['_']['columns']
79-
: TJoinedName extends Subquery ? Assume<TJoinedName['_']['selectedFields'], SelectedFields>
79+
: TJoinedTable extends Subquery ? Assume<TJoinedTable['_']['selectedFields'], SelectedFields>
8080
: never,
8181
TSelectMode
8282
>,
@@ -128,7 +128,7 @@ export interface MySqlSelectQueryBuilderHKT extends MySqlSelectHKTBase {
128128
_type: MySqlSelectQueryBuilder<
129129
this,
130130
this['tableName'],
131-
this['selection'],
131+
Assume<this['selection'], ColumnsSelection>,
132132
this['selectMode'],
133133
Assume<this['nullabilityMap'], Record<string, JoinNullability>>
134134
>;
@@ -137,7 +137,7 @@ export interface MySqlSelectQueryBuilderHKT extends MySqlSelectHKTBase {
137137
export interface MySqlSelectHKT extends MySqlSelectHKTBase {
138138
_type: MySqlSelect<
139139
this['tableName'],
140-
this['selection'],
140+
Assume<this['selection'], ColumnsSelection>,
141141
this['selectMode'],
142142
Assume<this['nullabilityMap'], Record<string, JoinNullability>>
143143
>;

0 commit comments

Comments
 (0)