Skip to content

Commit

Permalink
Rename mysql mode config key, add changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
dankochetov committed Aug 6, 2023
1 parent 4768d12 commit 4e264be
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 33 deletions.
174 changes: 174 additions & 0 deletions changelogs/drizzle-orm/0.28.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
## Breaking changes

### Removed support for filtering by nested relations

Current example won't work in `0.28.0`:

```ts
const usersWithPosts = await db.query.users.findMany({
where: (table, { sql }) => (sql`json_array_length(${table.posts}) > 0`),
with: {
posts: true,
},
});
```

The `table` object in the `where` callback won't have fields from `with` and `extras`. We removed them to be able to build more efficient relational queries, which improved row reads and performance.

If you have used those fields in the `where` callback before, there are several workarounds:

1. Applying those filters manually on the code level after the rows are fetched;
2. Using the core API.

### Added Relational Queries `mode` config for `mysql2` driver

Drizzle relational queries always generate exactly one SQL statement to run on the database and it has certain caveats. To have best in class support for every database out there we've introduced modes.

Drizzle relational queries use lateral joins of subqueries under the hood and for now PlanetScale does not support them.

When using `mysql2` driver with regular MySQL database - you should specify mode: "default".
When using `mysql2` driver with PlanetScale - you need to specify mode: "planetscale".

```ts
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';

const connection = await mysql.createConnection({
uri: process.env.PLANETSCALE_DATABASE_URL,
});

const db = drizzle(connection, { schema, mode: 'planetscale' });
```

## Improved IntelliSense performance for large schemas

We've run the diagnostics on a database schema with 85 tables, 666 columns, 26 enums, 172 indexes and 133 foreign keys. We've optimized internal types which resulted in **430%** speed up in IntelliSense.

## Improved Relational Queries Permormance and Read Usage

In this release we've fully changed a way query is generated for Relational Queri API.

As a summary we've made current set of changes in query generation startegy:

1. Lateral Joins: In the new version we're utilizing lateral joins, denoted by the "LEFT JOIN LATERAL" clauses, to retrieve specific data from related tables efficiently For MySQL in PlanetScale and SQLite, we've used simple subquery selects, which improved a query plan and overall performance

2. Selective Data Retrieval: In the new version we're retrieving only the necessary data from tables. This targeted data retrieval reduces the amount of unnecessary information fetched, resulting in a smaller dataset to process and faster execution.

3. Reduced Aggregations: In the new version we've reduced the number of aggregation functions (e.g., COUNT, json_agg). By using json_build_array directly within the lateral joins, drizzle is aggregating the data in a more streamlined manner, leading to improved query performance.

4. Simplified Grouping: In the new version the GROUP BY clause is removed, as the lateral joins and subqueries already handle data aggregation more efficiently.

For this drizzle query

```ts
const items = await db.query.comments.findMany({
limit,
orderBy: comments.id,
with: {
user: {
columns: { name: true },
},
post: {
columns: { title: true },
with: {
user: {
columns: { name: true },
},
},
},
},
});
```

Query that Drizzle generates now

```sql
select "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content",
"comments_user"."data" as "user",
"comments_post"."data" as "post"
from "comments"
left join lateral (select json_build_array("comments_user"."name") as "data"
from (select *
from "users" "comments_user"
where "comments_user"."id" = "comments"."user_id"
limit 1) "comments_user") "comments_user" on true
left join lateral (select json_build_array("comments_post"."title", "comments_post_user"."data") as "data"
from (select *
from "posts" "comments_post"
where "comments_post"."id" = "comments"."post_id"
limit 1) "comments_post"
left join lateral (select json_build_array("comments_post_user"."name") as "data"
from (select *
from "users" "comments_post_user"
where "comments_post_user"."id" = "comments_post"."user_id"
limit 1) "comments_post_user") "comments_post_user"
on true) "comments_post" on true
order by "comments"."id"
limit 1
```

Query generated before:

