prisma-sql 1.16.0 → 1.18.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,17 +2,24 @@
2
2
 
3
3
  Speed up Prisma reads **2-7x** by executing queries via postgres.js or better-sqlite3 instead of Prisma's query engine.
4
4
 
5
+ **Same API. Same types. Just faster.**
6
+
5
7
  ```typescript
6
- import { convertDMMFToModels } from 'prisma-sql'
7
- import { Prisma } from '@prisma/client'
8
+ import { PrismaClient, Prisma } from '@prisma/client'
9
+ import { speedExtension, convertDMMFToModels } from 'prisma-sql'
10
+ import postgres from 'postgres'
8
11
 
9
12
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
10
- const sql = postgres(DATABASE_URL)
13
+ const sql = postgres(process.env.DATABASE_URL)
11
14
  const prisma = new PrismaClient().$extends(
12
15
  speedExtension({ postgres: sql, models }),
13
16
  )
14
17
 
15
- const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
18
+ // Just use Prisma normally - queries are automatically faster
19
+ const users = await prisma.user.findMany({
20
+ where: { status: 'ACTIVE' },
21
+ include: { posts: true },
22
+ })
16
23
  ```
17
24
 
18
25
  ## Why?
@@ -44,81 +51,6 @@ npm install prisma-sql better-sqlite3
44
51
 
45
52
  ## Quick Start
46
53
 
47
- You can use prisma-sql in two ways:
48
-
49
- ### 1. Generator Mode (Recommended - Fastest)
50
-
51
- Prebake SQL queries at build time for maximum performance.
52
-
53
- **Add generator to schema:**
54
-
55
- ```prisma
56
- // schema.prisma
57
- datasource db {
58
- provider = "postgresql"
59
- url = env("DATABASE_URL")
60
- }
61
-
62
- generator client {
63
- provider = "prisma-client-js"
64
- }
65
-
66
- generator sql {
67
- provider = "prisma-sql-generator"
68
- // Dialect auto-detected from datasource.provider
69
- }
70
-
71
- model User {
72
- id Int @id @default(autoincrement())
73
- email String @unique
74
- status String
75
- posts Post[]
76
-
77
- /// @sql.findMany({ where: { status: "ACTIVE" } })
78
- /// @sql.findMany({ where: { status: "PENDING" } })
79
- /// @sql.count({ where: { status: "ACTIVE" } })
80
- }
81
-
82
- model Post {
83
- id Int @id @default(autoincrement())
84
- title String
85
- published Boolean
86
- authorId Int
87
- author User @relation(fields: [authorId], references: [id])
88
-
89
- /// @sql.findMany({ where: { published: true }, include: { author: true } })
90
- }
91
- ```
92
-
93
- **Generate and use:**
94
-
95
- ```bash
96
- npx prisma generate
97
- ```
98
-
99
- ```typescript
100
- import { PrismaClient } from '@prisma/client'
101
- import { createExtension } from '.prisma/client/sql'
102
- import postgres from 'postgres'
103
-
104
- const sql = postgres(process.env.DATABASE_URL)
105
- const prisma = new PrismaClient().$extends(createExtension({ postgres: sql }))
106
-
107
- // ⚡ PREBAKED - Uses pre-generated SQL (instant)
108
- const activeUsers = await prisma.user.findMany({
109
- where: { status: 'ACTIVE' },
110
- })
111
-
112
- // 🔨 RUNTIME - Generates SQL on-the-fly (still fast)
113
- const searchUsers = await prisma.user.findMany({
114
- where: { email: { contains: '@example.com' } },
115
- })
116
- ```
117
-
118
- ### 2. Runtime Mode (Simpler, No Build Step)
119
-
120
- Generate SQL on-the-fly without code generation.
121
-
122
54
  **PostgreSQL:**
123
55
 
124
56
  ```typescript
@@ -128,10 +60,12 @@ import postgres from 'postgres'
128
60
 
129
61
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
130
62
  const sql = postgres(process.env.DATABASE_URL)
63
+
131
64
  const prisma = new PrismaClient().$extends(
132
65
  speedExtension({ postgres: sql, models }),
133
66
  )
134
67
 
68
+ // Use Prisma exactly as before - it's just faster now
135
69
  const users = await prisma.user.findMany({
136
70
  where: { status: 'ACTIVE' },
137
71
  include: { posts: true },
@@ -147,6 +81,7 @@ import Database from 'better-sqlite3'
147
81
 
148
82
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
149
83
  const db = new Database('./data.db')
84
+
150
85
  const prisma = new PrismaClient().$extends(
151
86
  speedExtension({ sqlite: db, models }),
152
87
  )
@@ -154,192 +89,40 @@ const prisma = new PrismaClient().$extends(
154
89
  const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
155
90
  ```
156
91
 
