Skip to content

Commit e8d0715

Browse files
authored
Merge pull request #179 from drizzle-team/orm-99-with-statement
Implement WITH clause, fix subquery bugs
2 parents 89057df + b3094d9 commit e8d0715

Some content is hidden

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

62 files changed

+2249
-576
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const allUsers /* : User[] */ = await db.select().from(users);
108108
const upperCaseNames /* : { id: number; name: string }[] */ = await db
109109
.select({
110110
id: users.id,
111-
name: sql`upper(${users.fullName})`.as<string>(),
111+
name: sql<string>`upper(${users.fullName})`,
112112
})
113113
.from(users);
114114

changelogs/drizzle-orm/0.20.0.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
- 🎉 **Implemented support for WITH clause ([docs](/drizzle-orm/src/pg-core/README.md#with-clause)). Example usage:**
3+
4+
```ts
5+
const sq = db
6+
.select()
7+
.from(users)
8+
.prepareWithSubquery('sq');
9+
10+
const result = await db
11+
.with(sq)
12+
.select({
13+
id: sq.id,
14+
name: sq.name,
15+
total: sql<number>`count(${sq.id})::int`(),
16+
})
17+
.from(sq)
18+
.groupBy(sq.id, sq.name);
19+
```
20+
21+
- 🐛 Fixed various bugs with selecting/joining of subqueries.
22+
- ❗ Renamed `.subquery('alias')` to `.as('alias')`.
23+
-``sql`query`.as<type>()`` is now ``sql<type>`query`()``. Old syntax is still supported, but is deprecated and will be removed in one of the next releases.

dprint.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
"includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md}"],
1212
"excludes": ["**/node_modules", "**/*-lock.json"],
1313
"plugins": [
14-
"https://plugins.dprint.dev/typescript-0.70.0.wasm",
15-
"https://plugins.dprint.dev/json-0.15.3.wasm",
16-
"https://plugins.dprint.dev/markdown-0.13.3.wasm"
14+
"https://plugins.dprint.dev/typescript-0.83.0.wasm",
15+
"https://plugins.dprint.dev/json-0.17.0.wasm",
16+
"https://plugins.dprint.dev/markdown-0.15.2.wasm"
1717
]
1818
}

drizzle-orm/package.json

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

drizzle-orm/src/aws-data-api/pg/migrator.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { MigrationConfig, readMigrationFiles } from '~/migrator';
22
import { sql } from '~/sql';
33
import { AwsDataApiPgDatabase } from './driver';
4-
import { AwsDataApiSession } from './session';
54

65
export async function migrate(db: AwsDataApiPgDatabase, config: string | MigrationConfig) {
76
const migrations = readMigrationFiles(config);
87

9-
// Write own aws datapi migrator
10-
const session = db.session as AwsDataApiSession;
8+
// TODO: Write own aws datapi migrator
9+
const { session } = db;
1110

1211
const migrationTableCreate = sql`CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
1312
id SERIAL PRIMARY KEY,

drizzle-orm/src/mysql-core/README.md

+43-2
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,47 @@ await db.select().from(users).orderBy(desc(users.name));
375375
await db.select().from(users).orderBy(asc(users.name), desc(users.name));
376376
```
377377

378+
#### Conditionally select fields
379+
380+
```typescript
381+
async function selectUsers(withName: boolean) {
382+
return db
383+
.select({
384+
id: users.id,
385+
...(withName ? { name: users.name } : {}),
386+
})
387+
.from(users);
388+
}
389+
390+
const users = await selectUsers(true);
391+
```
392+
393+
#### WITH clause
394+
395+
```typescript
396+
const sq = db.select().from(users).where(eq(users.id, 42)).prepareWithSubquery('sq');
397+
const result = await db.with(sq).select().from(sq);
398+
```
399+
400+
> **Note**: Keep in mind, that if you need to select raw `sql` in a WITH subquery and reference that field in other queries, you must add an alias to it:
401+
402+
```typescript
403+
const sq = db
404+
.select({
405+
name: sql<string>`upper(${users.name})`.as('name'),
406+
})
407+
.from(users)
408+
.prepareWithSubquery('sq');
409+
410+
const result = await db
411+
.select({
412+
name: sq.name,
413+
})
414+
.from(sq);
415+
```
416+
417+
Otherwise, the field type will become `DrizzleTypeError` and you won't be able to reference it in other queries. If you ignore the type error and still try to reference the field, you will get a runtime error, because we cannot reference that field without an alias.
418+
378419
#### Select from subquery
379420

380421
```typescript
@@ -430,8 +471,8 @@ notIlike(column, value)
430471

431472
not(sqlExpression)
432473

433-
and(expressions: SQL[])
434-
or(expressions: SQL[])
474+
and(...expressions: SQL[])
475+
or(...expressions: SQL[])
435476

436477
```
437478

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

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResultSetHeader } from 'mysql2/promise';
22
import { SQLWrapper } from '~/sql';
3+
import { WithSubquery } from '~/subquery';
34
import { MySqlDialect } from './dialect';
45
import { MySqlDelete, MySqlInsertBuilder, MySqlSelectBuilder, MySqlUpdateBuilder } from './query-builders';
56
import { SelectFields } from './query-builders/select.types';
@@ -14,6 +15,18 @@ export class MySqlDatabase<TQueryResult extends QueryResultHKT, TSession extends
1415
readonly session: TSession,
1516
) {}
1617

