Skip to content

Commit fa9aa1e

Browse files
Cache (#4447)
Co-authored-by: Cahid Arda Öz <cahidardaooz@gmail.com>
1 parent 3f4a6aa commit fa9aa1e

File tree

120 files changed

+4630
-227
lines changed

Some content is hidden

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

120 files changed

+4630
-227
lines changed

.github/workflows/release-feature-branch.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ jobs:
237237

238238
- uses: actions/setup-node@v4
239239
with:
240-
node-version: '18.18'
240+
node-version: '22'
241241
registry-url: 'https://registry.npmjs.org'
242242

243243
- uses: pnpm/action-setup@v3
@@ -334,7 +334,7 @@ jobs:
334334

335335
- uses: actions/setup-node@v4
336336
with:
337-
node-version: '18.18'
337+
node-version: '22'
338338
registry-url: 'https://registry.npmjs.org'
339339

340340
- uses: pnpm/action-setup@v3
@@ -415,4 +415,4 @@ jobs:
415415
working-directory: ${{ matrix.package }}
416416
shell: bash
417417
env:
418-
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
418+
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}

.github/workflows/release-latest.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ jobs:
360360

361361
- uses: actions/setup-node@v4
362362
with:
363-
node-version: '18.18'
363+
node-version: '22'
364364
registry-url: 'https://registry.npmjs.org'
365365

366366
- uses: pnpm/action-setup@v3

.github/workflows/unpublish-release-feature-branch.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121

2222
- uses: actions/setup-node@v4
2323
with:
24-
node-version: '18.18'
24+
node-version: '22'
2525
registry-url: 'https://registry.npmjs.org'
2626

2727
- name: Unpublish

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18.18
1+
22

changelogs/drizzle-orm/0.44.0.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
## Error handling
2+
3+
Starting from this version, we’ve introduced a new `DrizzleQueryError` that wraps all errors from database drivers and provides a set of useful information:
4+
5+
1. A proper stack trace to identify which exact `Drizzle` query failed
6+
2. The generated SQL string and its parameters
7+
3. The original stack trace from the driver that caused the DrizzleQueryError
8+
9+
## Drizzle `cache` module
10+
11+
Drizzle sends every query straight to your database by default. There are no hidden actions, no automatic caching or invalidation - you’ll always see exactly what runs. If you want caching, you must opt in.
12+
13+
By default, Drizzle uses a explicit caching strategy (i.e. `global: false`), so nothing is ever cached unless you ask. This prevents surprises or hidden performance traps in your application. Alternatively, you can flip on all caching (global: true) so that every select will look in cache first.
14+
15+
Out first native integration was built together with Upstash team and let you natively use `upstash` as a cache for your drizzle queries
16+
17+
```ts
18+
import { upstashCache } from "drizzle-orm/cache/upstash";
19+
import { drizzle } from "drizzle-orm/...";
20+
21+
const db = drizzle(process.env.DB_URL!, {
22+
cache: upstashCache({
23+
// 👇 Redis credentials (optional — can also be pulled from env vars)
24+
url: '<UPSTASH_URL>',
25+
token: '<UPSTASH_TOKEN>',
26+
// 👇 Enable caching for all queries by default (optional)
27+
global: true,
28+
// 👇 Default cache behavior (optional)
29+
config: { ex: 60 }
30+
})
31+
});
32+
```
33+
34+
You can also implement your own cache, as Drizzle exposes all the necessary APIs, such as get, put, mutate, etc.
35+
You can find full implementation details on the [website](https://orm.drizzle.team/docs/cache#custom-cache)
36+
37+
```ts
38+
import Keyv from "keyv";
39+
export class TestGlobalCache extends Cache {
40+
private globalTtl: number = 1000;
41+
// This object will be used to store which query keys were used
42+
// for a specific table, so we can later use it for invalidation.
43+
private usedTablesPerKey: Record<string, string[]> = {};
44+
constructor(private kv: Keyv = new Keyv()) {
45+
super();
46+
}
47+
// For the strategy, we have two options:
48+
// - 'explicit': The cache is used only when .$withCache() is added to a query.
49+
// - 'all': All queries are cached globally.
50+
// The default behavior is 'explicit'.
51+
override strategy(): "explicit" | "all" {
52+
return "all";
53+
}
54+
// This function accepts query and parameters that cached into key param,
55+
// allowing you to retrieve response values for this query from the cache.
56+
override async get(key: string): Promise<any[] | undefined> {
57+
...
58+
}
59+
// This function accepts several options to define how cached data will be stored:
60+
// - 'key': A hashed query and parameters.
61+
// - 'response': An array of values returned by Drizzle from the database.
62+
// - 'tables': An array of tables involved in the select queries. This information is needed for cache invalidation.
63+
//
64+
// For example, if a query uses the "users" and "posts" tables, you can store this information. Later, when the app executes
65+
// any mutation statements on these tables, you can remove the corresponding key from the cache.
66+
// If you're okay with eventual consistency for your queries, you can skip this option.
67+
override async put(
68+
key: string,
69+
response: any,
70+
tables: string[],
71+
config?: CacheConfig,
72+
): Promise<void> {
73+
...
74+
}
75+
// This function is called when insert, update, or delete statements are executed.
76+
// You can either skip this step or invalidate queries that used the affected tables.
77+
//
78+
// The function receives an object with two keys:
79+
// - 'tags': Used for queries labeled with a specific tag, allowing you to invalidate by that tag.
80+
// - 'tables': The actual tables affected by the insert, update, or delete statements,
81+
// helping you track which tables have changed since the last cache update.
82+
override async onMutate(params: {
83+
tags: string | string[];
84+
tables: string | string[] | Table<any> | Table<any>[];
85+
}): Promise<void> {
86+
...
87+
}
88+
}
89+
```
90+
91+
For more usage example you can check our [docs](https://orm.drizzle.team/docs/cache#cache-usage-examples)

drizzle-orm/package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-orm",
3-
"version": "0.43.1",
3+
"version": "0.44.0",
44
"description": "Drizzle ORM package for SQL databases",
55
"type": "module",
66
"scripts": {
@@ -64,14 +64,15 @@
6464
"better-sqlite3": ">=7",
6565
"bun-types": "*",
6666
"expo-sqlite": ">=14.0.0",
67-
"gel": ">=2",
6867
"knex": "*",
6968
"kysely": "*",
7069
"mysql2": ">=2",
7170
"pg": ">=8",
7271
"postgres": ">=3",
7372
"sql.js": ">=1",
74-
"sqlite3": ">=5"
73+
"sqlite3": ">=5",
74+
"gel": ">=2",
75+
"@upstash/redis": ">=1.34.7"
7576
},
7677
"peerDependenciesMeta": {
7778
"mysql2": {
@@ -157,6 +158,9 @@
157158
},
158159
"@prisma/client": {
159160
"optional": true
161+
},
162+
"@upstash/redis": {
163+
"optional": true
160164
}
161165
},
162166
"devDependencies": {
@@ -173,11 +177,12 @@
173177
"@planetscale/database": "^1.16.0",
174178
"@prisma/client": "5.14.0",
175179
"@tidbcloud/serverless": "^0.1.1",
176-
"@types/better-sqlite3": "^7.6.4",
180+
"@types/better-sqlite3": "^7.6.12",
177181
"@types/node": "^20.2.5",
178182
"@types/pg": "^8.10.1",
179183
"@types/react": "^18.2.45",
180184
"@types/sql.js": "^1.4.4",
185+
"@upstash/redis": "^1.34.3",
181186
"@vercel/postgres": "^0.8.0",
182187
"@xata.io/client": "^0.29.3",
183188
"better-sqlite3": "^11.9.1",

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { AwsDataApiSession } from './session.ts';
2121

2222
export interface PgDriverOptions {
2323
logger?: Logger;
24+
cache?: Cache;
2425
database: string;
2526
resourceArn: string;
2627
secretArn: string;
@@ -118,9 +119,13 @@ function construct<TSchema extends Record<string, unknown> = Record<string, neve
118119
};
119120
}
120121

121-
const session = new AwsDataApiSession(client, dialect, schema, { ...config, logger }, undefined);
122+
const session = new AwsDataApiSession(client, dialect, schema, { ...config, logger, cache: config.cache }, undefined);
122123
const db = new AwsDataApiPgDatabase(dialect, session, schema as any);
123124
(<any> db).$client = client;
125+
(<any> db).$cache = config.cache;
126+
if ((<any> db).$cache) {
127+
(<any> db).$cache['invalidate'] = config.cache?.onMutate;
128+
}
124129

125130
return db as any;
126131
}

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
ExecuteStatementCommand,
66
RollbackTransactionCommand,
77
} from '@aws-sdk/client-rds-data';
8+
import type { Cache } from '~/cache/core/cache.ts';
9+
import { NoopCache } from '~/cache/core/cache.ts';
10+
import type { WithCacheConfig } from '~/cache/core/types.ts';
811
import { entityKind } from '~/entity.ts';
912
import type { Logger } from '~/logger.ts';
1013
import {
@@ -33,17 +36,23 @@ export class AwsDataApiPreparedQuery<
3336

3437
constructor(
3538
private client: AwsDataApiClient,
36-
queryString: string,
39+
private queryString: string,
3740
private params: unknown[],
3841
private typings: QueryTypingsValue[],
3942
private options: AwsDataApiSessionOptions,
43+
cache: Cache,
44+
queryMetadata: {
45+
type: 'select' | 'update' | 'delete' | 'insert';
46+
tables: string[];
47+
} | undefined,
48+
cacheConfig: WithCacheConfig | undefined,
4049
private fields: SelectedFieldsOrdered | undefined,
4150
/** @internal */
4251
readonly transactionId: string | undefined,
4352
private _isResponseInArrayMode: boolean,
4453
private customResultMapper?: (rows: unknown[][]) => T['execute'],
4554
) {
46-
super({ sql: queryString, params });
55+
super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
4756
this.rawQuery = new ExecuteStatementCommand({
4857
sql: queryString,
4958
parameters: [],
@@ -108,7 +117,9 @@ export class AwsDataApiPreparedQuery<
108117

109118
this.options.logger?.logQuery(this.rawQuery.input.sql!, this.rawQuery.input.parameters);
110119

111-
const result = await this.client.send(this.rawQuery);
120+
const result = await this.queryWithCache(this.queryString, params, async () => {
121+
return await this.client.send(this.rawQuery);
122+
});
112123
const rows = result.records?.map((row) => {
113124
return row.map((field) => getValueFromDataApi(field));
114125
}) ?? [];
@@ -139,6 +150,7 @@ export class AwsDataApiPreparedQuery<
139150

140151
export interface AwsDataApiSessionOptions {
141152
logger?: Logger;
153+
cache?: Cache;
142154
database: string;
143155
resourceArn: string;
144156
secretArn: string;
@@ -158,6 +170,7 @@ export class AwsDataApiSession<
158170

159171
/** @internal */
160172
readonly rawQuery: AwsDataApiQueryBase;
173+
private cache: Cache;
161174

162175
constructor(
163176
/** @internal */
@@ -174,6 +187,7 @@ export class AwsDataApiSession<
174187
resourceArn: options.resourceArn,
175188
database: options.database,
176189
};
190+
this.cache = options.cache ?? new NoopCache();
177191
}
178192

179193
prepareQuery<
@@ -188,6 +202,8 @@ export class AwsDataApiSession<
188202
name: string | undefined,
189203
isResponseInArrayMode: boolean,
190204
customResultMapper?: (rows: unknown[][]) => T['execute'],
205+
queryMetadata?: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[] },
206+
cacheConfig?: WithCacheConfig,
191207
transactionId?: string,
192208
): AwsDataApiPreparedQuery<T> {
193209
return new AwsDataApiPreparedQuery(
@@ -196,6 +212,9 @@ export class AwsDataApiSession<
196212
query.params,
197213
query.typings ?? [],
198214
this.options,
215+
this.cache,
216+
queryMetadata,
217+
cacheConfig,
199218
fields,
200219
transactionId ?? this.transactionId,
201220
isResponseInArrayMode,
@@ -210,6 +229,8 @@ export class AwsDataApiSession<
210229
undefined,
211230
false,
212231
undefined,
232+
undefined,
233+
undefined,
213234
this.transactionId,
214235
).execute();
215236
}

drizzle-orm/src/better-sqlite3/driver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class BetterSQLite3Database<TSchema extends Record<string, unknown> = Rec
2929

3030
function construct<TSchema extends Record<string, unknown> = Record<string, never>>(
3131
client: Database,
32-
config: DrizzleConfig<TSchema> = {},
32+
config: Omit<DrizzleConfig<TSchema>, 'cache'> = {},
3333
): BetterSQLite3Database<TSchema> & {
3434
$client: Database;
3535
} {
@@ -57,6 +57,10 @@ function construct<TSchema extends Record<string, unknown> = Record<string, neve
5757
const session = new BetterSQLiteSession(client, dialect, schema, { logger });
5858
const db = new BetterSQLite3Database('sync', dialect, session, schema);
5959
(<any> db).$client = client;
60+
// (<any> db).$cache = config.cache;
61+
// if ((<any> db).$cache) {
62+
// (<any> db).$cache['invalidate'] = config.cache?.onMutate;
63+
// }
6064

6165
return db as any;
6266
}

0 commit comments

Comments
 (0)