157
- ## Generator Mode vs Runtime Mode
158
-
159
- ### Generator Mode (Recommended)
160
-
161
- **Pros:**
162
-
163
- - ⚡ **Fastest** - SQL pre-generated at build time
164
- - 🎯 **Zero overhead** for known queries
165
- - 🔨 **Auto-fallback** to runtime for unknown queries
166
- - 📊 **Track performance** with `prebaked` flag
167
-
168
- **Cons:**
169
-
170
- - Requires `npx prisma generate` step
171
- - Need to define common queries upfront
172
-
173
- **When to use:**
174
-
175
- - ✅ Production applications
176
- - ✅ Known query patterns (dashboards, APIs)
177
- - ✅ Want maximum performance
178
- - ✅ Build step is acceptable
179
-
180
- ### Runtime Mode
181
-
182
- **Pros:**
183
-
184
- - 🚀 **Simple** - no build step
185
- - 🔧 **Flexible** - handles any query
186
- - 📝 **Easy prototyping**
187
-
188
- **Cons:**
189
-
190
- - Small overhead (~0.2ms) for SQL generation
191
- - Still 2-7x faster than Prisma
192
-
193
- **When to use:**
194
-
195
- - ✅ Rapid prototyping
196
- - ✅ Fully dynamic queries
197
- - ✅ No build step desired
198
- - ✅ Query patterns unknown
199
-
200
- ## Generator Mode Details
201
-
202
- ### How It Works
203
-
204
- ```
205
- Build Time:
206
- schema.prisma
207
-
208
- /// @sql.findMany({ where: { status: "ACTIVE" } })
209
-
210
- npx prisma generate
211
-
212
- .prisma/client/sql/index.ts
213
-
214
- QUERIES = {
215
- User: {
216
- '{"where":{"status":"ACTIVE"}}': {
217
- sql: 'SELECT * FROM users WHERE status = $1',
218
- params: ['ACTIVE'],
219
- dynamicKeys: []
220
- }
221
- }
222
- }
223
-
224
- Runtime:
225
- prisma.user.findMany({ where: { status: 'ACTIVE' } })
226
-
227
- Normalize query → '{"where":{"status":"ACTIVE"}}'
228
-
229
- QUERIES.User[query] found?
230
-
231
- YES → ⚡ Use prebaked SQL (0.03ms overhead)
232
-
233
- NO → 🔨 Generate SQL runtime (0.2ms overhead)
234
-
235
- Execute via postgres.js/better-sqlite3
236
- ```
237
-
238
- ### Generator Configuration
239
-
240
- ```prisma
241
- generator sql {
242
- provider = "prisma-sql-generator"
243
-
244
- // Optional: Override auto-detected dialect (rarely needed)
245
- // dialect = "postgres" // or "sqlite"
246
-
247
- // Optional: Output directory (default: ../node_modules/.prisma/client/sql)
248
- // output = "./generated/sql"
249
-
250
- // Optional: Skip invalid directives instead of failing (default: false)
251
- // skipInvalid = "true"
252
- }
253
- ```
254
-
255
- ### Supported Directive Patterns
256
-
257
- ```prisma
258
- model User {
259
- // Simple queries
260
- /// @sql.findMany({ where: { status: "ACTIVE" } })
261
- /// @sql.findFirst({ where: { email: "test@example.com" } })
262
- /// @sql.findUnique({ where: { id: 1 } })
263
-
264
- // With relations
265
- /// @sql.findMany({ include: { posts: true } })
266
- /// @sql.findMany({ include: { posts: { where: { published: true } } } })
267
-
268
- // With pagination
269
- /// @sql.findMany({ take: 10, skip: 20, orderBy: { createdAt: "desc" } })
270
-
271
- // Aggregations
272
- /// @sql.count({ where: { status: "ACTIVE" } })
273
- /// @sql.aggregate({ _count: { _all: true }, _avg: { age: true } })
274
- /// @sql.groupBy({ by: ["status"], _count: { _all: true } })
275
-
276
- // Dynamic parameters
277
- /// @sql.findMany({ where: { status: { $param: "status" } } })
278
- /// @sql.findMany({ where: { age: { gte: { $param: "minAge" } } } })
279
- }
280
- ```
281
-
282
- ### Dynamic Parameters
283
-
284
- Use `$param` for runtime values:
285
-
286
- ```prisma
287
- model User {
288
- /// @sql.findMany({ where: { status: { $param: "status" } } })
289
- /// @sql.findMany({ where: { age: { gte: { $param: "minAge" } } } })
290
- }
291
- ```
292
-
293
- ```typescript
294
- // Prebaked SQL with runtime parameters
295
- const users = await prisma.user.findMany({
296
- where: { status: 'ACTIVE' }, // Matches directive with $param
297
- })
298
-
299
- // SQL: SELECT * FROM users WHERE status = $1
300
- // Params: ['ACTIVE']
301
- ```
302
-
303
- ### Performance Tracking
304
-
305
- ```typescript
306
- const prisma = new PrismaClient().$extends(
307
- createExtension({
308
- postgres: sql,
309
- debug: true,
310
- onQuery: (info) => {
311
- console.log(`${info.model}.${info.method}: ${info.duration}ms`)
312
- console.log(info.prebaked ? '⚡ PREBAKED' : '🔨 RUNTIME')
313
- },
314
- }),
315
- )
316
- ```
92
+ That's it! All your read queries are now 2-7x faster with zero code changes.
317
93
 
318
- ## What Gets Faster
94
+ ## Performance
319
95
 
320
- **Accelerated (via raw SQL):**
96
+ Benchmarks from 137 E2E tests comparing identical queries:
321
97
 
322
- - `findMany`, `findFirst`, `findUnique`
323
- - `count`
324
- - `aggregate` (\_count, \_sum, \_avg, \_min, \_max)
325
- - `groupBy` with having clauses
98
+ ### PostgreSQL Results (Highlights)
326
99
 
327
- **Unchanged (still uses Prisma):**
100
+ | Query Type | Prisma v6 | Prisma v7 | This Extension | Speedup vs v7 |
101
+ | ------------------- | --------- | --------- | -------------- | ------------- |
102
+ | Simple where | 0.40ms | 0.34ms | 0.17ms | **2.0x** ⚡ |
103
+ | Complex conditions | 13.10ms | 6.90ms | 2.37ms | **2.9x** ⚡ |
104
+ | With relations | 0.83ms | 0.72ms | 0.41ms | **1.8x** ⚡ |
105
+ | Nested relations | 28.35ms | 14.34ms | 4.81ms | **3.0x** ⚡ |
106
+ | Aggregations | 0.42ms | 0.44ms | 0.24ms | **1.8x** ⚡ |
107
+ | Multi-field orderBy | 3.97ms | 2.38ms | 1.09ms | **2.2x** ⚡ |
328
108
 
329
- - `create`, `update`, `delete`, `upsert`
330
- - `createMany`, `updateMany`, `deleteMany`
331
- - Transactions (`$transaction`)
332
- - Middleware
109
+ **Overall:** 2.10x faster than Prisma v7, 2.39x faster than v6
333
110
 
334
- ---
111
+ ### SQLite Results (Highlights)
335
112
 