18+
with(...queries: WithSubquery[]) {
19+
const self = this;
20+
21+
function select(): MySqlSelectBuilder<undefined>;
22+
function select<TSelection extends SelectFields>(fields: TSelection): MySqlSelectBuilder<TSelection>;
23+
function select(fields?: SelectFields): MySqlSelectBuilder<SelectFields | undefined> {
24+
return new MySqlSelectBuilder(fields ?? undefined, self.session, self.dialect, queries);
25+
}
26+
27+
return { select };
28+
}
29+
1730
select(): MySqlSelectBuilder<undefined>;
1831
select<TSelection extends SelectFields>(fields: TSelection): MySqlSelectBuilder<TSelection>;
1932
select(fields?: SelectFields): MySqlSelectBuilder<SelectFields | undefined> {

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

+31-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AnyColumn, Column } from '~/column';
22
import { MigrationMeta } from '~/migrator';
3-
import { Name, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql';
3+
import { Name, Query, SQL, sql, SQLSourceParam } from '~/sql';
44
import { Subquery, SubqueryConfig } from '~/subquery';
55
import { getTableName, Table } from '~/table';
66
import { UpdateSet } from '~/utils';
@@ -126,8 +126,10 @@ export class MySqlDialect {
126126
.map(({ field }, i) => {
127127
const chunk: SQLSourceParam[] = [];
128128

129-
if (field instanceof SQLResponse || field instanceof SQL) {
130-
const query = field instanceof SQLResponse ? field.sql : field;
129+
if (field instanceof SQL.Aliased && field.isSubquerySelectionField) {
130+
chunk.push(new Name(field.fieldAlias));
131+
} else if (field instanceof SQL.Aliased || field instanceof SQL) {
132+
const query = field instanceof SQL.Aliased ? field.sql : field;
131133

132134
if (isSingleTable) {
133135
chunk.push(
@@ -143,6 +145,10 @@ export class MySqlDialect {
143145
} else {
144146
chunk.push(query);
145147
}
148+
149+
if (field instanceof SQL.Aliased) {
150+
chunk.push(sql` as ${new Name(field.fieldAlias)}`);
151+
}
146152
} else if (field instanceof Column) {
147153
if (isSingleTable) {
148154
chunk.push(new Name(field.name));
@@ -163,25 +169,39 @@ export class MySqlDialect {
163169
}
164170

165171
buildSelectQuery(
166-
{ fieldsList: fields, where, table, joins, orderBy, groupBy, limit, offset }: MySqlSelectConfig,
172+
{ withList, fieldsList: fields, where, table, joins, orderBy, groupBy, limit, offset }: MySqlSelectConfig,
167173
): SQL {
168174
fields.forEach((f) => {
169175
let tableName: string;
170176
if (
171-
f.field instanceof Column && f.field.table !== table && !((tableName = getTableName(f.field.table)) in joins)
177+
f.field instanceof Column
178+
&& getTableName(f.field.table)
179+
!== (table instanceof Subquery ? table[SubqueryConfig].alias : getTableName(table))
180+
&& !((tableName = getTableName(f.field.table)) in joins)
172181
) {
173182
throw new Error(
174-
`Column "${f.field.name}" was selected, but its table "${tableName}" was not joined`,
183+
`Your "${
184+
f.path.join('->')
185+
}" field references a column "${tableName}"."${f.field.name}", but the table "${tableName}" is not part of the query! Did you forget to join it?`,
175186
);
176187
}
177188
});
178189

179190
const joinKeys = Object.keys(joins);
180191
const isSingleTable = joinKeys.length === 0;
181192

182-
const tableSql = table instanceof Subquery
183-
? sql`(${table})${isSingleTable ? undefined : new Name(table[SubqueryConfig].alias)}`
184-
: table;
193+
let withSql: SQL | undefined;
194+
if (withList.length) {
195+
const withSqlChunks = [sql`with `];
196+
withList.forEach((w, i) => {
197+
withSqlChunks.push(sql`${new Name(w[SubqueryConfig].alias)} as (${w[SubqueryConfig].sql})`);
198+
if (i < withList.length - 1) {
199+
withSqlChunks.push(sql`, `);
200+
}
201+
});
202+
withSqlChunks.push(sql` `);
203+
withSql = sql.fromList(withSqlChunks);
204+
}
185205

186206
const selection = this.buildSelection(fields, { isSingleTable });
187207

@@ -206,9 +226,7 @@ export class MySqlDialect {
206226
);
207227
} else {
208228
joinsArray.push(
209-
sql`${sql.raw(joinMeta.joinType)} join (${table[SubqueryConfig].sql}) ${new Name(
210-
tableAlias,
211-
)} on ${joinMeta.on}`,
229+
sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`,
212230
);
213231
}
214232
if (index < joinKeys.length - 1) {
@@ -246,7 +264,7 @@ export class MySqlDialect {
246264

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

249-
return sql`select ${selection} from ${tableSql}${joinsSql}${whereSql}${groupBySql}${orderBySql}${limitSql}${offsetSql}`;
267+
return sql`${withSql}select ${selection} from ${table}${joinsSql}${whereSql}${groupBySql}${orderBySql}${limitSql}${offsetSql}`;
250268
}
251269

252270
buildInsertQuery({ table, values, onConflict, returning }: MySqlInsertConfig): SQL {

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

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import { param, SQL, sql, SQLSourceParam } from '~/sql';
1+
import { bindIfParam } from '~/expressions';
2+
import { Placeholder, SQL, sql, SQLSourceParam, SQLWrapper } from '~/sql';
23
import { AnyMySqlColumn } from './columns/common';
34

45
export * from '~/expressions';
56

6-
export function concat(column: AnyMySqlColumn, value: string): SQL {
7-
return sql`${column} || ${param(value, column)}`;
7+
export function concat(column: AnyMySqlColumn | SQL.Aliased, value: string | Placeholder | SQLWrapper): SQL {
8+
return sql`${column} || ${bindIfParam(value, column)}`;
89
}
910

10-
export function substring(column: AnyMySqlColumn, { from, for: _for }: { from?: number; for?: number }): SQL {
11+
export function substring(
12+
column: AnyMySqlColumn | SQL.Aliased,
13+
{ from, for: _for }: { from?: number | Placeholder | SQLWrapper; for?: number | Placeholder | SQLWrapper },
14+
): SQL {
1115
const chunks: SQLSourceParam[] = [sql`substring(`, column];
1216
if (from !== undefined) {
13-
chunks.push(sql` from `, param(from, column));
17+
chunks.push(sql` from `, bindIfParam(from, column));
1418
}
1519
if (_for !== undefined) {
16-
chunks.push(sql` for `, param(_for, column));
20+
chunks.push(sql` for `, bindIfParam(_for, column));
1721
}
1822
chunks.push(sql`)`);
1923
return sql.fromList(chunks);

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

-13
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ export interface MySqlDeleteConfig {
2020
export interface MySqlDelete<
2121
TTable extends AnyMySqlTable,
2222
TQueryResult extends QueryResultHKT,
23-
TReturning = undefined,
2423
> extends QueryPromise<QueryResultKind<TQueryResult, never>> {}
2524

2625
export class MySqlDelete<
2726
TTable extends AnyMySqlTable,
2827
TQueryResult extends QueryResultHKT,
29-
TReturning = undefined,
3028
> extends QueryPromise<QueryResultKind<TQueryResult, never>> implements SQLWrapper {
3129
private config: MySqlDeleteConfig;
3230

@@ -46,17 +44,6 @@ export class MySqlDelete<
4644
return this;
4745
}
4846

49-
// returning(): Omit<MySqlDelete<TTable, InferModel<TTable>>, 'where' | 'returning'>;
50-
// returning<TSelectedFields extends SelectFields>(
51-
// fields: TSelectedFields,
52-
// ): Omit<MySqlDelete<TTable, SelectResultFields<TSelectedFields>>, 'where' | 'returning'>;
53-
// returning(
54-
// fields: SelectFields = this.config.table[MySqlTable.Symbol.Columns],
55-
// ): Omit<MySqlDelete<TTable, any>, 'where' | 'returning'> {
56-
// this.config.returning = orderSelectedFields(fields);
57-
// return this;
58-
// }
59-
6047
/** @internal */
6148
getSQL(): SQL {
6249
return this.dialect.buildDeleteQuery(this.config);

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

-17
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,6 @@ export class MySqlInsert<TTable extends AnyMySqlTable, TQueryResult extends Quer
7474
this.config = { table, values };
7575
}
7676

77-
// onDuplicateDoUpdate(
78-
// target:
79-
// | SQL<GetTableConfig<TTable, 'name'>>
80-
// | ((constraints: GetTableConflictConstraints<TTable>) => Check<GetTableConfig<TTable, 'name'>>),
81-
// set: MySqlUpdateSet<TTable>,
82-
// ): Pick<this, 'returning' | 'getQuery' | 'execute'> {
83-
// const setSql = this.dialect.buildUpdateSet<GetTableConfig<TTable, 'name'>>(this.config.table, set);
84-
85-
// if (target instanceof SQL) {
86-
// this.config.onConflict = sql<GetTableConfig<TTable, 'name'>>`${target} do update set ${setSql}`;
87-
// } else {
88-
// const targetSql = new Name(target(this.config.table[tableConflictConstraints]).name);
89-
// this.config.onConflict = sql`on constraint ${targetSql} do update set ${setSql}`;
90-
// }
91-
// return this;
92-
// }
93-
9477
onDuplicateKeyUpdate(config: {
9578
// target?: IndexColumn | IndexColumn[];
9679
set: MySqlUpdateSetSource<TTable>;

0 commit comments

Comments
 (0)