```sql
SELECT "id",
"user_id",
"post_id",
"content",
"user"::JSON,
"post"::JSON
FROM
(SELECT "comments".*,
CASE
WHEN count("comments_post"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_post"."title", "comments_post"."user"::JSON))::text
END AS "post"
FROM
(SELECT "comments".*,
CASE
WHEN count("comments_user"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_user"."name"))::text
END AS "user"
FROM "comments"
LEFT JOIN
(SELECT "comments_user".*
FROM "users" "comments_user") "comments_user" ON "comments"."user_id" = "comments_user"."id"
GROUP BY "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content") "comments"
LEFT JOIN
(SELECT "comments_post".*
FROM
(SELECT "comments_post".*,
CASE
WHEN count("comments_post_user"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_post_user"."name"))
END AS "user"
FROM "posts" "comments_post"
LEFT JOIN
(SELECT "comments_post_user".*
FROM "users" "comments_post_user") "comments_post_user" ON "comments_post"."user_id" = "comments_post_user"."id"
GROUP BY "comments_post"."id") "comments_post") "comments_post" ON "comments"."post_id" = "comments_post"."id"
GROUP BY "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content",
"comments"."user") "comments"
LIMIT 1
```

## Possibility to insert rows with default values for all columns

You can now provide an empty object or an array of empty objects, and Drizzle will insert all defaults into the database.

```ts
// Insert 1 row with all defaults
await db.insert(usersTable).values({});

// Insert 2 rows with all defaults
await db.insert(usersTable).values([{}, {}]);
```
1 change: 1 addition & 0 deletions changelogs/drizzle-zod/0.5.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added compatibility with Drizzle 0.28.0
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.27.3",
"version": "0.28.0",
"description": "Drizzle ORM package for SQL databases",
"type": "module",
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions drizzle-orm/src/mysql-core/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { RelationalQueryBuilder } from './query-builders/query';
import type { SelectedFields } from './query-builders/select.types';
import type {
Mode,
MySqlSession,
MySqlTransaction,
MySqlTransactionConfig,
Expand Down Expand Up @@ -52,7 +53,7 @@ export class MySqlDatabase<
/** @internal */
readonly session: MySqlSession<any, any, any, any>,
schema: RelationalSchemaConfig<TSchema> | undefined,
protected readonly noLateralInRQB?: boolean,
protected readonly mode: Mode,
) {
this._ = schema
? { schema: schema.schema, tableNamesMap: schema.tableNamesMap }
Expand All @@ -69,7 +70,7 @@ export class MySqlDatabase<
columns,
dialect,
session,
this.noLateralInRQB,
this.mode,
);
}
}
Expand Down
15 changes: 8 additions & 7 deletions drizzle-orm/src/mysql-core/query-builders/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type SQL } from '~/sql';
import { type KnownKeysOnly } from '~/utils';
import { type MySqlDialect } from '../dialect';
import {
type Mode,
type MySqlSession,
type PreparedQueryConfig,
type PreparedQueryHKTBase,
Expand All @@ -33,7 +34,7 @@ export class RelationalQueryBuilder<
private tableConfig: TableRelationalConfig,
private dialect: MySqlDialect,
private session: MySqlSession,
private noLateral?: boolean,
private mode: Mode,
) {}

findMany<TConfig extends DBQueryConfig<'many', true, TSchema, TFields>>(
Expand All @@ -49,7 +50,7 @@ export class RelationalQueryBuilder<
this.session,
config ? (config as DBQueryConfig<'many', true>) : {},
'many',
this.noLateral,
this.mode,
);
}

Expand All @@ -66,7 +67,7 @@ export class RelationalQueryBuilder<
this.session,
config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 },
'first',
this.noLateral,
this.mode,
);
}
}
Expand All @@ -88,14 +89,14 @@ export class MySqlRelationalQuery<
private dialect: MySqlDialect,
private session: MySqlSession,
private config: DBQueryConfig<'many', true> | true,
private mode: 'many' | 'first',
private noLateral?: boolean,
private queryMode: 'many' | 'first',
private mode?: Mode,
) {
super();
}