336
- ## Performance
113
+ | Query Type | Prisma v6 | Prisma v7 | This Extension | Speedup vs v7 |
114
+ | ------------------ | --------- | --------- | -------------- | ------------- |
115
+ | Simple where | 0.45ms | 0.23ms | 0.03ms | **7.7x** ⚡ |
116
+ | Complex conditions | 10.32ms | 3.87ms | 0.93ms | **4.2x** ⚡ |
117
+ | Relation filters | 166.62ms | 128.44ms | 2.40ms | **53.5x** ⚡ |
118
+ | Count queries | 0.17ms | 0.07ms | 0.01ms | **7.0x** ⚡ |
337
119
 
338
- Benchmarks from 137 E2E tests comparing identical queries against Prisma v6, Prisma v7, and Drizzle:
120
+ **Overall:** 5.48x faster than Prisma v7, 7.51x faster than v6
339
121
 
340
- ### BENCHMARK RESULTS - Prisma v6 vs v7 vs Generated SQL
122
+ <details>
123
+ <summary><b>View Full Benchmark Results (137 queries)</b></summary>
341
124
 
342
- ## POSTGRES Results:
125
+ ### POSTGRES - Complete Results
343
126
 
344
127
  | Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
345
128
  | ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
@@ -403,17 +186,7 @@ Benchmarks from 137 E2E tests comparing identical queries against Prisma v6, Pri
403
186
  | ILIKE special chars | 0.22ms | 0.26ms | 0.14ms | N/A | 1.54x | 1.76x |
404
187
  | LIKE case sensitive | 0.19ms | 0.22ms | 0.12ms | N/A | 1.62x | 1.85x |
405
188
 
406
- ##### Summary:
407
-
408
- - Generated SQL vs Prisma v6: **2.39x faster**
409
- - Generated SQL vs Prisma v7: **2.10x faster**
410
- - Generated SQL vs Drizzle: **1.53x faster**
411
-
412
- ---
413
-
414
- #### SQLITE:
415
-
416
- ---
189
+ ### SQLITE - Complete Results
417
190
 
418
191
  | Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
419
192
  | ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
@@ -474,48 +247,27 @@ Benchmarks from 137 E2E tests comparing identical queries against Prisma v6, Pri
474
247
  | \_count relation | 0.62ms | 0.46ms | 0.32ms | N/A | 1.93x | 1.44x |
475
248
  | \_count multi-relation | 0.14ms | 0.17ms | 0.04ms | N/A | 3.22x | 4.09x |
476
249
 
