prisma-sql 1.76.0 β†’ 1.76.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -2,97 +2,95 @@
2
2
 
3
3
  <img width="250" height="170" alt="image" src="https://github.com/user-attachments/assets/3f9233f2-5d5c-41e3-b1cd-ced7ce0b54c2" />
4
4
 
5
- Prerender Prisma SQL and run via postgres.js or better-sqlite3.
5
+ Prerender Prisma queries to SQL and execute them directly via `postgres.js` or `better-sqlite3`.
6
6
 
7
- **Same API. Same types. Just faster.**
7
+ **Same Prisma API. Same Prisma types. Lower read overhead.**
8
8
 
9
- ```typescript
9
+ ```ts
10
10
  import { PrismaClient } from '@prisma/client'
11
11
  import { speedExtension, type SpeedClient } from './generated/sql'
12
12
  import postgres from 'postgres'
13
13
 
14
- const sql = postgres(process.env.DATABASE_URL)
14
+ const sql = postgres(process.env.DATABASE_URL!)
15
15
  const basePrisma = new PrismaClient()
16
16
 
17
17
  export const prisma = basePrisma.$extends(
18
18
  speedExtension({ postgres: sql }),
19
19
  ) as SpeedClient<typeof basePrisma>
20
20
 
21
- // Regular queries - 2-7x faster
22
21
  const users = await prisma.user.findMany({
23
22
  where: { status: 'ACTIVE' },
24
23
  include: { posts: true },
25
24
  })
26
25
 
27
- // Batch queries - combine multiple queries into one database call
28
26
  const dashboard = await prisma.$batch((batch) => ({
29
27
  activeUsers: batch.user.count({ where: { status: 'ACTIVE' } }),
30
- recentPosts: batch.post.findMany({ take: 10 }),
31
- taskStats: batch.task.aggregate({ _count: true }),
28
+ recentPosts: batch.post.findMany({
29
+ take: 10,
30
+ orderBy: { createdAt: 'desc' },
31
+ }),
32
+ taskStats: batch.task.aggregate({
33
+ _count: true,
34
+ _avg: { estimatedHours: true },
35
+ }),
32
36
  }))
33
37
  ```
34
38
 
39
+ ## What it does
35
40
 
36
- ## What's New in v1.58.0
41
+ `prisma-sql` accelerates Prisma **read** queries by skipping Prisma's read execution path and running generated SQL directly through a database-native client.
37
42
 
38
- ### πŸš€ Batch Queries - Run Multiple Queries in a Single Database Round Trip
43
+ It keeps the Prisma client for:
39
44
 
40
- Instead of making separate database calls:
45
+ - schema and migrations
46
+ - generated types
47
+ - writes
48
+ - fallback for unsupported cases
41
49
 
42
- ```typescript
43
- const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
44
- const posts = await prisma.post.count()
45
- const stats = await prisma.task.aggregate({ _count: true })
46
- // ❌ 3 separate database round trips = slower
47
- ```
50
+ It accelerates:
48
51
 
49
- Batch them into ONE database call:
52
+ - `findMany`
53
+ - `findFirst`
54
+ - `findUnique`
55
+ - `count`
56
+ - `aggregate`
57
+ - `groupBy`
58
+ - PostgreSQL `$batch`
50
59
 
51
- ```typescript
52
- const results = await prisma.$batch((batch) => ({
53
- users: batch.user.findMany({ where: { status: 'ACTIVE' } }),
54
- posts: batch.post.count(),
55
- stats: batch.task.aggregate({ _count: true }),
56
- }))
57
- // βœ… 1 database round trip = 2-3x faster
58
- ```
60
+ ## Why use it
59
61
 
60
- **Result: 2.12x faster than sequential queries** (measured from real tests)
62
+ Prisma's DX is excellent, but read queries still pay runtime overhead for query-engine planning, validation, transformation, and result mapping.
61
63
 
62
- ---
64
+ `prisma-sql` moves that work out of the hot path:
63
65
 
64
- ## Why?
66
+ - builds SQL from Prisma-style query args
67
+ - can prebake hot queries at generate time
68
+ - executes via `postgres.js` or `better-sqlite3`
69
+ - maps results back to Prisma-like shapes
65
70
 
66
- Prisma's query engine adds overhead even in v7:
71
+ The goal is simple:
67
72
 
68
- - Query translation and validation layer
69
- - Type checking and transformation
70
- - Query planning and optimization
71
- - Result serialization and mapping
72
-
73
- This extension bypasses the engine for read queries and executes raw SQL directly via postgres.js or better-sqlite3.
74
-
75
- **Result:** Same API, same types, 2-7x faster reads.
73
+ - keep Prisma's developer experience
74
+ - cut read-path overhead
75
+ - stay compatible with existing Prisma code
76
76
 
77
77
  ## Installation
78
78
 
79
- **PostgreSQL:**
79
+ ### PostgreSQL
80
80
 
81
81
  ```bash
82
82
  npm install prisma-sql postgres
83
83
  ```
84
84
 
85
- **SQLite:**
85
+ ### SQLite
86
86
 
87
87
  ```bash
88
88
  npm install prisma-sql better-sqlite3
89
89
  ```
90
90
 
91
- ## Quick Start
91
+ ## Quick start
92
92
 
93
- ### Step 1: Add Generator to Schema
94
-
95
- Add the SQL generator to your `schema.prisma`:
93
+ ### 1) Add the generator
96
94
 
97
95
  ```prisma
98
96
  generator client {
@@ -111,62 +109,41 @@ model User {
111
109
  }
112
110
 
113
111
  model Post {
114
- id Int @id @default(autoincrement())
115
- title String
116
- authorId Int
117
- author User @relation(fields: [authorId], references: [id])
112
+ id Int @id @default(autoincrement())
113
+ title String
114
+ authorId Int
115
+ author User @relation(fields: [authorId], references: [id])
118
116
  }
119
117
  ```
120
118
 
121
- ### Step 2: Generate
119
+ ### 2) Generate
122
120
 
123
121
  ```bash
124
122
  npx prisma generate
125
123
  ```
126
124
 
127
- This creates `./generated/sql/index.ts` with pre-converted models and optimized queries.
125
+ This generates `./generated/sql/index.ts`.
128
126
 
129
- ### Step 3: Use the Extension
127
+ ### 3) Extend Prisma
130
128
 
131
- **PostgreSQL with TypeScript:**
129
+ ### PostgreSQL
132
130
 
133
- ```typescript
131
+ ```ts
134
132
  import { PrismaClient } from '@prisma/client'
135
133
  import { speedExtension, type SpeedClient } from './generated/sql'
136
134
  import postgres from 'postgres'
137
135
 
138
- const sql = postgres(process.env.DATABASE_URL)
136
+ const sql = postgres(process.env.DATABASE_URL!)
139
137
  const basePrisma = new PrismaClient()
140
138
 
141
- // Type the extended client properly
142
139
  export const prisma = basePrisma.$extends(
143
140
  speedExtension({ postgres: sql }),
144
141
  ) as SpeedClient<typeof basePrisma>
145
-
146
- // Regular queries - 2-7x faster
147
- const users = await prisma.user.findMany({
148
- where: { status: 'ACTIVE' },
149
- include: { posts: true },
150
- })
151
-
152
- // Batch queries - combine multiple queries into one database call
153
- const dashboard = await prisma.$batch((batch) => ({
154
- activeUsers: batch.user.count({ where: { status: 'ACTIVE' } }),
155
- recentPosts: batch.post.findMany({
156
- take: 10,
157
- orderBy: { createdAt: 'desc' },
158
- }),
159
- stats: batch.task.aggregate({
160
- _count: true,
161
- _avg: { estimatedHours: true },
162
- }),
163
- }))
164
- // dashboard.activeUsers, dashboard.recentPosts, dashboard.stats
165
142
  ```
166
143
 
167
- **SQLite:**
144
+ ### SQLite
168
145
 
169
- ```typescript
146
+ ```ts
170
147
  import { PrismaClient } from '@prisma/client'
171
148
  import { speedExtension, type SpeedClient } from './generated/sql'
172
149
  import Database from 'better-sqlite3'
@@ -177,22 +154,20 @@ const basePrisma = new PrismaClient()
177
154
  export const prisma = basePrisma.$extends(
178
155
  speedExtension({ sqlite: db }),
179
156
  ) as SpeedClient<typeof basePrisma>
180
-
181
- const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
182
157
  ```
183
158
 
184
- **With existing extensions:**
159
+ ### With existing Prisma extensions
160
+
161
+ Apply `speedExtension` last so it sees the final query surface.
185
162
 
186
- ```typescript
163
+ ```ts
187
164
  import { PrismaClient } from '@prisma/client'
188
165
  import { speedExtension, type SpeedClient } from './generated/sql'
189
- import { myCustomExtension } from './my-extension'
190
166
  import postgres from 'postgres'
191
167
 
192
- const sql = postgres(process.env.DATABASE_URL)
168
+ const sql = postgres(process.env.DATABASE_URL!)
193
169
  const basePrisma = new PrismaClient()
194
170
 
195
- // Chain extensions - speedExtension should be last
196
171
  const extendedPrisma = basePrisma
197
172
  .$extends(myCustomExtension)
198
173
  .$extends(anotherExtension)
@@ -202,346 +177,235 @@ export const prisma = extendedPrisma.$extends(
202
177
  ) as SpeedClient<typeof extendedPrisma>
203
178
  ```
204
179
 
205
- That's it! All your read queries are now 2-7x faster with zero runtime overhead.
180
+ ## Supported queries
206
181
 
207
- ## Performance
182
+ ### Accelerated
208
183
 
209
- Benchmarks from 137 E2E tests comparing identical queries:
210
-
211
- ### PostgreSQL Results (Highlights)
212
-
213
- | Query Type | Prisma v6 | Prisma v7 | This Extension | Speedup vs v7 |
214
- | ----------------------------- | ---------- | --------- | -------------- | ------------- |
215
- | Simple where | 0.40ms | 0.34ms | 0.17ms | **2.0x** ⚑ |
216
- | Complex conditions | 13.10ms | 6.90ms | 2.37ms | **2.9x** ⚑ |
217
- | With relations | 0.83ms | 0.72ms | 0.41ms | **1.8x** ⚑ |
218
- | Nested relations | 28.35ms | 14.34ms | 4.81ms | **3.0x** ⚑ |
219
- | Aggregations | 0.42ms | 0.44ms | 0.24ms | **1.8x** ⚑ |
220
- | Multi-field orderBy | 3.97ms | 2.38ms | 1.09ms | **2.2x** ⚑ |
221
- | **Batch queries (4 queries)** | **1.43ms** | **-** | **0.67ms** | **2.12x** ⚑ |
222
-
223
- **Overall:** 2.10x faster than Prisma v7, 2.39x faster than v6
224
-
225
- ### SQLite Results (Highlights)
226
-
227
- | Query Type | Prisma v6 | Prisma v7 | This Extension | Speedup vs v7 |
228
- | ------------------ | --------- | --------- | -------------- | ------------- |
229
- | Simple where | 0.45ms | 0.23ms | 0.03ms | **7.7x** ⚑ |
230
- | Complex conditions | 10.32ms | 3.87ms | 0.93ms | **4.2x** ⚑ |
231
- | Relation filters | 166.62ms | 128.44ms | 2.40ms | **53.5x** ⚑ |
232
- | Count queries | 0.17ms | 0.07ms | 0.01ms | **7.0x** ⚑ |
233
-
234
- **Overall:** 5.48x faster than Prisma v7, 7.51x faster than v6
235
-
236
- <details>
237
- <summary><b>View Full Benchmark Results (137 queries)</b></summary>
238
-
239
- ### POSTGRES - Complete Results
240
-
241
- | Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
242
- | ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
243
- | findMany basic | 0.45ms | 0.35ms | 0.18ms | 0.29ms | 2.53x | 1.74x |
244
- | findMany where = | 0.40ms | 0.34ms | 0.17ms | 0.24ms | 2.39x | 1.55x |
245
- | findMany where >= | 15.88ms | 8.03ms | 2.86ms | 6.18ms | 5.55x | 2.92x |
246
- | findMany where IN | 0.52ms | 0.52ms | 0.29ms | 0.38ms | 1.79x | 1.38x |
247
- | findMany where null | 0.23ms | 0.29ms | 0.10ms | 0.16ms | 2.20x | 2.56x |
248
- | findMany ILIKE | 0.24ms | 0.22ms | 0.18ms | 0.17ms | 1.29x | 0.99x |
249
- | findMany AND | 2.36ms | 1.20ms | 0.44ms | 1.42ms | 5.41x | 2.73x |
250
- | findMany OR | 13.10ms | 6.90ms | 2.37ms | 5.58ms | 5.53x | 2.93x |
251
- | findMany NOT | 0.51ms | 1.14ms | 0.30ms | 0.33ms | 1.73x | 3.74x |
252
- | findMany orderBy | 2.19ms | 2.31ms | 0.85ms | 0.72ms | 2.58x | 2.05x |
253
- | findMany pagination | 0.23ms | 0.28ms | 0.20ms | 0.19ms | 1.15x | 1.39x |
254
- | findMany select | 0.23ms | 0.22ms | 0.09ms | 0.13ms | 2.57x | 2.21x |
255
- | findMany relation some | 0.83ms | 0.72ms | 0.41ms | N/A | 2.04x | 1.72x |
256
- | findMany relation every | 0.70ms | 0.79ms | 0.47ms | N/A | 1.50x | 1.70x |
257
- | findMany relation none | 28.35ms | 14.34ms | 4.81ms | N/A | 5.90x | 2.75x |
258
- | findMany nested relation | 0.70ms | 0.72ms | 0.71ms | N/A | 0.98x | 1.36x |
259
- | findMany complex | 1.18ms | 1.19ms | 0.48ms | 0.64ms | 2.45x | 2.81x |
260
- | findFirst | 0.22ms | 0.25ms | 0.15ms | 0.20ms | 1.45x | 3.05x |
261
- | findFirst skip | 0.26ms | 0.32ms | 0.15ms | 0.23ms | 1.75x | 3.09x |
262
- | findUnique id | 0.20ms | 0.21ms | 0.13ms | 0.13ms | 1.52x | 2.53x |
263
- | findUnique email | 0.18ms | 0.19ms | 0.09ms | 0.12ms | 2.03x | 2.17x |
264
- | count | 0.11ms | 0.12ms | 0.04ms | 0.07ms | 2.95x | 2.50x |
265
- | count where | 0.43ms | 0.47ms | 0.26ms | 0.27ms | 1.62x | 1.90x |
266
- | aggregate count | 0.22ms | 0.24ms | 0.13ms | N/A | 1.63x | 1.51x |
267
- | aggregate sum/avg | 0.30ms | 0.32ms | 0.23ms | N/A | 1.32x | 1.35x |
268
- | aggregate where | 0.42ms | 0.44ms | 0.24ms | N/A | 1.75x | 1.84x |
269
- | aggregate min/max | 0.30ms | 0.32ms | 0.23ms | N/A | 1.29x | 1.32x |
270
- | aggregate complete | 0.36ms | 0.41ms | 0.27ms | N/A | 1.36x | 1.39x |
271
- | groupBy | 0.38ms | 0.41ms | 0.29ms | N/A | 1.33x | 1.36x |
272
- | groupBy count | 0.44ms | 0.42ms | 0.32ms | N/A | 1.36x | 1.37x |
273
- | groupBy multi | 0.53ms | 0.51ms | 0.37ms | N/A | 1.43x | 1.43x |
274
- | groupBy having | 0.52ms | 0.50ms | 0.40ms | N/A | 1.29x | 1.40x |
275
- | groupBy + where | 0.52ms | 0.49ms | 0.31ms | N/A | 1.65x | 1.88x |
276
- | groupBy aggregates | 0.50ms | 0.48ms | 0.38ms | N/A | 1.31x | 1.32x |
277
- | groupBy min/max | 0.49ms | 0.50ms | 0.38ms | N/A | 1.29x | 1.35x |
278
- | include posts | 2.53ms | 1.59ms | 1.85ms | N/A | 1.37x | 0.81x |
279
- | include profile | 0.47ms | 0.60ms | 0.21ms | N/A | 2.26x | 2.89x |
280
- | include 3 levels | 1.56ms | 1.64ms | 1.15ms | N/A | 1.36x | 1.33x |
281
- | include 4 levels | 1.80ms | 1.76ms | 0.99ms | N/A | 1.81x | 1.74x |
282
- | include + where | 1.21ms | 1.06ms | 1.47ms | N/A | 0.82x | 0.69x |
283
- | include + select nested | 1.23ms | 0.84ms | 1.43ms | N/A | 0.86x | 0.54x |
284
- | findMany startsWith | 0.21ms | 0.24ms | 0.14ms | 0.17ms | 1.46x | 1.73x |
285
- | findMany endsWith | 0.48ms | 0.38ms | 0.19ms | 0.28ms | 2.51x | 1.87x |
286
- | findMany NOT contains | 0.47ms | 0.40ms | 0.18ms | 0.25ms | 2.62x | 2.49x |
287
- | findMany LIKE | 0.19ms | 0.22ms | 0.09ms | 0.14ms | 2.19x | 2.61x |
288
- | findMany < | 25.94ms | 14.16ms | 4.16ms | 9.59ms | 6.23x | 3.12x |
289
- | findMany <= | 26.79ms | 13.87ms | 4.90ms | 9.73ms | 5.46x | 3.07x |
290
- | findMany > | 15.22ms | 7.55ms | 2.69ms | 5.74ms | 5.66x | 2.71x |
291
- | findMany NOT IN | 0.54ms | 0.42ms | 0.26ms | 0.36ms | 2.07x | 1.42x |
292
- | findMany isNot null | 0.52ms | 0.39ms | 0.19ms | 0.24ms | 2.75x | 2.21x |
293
- | orderBy multi-field | 3.97ms | 2.38ms | 1.09ms | 1.54ms | 3.65x | 3.71x |
294
- | distinct status | 8.72ms | 7.84ms | 2.15ms | N/A | 4.06x | 4.68x |
295
- | distinct multi | 11.71ms | 11.09ms | 2.09ms | N/A | 5.61x | 5.30x |
296
- | cursor pagination | 0.29ms | 0.34ms | 0.23ms | N/A | 1.25x | 1.62x |
297
- | select + include | 0.89ms | 0.70ms | 0.17ms | N/A | 5.19x | 3.46x |
298
- | \_count relation | 0.71ms | 0.66ms | 0.55ms | N/A | 1.29x | 1.20x |
299
- | \_count multi-relation | 0.24ms | 0.29ms | 0.16ms | N/A | 1.52x | 1.90x |
300
- | ILIKE special chars | 0.22ms | 0.26ms | 0.14ms | N/A | 1.54x | 1.76x |
301
- | LIKE case sensitive | 0.19ms | 0.22ms | 0.12ms | N/A | 1.62x | 1.85x |
302
-
303
- ### SQLITE - Complete Results
304
-
305
- | Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
306
- | ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
307
- | findMany basic | 0.44ms | 0.27ms | 0.04ms | 0.17ms | 9.59x | 5.47x |
308
- | findMany where = | 0.45ms | 0.23ms | 0.03ms | 0.10ms | 14.14x | 6.25x |
309
- | findMany where >= | 12.72ms | 4.70ms | 1.02ms | 2.09ms | 12.51x | 4.16x |
310
- | findMany where IN | 0.40ms | 0.28ms | 0.04ms | 0.10ms | 10.35x | 6.55x |
311
- | findMany where null | 0.15ms | 0.19ms | 0.01ms | 0.06ms | 10.97x | 12.56x |
312
- | findMany LIKE | 0.15ms | 0.17ms | 0.02ms | 0.06ms | 8.64x | 9.41x |
313
- | findMany AND | 1.49ms | 0.95ms | 0.26ms | 0.43ms | 5.75x | 3.45x |
314
- | findMany OR | 10.32ms | 3.87ms | 0.93ms | 1.85ms | 11.09x | 3.64x |
315
- | findMany NOT | 0.42ms | 0.28ms | 0.03ms | 0.09ms | 12.59x | 7.05x |
316
- | findMany orderBy | 2.24ms | 1.92ms | 1.76ms | 1.81ms | 1.27x | 1.11x |
317
- | findMany pagination | 0.13ms | 0.15ms | 0.02ms | 0.06ms | 5.69x | 6.24x |
318
- | findMany select | 0.15ms | 0.11ms | 0.02ms | 0.04ms | 9.50x | 6.22x |
319
- | findMany relation some | 4.50ms | 0.56ms | 0.40ms | N/A | 11.15x | 1.32x |
320
- | findMany relation every | 9.53ms | 9.54ms | 6.38ms | N/A | 1.49x | 1.45x |
321
- | findMany relation none | 166.62ms | 128.44ms | 2.40ms | N/A | 69.43x | 49.51x |
322
- | findMany nested relation | 1.00ms | 0.51ms | 0.31ms | N/A | 3.28x | 1.70x |
323
- | findMany complex | 0.79ms | 0.83ms | 0.43ms | 0.48ms | 1.84x | 1.74x |
324
- | findFirst | 0.16ms | 0.17ms | 0.01ms | 0.06ms | 11.57x | 12.00x |
325
- | findFirst skip | 0.25ms | 0.23ms | 0.03ms | 0.08ms | 8.62x | 13.31x |
326
- | findUnique id | 0.12ms | 0.15ms | 0.01ms | 0.05ms | 9.92x | 11.62x |
327
- | findUnique email | 0.12ms | 0.15ms | 0.01ms | 0.05ms | 8.73x | 11.43x |
328
- | count | 0.17ms | 0.07ms | 0.01ms | 0.02ms | 13.33x | 10.73x |
329
- | count where | 0.28ms | 0.28ms | 0.16ms | 0.17ms | 1.77x | 1.85x |
330
- | aggregate count | 0.15ms | 0.11ms | 0.01ms | N/A | 14.80x | 9.69x |
331
- | aggregate sum/avg | 0.27ms | 0.25ms | 0.15ms | N/A | 1.82x | 1.62x |
332
- | aggregate where | 0.25ms | 0.26ms | 0.15ms | N/A | 1.66x | 1.73x |
333
- | aggregate min/max | 0.28ms | 0.25ms | 0.16ms | N/A | 1.80x | 1.52x |
334
- | aggregate complete | 0.39ms | 0.34ms | 0.21ms | N/A | 1.81x | 1.61x |
335
- | groupBy | 0.56ms | 0.53ms | 0.44ms | N/A | 1.28x | 1.22x |
336
- | groupBy count | 0.57ms | 0.57ms | 0.45ms | N/A | 1.28x | 1.27x |
337
- | groupBy multi | 1.14ms | 1.08ms | 0.95ms | N/A | 1.20x | 1.17x |
338
- | groupBy having | 0.64ms | 0.64ms | 0.47ms | N/A | 1.37x | 1.32x |
339
- | groupBy + where | 0.31ms | 0.33ms | 0.18ms | N/A | 1.70x | 1.84x |
340
- | groupBy aggregates | 0.71ms | 0.66ms | 0.54ms | N/A | 1.32x | 1.23x |
341
- | groupBy min/max | 0.72ms | 0.70ms | 0.56ms | N/A | 1.29x | 1.25x |
342
- | include posts | 1.88ms | 1.13ms | 0.90ms | N/A | 2.10x | 1.12x |
343
- | include profile | 0.32ms | 0.41ms | 0.05ms | N/A | 6.17x | 6.48x |
344
- | include 3 levels | 1.11ms | 1.08ms | 0.63ms | N/A | 1.77x | 1.86x |
345
- | include 4 levels | 1.15ms | 1.10ms | 0.42ms | N/A | 2.72x | 2.70x |
346
- | include + where | 0.77ms | 0.72ms | 0.11ms | N/A | 7.13x | 6.99x |
347
- | include + select nested | 0.73ms | 0.53ms | 0.83ms | N/A | 0.88x | 0.64x |
348
- | findMany startsWith | 0.15ms | 0.16ms | 0.02ms | 0.06ms | 6.73x | 6.98x |
349
- | findMany endsWith | 0.43ms | 0.26ms | 0.04ms | 0.15ms | 9.74x | 5.22x |
350
- | findMany NOT contains | 0.45ms | 0.28ms | 0.04ms | 0.11ms | 11.65x | 6.57x |
351
- | findMany < | 21.60ms | 8.27ms | 1.88ms | 4.07ms | 11.49x | 4.24x |
352
- | findMany <= | 22.34ms | 8.50ms | 1.97ms | 4.40ms | 11.36x | 4.25x |
353
- | findMany > | 11.54ms | 4.33ms | 0.94ms | 2.13ms | 12.22x | 4.17x |
354
- | findMany NOT IN | 0.42ms | 0.28ms | 0.04ms | 0.12ms | 10.40x | 6.23x |
355
- | findMany isNot null | 0.45ms | 0.27ms | 0.03ms | 0.11ms | 13.03x | 6.91x |
356
- | orderBy multi-field | 0.66ms | 0.59ms | 0.37ms | 0.43ms | 1.78x | 1.67x |
357
- | distinct status | 10.61ms | 6.79ms | 4.09ms | N/A | 2.59x | 1.53x |
358
- | distinct multi | 11.66ms | 7.12ms | 5.09ms | N/A | 2.29x | 1.34x |
359
- | cursor pagination | 0.21ms | 0.26ms | 0.04ms | N/A | 4.60x | 5.52x |
360
- | select + include | 0.51ms | 0.43ms | 0.04ms | N/A | 13.19x | 11.31x |
361
- | \_count relation | 0.62ms | 0.46ms | 0.32ms | N/A | 1.93x | 1.44x |
362
- | \_count multi-relation | 0.14ms | 0.17ms | 0.04ms | N/A | 3.22x | 4.09x |
363
-
364
- </details>
365
-
366
- > **Note:** Benchmarks run on MacBook Pro M1 with PostgreSQL 15 and SQLite 3.43. Results vary based on database config, indexes, query complexity, and hardware. Run your own benchmarks for accurate measurements.
367
-
368
- ## What Gets Faster
369
-
370
- **Accelerated (via raw SQL):**
371
-
372
- - βœ… `findMany`, `findFirst`, `findUnique`
373
- - βœ… `count`
374
- - βœ… `aggregate` (\_count, \_sum, \_avg, \_min, \_max)
375
- - βœ… `groupBy` with having clauses
376
- - βœ… `$batch` - multiple queries in one database round trip
377
-
378
- **Unchanged (still uses Prisma):**
379
-
380
- - `create`, `update`, `delete`, `upsert`
381
- - `createMany`, `updateMany`, `deleteMany`
382
- - Transactions (`$transaction`)
383
- - Middleware
184
+ - `findMany`
185
+ - `findFirst`
186
+ - `findUnique`
187
+ - `count`
188
+ - `aggregate`
189
+ - `groupBy`
190
+ - `$batch` for PostgreSQL
384
191
 
385
- ## Configuration
192
+ ### Not accelerated
386
193
 
387
- ### Debug Mode
194
+ These continue to run through Prisma:
388
195
 
389
- See generated SQL for every query:
196
+ - `create`
197
+ - `update`
198
+ - `delete`
199
+ - `upsert`
200
+ - `createMany`
201
+ - `updateMany`
202
+ - `deleteMany`
390
203
 
391
- ```typescript
392
- import { speedExtension, type SpeedClient } from './generated/sql'
204
+ ### Fallback behavior
393
205
 
394
- const prisma = new PrismaClient().$extends(
395
- speedExtension({
396
- postgres: sql,
397
- debug: true, // Logs SQL for every query
398
- }),
399
- ) as SpeedClient<typeof PrismaClient>
400
- ```
206
+ If a query shape is unsupported or cannot be accelerated safely, the extension falls back to Prisma instead of returning incorrect results.
401
207
 
402
- ### Performance Monitoring
208
+ Enable `debug: true` to see generated SQL and fallback behavior.
403
209
 
404
- Track query performance:
210
+ ## Features
405
211
 
406
- ```typescript
407
- import { speedExtension, type SpeedClient } from './generated/sql'
212
+ ### 1) Runtime SQL generation
408
213
 
409
- const prisma = new PrismaClient().$extends(
410
- speedExtension({
411
- postgres: sql,
412
- onQuery: (info) => {
413
- console.log(`${info.model}.${info.method}: ${info.duration}ms`)
414
- console.log(`Prebaked: ${info.prebaked}`)
415
-
416
- if (info.duration > 100) {
417
- logger.warn('Slow query', {
418
- model: info.model,
419
- method: info.method,
420
- sql: info.sql,
421
- })
422
- }
423
- },
424
- }),
425
- ) as SpeedClient<typeof PrismaClient>
214
+ Any supported read query can be converted from Prisma args into SQL at runtime.
215
+
216
+ ```ts
217
+ const users = await prisma.user.findMany({
218
+ where: {
219
+ status: 'ACTIVE',
220
+ email: { contains: '@example.com' },
221
+ },
222
+ orderBy: { createdAt: 'desc' },
223
+ take: 20,
224
+ })
426
225
  ```
427
226
 
428
- The `onQuery` callback receives:
227
+ ### 2) Prebaked hot queries with `@optimize`
429
228
 
430
- ```typescript
431
- interface QueryInfo {
432
- model: string // "User" or "_batch" for batch queries
433
- method: string // "findMany", "batch", etc
434
- sql: string // The executed SQL
435
- params: unknown[] // SQL parameters
436
- duration: number // Query duration in ms
437
- prebaked: boolean // true if using @optimize directive
229
+ For the hottest query shapes, you can prebake SQL at generate time.
230
+
231
+ ```prisma
232
+ /// @optimize {
233
+ /// "method": "findMany",
234
+ /// "query": {
235
+ /// "where": { "status": "ACTIVE" },
236
+ /// "orderBy": { "createdAt": "desc" },
237
+ /// "skip": "$skip",
238
+ /// "take": "$take"
239
+ /// }
240
+ /// }
241
+ model User {
242
+ id Int @id @default(autoincrement())
243
+ email String @unique
244
+ status String
245
+ createdAt DateTime @default(now())
438
246
  }
439
247
  ```
440
248
 
441
- ## Supported Queries
249
+ At runtime:
250
+
251
+ - matching query shape β†’ prebaked SQL
252
+ - non-matching query shape β†’ runtime SQL generation
253
+
254
+ ### 3) PostgreSQL batch queries
255
+
256
+ `$batch` combines multiple independent read queries into one round trip.
257
+
258
+ ```ts
259
+ const results = await prisma.$batch((batch) => ({
260
+ users: batch.user.findMany({ where: { status: 'ACTIVE' } }),
261
+ posts: batch.post.count(),
262
+ stats: batch.task.aggregate({ _count: true }),
263
+ }))
264
+ ```
265
+
266
+ ### 4) Include and relation reduction
267
+
268
+ For supported include trees, `prisma-sql` can execute flat SQL and reduce rows back into Prisma-like nested results.
269
+
270
+ ### 5) Aggregate result type handling
271
+
272
+ Aggregates are mapped back to Prisma-style value types instead of flattening everything into strings or plain numbers.
273
+
274
+ That includes preserving types like:
275
+
276
+ - `Decimal`
277
+ - `BigInt`
278
+ - `DateTime`
279
+ - `_count`
280
+
281
+ ## Query examples
442
282
 
443
283
  ### Filters
444
284
 
445
- ```typescript
446
- // Comparisons
285
+ ```ts
447
286
  { age: { gt: 18, lte: 65 } }
448
287
  { status: { in: ['ACTIVE', 'PENDING'] } }
449
288
  { status: { notIn: ['DELETED'] } }
450
289
 
451
- // String operations
452
290
  { email: { contains: '@example.com' } }
453
291
  { email: { startsWith: 'user' } }
454
292
  { email: { endsWith: '.com' } }
455
293
  { email: { contains: 'EXAMPLE', mode: 'insensitive' } }
456
294
 
457
- // Boolean logic
458
295
  { AND: [{ status: 'ACTIVE' }, { verified: true }] }
459
296
  { OR: [{ role: 'ADMIN' }, { role: 'MODERATOR' }] }
460
297
  { NOT: { status: 'DELETED' } }
461
298
 
462
- // Null checks
463
299
  { deletedAt: null }
464
300
  { deletedAt: { not: null } }
465
301
  ```
466
302
 
467
303
  ### Relations
468
304
 
469
- ```typescript
470
- // Include relations
305
+ ```ts
471
306
  {
472
307
  include: {
473
308
  posts: true,
474
- profile: true
309
+ profile: true,
475
310
  }
476
311
  }
312
+ ```
477
313
 
478
- // Nested includes with filters
314
+ ```ts
479
315
  {
480
316
  include: {
481
317
  posts: {
482
- include: { comments: true },
483
318
  where: { published: true },
484
319
  orderBy: { createdAt: 'desc' },
485
- take: 5
486
- }
487
- }
320
+ take: 5,
321
+ include: {
322
+ comments: true,
323
+ },
324
+ },
325
+ },
488
326
  }
327
+ ```
489
328
 
490
- // Relation filters
329
+ ```ts
491
330
  {
492
331
  where: {
493
- posts: { some: { published: true } }
494
- }
332
+ posts: { some: { published: true } },
333
+ },
495
334
  }
335
+ ```
496
336
 
337
+ ```ts
497
338
  {
498
339
  where: {
499
- posts: { every: { published: true } }
500
- }
340
+ posts: { every: { published: true } },
341
+ },
501
342
  }
343
+ ```
502
344
 
345
+ ```ts
503
346
  {
504
347
  where: {
505
- posts: { none: { published: false } }
506
- }
348
+ posts: { none: { published: false } },
349
+ },
507
350
  }
508
351
  ```
509
352
 
510
- ### Pagination & Ordering
353
+ ### Pagination and ordering
511
354
 
512
- ```typescript
513
- // Basic pagination
355
+ ```ts
514
356
  {
515
357
  take: 10,
516
358
  skip: 20,
517
- orderBy: { createdAt: 'desc' }
359
+ orderBy: { createdAt: 'desc' },
518
360
  }
361
+ ```
519
362
 
520
- // Cursor-based pagination
363
+ ```ts
521
364
  {
522
365
  cursor: { id: 100 },
523
- take: 10,
524
366
  skip: 1,
525
- orderBy: { id: 'asc' }
367
+ take: 10,
368
+ orderBy: { id: 'asc' },
526
369
  }
370
+ ```
527
371
 
528
- // Multi-field ordering
372
+ ```ts
529
373
  {
530
374
  orderBy: [
531
375
  { status: 'asc' },
532
376
  { priority: 'desc' },
533
- { createdAt: 'desc' }
534
- ]
377
+ { createdAt: 'desc' },
378
+ ],
535
379
  }
536
380
  ```
537
381
 
538
- ### Aggregations
382
+ ### Composite cursor pagination
539
383
 
540
- ```typescript
541
- // Count
542
- await prisma.user.count({ where: { status: 'ACTIVE' } })
384
+ For composite cursors, use an `orderBy` that starts with the cursor fields in the same order.
543
385
 
544
- // Aggregate
386
+ ```ts
387
+ {
388
+ cursor: { tenantId: 10, id: 500 },
389
+ skip: 1,
390
+ take: 20,
391
+ orderBy: [
392
+ { tenantId: 'asc' },
393
+ { id: 'asc' },
394
+ ],
395
+ }
396
+ ```
397
+
398
+ This matches keyset pagination expectations and avoids unstable page boundaries.
399
+
400
+ ### Aggregates
401
+
402
+ ```ts
403
+ await prisma.user.count({
404
+ where: { status: 'ACTIVE' },
405
+ })
406
+ ```
407
+
408
+ ```ts
545
409
  await prisma.task.aggregate({
546
410
  where: { status: 'DONE' },
547
411
  _count: { _all: true },
@@ -550,8 +414,9 @@ await prisma.task.aggregate({
550
414
  _min: { startedAt: true },
551
415
  _max: { completedAt: true },
552
416
  })
417
+ ```
553
418
 
554
- // Group by
419
+ ```ts
555
420
  await prisma.task.groupBy({
556
421
  by: ['status', 'priority'],
557
422
  _count: { _all: true },
@@ -564,393 +429,364 @@ await prisma.task.groupBy({
564
429
  })
565
430
  ```
566
431
 
567
- ## Advanced Usage
432
+ ## Cardinality planner
568
433
 
569
- ### Batch Queries ($batch)
434
+ The cardinality planner is the piece that decides how relation-heavy reads should be executed for best performance.
570
435
 
571
- Execute multiple queries in a single database round trip. Perfect for dashboard queries, aggregations, and any scenario where you need multiple pieces of data at once.
436
+ In practice, it helps choose between strategies such as:
572
437
 
573
- **How It Works:**
438
+ - direct joins
439
+ - lateral/subquery-style fetches
440
+ - flat row expansion + reducer
441
+ - segmented follow-up loading for high fan-out relations
574
442
 
575
- Instead of this (3 database round trips):
443
+ This matters because the fastest strategy depends on **cardinality**, not just query shape.
576
444
 
577
- ```
578
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” Query 1 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
579
- β”‚ App β”‚ ──────────────────▢│ Database β”‚
580
- β”‚ β”‚ ◀──────────────────│ β”‚
581
- β”‚ β”‚ Query 2 β”‚ β”‚
582
- β”‚ β”‚ ──────────────────▢│ β”‚
583
- β”‚ β”‚ ◀──────────────────│ β”‚
584
- β”‚ β”‚ Query 3 β”‚ β”‚
585
- β”‚ β”‚ ──────────────────▢│ β”‚
586
- β”‚ β”‚ ◀──────────────────│ β”‚
587
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
588
- Total: ~3ms (3 round trips Γ— ~1ms each)
589
- ```
445
+ A `profile` include behaves very differently from a `posts.comments.likes` include.
590
446
 
591
- You get this (1 database round trip):
447
+ ### Why it matters
592
448
 
593
- ```
594
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” Combined Query β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
595
- β”‚ App β”‚ ──────────────────▢│ Database β”‚
596
- β”‚ β”‚ ◀──────────────────│ β”‚
597
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
598
- Total: ~1ms (1 round trip with all queries)
599
- ```
449
+ A naive join strategy can explode row counts:
600
450
 
601
- **Real-world example - Dashboard Query:**
451
+ - `User -> Profile` is usually low fan-out
452
+ - `User -> Posts -> Comments` can multiply rows aggressively
453
+ - `Organization -> Users -> Sessions -> Events` can become huge very quickly
602
454
 
603
- ```typescript
604
- const dashboard = await prisma.$batch((batch) => ({
605
- // Organization stats
606
- totalOrgs: batch.organization.count(),
607
- activeOrgs: batch.organization.count({
608
- where: { status: 'ACTIVE' },
609
- }),
455
+ The planner tries to keep read amplification under control.
610
456
 
611
- // User stats
612
- totalUsers: batch.user.count(),
613
- activeUsers: batch.user.count({
614
- where: { status: 'ACTIVE' },
615
- }),
457
+ ### Best setup for the planner
616
458
 
617
- // Recent activity
618
- recentProjects: batch.project.findMany({
619
- take: 5,
620
- orderBy: { createdAt: 'desc' },
621
- include: { organization: true },
622
- }),
459
+ To get the best results, prepare your schema and indexes so the planner can make good choices.
623
460
 
624
- // Aggregations
625
- taskStats: batch.task.aggregate({
626
- _count: true,
627
- _avg: { estimatedHours: true },
628
- where: { status: 'IN_PROGRESS' },
629
- }),
630
- }))
461
+ #### 1) Model real cardinality accurately
631
462
 
632
- // All results available immediately
633
- console.log(`Active users: ${dashboard.activeUsers}`)
634
- console.log(`Recent projects:`, dashboard.recentProjects)
635
- console.log(`Avg task hours:`, dashboard.taskStats._avg.estimatedHours)
636
- ```
463
+ Use correct relation fields and uniqueness constraints.
637
464
 
638
- **Performance - From Real Tests:**
465
+ Good examples:
639
466
 
640
- Simple queries (4 queries):
467
+ ```prisma
468
+ model User {
469
+ id Int @id @default(autoincrement())
470
+ profile Profile?
471
+ posts Post[]
472
+ }
641
473
 
642
- - Sequential: 1.43ms (0.36ms per query)
643
- - Batch: 0.67ms (0.17ms per query)
644
- - **Speedup: 2.12x** ⚑
474
+ model Profile {
475
+ id Int @id @default(autoincrement())
476
+ userId Int @unique
477
+ user User @relation(fields: [userId], references: [id])
478
+ }
645
479
 
646
- Complex dashboard (8 queries with relations):
480
+ model Post {
481
+ id Int @id @default(autoincrement())
482
+ authorId Int
483
+ author User @relation(fields: [authorId], references: [id])
647
484
 
648
- - Sequential: 9.90ms
649
- - Batch: 6.07ms
650
- - **Speedup: 1.63x** ⚑
485
+ @@index([authorId])
486
+ }
487
+ ```
651
488
 
652
- Stress test (45 queries):
489
+ Why this helps:
653
490
 
654
- - Build: 1.48ms (0.03ms per query)
655
- - Execute: 3.13ms
656
- - Parse: 0.09ms
657
- - **Total: 4.71ms for 45 queries** ⚑
491
+ - `@unique` on one-to-one foreign keys tells the planner the relation is bounded
492
+ - indexes on one-to-many foreign keys make follow-up or segmented loading cheap
658
493
 
659
- **Under the hood:**
494
+ #### 2) Index every foreign key used in includes and relation filters
660
495
 
661
- The library uses PostgreSQL CTEs (Common Table Expressions) to combine queries:
496
+ At minimum, index:
662
497
 
663
- ```sql
664
- -- What gets executed for the dashboard example above
665
- WITH
666
- batch_0 AS (SELECT count(*)::int AS "_count._all" FROM "organizations"),
667
- batch_1 AS (SELECT count(*)::int AS "_count._all" FROM "organizations" WHERE status = $1),
668
- batch_2 AS (SELECT count(*)::int AS "_count._all" FROM "users"),
669
- batch_3 AS (SELECT count(*)::int AS "_count._all" FROM "users" WHERE status = $2),
670
- batch_4 AS (SELECT * FROM "projects" ORDER BY created_at DESC LIMIT 5),
671
- batch_5 AS (SELECT count(*)::int, avg(estimated_hours) FROM "tasks" WHERE status = $3)
672
- SELECT
673
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_0 t) AS k0,
674
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_1 t) AS k1,
675
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_2 t) AS k2,
676
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_3 t) AS k3,
677
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_4 t) AS k4,
678
- (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_5 t) AS k5
679
- ```
498
+ - all `@relation(fields: [...])` foreign keys on the child side
499
+ - fields used in nested `where`
500
+ - fields used in nested `orderBy`
501
+ - fields used in cursor pagination
680
502
 
681
- **Special optimization for count queries:**
503
+ Example:
682
504
 
683
- When batching multiple count queries on the same table, they get merged into a single query using `FILTER` clauses:
505
+ ```prisma
506
+ model Comment {
507
+ id Int @id @default(autoincrement())
508
+ postId Int
509
+ createdAt DateTime @default(now())
510
+ published Boolean @default(false)
684
511
 
685
- ```typescript
686
- const counts = await prisma.$batch((batch) => ({
687
- total: batch.user.count(),
688
- active: batch.user.count({ where: { status: 'ACTIVE' } }),
689
- pending: batch.user.count({ where: { status: 'PENDING' } }),
690
- inactive: batch.user.count({ where: { status: 'INACTIVE' } }),
691
- }))
512
+ post Post @relation(fields: [postId], references: [id])
513
+
514
+ @@index([postId])
515
+ @@index([postId, createdAt])
516
+ @@index([postId, published])
517
+ }
692
518
  ```
693
519
 
694
- Gets optimized to:
520
+ #### 3) Prefer deterministic nested ordering
521
+
522
+ When including collections, always provide a stable order when practical.
695
523
 
696
- ```sql
697
- SELECT
698
- count(*) AS total,
699
- count(*) FILTER (WHERE status = $1) AS active,
700
- count(*) FILTER (WHERE status = $2) AS pending,
701
- count(*) FILTER (WHERE status = $3) AS inactive
702
- FROM users
524
+ ```ts
525
+ const users = await prisma.user.findMany({
526
+ include: {
527
+ posts: {
528
+ orderBy: { createdAt: 'desc' },
529
+ take: 5,
530
+ },
531
+ },
532
+ })
703
533
  ```
704
534
 
705
- **Supported methods in batch:**
535
+ That helps both the planner and the reducer keep result shapes predictable.
706
536
 
707
- - βœ… `findMany` - fetch multiple records
708
- - βœ… `findFirst` - fetch first matching record
709
- - βœ… `findUnique` - fetch by unique field
710
- - βœ… `count` - count records (with special optimization)
711
- - βœ… `aggregate` - compute aggregations
712
- - βœ… `groupBy` - group and aggregate
537
+ #### 4) Avoid unbounded deep fan-out in a single query
713
538
 
714
- **Important: Don't await inside the batch callback**
539
+ This is the biggest real-world improvement lever.
715
540
 
716
- ```typescript
717
- // ❌ Wrong - will throw error
718
- await prisma.$batch(async (batch) => ({
719
- users: await batch.user.findMany(), // Don't await!
720
- posts: await batch.post.findMany(), // Don't await!
721
- }))
541
+ Less ideal:
722
542
 
723
- // βœ… Correct - return queries without awaiting
724
- await prisma.$batch((batch) => ({
725
- users: batch.user.findMany(), // Return the query
726
- posts: batch.post.findMany(), // Return the query
727
- }))
543
+ ```ts
544
+ await prisma.organization.findMany({
545
+ include: {
546
+ users: {
547
+ include: {
548
+ posts: {
549
+ include: {
550
+ comments: true,
551
+ },
552
+ },
553
+ },
554
+ },
555
+ },
556
+ })
728
557
  ```
729
558
 
730
- **Type Safety:**
559
+ Usually better:
731
560
 
732
- ```typescript
733
- import { speedExtension, type SpeedClient } from './generated/sql'
561
+ - page parents
562
+ - cap nested collections with `take`
563
+ - add nested `where`
564
+ - split unrelated heavy branches into `$batch`
734
565
 
735
- // Properly type your client
736
- const prisma = basePrisma.$extends(
737
- speedExtension({ postgres: sql }),
738
- ) as SpeedClient<typeof basePrisma>
566
+ Example:
739
567
 
740
- // TypeScript knows the exact shape of results
741
- const results = await prisma.$batch((batch) => ({
742
- users: batch.user.findMany({ select: { id: true, email: true } }),
743
- count: batch.post.count(),
568
+ ```ts
569
+ const result = await prisma.$batch((batch) => ({
570
+ orgs: batch.organization.findMany({
571
+ take: 20,
572
+ orderBy: { id: 'asc' },
573
+ }),
574
+ recentUsers: batch.user.findMany({
575
+ take: 50,
576
+ orderBy: { createdAt: 'desc' },
577
+ }),
744
578
  }))
745
-
746
- // βœ… TypeScript autocomplete works
747
- results.users[0].email // string
748
- results.count // number
749
579
  ```
750
580
 
751
- **Use cases:**
752
-
753
- 1. **Dashboard queries** - Load all dashboard data in one call
754
- 2. **Analytics** - Multiple aggregations at once
755
- 3. **Comparison queries** - Compare different time periods
756
- 4. **Multi-tenant data** - Fetch data for multiple tenants
757
- 5. **Search with counts** - Get results + multiple facet counts
581
+ #### 5) Use one-to-one uniqueness where it is actually one-to-one
758
582
 
759
- **Limitations:**
583
+ If the database guarantees one child row, encode that in Prisma.
760
584
 
761
- - PostgreSQL only (SQLite not yet supported)
762
- - Queries run in parallel, not in a transaction
763
- - Each query must be independent (can't reference results from other queries)
764
- - For transactional guarantees, use `$transaction` instead
585
+ This can let the planner avoid unnecessarily defensive high-fanout strategies.
765
586
 
766
- ### Prebaked SQL Queries (@optimize)
587
+ #### 6) Keep nested filters sargable
767
588
 
768
- For maximum performance, prebake your most common queries at build time using `@optimize` directives. This reduces overhead from ~0.2ms (runtime) to ~0.03ms.
589
+ Prefer predicates that use indexed equality/range conditions.
769
590
 
770
- **Add optimize directives to your models:**
591
+ Better:
771
592
 
772
- ```prisma
773
- /// @optimize {
774
- /// "method": "findMany",
775
- /// "query": {
776
- /// "skip": "$skip",
777
- /// "take": "$take",
778
- /// "orderBy": { "createdAt": "desc" },
779
- /// "where": { "status": "ACTIVE" }
780
- /// }
781
- /// }
782
- /// @optimize { "method": "count", "query": {} }
783
- model User {
784
- id Int @id @default(autoincrement())
785
- email String @unique
786
- status String
787
- createdAt DateTime @default(now())
788
- posts Post[]
593
+ ```ts
594
+ {
595
+ include: {
596
+ comments: {
597
+ where: {
598
+ postId: 42,
599
+ createdAt: { gte: someDate },
600
+ },
601
+ orderBy: { createdAt: 'desc' },
602
+ },
603
+ },
789
604
  }
790
605
  ```
791
606
 
792
- **Generate:**
607
+ Less planner-friendly:
793
608
 
794
- ```bash
795
- npx prisma generate
796
- ```
609
+ - broad `contains` / `%term%` everywhere
610
+ - unindexed OR-heavy nested filters
611
+ - deep includes without limits
797
612
 
798
- **Use:**
613
+ ### What to configure
799
614
 
800
- ```typescript
801
- import { speedExtension, type SpeedClient } from './generated/sql'
615
+ Use the cardinality planner wherever your generator/runtime exposes it.
802
616
 
803
- const prisma = new PrismaClient().$extends(
804
- speedExtension({ postgres: sql }),
805
- ) as SpeedClient<typeof PrismaClient>
617
+ Because config names can differ between versions, the safe rule is:
806
618
 
807
- // ⚑ PREBAKED - Uses pre-generated SQL (~0.03ms overhead)
808
- const activeUsers = await prisma.user.findMany({
809
- where: { status: 'ACTIVE' },
810
- skip: 0,
811
- take: 10,
812
- orderBy: { createdAt: 'desc' },
813
- })
619
+ - enable the planner in generator/runtime config if your build exposes that switch
620
+ - keep it on for relation-heavy workloads
621
+ - tune any thresholds only after measuring with real production-shaped queries
814
622
 
815
- // πŸ”¨ RUNTIME - Generates SQL on-the-fly (~0.2ms overhead, still fast!)
816
- const searchUsers = await prisma.user.findMany({
817
- where: { email: { contains: '@example.com' } },
818
- })
819
- ```
623
+ If your project has planner thresholds, start conservatively:
820
624
 
821
- The extension automatically:
625
+ - prefer bounded strategies for one-to-one and unique includes
626
+ - prefer segmented or reduced strategies for one-to-many and many-to-many
627
+ - lower thresholds for deep includes with large child tables
628
+ - raise thresholds only after verifying lower fan-out in production data
822
629
 
823
- - Uses prebaked SQL for matching queries (instant)
824
- - Falls back to runtime generation for non-matching queries (still fast)
825
- - Tracks which queries are prebaked via `onQuery` callback
630
+ ### How to verify the planner is helping
826
631
 
827
- **Dynamic Parameters:**
632
+ Use `debug` and `onQuery`.
828
633
 
829
- Use `$paramName` syntax for runtime values:
634
+ Look for:
830
635
 
831
- ```prisma
832
- /// @optimize {
833
- /// "method": "findMany",
834
- /// "query": {
835
- /// "where": { "status": "$status" },
836
- /// "skip": "$skip",
837
- /// "take": "$take"
838
- /// }
839
- /// }
840
- model User {
841
- id Int @id
842
- status String
843
- }
636
+ - large latency spikes on include-heavy queries
637
+ - unusually large result sets for a small parent page
638
+ - repeated slow nested includes on high-fanout relations
639
+
640
+ ```ts
641
+ const prisma = basePrisma.$extends(
642
+ speedExtension({
643
+ postgres: sql,
644
+ debug: true,
645
+ onQuery: (info) => {
646
+ console.log(`${info.model}.${info.method} ${info.duration}ms`)
647
+ console.log(info.sql)
648
+ },
649
+ }),
650
+ ) as SpeedClient<typeof basePrisma>
844
651
  ```
845
652
 
846
- **Generator Configuration:**
653
+ What good results look like:
847
654
 
848
- ```prisma
849
- generator sql {
850
- provider = "prisma-sql-generator"
655
+ - small parent page stays small in latency
656
+ - bounded child includes remain predictable
657
+ - high-fanout includes stop exploding row counts
658
+ - moving a heavy include into `$batch` or splitting it improves latency materially
851
659
 
852
- # Optional: Override auto-detected dialect
853
- # dialect = "postgres" # or "sqlite"
660
+ ### Practical recommendations
854
661
 
855
- # Optional: Custom output directory
856
- # output = "./generated/sql"
662
+ For best results with the planner:
857
663
 
858
- # Optional: Skip invalid directives instead of failing
859
- # skipInvalid = "true"
860
- }
664
+ 1. index all relation keys
665
+ 2. encode one-to-one relations with `@unique`
666
+ 3. use stable `orderBy`
667
+ 4. cap nested collections with `take`
668
+ 5. page parents before including deep trees
669
+ 6. split unrelated heavy branches into `$batch`
670
+ 7. benchmark with real data distributions, not toy fixtures
671
+
672
+ ## Batch queries
673
+
674
+ `$batch` runs multiple independent read queries in one PostgreSQL round trip.
675
+
676
+ ```ts
677
+ const dashboard = await prisma.$batch((batch) => ({
678
+ totalUsers: batch.user.count(),
679
+ activeUsers: batch.user.count({
680
+ where: { status: 'ACTIVE' },
681
+ }),
682
+ recentProjects: batch.project.findMany({
683
+ take: 5,
684
+ orderBy: { createdAt: 'desc' },
685
+ include: { organization: true },
686
+ }),
687
+ taskStats: batch.task.aggregate({
688
+ _count: true,
689
+ _avg: { estimatedHours: true },
690
+ where: { status: 'IN_PROGRESS' },
691
+ }),
692
+ }))
861
693
  ```
862
694
 
863
- ### Edge Runtime
695
+ ### Rules
864
696
 
865
- **Vercel Edge Functions:**
697
+ Do not `await` inside the callback.
866
698
 
867
- ```typescript
868
- import { PrismaClient } from '@prisma/client'
869
- import { speedExtension, type SpeedClient } from './generated/sql'
870
- import postgres from 'postgres'
699
+ Incorrect:
871
700
 
872
- const sql = postgres(process.env.DATABASE_URL)
873
- const prisma = new PrismaClient().$extends(
874
- speedExtension({ postgres: sql }),
875
- ) as SpeedClient<typeof PrismaClient>
701
+ ```ts
702
+ await prisma.$batch(async (batch) => ({
703
+ users: await batch.user.findMany(),
704
+ }))
705
+ ```
876
706
 
877
- export const config = { runtime: 'edge' }
707
+ Correct:
878
708
 
879
- export default async function handler(req: Request) {
880
- const users = await prisma.user.findMany()
881
- return Response.json(users)
882
- }
709
+ ```ts
710
+ await prisma.$batch((batch) => ({
711
+ users: batch.user.findMany(),
712
+ }))
883
713
  ```
884
714
 
885
- **Cloudflare Workers:**
715
+ ### Best use cases
886
716
 
887
- For Cloudflare Workers, use the standalone SQL generation API:
717
+ - dashboards
718
+ - analytics summaries
719
+ - counts + page data
720
+ - multiple independent aggregates
721
+ - splitting unrelated heavy reads instead of building one massive include tree
888
722
 
889
- ```typescript
890
- import { createToSQL } from 'prisma-sql'
891
- import { MODELS } from './generated/sql'
723
+ ### Limitations
892
724
 
893
- const toSQL = createToSQL(MODELS, 'sqlite')
725
+ - PostgreSQL only
726
+ - queries are independent
727
+ - not transactional
728
+ - use `$transaction` when you need transactional guarantees
894
729
 
895
- export default {
896
- async fetch(request: Request, env: Env) {
897
- const { sql, params } = toSQL('User', 'findMany', {
898
- where: { status: 'ACTIVE' },
899
- })
730
+ ## Configuration
900
731
 
901
- const result = await env.DB.prepare(sql)
902
- .bind(...params)
903
- .all()
904
- return Response.json(result.results)
905
- },
906
- }
732
+ ### Debug logging
733
+
734
+ ```ts
735
+ const prisma = basePrisma.$extends(
736
+ speedExtension({
737
+ postgres: sql,
738
+ debug: true,
739
+ }),
740
+ ) as SpeedClient<typeof basePrisma>
907
741
  ```
908
742
 
909
- ## Generator Mode Details
743
+ ### Performance hook
910
744
 
911
- ### How It Works
745
+ ```ts
746
+ const prisma = basePrisma.$extends(
747
+ speedExtension({
748
+ postgres: sql,
749
+ onQuery: (info) => {
750
+ console.log(`${info.model}.${info.method}: ${info.duration}ms`)
751
+ console.log(`prebaked=${info.prebaked}`)
752
+ },
753
+ }),
754
+ ) as SpeedClient<typeof basePrisma>
755
+ ```
756
+
757
+ The callback receives:
912
758
 
759
+ ```ts
760
+ interface QueryInfo {
761
+ model: string
762
+ method: string
763
+ sql: string
764
+ params: unknown[]
765
+ duration: number
766
+ prebaked: boolean
767
+ }
913
768
  ```
914
- Build Time:
915
- schema.prisma
916
- ↓
917
- /// @optimize { "method": "findMany", "query": { "where": { "status": "ACTIVE" } } }
918
- ↓
919
- npx prisma generate
920
- ↓
921
- generated/sql/index.ts
922
- ↓
923
- export const MODELS = [...] // Pre-converted models
924
- const QUERIES = { // Pre-generated SQL
925
- User: {
926
- findMany: {
927
- '{"where":{"status":"ACTIVE"}}': {
928
- sql: 'SELECT * FROM users WHERE status = $1',
929
- params: ['ACTIVE'],
930
- dynamicKeys: []
931
- }
932
- }
933
- }
934
- }
935
- export function speedExtension() { ... }
936
-
937
- Runtime:
938
- prisma.user.findMany({ where: { status: 'ACTIVE' } })
939
- ↓
940
- Normalize query β†’ '{"where":{"status":"ACTIVE"}}'
941
- ↓
942
- QUERIES.User.findMany[query] found?
943
- ↓
944
- YES β†’ ⚑ Use prebaked SQL (0.03ms overhead)
945
- ↓
946
- NO β†’ πŸ”¨ Generate SQL runtime (0.2ms overhead)
947
- ↓
948
- Execute via postgres.js/better-sqlite3
769
+
770
+ ## Generator configuration
771
+
772
+ ```prisma
773
+ generator sql {
774
+ provider = "prisma-sql-generator"
775
+
776
+ // optional
777
+ // dialect = "postgres"
778
+
779
+ // optional
780
+ // output = "./generated/sql"
781
+
782
+ // optional
783
+ // skipInvalid = "true"
784
+ }
949
785
  ```
950
786
 
951
- ### Optimize Directive Examples
787
+ ## `@optimize` examples
952
788
 
953
- **Basic query:**
789
+ ### Basic prebaked query
954
790
 
955
791
  ```prisma
956
792
  /// @optimize {
@@ -965,20 +801,24 @@ model User {
965
801
  }
966
802
  ```
967
803
 
968
- **With pagination:**
804
+ ### Dynamic parameters
969
805
 
970
806
  ```prisma
971
807
  /// @optimize {
972
808
  /// "method": "findMany",
973
809
  /// "query": {
810
+ /// "where": { "status": "$status" },
974
811
  /// "skip": "$skip",
975
- /// "take": "$take",
976
- /// "orderBy": { "createdAt": "desc" }
812
+ /// "take": "$take"
977
813
  /// }
978
814
  /// }
815
+ model User {
816
+ id Int @id
817
+ status String
818
+ }
979
819
  ```
980
820
 
981
- **With relations:**
821
+ ### Nested include
982
822
 
983
823
  ```prisma
984
824
  /// @optimize {
@@ -993,298 +833,216 @@ model User {
993
833
  /// }
994
834
  /// }
995
835
  /// }
836
+ model User {
837
+ id Int @id
838
+ posts Post[]
839
+ }
996
840
  ```
997
841
 
998
- **Complex query:**
999
-
1000
- ```prisma
1001
- /// @optimize {
1002
- /// "method": "findMany",
1003
- /// "query": {
1004
- /// "skip": "$skip",
1005
- /// "take": "$take",
1006
- /// "orderBy": { "createdAt": "desc" },
1007
- /// "include": {
1008
- /// "company": {
1009
- /// "where": { "deletedAt": null },
1010
- /// "select": { "id": true, "name": true }
1011
- /// }
1012
- /// }
1013
- /// }
1014
- /// }
1015
- ```
1016
-
1017
- ## Migration Guide
1018
-
1019
- ### From Prisma Client
842
+ ## Edge usage
1020
843
 
1021
- **Before:**
844
+ ### Vercel Edge
1022
845
 
1023
- ```typescript
1024
- const prisma = new PrismaClient()
1025
- const users = await prisma.user.findMany()
1026
- ```
1027
-
1028
- **After:**
1029
-
1030
- ```typescript
846
+ ```ts
1031
847
  import { PrismaClient } from '@prisma/client'
1032
848
  import { speedExtension, type SpeedClient } from './generated/sql'
1033
849
  import postgres from 'postgres'
1034
850
 
1035
- const sql = postgres(process.env.DATABASE_URL)
1036
- const basePrisma = new PrismaClient()
1037
-
1038
- export const prisma = basePrisma.$extends(
851
+ const sql = postgres(process.env.DATABASE_URL!)
852
+ const prisma = new PrismaClient().$extends(
1039
853
  speedExtension({ postgres: sql }),
1040
- ) as SpeedClient<typeof basePrisma>
1041
-
1042
- const users = await prisma.user.findMany() // Same API, just faster
1043
- ```
1044
-
1045
- ### From Drizzle
1046
-
1047
- **Before:**
1048
-
1049
- ```typescript
1050
- import { drizzle } from 'drizzle-orm/postgres-js'
1051
- import postgres from 'postgres'
854
+ ) as SpeedClient<typeof PrismaClient>
1052
855
 
1053
- const sql = postgres(DATABASE_URL)
1054
- const db = drizzle(sql)
856
+ export const config = { runtime: 'edge' }
1055
857
 
1056
- const users = await db
1057
- .select()
1058
- .from(usersTable)
1059
- .where(eq(usersTable.status, 'ACTIVE'))
858
+ export default async function handler() {
859
+ const users = await prisma.user.findMany()
860
+ return Response.json(users)
861
+ }
1060
862
  ```
1061
863
 
1062
- **After:**
1063
-
1064
- ```typescript
1065
- import { PrismaClient } from '@prisma/client'
1066
- import { speedExtension, type SpeedClient } from './generated/sql'
1067
- import postgres from 'postgres'
864
+ ### Cloudflare Workers
1068
865
 
1069
- const sql = postgres(DATABASE_URL)
1070
- const basePrisma = new PrismaClient()
1071
-
1072
- export const prisma = basePrisma.$extends(
1073
- speedExtension({ postgres: sql }),
1074
- ) as SpeedClient<typeof basePrisma>
866
+ Use the standalone SQL generation API.
1075
867
 
1076
- const users = await prisma.user.findMany({
1077
- where: { status: 'ACTIVE' },
1078
- })
1079
- ```
868
+ ```ts
869
+ import { createToSQL } from 'prisma-sql'
870
+ import { MODELS } from './generated/sql'
1080
871
 
1081
- ## Limitations
872
+ const toSQL = createToSQL(MODELS, 'sqlite')
1082
873
 
1083
- ### Partially Supported
874
+ export default {
875
+ async fetch(request: Request, env: Env) {
876
+ const { sql, params } = toSQL('User', 'findMany', {
877
+ where: { status: 'ACTIVE' },
878
+ })
1084
879
 
1085
- These features work but have limitations:
880
+ const result = await env.DB.prepare(sql)
881
+ .bind(...params)
882
+ .all()
1086
883
 
1087
- - ⚠️ **Array operations**: Basic operations (`has`, `hasSome`, `hasEvery`, `isEmpty`) work. Advanced filtering not yet supported.
1088
- - ⚠️ **JSON operations**: Path-based filtering works. Advanced JSON functions not yet supported.
884
+ return Response.json(result.results)
885
+ },
886
+ }
887
+ ```
1089
888
 
1090
- ### Not Yet Supported
889
+ ## Performance
1091
890
 
1092
- These Prisma features will fall back to Prisma Client:
891
+ Performance depends on:
1093
892
 
1094
- - ❌ Full-text search (`search` operator)
1095
- - ❌ Composite types (MongoDB-style embedded documents)
1096
- - ❌ Raw database features (PostGIS, pg_trgm, etc.)
1097
- - ❌ Some advanced aggregations in `groupBy`
893
+ - database type
894
+ - query shape
895
+ - indexing
896
+ - relation fan-out
897
+ - whether the query is prebaked
898
+ - whether the cardinality planner can choose a bounded strategy
1098
899
 
1099
- Enable `debug: true` to see which queries are accelerated vs fallback.
900
+ Typical gains are strongest when:
1100
901
 
1101
- ### Database Support
902
+ - Prisma overhead dominates total time
903
+ - includes are moderate but structured well
904
+ - query shapes repeat
905
+ - indexes exist on relation and filter columns
1102
906
 
1103
- - βœ… PostgreSQL 12+
1104
- - βœ… SQLite 3.35+
1105
- - ❌ MySQL (not yet implemented)
1106
- - ❌ MongoDB (not applicable - document database)
1107
- - ❌ SQL Server (not yet implemented)
1108
- - ❌ CockroachDB (not yet tested)
907
+ Run your own benchmarks on production-shaped data.
1109
908
 
1110
909
  ## Troubleshooting
1111
910
 
1112
- ### "speedExtension requires postgres or sqlite client"
911
+ ### `speedExtension requires postgres or sqlite client`
1113
912
 
1114
- Make sure you're importing from the generated file and passing the database client:
913
+ Pass a database-native client to the generated extension.
1115
914
 
1116
- ```typescript
1117
- import { speedExtension, type SpeedClient } from './generated/sql'
1118
- import postgres from 'postgres'
1119
-
1120
- const sql = postgres(process.env.DATABASE_URL)
1121
- const prisma = new PrismaClient().$extends(
1122
- speedExtension({ postgres: sql }), // βœ… Pass postgres client
1123
- ) as SpeedClient<typeof PrismaClient>
915
+ ```ts
916
+ const prisma = new PrismaClient().$extends(speedExtension({ postgres: sql }))
1124
917
  ```
1125
918
 
1126
- ### "Generated code is for postgres, but you provided sqlite"
919
+ ### Generated dialect mismatch
920
+
921
+ If generated code targets PostgreSQL, do not pass SQLite, and vice versa.
1127
922
 
1128
- The generator auto-detects your database from `schema.prisma`. If you need to override:
923
+ Override dialect in the generator if needed.
1129
924
 
1130
925
  ```prisma
1131
926
  generator sql {
1132
927
  provider = "prisma-sql-generator"
1133
- dialect = "postgres" # or "sqlite"
928
+ dialect = "postgres"
1134
929
  }
1135
930
  ```
1136
931
 
1137
- ### "Results don't match Prisma Client"
932
+ ### Results differ from Prisma
1138
933
 
1139
- Enable debug mode and compare SQL:
1140
-
1141
- ```typescript
1142
- import { speedExtension, type SpeedClient } from './generated/sql'
934
+ Turn on debug logging and compare generated SQL with Prisma query logs.
1143
935
 
936
+ ```ts
1144
937
  const prisma = new PrismaClient().$extends(
1145
938
  speedExtension({
1146
939
  postgres: sql,
1147
- debug: true, // Shows generated SQL
940
+ debug: true,
1148
941
  }),
1149
- ) as SpeedClient<typeof PrismaClient>
942
+ )
1150
943
  ```
1151
944
 
1152
- Compare with Prisma's query log:
945
+ If behavior differs, open an issue with:
1153
946
 
1154
- ```typescript
1155
- new PrismaClient({ log: ['query'] })
1156
- ```
947
+ - schema excerpt
948
+ - Prisma query
949
+ - generated SQL
950
+ - expected result
951
+ - actual result
952
+
953
+ ### Performance is worse on a relation-heavy query
1157
954
 
1158
- File an issue if results differ: https://github.com/multipliedtwice/prisma-to-sql/issues
955
+ Check these first:
1159
956
 
1160
- ### "Connection pool exhausted"
957
+ - missing foreign-key indexes
958
+ - deep unbounded includes
959
+ - no nested `take`
960
+ - unstable or missing `orderBy`
961
+ - high-fanout relation trees that should be split into `$batch`
1161
962
 
1162
- Increase postgres.js pool size:
963
+ ### Connection pool exhaustion
1163
964
 
1164
- ```typescript
1165
- const sql = postgres(DATABASE_URL, {
1166
- max: 50, // Default is 10
965
+ Increase `postgres.js` pool size if needed.
966
+
967
+ ```ts
968
+ const sql = postgres(process.env.DATABASE_URL!, {
969
+ max: 50,
1167
970
  })
1168
971
  ```
1169
972
 
1170
- ### "Performance not improving"
973
+ ## Limitations
1171
974
 
1172
- Some queries won't see dramatic improvements:
975
+ ### Partially supported
1173
976
 
1174
- - Very simple `findUnique` by ID (already fast)
1175
- - Queries with no WHERE clause on small tables
1176
- - Aggregations on unindexed fields
977
+ - basic array operators
978
+ - basic JSON path filtering
1177
979
 
1178
- Use `onQuery` to measure actual speedup:
980
+ ### Not yet supported
1179
981
 
1180
- ```typescript
1181
- import { speedExtension, type SpeedClient } from './generated/sql'
982
+ These should fall back to Prisma:
1182
983
 
1183
- const prisma = new PrismaClient().$extends(
1184
- speedExtension({
1185
- postgres: sql,
1186
- onQuery: (info) => {
1187
- console.log(`${info.method} took ${info.duration}ms`)
1188
- },
1189
- }),
1190
- ) as SpeedClient<typeof PrismaClient>
1191
- ```
984
+ - full-text `search`
985
+ - composite/document-style embedded types
986
+ - vendor-specific extensions not yet modeled by the SQL builder
987
+ - some advanced `groupBy` edge cases
1192
988
 
1193
989
  ## FAQ
1194
990
 
1195
- **Q: Do I need to keep using Prisma Client?**
1196
- A: Yes. You need Prisma for schema management, migrations, types, and write operations. This extension only speeds up reads.
991
+ **Do I still need Prisma?**
992
+ Yes. Prisma remains the source of truth for schema, migrations, types, writes, and fallback behavior.
1197
993
 
1198
- **Q: Does it work with my existing schema?**
1199
- A: Yes. No schema changes required except adding the generator. It works with your existing Prisma schema and generated client.
994
+ **Does this replace Prisma Client?**
995
+ No. It extends Prisma Client.
1200
996
 
1201
- **Q: What about writes (create, update, delete)?**
1202
- A: Writes still use Prisma Client. This extension only accelerates reads.
997
+ **What gets accelerated?**
998
+ Supported read queries only.
1203
999
 
1204
- **Q: Is it production ready?**
1205
- A: Yes. 137 E2E tests verify exact parity with Prisma Client across both Prisma v6 and v7. Used in production.
1000
+ **What about writes?**
1001
+ Writes continue through Prisma.
1206
1002
 
1207
- **Q: Can I use it with PlanetScale, Neon, Supabase?**
1208
- A: Yes. Works with any PostgreSQL-compatible database. Just pass the connection string to postgres.js.
1003
+ **Do I need `@optimize`?**
1004
+ No. It is optional. It only reduces the overhead of repeated hot query shapes.
1209
1005
 
1210
- **Q: Does it support Prisma middlewares?**
1211
- A: The extension runs after middlewares. For middleware to see actual SQL, use Prisma's query logging.
1006
+ **Does `$batch` work with SQLite?**
1007
+ Not currently.
1212
1008
 
1213
- **Q: Can I still use `$queryRaw` and `$executeRaw`?**
1214
- A: Yes. Those methods are unaffected.
1009
+ **Is it safe to use in production?**
1010
+ Use it the same way you would adopt any query-path optimization layer: benchmark it on real data, compare against Prisma for parity, and keep Prisma fallback enabled for unsupported cases.
1215
1011
 
1216
- **Q: What's the overhead of SQL generation?**
1217
- A: Runtime mode: ~0.2ms per query. Generator mode with `@optimize`: ~0.03ms for prebaked queries. Still 2-7x faster than Prisma overall.
1012
+ ## Migration
1218
1013
 
1219
- **Q: Do I need @optimize directives?**
1220
- A: No! The generator works without them. `@optimize` directives are optional for squeezing out the last bit of performance on your hottest queries.
1014
+ ### Before
1221
1015
 
1222
- **Q: Can I use batch queries with transactions?**
1223
- A: No. `$batch` executes queries in parallel without transactional guarantees. For transactions, use `$transaction` instead.
1224
-
1225
- **Q: Does batch work with SQLite?**
1226
- A: Not yet. `$batch` is currently PostgreSQL only. SQLite support coming soon.
1227
-
1228
- ## Examples
1229
-
1230
- - [Generator Mode Example](./examples/generator-mode) - Complete working example
1231
- - [PostgreSQL E2E Tests](./tests/e2e/postgres.test.ts) - Comprehensive query examples
1232
- - [SQLite E2E Tests](./tests/e2e/sqlite.e2e.test.ts) - SQLite-specific queries
1233
- - [Batch Query Tests](./tests/sql-injection/batch-transaction.test.ts) - Batch query examples
1234
-
1235
- To run examples locally:
1236
-
1237
- ```bash
1238
- git clone https://github.com/multipliedtwice/prisma-to-sql
1239
- cd prisma-sql
1240
- npm install
1241
- npm test
1016
+ ```ts
1017
+ const prisma = new PrismaClient()
1018
+ const users = await prisma.user.findMany()
1242
1019
  ```
1243
1020
 
1244
- ## Benchmarking
1245
-
1246
- Benchmark your own queries:
1021
+ ### After
1247
1022
 
1248
- ```typescript
1023
+ ```ts
1024
+ import { PrismaClient } from '@prisma/client'
1249
1025
  import { speedExtension, type SpeedClient } from './generated/sql'
1026
+ import postgres from 'postgres'
1250
1027
 
1251
- const queries: { name: string; duration: number; prebaked: boolean }[] = []
1252
-
1253
- const prisma = new PrismaClient().$extends(
1254
- speedExtension({
1255
- postgres: sql,
1256
- onQuery: (info) => {
1257
- queries.push({
1258
- name: `${info.model}.${info.method}`,
1259
- duration: info.duration,
1260
- prebaked: info.prebaked,
1261
- })
1262
- },
1263
- }),
1264
- ) as SpeedClient<typeof PrismaClient>
1028
+ const sql = postgres(process.env.DATABASE_URL!)
1029
+ const basePrisma = new PrismaClient()
1265
1030
 
1266
- await prisma.user.findMany({ where: { status: 'ACTIVE' } })
1267
- await prisma.post.findMany({ include: { author: true } })
1268
- await prisma.$batch((batch) => ({
1269
- users: batch.user.count(),
1270
- posts: batch.post.count(),
1271
- }))
1031
+ export const prisma = basePrisma.$extends(
1032
+ speedExtension({ postgres: sql }),
1033
+ ) as SpeedClient<typeof basePrisma>
1272
1034
 
1273
- console.table(queries)
1035
+ const users = await prisma.user.findMany()
1274
1036
  ```
1275
1037
 
1276
- ## Contributing
1277
-
1278
- PRs welcome! Priority areas:
1038
+ ## Examples
1279
1039
 
1280
- - MySQL support implementation
1281
- - SQLite batch query support
1282
- - Additional PostgreSQL/SQLite operators
1283
- - Performance optimizations
1284
- - Edge runtime compatibility
1285
- - Documentation improvements
1040
+ - `examples/generator-mode`
1041
+ - `tests/e2e/postgres.test.ts`
1042
+ - `tests/e2e/sqlite.e2e.test.ts`
1043
+ - `tests/sql-injection/batch-transaction.test.ts`
1286
1044
 
1287
- Setup:
1045
+ ## Development
1288
1046
 
1289
1047
  ```bash
1290
1048
  git clone https://github.com/multipliedtwice/prisma-to-sql
@@ -1294,52 +1052,6 @@ npm run build
1294
1052
  npm test
1295
1053
  ```
1296
1054
 
1297
- Please ensure:
1298
-
1299
- - All tests pass (`npm test`)
1300
- - New features have tests
1301
- - Types are properly exported
1302
- - README is updated
1303
-
1304
- ## How It Works
1305
-
1306
- ```
1307
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1308
- β”‚ prisma.user.findMany({ where: { status: 'ACTIVE' }})β”‚
1309
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1310
- β”‚
1311
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1312
- β”‚ Generated Extension β”‚
1313
- β”‚ Uses internal MODELSβ”‚
1314
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1315
- β”‚
1316
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1317
- β”‚ Check for prebaked β”‚
1318
- β”‚ query in QUERIES β”‚
1319
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1320
- β”‚
1321
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1322
- β”‚ Generate SQL β”‚
1323
- β”‚ (if not prebaked) β”‚
1324
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1325
- β”‚
1326
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1327
- β”‚ SELECT ... FROM usersβ”‚
1328
- β”‚ WHERE status = $1 β”‚
1329
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1330
- β”‚
1331
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1332
- β”‚ Execute via β”‚
1333
- β”‚ postgres.js β”‚ ← Bypasses Prisma's query engine
1334
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1335
- β”‚
1336
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1337
- β”‚ Return results β”‚
1338
- β”‚ (same format as β”‚
1339
- β”‚ Prisma) β”‚
1340
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1341
- ```
1342
-
1343
1055
  ## License
1344
1056
 
1345
1057
  MIT
@@ -1349,8 +1061,3 @@ MIT
1349
1061
  - [NPM Package](https://www.npmjs.com/package/prisma-sql)
1350
1062
  - [GitHub Repository](https://github.com/multipliedtwice/prisma-to-sql)
1351
1063
  - [Issue Tracker](https://github.com/multipliedtwice/prisma-to-sql/issues)
1352
-
1353
- ---
1354
-
1355
- **Made for developers who need Prisma's DX with raw SQL performance.**
1356
- ````