prisma-sql 1.76.1 β†’ 1.77.0

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
185
160
 
186
- ```typescript
161
+ Apply `speedExtension` last so it sees the final query surface.
162
+
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
+ ],
379
+ }
380
+ ```
381
+
382
+ ### Composite cursor pagination
383
+
384
+ For composite cursors, use an `orderBy` that starts with the cursor fields in the same order.
385
+
386
+ ```ts
387
+ {
388
+ cursor: { tenantId: 10, id: 500 },
389
+ skip: 1,
390
+ take: 20,
391
+ orderBy: [
392
+ { tenantId: 'asc' },
393
+ { id: 'asc' },
394
+ ],
535
395
  }
536
396
  ```
537
397
 
538
- ### Aggregations
398
+ This matches keyset pagination expectations and avoids unstable page boundaries.
539
399
 
540
- ```typescript
541
- // Count
542
- await prisma.user.count({ where: { status: 'ACTIVE' } })
400
+ ### Aggregates
543
401
 
544
- // Aggregate
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,347 @@ 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
+ ### What to configure
713
538
 
714
- **Important: Don't await inside the batch callback**
539
+ Use the cardinality planner wherever your generator/runtime exposes it.
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
+ Because config names can differ between versions, the safe rule is:
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
- }))
728
- ```
543
+ - enable the planner in generator/runtime config if your build exposes that switch
544
+ - keep it on for relation-heavy workloads
545
+ - tune any thresholds only after measuring with real production-shaped queries
729
546
 
730
- **Type Safety:**
547
+ If your project has planner thresholds, start conservatively:
731
548
 
732
- ```typescript
733
- import { speedExtension, type SpeedClient } from './generated/sql'
549
+ - prefer bounded strategies for one-to-one and unique includes
550
+ - prefer segmented or reduced strategies for one-to-many and many-to-many
551
+ - lower thresholds for deep includes with large child tables
552
+ - raise thresholds only after verifying lower fan-out in production data
734
553
 
735
- // Properly type your client
736
- const prisma = basePrisma.$extends(
737
- speedExtension({ postgres: sql }),
738
- ) as SpeedClient<typeof basePrisma>
554
+ ### How to verify the planner is helping
739
555
 
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(),
744
- }))
556
+ Use `debug` and `onQuery`.
745
557
 
746
- // βœ… TypeScript autocomplete works
747
- results.users[0].email // string
748
- results.count // number
749
- ```
558
+ Look for:
750
559
 
751
- **Use cases:**
560
+ - large latency spikes on include-heavy queries
561
+ - unusually large result sets for a small parent page
562
+ - repeated slow nested includes on high-fanout relations
752
563
 
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
564
+ ```ts
565
+ const prisma = basePrisma.$extends(
566
+ speedExtension({
567
+ postgres: sql,
568
+ debug: true,
569
+ onQuery: (info) => {
570
+ console.log(`${info.model}.${info.method} ${info.duration}ms`)
571
+ console.log(info.sql)
572
+ },
573
+ }),
574
+ ) as SpeedClient<typeof basePrisma>
575
+ ```
758
576
 
759
- **Limitations:**
577
+ What good results look like:
760
578
 
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
579
+ - small parent page stays small in latency
580
+ - bounded child includes remain predictable
581
+ - high-fanout includes stop exploding row counts
582
+ - moving a heavy include into `$batch` or splitting it improves latency materially
765
583
 
766
- ### Prebaked SQL Queries (@optimize)
584
+ ## Deployment without database access at build time
767
585
 
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.
586
+ The cardinality planner collects relation statistics and roundtrip cost measurements directly from the database during `prisma generate`. In CI/CD pipelines or containerized builds, the database is often unreachable.
769
587
 
770
- **Add optimize directives to your models:**
588
+ ### Skip planner during generation
771
589
 
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[]
789
- }
590
+ Set `PRISMA_SQL_SKIP_PLANNER=true` to skip stats collection at generate time. The generator will emit default planner values instead.
591
+
592
+ ```bash
593
+ PRISMA_SQL_SKIP_PLANNER=true npx prisma generate
790
594
  ```
791
595
 
792
- **Generate:**
596
+ ### Collect stats before server start
597
+
598
+ Run `prisma-sql-collect-stats` as a pre-start step, after deployment, when the database is reachable.
793
599
 