477
- ##### Summary:
478
-
479
- - Generated SQL vs Prisma v6: **7.51x faster**
480
- - Generated SQL vs Prisma v7: **5.48x faster**
481
- - Generated SQL vs Drizzle: **2.61x faster**
482
-
483
- > **Note on Prisma v7:** Prisma v7 introduced significant performance improvements (39% faster than v6 on PostgreSQL, 24% faster on SQLite), but this extension still provides 2-7x additional speedup over v7.
484
-
485
- > **Benchmarks:** These are representative results from our test suite running on a MacBook Pro M1 with PostgreSQL 15 and SQLite 3.43. Your mileage may vary based on:
486
- >
487
- > - Database configuration and indexes
488
- > - Query complexity and data volume
489
- > - Hardware and network latency
490
- > - Concurrent load
491
- >
492
- > Run benchmarks with your own schema and data for accurate measurements. See [Benchmarking](#benchmarking) section below.
250
+ </details>
493
251
 
494
- ---
495
-
496
- ## Configuration
497
-
498
- ### Basic Configuration
252
+ > **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.
499
253
 
500
- ```typescript
501
- import { Prisma } from '@prisma/client'
502
- import { convertDMMFToModels } from 'prisma-sql'
254
+ ## What Gets Faster
503
255
 
504
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
256
+ **Accelerated (via raw SQL):**
505
257
 
506
- speedExtension({
507
- postgres: sql,
508
- models,
258
+ - ✅ `findMany`, `findFirst`, `findUnique`
259
+ - ✅ `count`
260
+ - ✅ `aggregate` (\_count, \_sum, \_avg, \_min, \_max)
261
+ - ✅ `groupBy` with having clauses
509
262
 
510
- debug: true,
263
+ **Unchanged (still uses Prisma):**
511
264
 
512
- allowedModels: ['User', 'Post'],
265
+ - `create`, `update`, `delete`, `upsert`
266
+ - `createMany`, `updateMany`, `deleteMany`
267
+ - Transactions (`$transaction`)
268
+ - Middleware
513
269
 
514
- onQuery: (info) => {
515
- console.log(`${info.model}.${info.method}: ${info.duration}ms`)
516
- },
517
- })
518
- ```
270
+ ## Configuration
519
271
 
520
272
  ### Debug Mode
521
273
 
@@ -527,25 +279,13 @@ import { Prisma } from '@prisma/client'
527
279
 
528
280
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
529
281
 
530
- speedExtension({
531
- postgres: sql,
532
- models,
533
- debug: true,
534
- })
535
- ```
536
-
537
- ### Selective Models
538
-
539
- Only accelerate specific models:
540
-
541
- ```typescript
542
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
543
-
544
- speedExtension({
545
- postgres: sql,
546
- models,
547
- allowedModels: ['User', 'Post'],
548
- })
282
+ const prisma = new PrismaClient().$extends(
283
+ speedExtension({
284
+ postgres: sql,
285
+ models,
286
+ debug: true, // Logs SQL for every query
287
+ }),
288
+ )
549
289
  ```
550
290
 
551
291
  ### Performance Monitoring
@@ -555,162 +295,76 @@ Track query performance:
555
295
  ```typescript
556
296
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
557
297
 
558
- speedExtension({
559
- postgres: sql,
560
- models,
561
- onQuery: (info) => {
562
- console.log(`${info.model}.${info.method} completed in ${info.duration}ms`)
298
+ const prisma = new PrismaClient().$extends(
299
+ speedExtension({
300
+ postgres: sql,
301
+ models,
302
+ onQuery: (info) => {
303
+ console.log(`${info.model}.${info.method}: ${info.duration}ms`)
563
304
 
564
- if (info.duration > 100) {
565
- logger.warn('Slow query detected', {
566
- model: info.model,
567
- method: info.method,
568
- sql: info.sql,
569
- })
570
- }
571
- },
572
- })
305
+ if (info.duration > 100) {
306
+ logger.warn('Slow query', {
307
+ model: info.model,
308
+ method: info.method,
309
+ sql: info.sql,
310
+ })
311
+ }
312
+ },
313
+ }),
314
+ )
573
315
  ```
574
316
 
575
- ## Advanced Usage
576
-
577
- ### Read Replicas
578
-
579
- Send writes to primary, reads to replica:
317
+ The `onQuery` callback receives:
580
318
 
581
319
  ```typescript
582
- import { convertDMMFToModels } from 'prisma-sql'
583
- import { Prisma } from '@prisma/client'
584
-
585
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
586
-
587
- const primary = new PrismaClient()
588
-
589
- const replica = postgres(process.env.REPLICA_URL)
590
- const fastPrisma = new PrismaClient().$extends(
591
- speedExtension({ postgres: replica, models })
592
- )
593
-
594
- await primary.user.create({ data: { ... } })
595
- const users = await fastPrisma.user.findMany()
320
+ interface QueryInfo {
321
+ model: string // "User"
322
+ method: string // "findMany"
323
+ sql: string // The executed SQL
324
+ params: unknown[] // SQL parameters
325
+ duration: number // Query duration in ms
326
+ prebaked: boolean // true if using generated SQL (see Advanced Usage)
327
+ }
596
328
  ```
597
329
 
598
- ### Connection Pooling
330
+ ### Selective Models
599
331
 
600
- Configure postgres.js connection pool:
332
+ Only accelerate specific models:
601
333
 
602
334
  ```typescript
603
335
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
604
336
 
605
- const sql = postgres(process.env.DATABASE_URL, {
606
- max: 20,
607
- idle_timeout: 20,
608
- connect_timeout: 10,
609
- ssl: 'require',
610
- })
611
-
612
337
  const prisma = new PrismaClient().$extends(
613
- speedExtension({ postgres: sql, models }),
338
+ speedExtension({
339
+ postgres: sql,
340
+ models,
341
+ allowedModels: ['User', 'Post'], // Only speed up these models
342
+ }),
614
343
  )
615
344
  ```
616
345
 
617
- ### Gradual Rollout
346
+ ## Supported Queries
618
347
 
619
- Feature-flag the extension for safe rollout:
620
-
621
- ```typescript
622
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
623
- const USE_FAST_READS = process.env.FAST_READS === 'true'
624
-
625
- const sql = postgres(DATABASE_URL)
626
- const prisma = new PrismaClient()
627
-
628
- const db = USE_FAST_READS
629
- ? prisma.$extends(speedExtension({ postgres: sql, models }))
630
- : prisma
631
- ```
632
-
633
- ### Access Original Prisma
634
-
635
- ```typescript
636
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
637
- const prisma = new PrismaClient().$extends(
638
- speedExtension({ postgres: sql, models }),
639
- )
640
-
641
- const fast = await prisma.user.findMany()
642
-
643
- const slow = await prisma.$original.user.findMany()
644
- ```
645
-
646
- ## Edge Runtime
647
-
648
- ### Vercel Edge Functions
649
-
650
- ```typescript
651
- import { PrismaClient, Prisma } from '@prisma/client'
652
- import { speedExtension, convertDMMFToModels } from 'prisma-sql'
653
- import postgres from 'postgres'
654
-
655
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
656
- const sql = postgres(process.env.DATABASE_URL)
657
- const prisma = new PrismaClient().$extends(
658
- speedExtension({ postgres: sql, models }),
659
- )
660
-
661
- export const config = { runtime: 'edge' }
662
-
663
- export default async function handler(req: Request) {
664
- const users = await prisma.user.findMany()
665
- return Response.json(users)
666
- }
667
- ```
668
-
669
- ### Cloudflare Workers
670
-
671
- For Cloudflare Workers, use the standalone SQL generation API instead of the extension:
672
-
673
- ```typescript
674
- import { createToSQL, convertDMMFToModels } from 'prisma-sql'
675
- import { Prisma } from '@prisma/client'
676
-
677
- const models = convertDMMFToModels(Prisma.dmmf.datamodel)
678
- const toSQL = createToSQL(models, 'sqlite')
679
-
680
- export default {
681
- async fetch(request: Request, env: Env) {
682
- const { sql, params } = toSQL('User', 'findMany', {
683
- where: { status: 'ACTIVE' },
684
- })
685
-
686
- const result = await env.DB.prepare(sql)
687
- .bind(...params)
688
- .all()
689
- return Response.json(result.results)
690
- },
691
- }
692
- ```
693
-
694
- > **Note:** The Prisma Client extension is not recommended for Cloudflare Workers due to cold start overhead. Use the `createToSQL` API for edge deployments.
695
-
696
- ## Supported Queries
697
-
698
- ### Filters
348
+ ### Filters
699
349
 
700
350
  ```typescript
351
+ // Comparisons
701
352
  { age: { gt: 18, lte: 65 } }
702
353
  { status: { in: ['ACTIVE', 'PENDING'] } }
703
354
  { status: { notIn: ['DELETED'] } }
704
355
 
356
+ // String operations
705
357
  { email: { contains: '@example.com' } }
706
358
  { email: { startsWith: 'user' } }
707
359
  { email: { endsWith: '.com' } }
708
360
  { email: { contains: 'EXAMPLE', mode: 'insensitive' } }
709
361
 
362
+ // Boolean logic
710
363
  { AND: [{ status: 'ACTIVE' }, { verified: true }] }
711
364
  { OR: [{ role: 'ADMIN' }, { role: 'MODERATOR' }] }
712
365
  { NOT: { status: 'DELETED' } }
713
366
 
367
+ // Null checks
714
368
  { deletedAt: null }
715
369
  { deletedAt: { not: null } }
716
370
  ```
@@ -718,6 +372,7 @@ export default {
718
372
  ### Relations
719
373
 
720
374
  ```typescript
375
+ // Include relations
721
376
  {
722
377
  include: {
723
378
  posts: true,
@@ -725,6 +380,7 @@ export default {
725
380
  }
726
381
  }
727
382
 
383
+ // Nested includes with filters
728
384
  {
729
385
  include: {
730
386
  posts: {
@@ -736,27 +392,22 @@ export default {
736
392
  }
737
393
  }
738
394
 
395
+ // Relation filters
739
396
  {
740
397
  where: {
741
- posts: {
742
- some: { published: true }
743
- }
398
+ posts: { some: { published: true } }
744
399
  }
745
400
  }
746
401
 
747
402
  {
748
403
  where: {
749
- posts: {
750
- every: { published: true }
751
- }
404
+ posts: { every: { published: true } }
752
405
  }
753
406
  }
754
407
 
755
408
  {
756
409
  where: {
757
- posts: {
758
- none: { published: false }
759
- }
410
+ posts: { none: { published: false } }
760
411
  }
761
412
  }
762
413
  ```
@@ -764,12 +415,14 @@ export default {
764
415
  ### Pagination & Ordering
765
416
 
766
417
  ```typescript
418
+ // Basic pagination
767
419
  {
768
420
  take: 10,
769
421
  skip: 20,
770
422
  orderBy: { createdAt: 'desc' }
771
423
  }
772
424
 
425
+ // Cursor-based pagination
773
426
  {
774
427
  cursor: { id: 100 },
775
428
  take: 10,
@@ -777,6 +430,7 @@ export default {
777
430
  orderBy: { id: 'asc' }
778
431
  }
779
432
 
433
+ // Multi-field ordering
780
434
  {
781
435
  orderBy: [
782
436
  { status: 'asc' },
@@ -784,22 +438,15 @@ export default {
784
438
  { createdAt: 'desc' }
785
439
  ]
786
440
  }
787
-
788
- {
789
- orderBy: {
790
- name: {
791
- sort: 'asc',
792
- nulls: 'last'
793
- }
794
- }
795
- }
796
441
  ```
797
442
 
798
443
  ### Aggregations
799
444
 
800
445
  ```typescript
446
+ // Count
801
447
  await prisma.user.count({ where: { status: 'ACTIVE' } })
802
448
 
449
+ // Aggregate
803
450
  await prisma.task.aggregate({
804
451
  where: { status: 'DONE' },
805
452
  _count: { _all: true },
@@ -809,6 +456,7 @@ await prisma.task.aggregate({
809
456
  _max: { completedAt: true },
810
457
  })
811
458
 
459
+ // Group by
812
460
  await prisma.task.groupBy({
813
461
  by: ['status', 'priority'],
814
462
  _count: { _all: true },
@@ -821,73 +469,311 @@ await prisma.task.groupBy({
821
469
  })
822
470
  ```
823
471
 
824
- ### Distinct
472
+ ## Advanced Usage
473
+
474
+ ### Generator Mode - Prebaked SQL Queries
825
475
 
826
- ```typescript
827
- {
828
- distinct: ['status'],
829
- orderBy: { status: 'asc' }
476
+ For maximum performance, prebake your most common queries at build time. This reduces overhead from ~0.2ms (runtime) to ~0.03ms.
477
+
478
+ **1. Add generator to your schema:**
479
+
480
+ ```prisma
481
+ // schema.prisma
482
+ generator sql {
483
+ provider = "prisma-sql-generator"
830
484
  }
485
+ ```
831
486
 
832
- {
833
- distinct: ['status', 'priority'],
834
- orderBy: [
835
- { status: 'asc' },
836
- { priority: 'asc' }
837
- ]
487
+ **2. Add optimize directives to your models:**
488
+
489
+ ```prisma
490
+ /// @optimize {
491
+ /// "method": "findMany",
492
+ /// "query": {
493
+ /// "skip": "$skip",
494
+ /// "take": "$take",
495
+ /// "orderBy": { "createdAt": "desc" },
496
+ /// "where": { "status": "ACTIVE" }
497
+ /// }
498
+ /// }
499
+ /// @optimize { "method": "count", "query": {} }
500
+ model User {
501
+ id Int @id @default(autoincrement())
502
+ email String @unique
503
+ status String
504
+ createdAt DateTime @default(now())
505
+ posts Post[]
838
506
  }
839
507
  ```
840
508
 
841
- ## Migration Guide
509
+ **3. Generate:**
842
510
 
843
- ### From Prisma Client
511
+ ```bash
512
+ npx prisma generate
513
+ ```
844
514
 
845
- **Before:**
515
+ **4. Use the generated extension:**
846
516
 
847
517
  ```typescript
848
- const prisma = new PrismaClient()
849
- const users = await prisma.user.findMany()
518
+ import { PrismaClient } from '@prisma/client'
519
+ import { createExtension } from '../path/to/generated/sql'
520
+ import postgres from 'postgres'
521
+
522
+ const sql = postgres(process.env.DATABASE_URL)
523
+ const prisma = new PrismaClient().$extends(createExtension({ postgres: sql }))
524
+
525
+ // ⚡ PREBAKED - Uses pre-generated SQL (~0.03ms overhead)
526
+ const activeUsers = await prisma.user.findMany({
527
+ where: { status: 'ACTIVE' },
528
+ skip: 0,
529
+ take: 10,
530
+ orderBy: { createdAt: 'desc' },
531
+ })
532
+
533
+ // 🔨 RUNTIME - Generates SQL on-the-fly (~0.2ms overhead, still fast!)
534
+ const searchUsers = await prisma.user.findMany({
535
+ where: { email: { contains: '@example.com' } },
536
+ })
850
537
  ```
851
538
 
852
- **After (Generator Mode):**
539
+ The extension automatically:
540
+
541
+ - Uses prebaked SQL for matching queries (instant)
542
+ - Falls back to runtime generation for non-matching queries (still fast)
543
+ - Tracks which queries are prebaked via `onQuery` callback
544
+
545
+ **Dynamic Parameters:**
546
+
547
+ Use `$paramName` syntax for runtime values:
548
+
549
+ ```prisma
550
+ /// @optimize {
551
+ /// "method": "findMany",
552
+ /// "query": {
553
+ /// "where": { "status": "$status" },
554
+ /// "skip": "$skip",
555
+ /// "take": "$take"
556
+ /// }
557
+ /// }
558
+ model User {
559
+ id Int @id
560
+ status String
561
+ }
562
+ ```
563
+
564
+ **Generator Configuration:**
853
565
 
854
566
  ```prisma
855
- // schema.prisma
856
567
  generator sql {
857
568
  provider = "prisma-sql-generator"
858
- }
859
569
 
860
- model User {
861
- /// @sql.findMany({ where: { status: "ACTIVE" } })
570
+ // Optional: Override auto-detected dialect
571
+ // dialect = "postgres" // or "sqlite"
572
+
573
+ // Optional: Custom output directory
574
+ // output = "./generated/sql"
575
+
576
+ // Optional: Skip invalid directives instead of failing
577
+ // skipInvalid = "true"
862
578
  }
863
579
  ```
864
580
 
581
+ ### Access Original Prisma
582
+
865
583
  ```typescript
866
- import { createExtension } from '.prisma/client/sql'
867
- import postgres from 'postgres'
584
+ const models = convertDMMFToModels(Prisma.dmmf.datamodel)
585
+ const prisma = new PrismaClient().$extends(
586
+ speedExtension({ postgres: sql, models }),
587
+ )
868
588
 
869
- const sql = postgres(DATABASE_URL)
870
- const prisma = new PrismaClient().$extends(createExtension({ postgres: sql }))
589
+ // Fast (via raw SQL)
590
+ const fast = await prisma.user.findMany()
871
591
 
872
- const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
592
+ // Slow (via Prisma engine)
593
+ const slow = await prisma.$original.user.findMany()
873
594
  ```
874
595
 
875
- **After (Runtime Mode):**
596
+ ### Edge Runtime
597
+
598
+ **Vercel Edge Functions:**
876
599
 
877
600
  ```typescript
878
- import postgres from 'postgres'
601
+ import { PrismaClient, Prisma } from '@prisma/client'
879
602
  import { speedExtension, convertDMMFToModels } from 'prisma-sql'
880
- import { Prisma } from '@prisma/client'
603
+ import postgres from 'postgres'
881
604
 
882
605
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
883
- const sql = postgres(DATABASE_URL)
606
+ const sql = postgres(process.env.DATABASE_URL)
884
607
  const prisma = new PrismaClient().$extends(
885
608
  speedExtension({ postgres: sql, models }),
886
609
  )
887
610
 
611
+ export const config = { runtime: 'edge' }
612
+
613
+ export default async function handler(req: Request) {
614
+ const users = await prisma.user.findMany()
615
+ return Response.json(users)
616
+ }
617
+ ```
618
+
619
+ **Cloudflare Workers:**
620
+
621
+ For Cloudflare Workers, use the standalone SQL generation API:
622
+
623
+ ```typescript
624
+ import { createToSQL, convertDMMFToModels } from 'prisma-sql'
625
+ import { Prisma } from '@prisma/client'
626
+
627
+ const models = convertDMMFToModels(Prisma.dmmf.datamodel)
628
+ const toSQL = createToSQL(models, 'sqlite')
629
+
630
+ export default {
631
+ async fetch(request: Request, env: Env) {
632
+ const { sql, params } = toSQL('User', 'findMany', {
633
+ where: { status: 'ACTIVE' },
634
+ })
635
+
636
+ const result = await env.DB.prepare(sql)
637
+ .bind(...params)
638
+ .all()
639
+ return Response.json(result.results)
640
+ },
641
+ }
642
+ ```
643
+
644
+ ## Generator Mode Details
645
+
646
+ ### How It Works
647
+
648
+ ```
649
+ Build Time:
650
+ schema.prisma
651
+
652
+ /// @optimize { "method": "findMany", "query": { "where": { "status": "ACTIVE" } } }
653
+
654
+ npx prisma generate
655
+
656
+ generated/sql/index.ts
657
+
658
+ QUERIES = {
659
+ User: {
660
+ findMany: {
661
+ '{"where":{"status":"ACTIVE"}}': {
662
+ sql: 'SELECT * FROM users WHERE status = $1',
663
+ params: ['ACTIVE'],
664
+ dynamicKeys: []
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ Runtime:
671
+ prisma.user.findMany({ where: { status: 'ACTIVE' } })
672
+
673
+ Normalize query → '{"where":{"status":"ACTIVE"}}'
674
+
675
+ QUERIES.User.findMany[query] found?
676
+
677
+ YES → ⚡ Use prebaked SQL (0.03ms overhead)
678
+
679
+ NO → 🔨 Generate SQL runtime (0.2ms overhead)
680
+
681
+ Execute via postgres.js/better-sqlite3
682
+ ```
683
+
684
+ ### Optimize Directive Examples
685
+
686
+ **Basic query:**
687
+
688
+ ```prisma
689
+ /// @optimize {
690
+ /// "method": "findMany",
691
+ /// "query": {
692
+ /// "where": { "status": "ACTIVE" }
693
+ /// }
694
+ /// }
695
+ model User {
696
+ id Int @id
697
+ status String
698
+ }
699
+ ```
700
+
701
+ **With pagination:**
702
+
703
+ ```prisma
704
+ /// @optimize {
705
+ /// "method": "findMany",
706
+ /// "query": {
707
+ /// "skip": "$skip",
708
+ /// "take": "$take",
709
+ /// "orderBy": { "createdAt": "desc" }
710
+ /// }
711
+ /// }
712
+ ```
713
+
714
+ **With relations:**
715
+
716
+ ```prisma
717
+ /// @optimize {
718
+ /// "method": "findMany",
719
+ /// "query": {
720
+ /// "include": {
721
+ /// "posts": {
722
+ /// "where": { "published": true },
723
+ /// "orderBy": { "createdAt": "desc" },
724
+ /// "take": 5
725
+ /// }
726
+ /// }
727
+ /// }
728
+ /// }
729
+ ```
730
+
731
+ **Complex query:**
732
+
733
+ ```prisma
734
+ /// @optimize {
735
+ /// "method": "findMany",
736
+ /// "query": {
737
+ /// "skip": "$skip",
738
+ /// "take": "$take",
739
+ /// "orderBy": { "createdAt": "desc" },
740
+ /// "include": {
741
+ /// "company": {
742
+ /// "where": { "deletedAt": null },
743
+ /// "select": { "id": true, "name": true }
744
+ /// }
745
+ /// }
746
+ /// }
747
+ /// }
748
+ ```
749
+
750
+ ## Migration Guide
751
+
752
+ ### From Prisma Client
753
+
754
+ **Before:**
755
+
756
+ ```typescript
757
+ const prisma = new PrismaClient()
888
758
  const users = await prisma.user.findMany()
889
759
  ```
890
760
 
761
+ **After:**
762
+
763
+ ```typescript
764
+ import { PrismaClient, Prisma } from '@prisma/client'
765
+ import { speedExtension, convertDMMFToModels } from 'prisma-sql'
766
+ import postgres from 'postgres'
767
+
768
+ const models = convertDMMFToModels(Prisma.dmmf.datamodel)
769
+ const sql = postgres(process.env.DATABASE_URL)
770
+ const prisma = new PrismaClient().$extends(
771
+ speedExtension({ postgres: sql, models }),
772
+ )
773
+
774
+ const users = await prisma.user.findMany() // Same API, just faster
775
+ ```
776
+
891
777
  ### From Drizzle
892
778
 
893
779
  **Before:**
@@ -908,8 +794,9 @@ const users = await db
908
794
  **After:**
909
795
 
910
796
  ```typescript
797
+ import { PrismaClient, Prisma } from '@prisma/client'
911
798
  import { speedExtension, convertDMMFToModels } from 'prisma-sql'
912
- import { Prisma } from '@prisma/client'
799
+ import postgres from 'postgres'
913
800
 
914
801
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
915
802
  const sql = postgres(DATABASE_URL)
@@ -922,67 +809,25 @@ const users = await prisma.user.findMany({
922
809
  })
923
810
  ```
924
811
 
925
- ## Local Development
926
-
927
- ### Testing the Generator Locally
928
-
929
- **1. Build the package:**
930
-
931
- ```bash
932
- cd prisma-sql
933
- npm install
934
- npm run build
935
- ```
936
-
937
- **2. Link globally:**
938
-
939
- ```bash
940
- npm link
941
- ```
942
-
943
- **3. Use in your project:**
944
-
945
- ```bash
946
- cd your-project
947
- npm link prisma-sql
948
- npx prisma generate
949
- ```
950
-
951
- **Alternative: Use file path directly in schema:**
952
-
953
- ```prisma
954
- generator sql {
955
- provider = "node ../path/to/prisma-sql/dist/generator.cjs"
956
- }
957
- ```
958
-
959
- **Unlink when done:**
960
-
961
- ```bash
962
- npm unlink prisma-sql # In your project
963
- cd prisma-sql
964
- npm unlink # In prisma-sql directory
965
- ```
966
-
967
812
  ## Limitations
968
813
 
969
814
  ### Partially Supported
970
815
 
971
816
  These features work but have limitations:
972
817
 
973
- - ⚠️ **Array operations**: Basic operations (`has`, `hasSome`, `hasEvery`, `isEmpty`) work. Advanced filtering like `array_contains(array_field, [1,2,3])` not yet supported.
974
- - ⚠️ **JSON operations**: Path-based filtering works (`json.path(['field'], { equals: 'value' })`). Advanced JSON functions not yet supported.
818
+ - ⚠️ **Array operations**: Basic operations (`has`, `hasSome`, `hasEvery`, `isEmpty`) work. Advanced filtering not yet supported.
819
+ - ⚠️ **JSON operations**: Path-based filtering works. Advanced JSON functions not yet supported.
975
820
 
976
821
  ### Not Yet Supported
977
822
 
978
- These Prisma features are not supported and will fall back to Prisma Client:
823
+ These Prisma features will fall back to Prisma Client:
979
824
 
980
825
  - ❌ Full-text search (`search` operator)
981
826
  - ❌ Composite types (MongoDB-style embedded documents)
982
827
  - ❌ Raw database features (PostGIS, pg_trgm, etc.)
983
- - ❌ Some advanced aggregations in `groupBy` (nested aggregations)
828
+ - ❌ Some advanced aggregations in `groupBy`
984
829
 
985
- If you encounter unsupported queries, enable `debug: true` to see which queries are being converted and which fall back to Prisma.
830
+ Enable `debug: true` to see which queries are accelerated vs fallback.
986
831
 
987
832
  ### Database Support
988
833
 
@@ -1013,23 +858,9 @@ const prisma = new PrismaClient().$extends(
1013
858
  )
1014
859
  ```
1015
860
 
1016
- ### "Generator: Cannot find module"
1017
-
1018
- The package hasn't been built or linked:
1019
-
1020
- ```bash
1021
- # In prisma-sql directory
1022
- npm run build
1023
- npm link
1024
-
1025
- # In your project
1026
- npm link prisma-sql
1027
- npx prisma generate
1028
- ```
1029
-
1030
861
  ### "Results don't match Prisma Client"
1031
862
 
1032
- Enable debug mode to inspect generated SQL:
863
+ Enable debug mode and compare SQL:
1033
864
 
1034
865
  ```typescript
1035
866
  const models = convertDMMFToModels(Prisma.dmmf.datamodel)
@@ -1037,7 +868,7 @@ const models = convertDMMFToModels(Prisma.dmmf.datamodel)
1037
868
  speedExtension({
1038
869
  postgres: sql,
1039
870
  models,
1040
- debug: true,
871
+ debug: true, // Shows generated SQL
1041
872
  })
1042
873
  ```
1043
874
 
@@ -1055,19 +886,10 @@ Increase postgres.js pool size:
1055
886
 
1056
887
  ```typescript
1057
888
  const sql = postgres(DATABASE_URL, {
1058
- max: 50,
889
+ max: 50, // Default is 10
1059
890
  })
1060
891
  ```
1061
892
 
1062
- ### "Type errors after extending"
1063
-
1064
- Ensure `@prisma/client` is up to date:
1065
-
1066
- ```bash
1067
- npm update @prisma/client
1068
- npx prisma generate
1069
- ```
1070
-
1071
893
  ### "Performance not improving"
1072
894
 
1073
895
  Some queries won't see dramatic improvements:
@@ -1090,6 +912,48 @@ speedExtension({
1090
912
  })
1091
913
  ```
1092
914
 
915
+ ## Local Development
916
+
917
+ ### Testing the Generator Locally
918
+
919
+ **1. Build the package:**
920
+
921
+ ```bash
922
+ cd prisma-sql
923
+ npm install
924
+ npm run build
925
+ ```
926
+
927
+ **2. Link globally:**
928
+
929
+ ```bash
930
+ npm link
931
+ ```
932
+
933
+ **3. Use in your project:**
934
+
935
+ ```bash
936
+ cd your-project
937
+ npm link prisma-sql
938
+ npx prisma generate
939
+ ```
940
+
941
+ **Alternative: Use file path directly in schema:**
942
+
943
+ ```prisma
944
+ generator sql {
945
+ provider = "node ../path/to/prisma-sql/dist/generator.cjs"
946
+ }
947
+ ```
948
+
949
+ **Unlink when done:**
950
+
951
+ ```bash
952
+ npm unlink prisma-sql # In your project
953
+ cd prisma-sql
954
+ npm unlink # In prisma-sql directory
955
+ ```
956
+
1093
957
  ## FAQ
1094
958
 
1095
959
  **Q: Do I need to keep using Prisma Client?**
@@ -1099,7 +963,7 @@ A: Yes. You need Prisma for schema management, migrations, types, and write oper
1099
963
  A: Yes. No schema changes required. It works with your existing Prisma schema and generated client.
1100
964
 
1101
965
  **Q: What about writes (create, update, delete)?**
1102
- A: Writes still use Prisma Client. This extension only accelerates reads. For write-heavy workloads, this provides less benefit.
966
+ A: Writes still use Prisma Client. This extension only accelerates reads.
1103
967
 
1104
968
  **Q: Is it production ready?**
1105
969
  A: Yes. 137 E2E tests verify exact parity with Prisma Client across both Prisma v6 and v7. Used in production.
@@ -1108,29 +972,22 @@ A: Yes. 137 E2E tests verify exact parity with Prisma Client across both Prisma
1108
972
  A: Yes. Works with any PostgreSQL-compatible database. Just pass the connection string to postgres.js.
1109
973
 
1110
974
  **Q: Does it support Prisma middlewares?**
1111
- A: The extension runs after middlewares. If you need middleware to see the actual SQL, use Prisma's query logging.
975
+ A: The extension runs after middlewares. For middleware to see actual SQL, use Prisma's query logging.
1112
976
 
1113
977
  **Q: Can I still use `$queryRaw` and `$executeRaw`?**
1114
- A: Yes. Those methods are unaffected. You also still have direct access to the postgres.js client.
978
+ A: Yes. Those methods are unaffected.
1115
979
 
1116
980
  **Q: What's the overhead of SQL generation?**
1117
- A: Generator mode: ~0.03ms (prebaked queries). Runtime mode: ~0.2ms. Even with this overhead, total time is 2-7x faster than Prisma.
1118
-
1119
- **Q: How do I benchmark my own queries?**
1120
- A: Use the `onQuery` callback to measure each query, or see the [Benchmarking](#benchmarking) section below.
1121
-
1122
- **Q: How does performance compare to Prisma v7?**
1123
- A: Prisma v7 introduced significant improvements (~39% faster than v6 on PostgreSQL, ~24% on SQLite), but this extension still provides 2-7x additional speedup over v7 depending on query complexity.
981
+ A: Runtime mode: ~0.2ms per query. Generator mode: ~0.03ms for prebaked queries. Still 2-7x faster than Prisma overall.
1124
982
 
1125
983
  **Q: Should I use generator mode or runtime mode?**
1126
- A: Generator mode is recommended for production (faster). Runtime mode is better for prototyping or fully dynamic queries.
984
+ A: Start with runtime mode (simpler). Add generator mode for your hottest queries later if needed.
1127
985
 
1128
986
  ## Examples
1129
987
 
1130
988
  - [Generator Mode Example](./examples/generator-mode) - Complete working example
1131
989
  - [PostgreSQL E2E Tests](./tests/e2e/postgres.test.ts) - Comprehensive query examples
1132
990
  - [SQLite E2E Tests](./tests/e2e/sqlite.e2e.test.ts) - SQLite-specific queries
1133
- - [Runtime API Tests](./tests/e2e/runtime-api.test.ts) - All three APIs
1134
991
 
1135
992
  To run examples locally:
1136
993
 
@@ -1171,22 +1028,6 @@ await prisma.post.findMany({ include: { author: true } })
1171
1028
  console.table(queries)
1172
1029
  ```
1173
1030
 
1174
- Or run the full test suite benchmarks:
1175
-
1176
- ```bash
1177
- git clone https://github.com/multipliedtwice/prisma-sql
1178
- cd prisma-sql
1179
- npm install
1180
-
1181
- npx prisma db push
1182
-
1183
- DATABASE_URL="postgresql://..." npm run test:e2e:postgres
1184
-
1185
- npm run test:e2e:sqlite
1186
- ```
1187
-
1188
- Results include timing for Prisma vs Extension vs Drizzle (where applicable).
1189
-
1190
1031
  ## Contributing
1191
1032
 
1192
1033
  PRs welcome! Priority areas: