Skip to content

Commit

Permalink
Merge pull request #160 from drizzle-team/orm-129-too-complex-types-f…
Browse files Browse the repository at this point in the history
…or-multiple-joins

Improve joins, fix import paths
  • Loading branch information
dankochetov authored Feb 6, 2023
2 parents efdbdc9 + 8a420a1 commit cdd79a3
Show file tree
Hide file tree
Showing 73 changed files with 1,868 additions and 1,198 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ const upperCaseNames /* : { id: number; name: string }[] */ = await db.select(us
// You wouldn't BELIEVE how SMART the result type is! 😱
const allUsersWithCities = await db.select(users)
.fields({
user: {
id: users.id,
name: users.fullName,
id: users.id,
name: users.fullName,
city: {
id: cities.id,
name: cities.name,
},
cityId: cities.id,
cityName: cities.name,
})
.leftJoin(cities, eq(users.cityId, cities.id));

Expand All @@ -131,6 +131,7 @@ const deletedNames /* : { name: string }[] */ = await db.delete(users)
```

**See full docs for further reference:**

- [PostgreSQL](./drizzle-orm/src/pg-core/README.md)
- [MySQL](./drizzle-orm/src/mysql-core/README.md)
- [SQLite](./drizzle-orm/src/sqlite-core/README.md)
2 changes: 1 addition & 1 deletion changelogs/drizzle-orm-pg/0.16.0.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# drizzle-orm-pg 0.16.0

- Implemented [postgres.js](https://github.com/porsager/postgres) driver support ([docs](/drizzle-orm-pg/src/postgres.js/README.md))
- Implemented [postgres.js](https://github.com/porsager/postgres) driver support ([docs](/drizzle-orm-pg/src/postgres-js/README.md))
4 changes: 4 additions & 0 deletions changelogs/drizzle-orm/0.18.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Improved join result types for partial selects (refer to the [docs](/docs/joins.md) page for more information)
- Renamed import paths for Postgres.js and SQL.js drivers to avoid bundling errors:
- `drizzle-orm/postgres.js` -> `drizzle-orm/postgres-js`
- `drizzle-orm/sql.js` -> `drizzle-orm/sql-js`
247 changes: 247 additions & 0 deletions docs/joins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Drizzle ORM - Joins

As with other parts of Drizzle ORM, the joins syntax is a balance between the SQL-likeness and type safety.
Here's an example of how a common `1-to-many` relationship can be modelled.

```typescript
const users = pgTable('users', {
id: serial('id').primaryKey(),
firstName: text('first_name').notNull(),
lastName: text('last_name'),
cityId: int('city_id').references(() => cities.id),
});

const cities = pgTable('cities', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
```

Now, let's select all cities with all users that live in that city.

This is how you'd write it in raw SQL:

```sql
select
cities.id as city_id,
cities.name as city_name,
users.id as user_id,
users.first_name,
users.last_name
from cities
left join users on users.city_id = cities.id
```

And here's how to do the same with Drizzle ORM:

```typescript
const rows = await db.select(cities)
.fields({
cityId: cities.id,
cityName: cities.name,
userId: users.id,
firstName: users.firstName,
lastName: users.lastName,
})
.leftJoin(users, eq(users.cityId, cities.id));
```

`rows` will have the following type:

```typescript
{
cityId: number;
cityName: string;
userId: number | null;
firstName: string | null;
lastName: string | null;
}[]
```

As you can see, all the joined columns have been nullified. This might do the trick if you're using joins to form a single row of results, but in our case we have two separate entities in our row - a city and a user.
It might not be very convenient to check every field for nullability separately (or, even worse, just add an `!` after every field to "make compiler happy"). It would be much more useful if you could somehow run a single check
to verify that the user was joined and all of its fields are available.

**To achieve that, you can group the fields of a certain table in a nested object inside the `.fields()`:**

```typescript
const rows = await db.select(cities)
.fields({
cityId: cities.id,
cityName: cities.name,
user: {
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
},
})
.leftJoin(users, eq(users.cityId, cities.id));
```

In that case, the ORM will use dark TypeScript magic (as if it wasn't already) and figure out that you have a nested object where all the fields belong to the same table. So, the `rows` type will now look like this:

```typescript
{
cityId: number;
cityName: string;
user: {
id: number;
firstName: string;
lastName: string | null;
} | null;
}
```

This is much more convenient! Now, you can just do a single check for `row.user !== null`, and all the user fields will become available.

<hr />

Note that you can group any fields in a nested object however you like, but the single check optimization will only be applied to a certain nested object if all its fields belong to the same table.
So, for example, you can group the city fields, too:

```typescript
.fields({
city: {
id: cities.id,
name: cities.name,
},
user: {
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
},
})
```

And the result type will look like this:

```typescript
{
city: {
id: number;
name: string;
};
user: {
id: number;
firstName: string;
lastName: string | null;
} | null;
}
```

<hr />

If you just need all the fields from all the tables you're selecting and joining, you can skip the `.fields()` method altogether:

```typescript
const rows = await db.select(cities).leftJoin(users, eq(users.cityId, cities.id));
```

> **Note**: in this case, the DB table names will be used as the keys in the result object.
```typescript
{
cities: {
id: number;
name: string;
};
users: {
id: number;
firstName: string;
lastName: string | null;
cityId: number | null;
} | null;
}[]
```

<hr />

There are cases where you'd want to select all the fields from one table, but pick fields from others. In that case, instead of listing all the table fields, you can just pass a table:

```typescript
.fields({
cities, // shorthand for "cities: cities", the key can be anything
user: {
firstName: users.firstName,
},
})
```

```typescript
{
cities: {
id: number;
name: string;
};
user: {
firstName: string;
} | null;
}
```

<hr />

But what happens if you group columns from multiple tables in the same nested object? Nothing, really - they will still be all individually nullable, just grouped under the same object (as you might expect!):

```typescript
.fields({
id: cities.id,
cityAndUser: {
cityName: cities.name,
userId: users.id,
firstName: users.firstName,
lastName: users.lastName,
}
})
```

```typescript
{
id: number;
cityAndUser: {
cityName: string;
userId: number | null;
firstName: string | null;
lastName: string | null;
};
}
```

## Aggregating results

OK, so you have obtained all the cities and the users for every city. But what you **really** wanted is a **list** of users for every city, and what you currently have is an array of `city-user?` combinations. So, how do you transform it?
That's the neat part - you can do that however you'd like! No hand-holding here.

For example, one of the ways to do that would be `Array.reduce()`:

```typescript
import { InferModel } from 'drizzle-orm';

type User = InferModel<typeof users>;
type City = InferModel<typeof cities>;

const rows = await db.select(cities)
.fields({
city: cities,
user: users,
})
.leftJoin(users, eq(users.cityId, cities.id));

const result = rows.reduce<Record<number, { city: City; users: User[] }>>(
(acc, row) => {
const city = row.city;
const user = row.user;

if (!acc[city.id]) {
acc[city.id] = { city, users: [] };
}

if (user) {
acc[cityId].users.push(user);
}

return acc;
},
{},
);
```
2 changes: 1 addition & 1 deletion drizzle-orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-orm",
"version": "0.17.7",
"version": "0.18.0",
"description": "Drizzle ORM package for SQL databases",
"scripts": {
"build": "tsc && resolve-tspaths && cp ../README.md package.json dist/",
Expand Down
4 changes: 2 additions & 2 deletions drizzle-orm/src/aws-data-api/pg/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
RollbackTransactionCommandInput,
} from '@aws-sdk/client-rds-data';
import { Logger } from '~/logger';
import { SelectFieldsOrdered } from '~/operations';
import { PgDialect, PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/pg-core';
import { SelectFieldsOrdered } from '~/pg-core/query-builders/select.types';
import { fillPlaceholders, Query, QueryTypingsValue, SQL } from '~/sql';
import { mapResultRow } from '~/utils';
import { getValueFromDataApi, toValueParam } from '../common';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class AwsDataApiPreparedQuery<T extends PreparedQueryConfig> extends Prep

return result.records?.map((result) => {
const mappedResult = result.map((res) => getValueFromDataApi(res));
return mapResultRow<T['execute']>(fields, mappedResult);
return mapResultRow<T['execute']>(fields, mappedResult, this.joinsNotNullableMap);
});
}

Expand Down
8 changes: 4 additions & 4 deletions drizzle-orm/src/better-sqlite3/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Database, RunResult, Statement } from 'better-sqlite3';
import { Logger, NoopLogger } from '~/logger';
import { fillPlaceholders, Query } from '~/sql';
import { SQLiteSyncDialect } from '~/sqlite-core/dialect';
import { SelectFieldsOrdered } from '~/sqlite-core/operations';
import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types';
import {
PreparedQuery as PreparedQueryBase,
PreparedQueryConfig as PreparedQueryConfigBase,
Expand Down Expand Up @@ -34,7 +34,7 @@ export class BetterSQLiteSession extends SQLiteSession<'sync', RunResult> {

prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
query: Query,
fields?: SelectFieldsOrdered,
fields: SelectFieldsOrdered | undefined,
): PreparedQuery<T> {
const stmt = this.client.prepare(query.sql);
return new PreparedQuery(stmt, query.sql, query.params, this.logger, fields);
Expand Down Expand Up @@ -63,7 +63,7 @@ export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig>
all(placeholderValues?: Record<string, unknown>): T['all'] {
const { fields } = this;
if (fields) {
return this.values(placeholderValues).map((row) => mapResultRow(fields, row));
return this.values(placeholderValues).map((row) => mapResultRow(fields, row, this.joinsNotNullableMap));
}

const params = fillPlaceholders(this.params, placeholderValues ?? {});
Expand All @@ -82,7 +82,7 @@ export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig>

const value = this.stmt.raw().get(...params);

return mapResultRow(fields, value);
return mapResultRow(fields, value, this.joinsNotNullableMap);
}

values(placeholderValues?: Record<string, unknown>): T['values'] {
Expand Down
6 changes: 3 additions & 3 deletions drizzle-orm/src/bun-sqlite/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Database, Statement as BunStatement } from 'bun:sqlite';
import { Logger, NoopLogger } from '~/logger';
import { fillPlaceholders, Query } from '~/sql';
import { SQLiteSyncDialect } from '~/sqlite-core/dialect';
import { SelectFieldsOrdered } from '~/sqlite-core/operations';
import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types';
import {
PreparedQuery as PreparedQueryBase,
PreparedQueryConfig as PreparedQueryConfigBase,
Expand Down Expand Up @@ -66,7 +66,7 @@ export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig>
all(placeholderValues?: Record<string, unknown>): T['all'] {
const { fields } = this;
if (fields) {
return this.values(placeholderValues).map((row) => mapResultRow(fields, row));
return this.values(placeholderValues).map((row) => mapResultRow(fields, row, this.joinsNotNullableMap));
}

const params = fillPlaceholders(this.params, placeholderValues ?? {});
Expand All @@ -84,7 +84,7 @@ export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig>
return value;
}

return mapResultRow(fields, value);
return mapResultRow(fields, value, this.joinsNotNullableMap);
}

values(placeholderValues?: Record<string, unknown>): T['values'] {
Expand Down
6 changes: 4 additions & 2 deletions drizzle-orm/src/d1/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Logger, NoopLogger } from '~/logger';
import { fillPlaceholders, Query } from '~/sql';
import { SQLiteAsyncDialect } from '~/sqlite-core/dialect';
import { SelectFieldsOrdered } from '~/sqlite-core/operations';
import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types';
import {
PreparedQuery as PreparedQueryBase,
PreparedQueryConfig as PreparedQueryConfigBase,
Expand Down Expand Up @@ -62,7 +62,9 @@ export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig>
all(placeholderValues?: Record<string, unknown>): Promise<T['all']> {
const { fields } = this;
if (fields) {
return this.values(placeholderValues).then((values) => values.map((row) => mapResultRow(fields, row)));
return this.values(placeholderValues).then((values) =>
values.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap))
);
}

const params = fillPlaceholders(this.params, placeholderValues ?? {});
Expand Down
Loading

0 comments on commit cdd79a3

Please sign in to comment.