794
600
  ```bash
795
- npx prisma generate
601
+ prisma-sql-collect-stats \
602
+ --output dist/prisma/generated/sql/planner.generated.js \
603
+ --prisma-client dist/prisma/generated/client/index.js
796
604
  ```
797
605
 
798
- **Use:**
606
+ | Flag | Default | Description |
607
+ | ----------------- | -------------------------------------------------- | -------------------------------------------------------------- |
608
+ | `--output` | `./dist/prisma/generated/sql/planner.generated.js` | Path to the generated planner module |
609
+ | `--prisma-client` | `@prisma/client` | Path to the compiled Prisma client (must expose `Prisma.dmmf`) |
799
610
 
800
- ```typescript
801
- import { speedExtension, type SpeedClient } from './generated/sql'
802
-
803
- const prisma = new PrismaClient().$extends(
804
- speedExtension({ postgres: sql }),
805
- ) as SpeedClient<typeof PrismaClient>
611
+ The script reads `DATABASE_URL` from the environment (supports `.env` via `dotenv`). If the connection fails or times out, it exits silently without blocking startup.
806
612
 
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
- })
613
+ ### Example scripts
814
614
 
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
- })
615
+ ```json
616
+ {
617
+ "prisma:generate": "PRISMA_SQL_SKIP_PLANNER=true prisma generate",
618
+ "collect-planner-stats": "prisma-sql-collect-stats --output dist/prisma/generated/sql/planner.generated.js --prisma-client dist/prisma/generated/client/index.js",
619
+ "start:production": "yarn collect-planner-stats; node dist/src/index.js"
620
+ }
819
621
  ```
820
622
 
821
- The extension automatically:
623
+ The semicolon (`;`) after `collect-planner-stats` ensures the server starts even if stats collection fails. Use `&&` instead if you want startup to abort on failure.
822
624
 
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
625
+ ### What happens with default planner values
826
626
 
827
- **Dynamic Parameters:**
627
+ When stats are not collected, the planner uses conservative defaults:
828
628
 
829
- Use `$paramName` syntax for runtime values:
629
+ - `roundtripRowEquivalent`: 73
630
+ - `jsonRowFactor`: 1.5
631
+ - `relationStats`: empty (all relations treated as unknown cardinality)
830
632
 
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
- }
633
+ This means the planner cannot make informed decisions about join strategies. Queries still work correctly β€” the planner falls back to safe general-purpose strategies β€” but relation-heavy reads may not use the optimal execution plan.
634
+
635
+ ### Timeout control
636
+
637
+ Stats collection has a default timeout of 15 seconds. Override with:
638
+
639
+ ```bash
640
+ PRISMA_SQL_PLANNER_TIMEOUT_MS=5000 yarn collect-planner-stats
844
641
  ```
845
642
 
846
- **Generator Configuration:**
643
+ ### Practical recommendations
847
644
 
848
- ```prisma
849
- generator sql {
850
- provider = "prisma-sql-generator"
645
+ For best results with the planner:
851
646
 
852
- # Optional: Override auto-detected dialect
853
- # dialect = "postgres" # or "sqlite"
647
+ 1. index all relation keys
648
+ 2. encode one-to-one relations with `@unique`
649
+ 3. use stable `orderBy`
650
+ 4. cap nested collections with `take`
651
+ 5. page parents before including deep trees
652
+ 6. split unrelated heavy branches into `$batch`
653
+ 7. benchmark with real data distributions, not toy fixtures
854
654
 
855
- # Optional: Custom output directory
856
- # output = "./generated/sql"
655
+ ## Batch queries
857
656
 
858
- # Optional: Skip invalid directives instead of failing
859
- # skipInvalid = "true"
860
- }
657
+ `$batch` runs multiple independent read queries in one PostgreSQL round trip.
658
+
659
+ ```ts
660
+ const dashboard = await prisma.$batch((batch) => ({
661
+ totalUsers: batch.user.count(),
662
+ activeUsers: batch.user.count({
663
+ where: { status: 'ACTIVE' },
664
+ }),
665
+ recentProjects: batch.project.findMany({
666
+ take: 5,
667
+ orderBy: { createdAt: 'desc' },
668
+ include: { organization: true },
669
+ }),
670
+ taskStats: batch.task.aggregate({
671
+ _count: true,
672
+ _avg: { estimatedHours: true },
673
+ where: { status: 'IN_PROGRESS' },
674
+ }),
675
+ }))
861
676
  ```
862
677
 
863
- ### Edge Runtime
678
+ ### Rules
864
679
 
865
- **Vercel Edge Functions:**
680
+ Do not `await` inside the callback.
866
681
 
867
- ```typescript
868
- import { PrismaClient } from '@prisma/client'
869
- import { speedExtension, type SpeedClient } from './generated/sql'
870
- import postgres from 'postgres'
682
+ Incorrect:
871
683
 
872
- const sql = postgres(process.env.DATABASE_URL)
873
- const prisma = new PrismaClient().$extends(
874
- speedExtension({ postgres: sql }),
875
- ) as SpeedClient<typeof PrismaClient>
684
+ ```ts
685
+ await prisma.$batch(async (batch) => ({
686
+ users: await batch.user.findMany(),
687
+ }))
688
+ ```
876
689
 
877
- export const config = { runtime: 'edge' }
690
+ Correct:
878
691
 
879
- export default async function handler(req: Request) {
880
- const users = await prisma.user.findMany()
881
- return Response.json(users)
882
- }
692
+ ```ts
693
+ await prisma.$batch((batch) => ({
694
+ users: batch.user.findMany(),
695
+ }))
883
696
  ```
884
697
 
885
- **Cloudflare Workers:**
698
+ ### Best use cases
886
699
 
887
- For Cloudflare Workers, use the standalone SQL generation API:
700
+ - dashboards
701
+ - analytics summaries
702
+ - counts + page data
703
+ - multiple independent aggregates
704
+ - splitting unrelated heavy reads instead of building one massive include tree
888
705
 
889
- ```typescript
890
- import { createToSQL } from 'prisma-sql'
891
- import { MODELS } from './generated/sql'
706
+ ### Limitations
892
707
 
893
- const toSQL = createToSQL(MODELS, 'sqlite')
708
+ - PostgreSQL only
709
+ - queries are independent
710
+ - not transactional
711
+ - use `$transaction` when you need transactional guarantees
894
712
 
895
- export default {
896
- async fetch(request: Request, env: Env) {
897
- const { sql, params } = toSQL('User', 'findMany', {
898
- where: { status: 'ACTIVE' },
899
- })
713
+ ## Configuration
900
714
 
901
- const result = await env.DB.prepare(sql)
902
- .bind(...params)
903
- .all()
904
- return Response.json(result.results)
905
- },
906
- }
715
+ ### Debug logging
716
+
717
+ ```ts
718
+ const prisma = basePrisma.$extends(
719
+ speedExtension({
720
+ postgres: sql,
721
+ debug: true,
722
+ }),
723
+ ) as SpeedClient<typeof basePrisma>
907
724
  ```
908
725
 
909
- ## Generator Mode Details
726
+ ### Performance hook
910
727
 
911
- ### How It Works
728
+ ```ts
729
+ const prisma = basePrisma.$extends(
730
+ speedExtension({
731
+ postgres: sql,
732
+ onQuery: (info) => {
733
+ console.log(`${info.model}.${info.method}: ${info.duration}ms`)
734
+ console.log(`prebaked=${info.prebaked}`)
735
+ },
736
+ }),
737
+ ) as SpeedClient<typeof basePrisma>
738
+ ```
739
+
740
+ The callback receives:
912
741
 
742
+ ```ts
743
+ interface QueryInfo {
744
+ model: string
745
+ method: string
746
+ sql: string
747
+ params: unknown[]
748
+ duration: number
749
+ prebaked: boolean
750
+ }
913
751
  ```
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
752
+
753
+ ## Generator configuration
754
+
755
+ ```prisma
756
+ generator sql {
757
+ provider = "prisma-sql-generator"
758
+
759
+ // optional
760
+ // dialect = "postgres"
761
+
762
+ // optional
763
+ // output = "./generated/sql"
764
+
765
+ // optional
766
+ // skipInvalid = "true"
767
+ }
949
768
  ```
950
769
 