prepare() {
const query = this.noLateral
const query = this.mode === 'planetscale'
? this.dialect.buildRelationalQueryWithoutLateralSubqueries({
fullSchema: this.fullSchema,
schema: this.schema,
Expand All @@ -121,7 +122,7 @@ export class MySqlRelationalQuery<
undefined,
(rawRows) => {
const rows = rawRows.map((row) => mapRelationalRow(this.schema, this.tableConfig, row, query.selection));
if (this.mode === 'first') {
if (this.queryMode === 'first') {
return rows[0] as TResult;
}
return rows as TResult;
Expand Down
8 changes: 5 additions & 3 deletions drizzle-orm/src/mysql-core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { MySqlDatabase } from './db';
import type { MySqlDialect } from './dialect';
import type { SelectedFieldsOrdered } from './query-builders/select.types';

export type Mode = 'default' | 'planetscale';

export interface QueryResultHKT {
readonly $brand: 'MySqlQueryRowHKT';
readonly row: unknown;
Expand Down Expand Up @@ -119,10 +121,10 @@ export abstract class MySqlTransaction<
dialect: MySqlDialect,
session: MySqlSession,
protected schema: RelationalSchemaConfig<TSchema> | undefined,
protected readonly nestedIndex = 0,
noLateralInRQB?: boolean,
protected readonly nestedIndex: number,
mode: Mode,
) {
super(dialect, session, schema, noLateralInRQB);
super(dialect, session, schema, mode);
}

rollback(): never {
Expand Down
25 changes: 17 additions & 8 deletions drizzle-orm/src/mysql2/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { entityKind } from '~/entity';
import type { Logger } from '~/logger';
import { DefaultLogger } from '~/logger';
import { MySqlDialect } from '~/mysql-core/dialect';
import type { Mode } from '~/mysql-core/session';
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from '~/relations';
import { type DrizzleConfig } from '~/utils';
import { DrizzleError } from '..';
import { MySqlDatabase } from '.';
import type { MySql2Client, MySql2PreparedQueryHKT, MySql2QueryResultHKT } from './session';
import { MySql2Session } from './session';
Expand All @@ -30,8 +32,9 @@ export class MySql2Driver {

createSession(
schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined,
mode: Mode,
): MySql2Session<Record<string, unknown>, TablesRelationalConfig> {
return new MySql2Session(this.client, this.dialect, schema, { logger: this.options.logger });
return new MySql2Session(this.client, this.dialect, schema, { logger: this.options.logger, mode });
}
}

Expand All @@ -41,11 +44,9 @@ export type MySql2Database<
TSchema extends Record<string, unknown> = Record<string, never>,
> = MySqlDatabase<MySql2QueryResultHKT, MySql2PreparedQueryHKT, TSchema>;

export interface MySql2DrizzleConfig<TSchema extends Record<string, unknown> = Record<string, never>>
extends DrizzleConfig<TSchema>
{
noLateralInRQB?: boolean;
}
export type MySql2DrizzleConfig<TSchema extends Record<string, unknown> = Record<string, never>> =
& Omit<DrizzleConfig<TSchema>, 'schema'>
& ({ schema: TSchema; mode: Mode } | { schema?: undefined; mode?: Mode });

export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
client: MySql2Client | CallbackConnection | CallbackPool,
Expand All @@ -64,6 +65,12 @@ export function drizzle<TSchema extends Record<string, unknown> = Record<string,

let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
if (config.schema) {
if (config.mode === undefined) {
throw new DrizzleError(
'You need to specify "mode": "planetscale" or "default" when providing a schema. Read more: https://orm.drizzle.team/docs/rqb#modes',
);
}

const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
Expand All @@ -75,9 +82,11 @@ export function drizzle<TSchema extends Record<string, unknown> = Record<string,
};
}

const mode = config.mode ?? 'default';

const driver = new MySql2Driver(client as MySql2Client, dialect, { logger });
const session = driver.createSession(schema);
return new MySqlDatabase(dialect, session, schema, config.noLateralInRQB) as MySql2Database<TSchema>;
const session = driver.createSession(schema, mode);
return new MySqlDatabase(dialect, session, schema, mode) as MySql2Database<TSchema>;
}

interface CallbackClient {
Expand Down
Loading

0 comments on commit 4e264be

Please sign in to comment.