951
- ### Optimize Directive Examples
770
+ ## `@optimize` examples
952
771
 
953
- **Basic query:**
772
+ ### Basic prebaked query
954
773
 
955
774
  ```prisma
956
775
  /// @optimize {
@@ -965,20 +784,24 @@ model User {
965
784
  }
966
785
  ```
967
786
 
968
- **With pagination:**
787
+ ### Dynamic parameters
969
788
 
970
789
  ```prisma
971
790
  /// @optimize {
972
791
  /// "method": "findMany",
973
792
  /// "query": {
793
+ /// "where": { "status": "$status" },
974
794
  /// "skip": "$skip",
975
- /// "take": "$take",
976
- /// "orderBy": { "createdAt": "desc" }
795
+ /// "take": "$take"
977
796
  /// }
978
797
  /// }
798
+ model User {
799
+ id Int @id
800
+ status String
801
+ }
979
802
  ```
980
803
 
981
- **With relations:**
804
+ ### Nested include
982
805
 
983
806
  ```prisma
984
807
  /// @optimize {
@@ -993,298 +816,216 @@ model User {
993
816
  /// }
994
817
  /// }
995
818
  /// }
819
+ model User {
820
+ id Int @id
821
+ posts Post[]
822
+ }
996
823
  ```
997
824
 
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
1020
-
1021
- **Before:**
825
+ ## Edge usage
1022
826
 
1023
- ```typescript
1024
- const prisma = new PrismaClient()
1025
- const users = await prisma.user.findMany()
1026
- ```
1027
-
1028
- **After:**
827
+ ### Vercel Edge
1029
828
 
1030
- ```typescript
829
+ ```ts
1031
830
  import { PrismaClient } from '@prisma/client'
1032
831
  import { speedExtension, type SpeedClient } from './generated/sql'
1033
832
  import postgres from 'postgres'
1034
833
 
1035
- const sql = postgres(process.env.DATABASE_URL)
1036
- const basePrisma = new PrismaClient()
1037
-
1038
- export const prisma = basePrisma.$extends(
834
+ const sql = postgres(process.env.DATABASE_URL!)
835
+ const prisma = new PrismaClient().$extends(
1039
836
  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'
837
+ ) as SpeedClient<typeof PrismaClient>
1052
838
 
1053
- const sql = postgres(DATABASE_URL)
1054
- const db = drizzle(sql)
839
+ export const config = { runtime: 'edge' }
1055
840
 
1056
- const users = await db
1057
- .select()
1058
- .from(usersTable)
1059
- .where(eq(usersTable.status, 'ACTIVE'))
841
+ export default async function handler() {
842
+ const users = await prisma.user.findMany()
843
+ return Response.json(users)
844
+ }
1060
845
  ```
1061
846
 
1062
- **After:**
1063
-
1064
- ```typescript
1065
- import { PrismaClient } from '@prisma/client'
1066
- import { speedExtension, type SpeedClient } from './generated/sql'
1067
- import postgres from 'postgres'
1068
-
1069
- const sql = postgres(DATABASE_URL)
1070
- const basePrisma = new PrismaClient()
847
+ ### Cloudflare Workers
1071
848
 
1072
- export const prisma = basePrisma.$extends(
1073
- speedExtension({ postgres: sql }),
1074
- ) as SpeedClient<typeof basePrisma>
849
+ Use the standalone SQL generation API.
1075
850
 
1076
- const users = await prisma.user.findMany({
1077
- where: { status: 'ACTIVE' },
1078
- })
1079
- ```
851
+ ```ts
852
+ import { createToSQL } from 'prisma-sql'
853
+ import { MODELS } from './generated/sql'
1080
854
 
1081
- ## Limitations
855
+ const toSQL = createToSQL(MODELS, 'sqlite')
1082
856
 
1083
- ### Partially Supported
857
+ export default {
858
+ async fetch(request: Request, env: Env) {
859
+ const { sql, params } = toSQL('User', 'findMany', {
860
+ where: { status: 'ACTIVE' },
861
+ })
1084
862
 
1085
- These features work but have limitations:
863
+ const result = await env.DB.prepare(sql)
864
+ .bind(...params)
865
+ .all()
1086
866
 
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.
867
+ return Response.json(result.results)
868
+ },
869
+ }
870
+ ```
1089
871
 
1090
- ### Not Yet Supported
872
+ ## Performance
1091
873
 
1092
- These Prisma features will fall back to Prisma Client:
874
+ Performance depends on:
1093
875
 
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`
876
+ - database type
877
+ - query shape
878
+ - indexing
879
+ - relation fan-out
880
+ - whether the query is prebaked
881
+ - whether the cardinality planner can choose a bounded strategy
1098
882
 
1099
- Enable `debug: true` to see which queries are accelerated vs fallback.
883
+ Typical gains are strongest when:
1100
884
 
1101
- ### Database Support
885
+ - Prisma overhead dominates total time
886
+ - includes are moderate but structured well
887
+ - query shapes repeat
888
+ - indexes exist on relation and filter columns
1102
889
 
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)
890
+ Run your own benchmarks on production-shaped data.
1109
891
 
1110
892
  ## Troubleshooting
1111
893
 
1112
- ### "speedExtension requires postgres or sqlite client"
894
+ ### `speedExtension requires postgres or sqlite client`
1113
895
 
1114
- Make sure you're importing from the generated file and passing the database client:
896
+ Pass a database-native client to the generated extension.
1115
897
 
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>
898
+ ```ts
899
+ const prisma = new PrismaClient().$extends(speedExtension({ postgres: sql }))
1124
900
  ```
1125
901
 
1126
- ### "Generated code is for postgres, but you provided sqlite"
902
+ ### Generated dialect mismatch
903
+
904
+ If generated code targets PostgreSQL, do not pass SQLite, and vice versa.
1127
905
 
1128
- The generator auto-detects your database from `schema.prisma`. If you need to override:
906
+ Override dialect in the generator if needed.
1129
907
 
1130
908
  ```prisma
1131
909
  generator sql {
1132
910
  provider = "prisma-sql-generator"
1133
- dialect = "postgres" # or "sqlite"
911
+ dialect = "postgres"
1134
912
  }
1135
913
  ```
1136
914
 
1137
- ### "Results don't match Prisma Client"
1138
-
1139
- Enable debug mode and compare SQL:
915
+ ### Results differ from Prisma
1140
916
 
1141
- ```typescript
1142
- import { speedExtension, type SpeedClient } from './generated/sql'
917
+ Turn on debug logging and compare generated SQL with Prisma query logs.
1143
918
 
919
+ ```ts
1144
920
  const prisma = new PrismaClient().$extends(
1145
921
  speedExtension({
1146
922
  postgres: sql,
1147
- debug: true, // Shows generated SQL
923
+ debug: true,
1148
924
  }),
1149
- ) as SpeedClient<typeof PrismaClient>
925
+ )
1150
926
  ```
1151
927
 
1152
- Compare with Prisma's query log:
928
+ If behavior differs, open an issue with:
1153
929
 
1154
- ```typescript
1155
- new PrismaClient({ log: ['query'] })
1156
- ```
930
+ - schema excerpt
931
+ - Prisma query
932
+ - generated SQL
933
+ - expected result
934
+ - actual result
935
+
936
+ ### Performance is worse on a relation-heavy query
1157
937
 
1158
- File an issue if results differ: https://github.com/multipliedtwice/prisma-to-sql/issues
938
+ Check these first:
1159
939
 
1160
- ### "Connection pool exhausted"
940
+ - missing foreign-key indexes
941
+ - deep unbounded includes
942
+ - no nested `take`
943
+ - unstable or missing `orderBy`
944
+ - high-fanout relation trees that should be split into `$batch`
1161
945
 
1162
- Increase postgres.js pool size:
946
+ ### Connection pool exhaustion
1163
947
 
1164
- ```typescript
1165
- const sql = postgres(DATABASE_URL, {
1166
- max: 50, // Default is 10
948
+ Increase `postgres.js` pool size if needed.
949
+
950
+ ```ts
951
+ const sql = postgres(process.env.DATABASE_URL!, {
952
+ max: 50,
1167
953
  })
1168
954
  ```
1169
955
 
1170
- ### "Performance not improving"
956
+ ## Limitations
1171
957
 
1172
- Some queries won't see dramatic improvements:
958
+ ### Partially supported
1173
959
 
1174
- - Very simple `findUnique` by ID (already fast)
1175
- - Queries with no WHERE clause on small tables
1176
- - Aggregations on unindexed fields
960
+ - basic array operators
961
+ - basic JSON path filtering
1177
962
 
1178
- Use `onQuery` to measure actual speedup:
963
+ ### Not yet supported
1179
964
 
1180
- ```typescript
1181
- import { speedExtension, type SpeedClient } from './generated/sql'
965
+ These should fall back to Prisma:
1182
966
 
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
- ```
967
+ - full-text `search`
968
+ - composite/document-style embedded types
969
+ - vendor-specific extensions not yet modeled by the SQL builder
970
+ - some advanced `groupBy` edge cases
1192
971
 
1193
972
  ## FAQ
1194
973
 
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.
974
+ **Do I still need Prisma?**
975
+ Yes. Prisma remains the source of truth for schema, migrations, types, writes, and fallback behavior.
1197
976
 
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.
977
+ **Does this replace Prisma Client?**
978
+ No. It extends Prisma Client.
1200
979
 
1201
- **Q: What about writes (create, update, delete)?**
1202
- A: Writes still use Prisma Client. This extension only accelerates reads.
980
+ **What gets accelerated?**
981
+ Supported read queries only.
1203
982
 
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.
983
+ **What about writes?**
984
+ Writes continue through Prisma.
1206
985
 
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.
986
+ **Do I need `@optimize`?**
987
+ No. It is optional. It only reduces the overhead of repeated hot query shapes.
1209
988
 
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.
989
+ **Does `$batch` work with SQLite?**
990
+ Not currently.
1212
991
 
1213
- **Q: Can I still use `$queryRaw` and `$executeRaw`?**
1214
- A: Yes. Those methods are unaffected.
992
+ **Is it safe to use in production?**
993
+ 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
994
 
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.
995
+ ## Migration
1218
996
 
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.
997
+ ### Before
1221
998
 
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
999
+ ```ts
1000
+ const prisma = new PrismaClient()
1001
+ const users = await prisma.user.findMany()
1242
1002
  ```
1243
1003
 
1244
- ## Benchmarking
1245
-
1246
- Benchmark your own queries:
1004
+ ### After
1247
1005
 
1248
- ```typescript
1006
+ ```ts
1007
+ import { PrismaClient } from '@prisma/client'
1249
1008
  import { speedExtension, type SpeedClient } from './generated/sql'
1009
+ import postgres from 'postgres'
1250
1010
 
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>
1011
+ const sql = postgres(process.env.DATABASE_URL!)
1012
+ const basePrisma = new PrismaClient()
1265
1013
 
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
- }))
1014
+ export const prisma = basePrisma.$extends(
1015
+ speedExtension({ postgres: sql }),
1016
+ ) as SpeedClient<typeof basePrisma>
1272
1017
 
1273
- console.table(queries)
1018
+ const users = await prisma.user.findMany()
1274
1019
  ```
1275
1020
 
1276
- ## Contributing
1277
-
1278
- PRs welcome! Priority areas:
1021
+ ## Examples
1279
1022
 
1280
- - MySQL support implementation
1281
- - SQLite batch query support
1282
- - Additional PostgreSQL/SQLite operators
1283
- - Performance optimizations
1284
- - Edge runtime compatibility
1285
- - Documentation improvements
1023
+ - `examples/generator-mode`
1024
+ - `tests/e2e/postgres.test.ts`
1025
+ - `tests/e2e/sqlite.e2e.test.ts`
1026
+ - `tests/sql-injection/batch-transaction.test.ts`
1286
1027
 
1287
- Setup:
1028
+ ## Development
1288
1029
 
1289
1030
  ```bash
1290
1031
  git clone https://github.com/multipliedtwice/prisma-to-sql
@@ -1294,52 +1035,6 @@ npm run build
1294
1035
  npm test
1295
1036
  ```
1296
1037
 
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
1038
  ## License
1344
1039
 
1345
1040
  MIT
@@ -1349,8 +1044,3 @@ MIT
1349
1044
  - [NPM Package](https://www.npmjs.com/package/prisma-sql)
1350
1045
  - [GitHub Repository](https://github.com/multipliedtwice/prisma-to-sql)
1351
1046
  - [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
- ````