@ereo/db-drizzle 0.1.6

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 ADDED
@@ -0,0 +1,1401 @@
1
+ # @ereo/db-drizzle
2
+
3
+ Drizzle ORM adapter for the EreoJS database abstraction layer. This package provides seamless integration between Drizzle ORM and EreoJS applications with support for multiple database drivers, including edge-compatible options.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [API Reference](#api-reference)
11
+ - [createDrizzleAdapter](#createdrizzleadapter)
12
+ - [Configuration Helpers](#configuration-helpers)
13
+ - [Environment Detection](#environment-detection)
14
+ - [Configuration Options](#configuration-options)
15
+ - [PostgreSQL (postgres-js)](#postgresql-postgres-js)
16
+ - [Neon HTTP](#neon-http)
17
+ - [Neon WebSocket](#neon-websocket)
18
+ - [PlanetScale](#planetscale)
19
+ - [LibSQL/Turso](#libsqlturso)
20
+ - [Bun SQLite](#bun-sqlite)
21
+ - [better-sqlite3](#better-sqlite3)
22
+ - [Cloudflare D1](#cloudflare-d1)
23
+ - [Drizzle ORM Integration](#drizzle-orm-integration)
24
+ - [Use Cases](#use-cases)
25
+ - [Basic Query Execution](#basic-query-execution)
26
+ - [Transactions](#transactions)
27
+ - [Request-Scoped Queries with Deduplication](#request-scoped-queries-with-deduplication)
28
+ - [Edge Deployment](#edge-deployment)
29
+ - [Health Checks](#health-checks)
30
+ - [Migrations](#migrations)
31
+ - [Error Handling](#error-handling)
32
+ - [TypeScript Types Reference](#typescript-types-reference)
33
+ - [Troubleshooting / FAQ](#troubleshooting--faq)
34
+
35
+ ---
36
+
37
+ ## Overview
38
+
39
+ `@ereo/db-drizzle` bridges Drizzle ORM with the EreoJS database abstraction layer (`@ereo/db`). It implements the `DatabaseAdapter` interface, providing:
40
+
41
+ - **Multi-driver support**: PostgreSQL, MySQL (PlanetScale), SQLite (multiple variants), and Cloudflare D1
42
+ - **Edge compatibility**: First-class support for serverless and edge runtimes
43
+ - **Request-scoped query deduplication**: Automatic caching of identical queries within a request
44
+ - **Type safety**: Full TypeScript support with Drizzle schema inference
45
+ - **Connection management**: Automatic connection pooling and lifecycle management
46
+
47
+ ### Supported Drivers
48
+
49
+ | Driver | Database | Edge Compatible | Use Case |
50
+ |--------|----------|-----------------|----------|
51
+ | `postgres-js` | PostgreSQL | No | Traditional server deployments |
52
+ | `neon-http` | PostgreSQL (Neon) | Yes | Edge/serverless with Neon |
53
+ | `neon-websocket` | PostgreSQL (Neon) | Yes | Edge with connection pooling |
54
+ | `planetscale` | MySQL | Yes | Edge/serverless with PlanetScale |
55
+ | `libsql` | SQLite (Turso) | Yes | Edge/serverless with Turso |
56
+ | `bun-sqlite` | SQLite | No | Bun runtime local development |
57
+ | `better-sqlite3` | SQLite | No | Node.js local development |
58
+ | `d1` | SQLite (Cloudflare) | Yes | Cloudflare Workers |
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ # Install the package
66
+ bun add @ereo/db-drizzle
67
+
68
+ # Install Drizzle ORM (required peer dependency)
69
+ bun add drizzle-orm
70
+
71
+ # Install driver-specific dependencies (pick one based on your database)
72
+ # PostgreSQL
73
+ bun add postgres
74
+
75
+ # Neon (serverless PostgreSQL)
76
+ bun add @neondatabase/serverless
77
+
78
+ # PlanetScale (serverless MySQL)
79
+ bun add @planetscale/database
80
+
81
+ # LibSQL/Turso
82
+ bun add @libsql/client
83
+
84
+ # better-sqlite3 (Node.js)
85
+ bun add better-sqlite3
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Quick Start
91
+
92
+ ### 1. Define Your Drizzle Schema
93
+
94
+ ```typescript
95
+ // db/schema.ts
96
+ import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
97
+
98
+ export const users = pgTable('users', {
99
+ id: serial('id').primaryKey(),
100
+ name: text('name').notNull(),
101
+ email: text('email').notNull().unique(),
102
+ createdAt: timestamp('created_at').defaultNow(),
103
+ });
104
+
105
+ export const posts = pgTable('posts', {
106
+ id: serial('id').primaryKey(),
107
+ title: text('title').notNull(),
108
+ content: text('content'),
109
+ authorId: serial('author_id').references(() => users.id),
110
+ });
111
+ ```
112
+
113
+ ### 2. Configure the Adapter
114
+
115
+ ```typescript
116
+ // ereo.config.ts
117
+ import { defineConfig } from '@ereo/core';
118
+ import {
119
+ createDrizzleAdapter,
120
+ definePostgresConfig,
121
+ createDatabasePlugin,
122
+ } from '@ereo/db-drizzle';
123
+ import * as schema from './db/schema';
124
+
125
+ const config = definePostgresConfig({
126
+ url: process.env.DATABASE_URL!,
127
+ schema,
128
+ });
129
+
130
+ const adapter = createDrizzleAdapter(config);
131
+
132
+ export default defineConfig({
133
+ plugins: [createDatabasePlugin(adapter)],
134
+ });
135
+ ```
136
+
137
+ ### 3. Use in Routes
138
+
139
+ ```typescript
140
+ // routes/users.ts
141
+ import { createLoader } from '@ereo/core';
142
+ import { useDb } from '@ereo/db-drizzle';
143
+ import { users } from '../db/schema';
144
+
145
+ export const loader = createLoader({
146
+ load: async ({ context }) => {
147
+ const db = useDb(context);
148
+ const allUsers = await db.client.select().from(users);
149
+ return { users: allUsers };
150
+ },
151
+ });
152
+ ```
153
+
154
+ ---
155
+
156
+ ## API Reference
157
+
158
+ ### createDrizzleAdapter
159
+
160
+ Creates a Drizzle database adapter implementing the `DatabaseAdapter` interface.
161
+
162
+ ```typescript
163
+ function createDrizzleAdapter<TSchema = unknown>(
164
+ config: DrizzleConfig
165
+ ): DatabaseAdapter<TSchema>
166
+ ```
167
+
168
+ **Parameters:**
169
+ - `config` - A `DrizzleConfig` object specifying the driver and connection options
170
+
171
+ **Returns:**
172
+ - A `DatabaseAdapter<TSchema>` instance
173
+
174
+ **Example:**
175
+ ```typescript
176
+ import { createDrizzleAdapter } from '@ereo/db-drizzle';
177
+
178
+ const adapter = createDrizzleAdapter({
179
+ driver: 'postgres-js',
180
+ url: process.env.DATABASE_URL!,
181
+ schema,
182
+ logger: true,
183
+ });
184
+ ```
185
+
186
+ ### Configuration Helpers
187
+
188
+ #### defineDrizzleConfig
189
+
190
+ Generic type-safe configuration builder that provides autocomplete based on the selected driver.
191
+
192
+ ```typescript
193
+ function defineDrizzleConfig<T extends DrizzleConfig>(config: T): T
194
+ ```
195
+
196
+ **Example:**
197
+ ```typescript
198
+ const config = defineDrizzleConfig({
199
+ driver: 'postgres-js',
200
+ url: process.env.DATABASE_URL!,
201
+ schema,
202
+ connection: {
203
+ ssl: 'require',
204
+ max: 10,
205
+ },
206
+ });
207
+ ```
208
+
209
+ #### definePostgresConfig
210
+
211
+ Creates a PostgreSQL configuration with sensible defaults.
212
+
213
+ ```typescript
214
+ function definePostgresConfig(
215
+ config: Omit<PostgresConfig, 'driver'>
216
+ ): PostgresConfig
217
+ ```
218
+
219
+ **Default values:**
220
+ - `ssl`: `'require'`
221
+ - `max`: `10` connections
222
+ - `idle_timeout`: `20` seconds
223
+ - `connect_timeout`: `10` seconds
224
+ - `prepare`: `true`
225
+
226
+ **Example:**
227
+ ```typescript
228
+ const config = definePostgresConfig({
229
+ url: process.env.DATABASE_URL!,
230
+ schema,
231
+ connection: {
232
+ max: 20, // Override default
233
+ },
234
+ });
235
+ ```
236
+
237
+ #### defineNeonHttpConfig
238
+
239
+ Creates a Neon HTTP configuration (edge-compatible).
240
+
241
+ ```typescript
242
+ function defineNeonHttpConfig(
243
+ config: Omit<NeonHttpConfig, 'driver'>
244
+ ): NeonHttpConfig
245
+ ```
246
+
247
+ **Example:**
248
+ ```typescript
249
+ const config = defineNeonHttpConfig({
250
+ url: process.env.DATABASE_URL!,
251
+ schema,
252
+ });
253
+ // config.edgeCompatible === true
254
+ ```
255
+
256
+ #### defineNeonWebSocketConfig
257
+
258
+ Creates a Neon WebSocket configuration with connection pooling.
259
+
260
+ ```typescript
261
+ function defineNeonWebSocketConfig(
262
+ config: Omit<NeonWebSocketConfig, 'driver'>
263
+ ): NeonWebSocketConfig
264
+ ```
265
+
266
+ **Default values:**
267
+ - `pool.max`: `5` connections
268
+ - `pool.idleTimeoutMs`: `10000` ms
269
+
270
+ **Example:**
271
+ ```typescript
272
+ const config = defineNeonWebSocketConfig({
273
+ url: process.env.DATABASE_URL!,
274
+ schema,
275
+ pool: {
276
+ max: 10,
277
+ },
278
+ });
279
+ ```
280
+
281
+ #### definePlanetScaleConfig
282
+
283
+ Creates a PlanetScale configuration (edge-compatible).
284
+
285
+ ```typescript
286
+ function definePlanetScaleConfig(
287
+ config: Omit<PlanetScaleConfig, 'driver'>
288
+ ): PlanetScaleConfig
289
+ ```
290
+
291
+ **Example:**
292
+ ```typescript
293
+ const config = definePlanetScaleConfig({
294
+ url: process.env.DATABASE_URL!,
295
+ schema,
296
+ });
297
+ ```
298
+
299
+ #### defineLibSQLConfig
300
+
301
+ Creates a LibSQL/Turso configuration (edge-compatible).
302
+
303
+ ```typescript
304
+ function defineLibSQLConfig(
305
+ config: Omit<LibSQLConfig, 'driver'>
306
+ ): LibSQLConfig
307
+ ```
308
+
309
+ **Example:**
310
+ ```typescript
311
+ const config = defineLibSQLConfig({
312
+ url: 'libsql://your-db.turso.io',
313
+ authToken: process.env.TURSO_AUTH_TOKEN,
314
+ schema,
315
+ });
316
+ ```
317
+
318
+ #### defineBunSQLiteConfig
319
+
320
+ Creates a Bun SQLite configuration with optimized PRAGMA settings.
321
+
322
+ ```typescript
323
+ function defineBunSQLiteConfig(
324
+ config: Omit<BunSQLiteConfig, 'driver'>
325
+ ): BunSQLiteConfig
326
+ ```
327
+
328
+ **Default PRAGMA values:**
329
+ - `journal_mode`: `'WAL'`
330
+ - `synchronous`: `'NORMAL'`
331
+ - `foreign_keys`: `true`
332
+ - `cache_size`: `10000`
333
+
334
+ **Example:**
335
+ ```typescript
336
+ const config = defineBunSQLiteConfig({
337
+ url: './data/app.db',
338
+ schema,
339
+ });
340
+ ```
341
+
342
+ #### defineBetterSQLite3Config
343
+
344
+ Creates a better-sqlite3 configuration for Node.js.
345
+
346
+ ```typescript
347
+ function defineBetterSQLite3Config(
348
+ config: Omit<BetterSQLite3Config, 'driver'>
349
+ ): BetterSQLite3Config
350
+ ```
351
+
352
+ **Example:**
353
+ ```typescript
354
+ const config = defineBetterSQLite3Config({
355
+ url: './data/app.db',
356
+ schema,
357
+ options: {
358
+ readonly: false,
359
+ fileMustExist: false,
360
+ },
361
+ });
362
+ ```
363
+
364
+ #### defineD1Config
365
+
366
+ Creates a Cloudflare D1 configuration (edge-compatible).
367
+
368
+ ```typescript
369
+ function defineD1Config(
370
+ config: Omit<D1Config, 'driver'>
371
+ ): D1Config
372
+ ```
373
+
374
+ **Example:**
375
+ ```typescript
376
+ // In a Cloudflare Worker
377
+ export default {
378
+ async fetch(request, env) {
379
+ const config = defineD1Config({
380
+ url: '', // Not used for D1
381
+ d1: env.DB, // D1 binding from wrangler.toml
382
+ schema,
383
+ });
384
+ // ...
385
+ },
386
+ };
387
+ ```
388
+
389
+ #### defineEdgeConfig
390
+
391
+ Creates an edge-optimized configuration with a simplified API.
392
+
393
+ ```typescript
394
+ function defineEdgeConfig(options: EdgeConfigOptions): DrizzleConfig
395
+ ```
396
+
397
+ **Parameters:**
398
+ - `driver`: `'neon-http' | 'neon-websocket' | 'planetscale' | 'libsql' | 'd1'`
399
+ - `url`: Database connection URL
400
+ - `schema`: Optional Drizzle schema object
401
+ - `authToken`: Optional auth token (for LibSQL/Turso)
402
+ - `debug`: Optional debug logging flag
403
+
404
+ **Example:**
405
+ ```typescript
406
+ const config = defineEdgeConfig({
407
+ driver: 'neon-http',
408
+ url: process.env.DATABASE_URL!,
409
+ schema,
410
+ });
411
+ ```
412
+
413
+ ### Environment Detection
414
+
415
+ #### detectRuntime
416
+
417
+ Detects the current JavaScript runtime environment.
418
+
419
+ ```typescript
420
+ function detectRuntime(): RuntimeEnvironment
421
+
422
+ type RuntimeEnvironment =
423
+ | 'bun'
424
+ | 'node'
425
+ | 'cloudflare-workers'
426
+ | 'vercel-edge'
427
+ | 'deno'
428
+ | 'unknown';
429
+ ```
430
+
431
+ **Example:**
432
+ ```typescript
433
+ const runtime = detectRuntime();
434
+ if (runtime === 'cloudflare-workers') {
435
+ // Use D1 or edge-compatible driver
436
+ }
437
+ ```
438
+
439
+ #### isEdgeRuntime
440
+
441
+ Checks if the current environment is an edge runtime.
442
+
443
+ ```typescript
444
+ function isEdgeRuntime(): boolean
445
+ ```
446
+
447
+ **Returns:** `true` for Cloudflare Workers and Vercel Edge, `false` otherwise.
448
+
449
+ **Example:**
450
+ ```typescript
451
+ if (isEdgeRuntime()) {
452
+ console.log('Running on the edge');
453
+ }
454
+ ```
455
+
456
+ #### suggestDrivers
457
+
458
+ Suggests appropriate drivers for the current runtime environment.
459
+
460
+ ```typescript
461
+ function suggestDrivers(): DrizzleDriver[]
462
+ ```
463
+
464
+ **Returns by runtime:**
465
+ - **Bun**: `['bun-sqlite', 'postgres-js', 'libsql']`
466
+ - **Node.js**: `['better-sqlite3', 'postgres-js', 'libsql']`
467
+ - **Cloudflare Workers**: `['d1', 'neon-http', 'planetscale']`
468
+ - **Vercel Edge**: `['neon-http', 'planetscale', 'libsql']`
469
+ - **Deno**: `['postgres-js', 'libsql']`
470
+ - **Unknown**: `['neon-http', 'postgres-js']`
471
+
472
+ **Example:**
473
+ ```typescript
474
+ const recommended = suggestDrivers();
475
+ console.log(`Recommended drivers: ${recommended.join(', ')}`);
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Configuration Options
481
+
482
+ ### PostgreSQL (postgres-js)
483
+
484
+ ```typescript
485
+ interface PostgresConfig {
486
+ driver: 'postgres-js';
487
+ url: string;
488
+ schema?: Record<string, unknown>;
489
+ logger?: boolean;
490
+ debug?: boolean;
491
+ edgeCompatible?: boolean; // Defaults to false
492
+ connection?: {
493
+ ssl?: boolean | 'require' | 'prefer' | 'allow';
494
+ max?: number; // Maximum connections (default: 10)
495
+ idle_timeout?: number; // Seconds (default: 20)
496
+ connect_timeout?: number; // Seconds (default: 10)
497
+ prepare?: boolean; // Use prepared statements (default: true)
498
+ };
499
+ }
500
+ ```
501
+
502
+ ### Neon HTTP
503
+
504
+ ```typescript
505
+ interface NeonHttpConfig {
506
+ driver: 'neon-http';
507
+ url: string;
508
+ schema?: Record<string, unknown>;
509
+ logger?: boolean;
510
+ debug?: boolean;
511
+ edgeCompatible?: boolean; // Defaults to true
512
+ neon?: {
513
+ fetchOptions?: RequestInit; // Custom fetch options
514
+ };
515
+ }
516
+ ```
517
+
518
+ ### Neon WebSocket
519
+
520
+ ```typescript
521
+ interface NeonWebSocketConfig {
522
+ driver: 'neon-websocket';
523
+ url: string;
524
+ schema?: Record<string, unknown>;
525
+ logger?: boolean;
526
+ debug?: boolean;
527
+ edgeCompatible?: boolean; // Defaults to true
528
+ pool?: {
529
+ min?: number;
530
+ max?: number; // Default: 5
531
+ idleTimeoutMs?: number; // Default: 10000
532
+ acquireTimeoutMs?: number;
533
+ acquireRetries?: number;
534
+ };
535
+ }
536
+ ```
537
+
538
+ ### PlanetScale
539
+
540
+ ```typescript
541
+ interface PlanetScaleConfig {
542
+ driver: 'planetscale';
543
+ url: string;
544
+ schema?: Record<string, unknown>;
545
+ logger?: boolean;
546
+ debug?: boolean;
547
+ edgeCompatible?: boolean; // Defaults to true
548
+ planetscale?: {
549
+ fetch?: typeof fetch; // Custom fetch function
550
+ };
551
+ }
552
+ ```
553
+
554
+ ### LibSQL/Turso
555
+
556
+ ```typescript
557
+ interface LibSQLConfig {
558
+ driver: 'libsql';
559
+ url: string;
560
+ schema?: Record<string, unknown>;
561
+ logger?: boolean;
562
+ debug?: boolean;
563
+ edgeCompatible?: boolean; // Defaults to true
564
+ authToken?: string; // Turso authentication token
565
+ syncUrl?: string; // Sync URL for embedded replicas
566
+ }
567
+ ```
568
+
569
+ ### Bun SQLite
570
+
571
+ ```typescript
572
+ interface BunSQLiteConfig {
573
+ driver: 'bun-sqlite';
574
+ url: string; // File path to SQLite database
575
+ schema?: Record<string, unknown>;
576
+ logger?: boolean;
577
+ debug?: boolean;
578
+ edgeCompatible?: boolean; // Defaults to false
579
+ pragma?: {
580
+ journal_mode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF';
581
+ synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA';
582
+ foreign_keys?: boolean;
583
+ cache_size?: number;
584
+ };
585
+ }
586
+ ```
587
+
588
+ ### better-sqlite3
589
+
590
+ ```typescript
591
+ interface BetterSQLite3Config {
592
+ driver: 'better-sqlite3';
593
+ url: string; // File path to SQLite database
594
+ schema?: Record<string, unknown>;
595
+ logger?: boolean;
596
+ debug?: boolean;
597
+ edgeCompatible?: boolean; // Defaults to false
598
+ options?: {
599
+ readonly?: boolean;
600
+ fileMustExist?: boolean;
601
+ timeout?: number;
602
+ verbose?: (message: string) => void;
603
+ };
604
+ }
605
+ ```
606
+
607
+ ### Cloudflare D1
608
+
609
+ ```typescript
610
+ interface D1Config {
611
+ driver: 'd1';
612
+ url: string; // Not used, but required for interface
613
+ schema?: Record<string, unknown>;
614
+ logger?: boolean;
615
+ debug?: boolean;
616
+ edgeCompatible?: boolean; // Defaults to true
617
+ d1?: D1Database; // D1 binding from Cloudflare Workers
618
+ }
619
+ ```
620
+
621
+ ---
622
+
623
+ ## Drizzle ORM Integration
624
+
625
+ The adapter provides the full Drizzle client through the `client` property. All Drizzle query methods are available.
626
+
627
+ ### Schema Passing
628
+
629
+ Pass your Drizzle schema to enable relational queries and type inference:
630
+
631
+ ```typescript
632
+ import * as schema from './db/schema';
633
+
634
+ const config = definePostgresConfig({
635
+ url: process.env.DATABASE_URL!,
636
+ schema, // Enables relational queries
637
+ });
638
+
639
+ const adapter = createDrizzleAdapter(config);
640
+ const db = adapter.getClient();
641
+
642
+ // Relational queries work
643
+ const usersWithPosts = await db.query.users.findMany({
644
+ with: {
645
+ posts: true,
646
+ },
647
+ });
648
+ ```
649
+
650
+ ### Logger Integration
651
+
652
+ Enable Drizzle's built-in query logger:
653
+
654
+ ```typescript
655
+ const config = definePostgresConfig({
656
+ url: process.env.DATABASE_URL!,
657
+ schema,
658
+ logger: true, // Logs all queries to console
659
+ });
660
+ ```
661
+
662
+ ### Type Inference
663
+
664
+ Use Drizzle's type inference utilities:
665
+
666
+ ```typescript
667
+ import { InferSelect, InferInsert } from '@ereo/db-drizzle';
668
+ import { users } from './db/schema';
669
+
670
+ type User = InferSelect<typeof users>;
671
+ type NewUser = InferInsert<typeof users>;
672
+
673
+ // Or use Drizzle's built-in types
674
+ type User = typeof users.$inferSelect;
675
+ type NewUser = typeof users.$inferInsert;
676
+ ```
677
+
678
+ ---
679
+
680
+ ## Use Cases
681
+
682
+ ### Basic Query Execution
683
+
684
+ ```typescript
685
+ import { createDrizzleAdapter, definePostgresConfig } from '@ereo/db-drizzle';
686
+ import { eq } from 'drizzle-orm';
687
+ import * as schema from './db/schema';
688
+
689
+ const adapter = createDrizzleAdapter(
690
+ definePostgresConfig({
691
+ url: process.env.DATABASE_URL!,
692
+ schema,
693
+ })
694
+ );
695
+
696
+ const db = adapter.getClient();
697
+
698
+ // Select queries
699
+ const allUsers = await db.select().from(schema.users);
700
+
701
+ // Filtered queries
702
+ const user = await db
703
+ .select()
704
+ .from(schema.users)
705
+ .where(eq(schema.users.id, 1));
706
+
707
+ // Insert
708
+ const [newUser] = await db
709
+ .insert(schema.users)
710
+ .values({ name: 'Alice', email: 'alice@example.com' })
711
+ .returning();
712
+
713
+ // Update
714
+ await db
715
+ .update(schema.users)
716
+ .set({ name: 'Bob' })
717
+ .where(eq(schema.users.id, 1));
718
+
719
+ // Delete
720
+ await db
721
+ .delete(schema.users)
722
+ .where(eq(schema.users.id, 1));
723
+ ```
724
+
725
+ ### Transactions
726
+
727
+ #### Callback-Based Transactions (Recommended)
728
+
729
+ ```typescript
730
+ import { withTransaction } from '@ereo/db-drizzle';
731
+
732
+ // Automatic commit on success, rollback on error
733
+ const result = await adapter.transaction(async (tx) => {
734
+ const [user] = await tx
735
+ .insert(schema.users)
736
+ .values({ name: 'Alice', email: 'alice@example.com' })
737
+ .returning();
738
+
739
+ await tx
740
+ .insert(schema.posts)
741
+ .values({ title: 'First Post', authorId: user.id });
742
+
743
+ return user;
744
+ });
745
+ ```
746
+
747
+ #### Using withTransaction Helper
748
+
749
+ ```typescript
750
+ import { createLoader } from '@ereo/core';
751
+ import { useDb, withTransaction } from '@ereo/db-drizzle';
752
+
753
+ export const action = createAction({
754
+ action: async ({ context, request }) => {
755
+ const db = useDb(context);
756
+
757
+ const result = await withTransaction(context, async (tx) => {
758
+ // All operations use the same transaction
759
+ const [user] = await tx
760
+ .insert(schema.users)
761
+ .values({ name: 'Alice', email: 'alice@example.com' })
762
+ .returning();
763
+
764
+ return user;
765
+ });
766
+
767
+ return { user: result };
768
+ },
769
+ });
770
+ ```
771
+
772
+ #### Manual Transactions
773
+
774
+ ```typescript
775
+ const tx = await adapter.beginTransaction();
776
+
777
+ try {
778
+ const [user] = await tx.client
779
+ .insert(schema.users)
780
+ .values({ name: 'Alice', email: 'alice@example.com' })
781
+ .returning();
782
+
783
+ await tx.commit();
784
+ return user;
785
+ } catch (error) {
786
+ await tx.rollback();
787
+ throw error;
788
+ }
789
+ ```
790
+
791
+ ### Request-Scoped Queries with Deduplication
792
+
793
+ The adapter automatically deduplicates identical queries within a single request:
794
+
795
+ ```typescript
796
+ import { createLoader } from '@ereo/core';
797
+ import { useDb } from '@ereo/db-drizzle';
798
+
799
+ export const loader = createLoader({
800
+ load: async ({ context }) => {
801
+ const db = useDb(context);
802
+
803
+ // First call executes the query
804
+ const { result: users1, fromCache: cached1 } = await db.query(
805
+ 'SELECT * FROM users WHERE active = $1',
806
+ [true]
807
+ );
808
+ // cached1 === false
809
+
810
+ // Identical query returns cached result
811
+ const { result: users2, fromCache: cached2 } = await db.query(
812
+ 'SELECT * FROM users WHERE active = $1',
813
+ [true]
814
+ );
815
+ // cached2 === true
816
+
817
+ // Get deduplication statistics
818
+ const stats = db.getDedupStats();
819
+ console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
820
+
821
+ // Clear cache after mutations
822
+ await db.client.insert(schema.users).values({ ... });
823
+ db.clearDedup(); // or db.invalidate(['users']);
824
+
825
+ return { users: users1.rows };
826
+ },
827
+ });
828
+ ```
829
+
830
+ ### Edge Deployment
831
+
832
+ #### Vercel Edge Functions
833
+
834
+ ```typescript
835
+ // ereo.config.ts
836
+ import { defineConfig } from '@ereo/core';
837
+ import {
838
+ createDrizzleAdapter,
839
+ defineEdgeConfig,
840
+ createDatabasePlugin,
841
+ } from '@ereo/db-drizzle';
842
+ import * as schema from './db/schema';
843
+
844
+ const adapter = createDrizzleAdapter(
845
+ defineEdgeConfig({
846
+ driver: 'neon-http',
847
+ url: process.env.DATABASE_URL!,
848
+ schema,
849
+ })
850
+ );
851
+
852
+ export default defineConfig({
853
+ plugins: [createDatabasePlugin(adapter)],
854
+ });
855
+ ```
856
+
857
+ #### Cloudflare Workers with D1
858
+
859
+ ```typescript
860
+ // src/index.ts
861
+ import {
862
+ createDrizzleAdapter,
863
+ defineD1Config,
864
+ createDatabasePlugin,
865
+ } from '@ereo/db-drizzle';
866
+ import * as schema from './db/schema';
867
+
868
+ export default {
869
+ async fetch(request: Request, env: Env): Promise<Response> {
870
+ const adapter = createDrizzleAdapter(
871
+ defineD1Config({
872
+ url: '',
873
+ d1: env.DB,
874
+ schema,
875
+ })
876
+ );
877
+
878
+ const db = adapter.getClient();
879
+ const users = await db.select().from(schema.users);
880
+
881
+ return Response.json({ users });
882
+ },
883
+ };
884
+ ```
885
+
886
+ ### Health Checks
887
+
888
+ ```typescript
889
+ import { createDrizzleAdapter, definePostgresConfig } from '@ereo/db-drizzle';
890
+
891
+ const adapter = createDrizzleAdapter(
892
+ definePostgresConfig({
893
+ url: process.env.DATABASE_URL!,
894
+ })
895
+ );
896
+
897
+ // Perform health check
898
+ const health = await adapter.healthCheck();
899
+
900
+ if (health.healthy) {
901
+ console.log(`Database healthy, latency: ${health.latencyMs}ms`);
902
+ console.log(`Driver: ${health.metadata?.driver}`);
903
+ } else {
904
+ console.error(`Database unhealthy: ${health.error}`);
905
+ }
906
+
907
+ // Graceful shutdown
908
+ process.on('SIGTERM', async () => {
909
+ await adapter.disconnect();
910
+ process.exit(0);
911
+ });
912
+ ```
913
+
914
+ ---
915
+
916
+ ## Migrations
917
+
918
+ This package does not handle migrations directly. Use Drizzle Kit for schema migrations.
919
+
920
+ ### Setup Drizzle Kit
921
+
922
+ ```typescript
923
+ // drizzle.config.ts
924
+ import type { Config } from 'drizzle-kit';
925
+
926
+ export default {
927
+ schema: './db/schema.ts',
928
+ out: './drizzle',
929
+ driver: 'pg', // or 'mysql2', 'better-sqlite', 'libsql', 'd1'
930
+ dbCredentials: {
931
+ connectionString: process.env.DATABASE_URL!,
932
+ },
933
+ } satisfies Config;
934
+ ```
935
+
936
+ ### Common Commands
937
+
938
+ ```bash
939
+ # Generate migrations
940
+ bunx drizzle-kit generate:pg
941
+
942
+ # Apply migrations
943
+ bunx drizzle-kit push:pg
944
+
945
+ # Open Drizzle Studio
946
+ bunx drizzle-kit studio
947
+ ```
948
+
949
+ ### Migration with Different Drivers
950
+
951
+ ```typescript
952
+ // PostgreSQL
953
+ export default {
954
+ driver: 'pg',
955
+ dbCredentials: {
956
+ connectionString: process.env.DATABASE_URL!,
957
+ },
958
+ } satisfies Config;
959
+
960
+ // PlanetScale
961
+ export default {
962
+ driver: 'mysql2',
963
+ dbCredentials: {
964
+ uri: process.env.DATABASE_URL!,
965
+ },
966
+ } satisfies Config;
967
+
968
+ // LibSQL/Turso
969
+ export default {
970
+ driver: 'libsql',
971
+ dbCredentials: {
972
+ url: process.env.DATABASE_URL!,
973
+ authToken: process.env.DATABASE_AUTH_TOKEN,
974
+ },
975
+ } satisfies Config;
976
+
977
+ // Cloudflare D1
978
+ export default {
979
+ driver: 'd1',
980
+ dbCredentials: {
981
+ wranglerConfigPath: './wrangler.toml',
982
+ dbName: 'my-database',
983
+ },
984
+ } satisfies Config;
985
+ ```
986
+
987
+ ---
988
+
989
+ ## Error Handling
990
+
991
+ The adapter throws typed errors from `@ereo/db`:
992
+
993
+ ### ConnectionError
994
+
995
+ Thrown when the database connection fails.
996
+
997
+ ```typescript
998
+ import { ConnectionError } from '@ereo/db-drizzle';
999
+
1000
+ try {
1001
+ const db = adapter.getClient();
1002
+ } catch (error) {
1003
+ if (error instanceof ConnectionError) {
1004
+ console.error('Failed to connect:', error.message);
1005
+ // error.code === 'CONNECTION_ERROR'
1006
+ // error.cause contains the original error
1007
+ }
1008
+ }
1009
+ ```
1010
+
1011
+ ### QueryError
1012
+
1013
+ Thrown when a query fails to execute.
1014
+
1015
+ ```typescript
1016
+ import { QueryError } from '@ereo/db-drizzle';
1017
+
1018
+ try {
1019
+ await adapter.query('SELECT * FROM nonexistent');
1020
+ } catch (error) {
1021
+ if (error instanceof QueryError) {
1022
+ console.error('Query failed:', error.message);
1023
+ console.error('SQL:', error.query);
1024
+ console.error('Params:', error.params);
1025
+ }
1026
+ }
1027
+ ```
1028
+
1029
+ ### TransactionError
1030
+
1031
+ Thrown when a transaction operation fails.
1032
+
1033
+ ```typescript
1034
+ import { TransactionError } from '@ereo/db-drizzle';
1035
+
1036
+ try {
1037
+ await adapter.transaction(async (tx) => {
1038
+ // ...
1039
+ throw new Error('Something went wrong');
1040
+ });
1041
+ } catch (error) {
1042
+ if (error instanceof TransactionError) {
1043
+ console.error('Transaction failed:', error.message);
1044
+ }
1045
+ }
1046
+ ```
1047
+
1048
+ ### Error Handling Pattern
1049
+
1050
+ ```typescript
1051
+ import {
1052
+ ConnectionError,
1053
+ QueryError,
1054
+ TransactionError,
1055
+ DatabaseError,
1056
+ } from '@ereo/db-drizzle';
1057
+
1058
+ try {
1059
+ await performDatabaseOperation();
1060
+ } catch (error) {
1061
+ if (error instanceof ConnectionError) {
1062
+ // Handle connection issues (retry, alert, etc.)
1063
+ } else if (error instanceof QueryError) {
1064
+ // Handle query issues (log, validate input, etc.)
1065
+ } else if (error instanceof TransactionError) {
1066
+ // Handle transaction issues (retry, partial rollback, etc.)
1067
+ } else if (error instanceof DatabaseError) {
1068
+ // Generic database error
1069
+ } else {
1070
+ // Non-database error
1071
+ throw error;
1072
+ }
1073
+ }
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ ## TypeScript Types Reference
1079
+
1080
+ ### Driver Types
1081
+
1082
+ ```typescript
1083
+ // Supported database drivers
1084
+ type DrizzleDriver =
1085
+ | 'postgres-js'
1086
+ | 'neon-http'
1087
+ | 'neon-websocket'
1088
+ | 'planetscale'
1089
+ | 'libsql'
1090
+ | 'bun-sqlite'
1091
+ | 'better-sqlite3'
1092
+ | 'd1';
1093
+
1094
+ // Edge compatibility map
1095
+ const EDGE_COMPATIBLE_DRIVERS: Record<DrizzleDriver, boolean>;
1096
+ ```
1097
+
1098
+ ### Configuration Types
1099
+
1100
+ ```typescript
1101
+ // Union of all configuration types
1102
+ type DrizzleConfig =
1103
+ | PostgresConfig
1104
+ | NeonHttpConfig
1105
+ | NeonWebSocketConfig
1106
+ | PlanetScaleConfig
1107
+ | LibSQLConfig
1108
+ | BunSQLiteConfig
1109
+ | BetterSQLite3Config
1110
+ | D1Config;
1111
+
1112
+ // Edge configuration options
1113
+ interface EdgeConfigOptions {
1114
+ driver: 'neon-http' | 'neon-websocket' | 'planetscale' | 'libsql' | 'd1';
1115
+ url: string;
1116
+ schema?: Record<string, unknown>;
1117
+ authToken?: string;
1118
+ debug?: boolean;
1119
+ }
1120
+
1121
+ // Runtime environment
1122
+ type RuntimeEnvironment =
1123
+ | 'bun'
1124
+ | 'node'
1125
+ | 'cloudflare-workers'
1126
+ | 'vercel-edge'
1127
+ | 'deno'
1128
+ | 'unknown';
1129
+ ```
1130
+
1131
+ ### Re-exported Types from @ereo/db
1132
+
1133
+ ```typescript
1134
+ // Core adapter interface
1135
+ interface DatabaseAdapter<TSchema = unknown> {
1136
+ readonly name: string;
1137
+ readonly edgeCompatible: boolean;
1138
+ getClient(): TSchema;
1139
+ getRequestClient(context: AppContext): RequestScopedClient<TSchema>;
1140
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
1141
+ execute(sql: string, params?: unknown[]): Promise<MutationResult>;
1142
+ transaction<T>(fn: (tx: TSchema) => Promise<T>, options?: TransactionOptions): Promise<T>;
1143
+ beginTransaction(options?: TransactionOptions): Promise<Transaction<TSchema>>;
1144
+ healthCheck(): Promise<HealthCheckResult>;
1145
+ disconnect(): Promise<void>;
1146
+ }
1147
+
1148
+ // Request-scoped client with deduplication
1149
+ interface RequestScopedClient<TSchema> {
1150
+ readonly client: TSchema;
1151
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<DedupResult<QueryResult<T>>>;
1152
+ getDedupStats(): DedupStats;
1153
+ clearDedup(): void;
1154
+ invalidate(tables?: string[]): void;
1155
+ }
1156
+
1157
+ // Query results
1158
+ interface QueryResult<T = unknown> {
1159
+ rows: T[];
1160
+ rowCount: number;
1161
+ }
1162
+
1163
+ interface MutationResult {
1164
+ rowsAffected: number;
1165
+ lastInsertId?: number | bigint;
1166
+ }
1167
+
1168
+ interface DedupResult<T> {
1169
+ result: T;
1170
+ fromCache: boolean;
1171
+ cacheKey: string;
1172
+ }
1173
+
1174
+ interface DedupStats {
1175
+ total: number;
1176
+ deduplicated: number;
1177
+ unique: number;
1178
+ hitRate: number;
1179
+ }
1180
+
1181
+ // Transaction types
1182
+ interface TransactionOptions {
1183
+ isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable';
1184
+ readOnly?: boolean;
1185
+ timeout?: number;
1186
+ }
1187
+
1188
+ interface Transaction<TSchema> {
1189
+ readonly client: TSchema;
1190
+ commit(): Promise<void>;
1191
+ rollback(): Promise<void>;
1192
+ readonly isActive: boolean;
1193
+ }
1194
+
1195
+ // Health check
1196
+ interface HealthCheckResult {
1197
+ healthy: boolean;
1198
+ latencyMs: number;
1199
+ error?: string;
1200
+ metadata?: Record<string, unknown>;
1201
+ }
1202
+ ```
1203
+
1204
+ ### D1 Types (Cloudflare Workers)
1205
+
1206
+ ```typescript
1207
+ interface D1Database {
1208
+ prepare(query: string): D1PreparedStatement;
1209
+ dump(): Promise<ArrayBuffer>;
1210
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
1211
+ exec(query: string): Promise<D1ExecResult>;
1212
+ }
1213
+
1214
+ interface D1PreparedStatement {
1215
+ bind(...values: unknown[]): D1PreparedStatement;
1216
+ first<T = unknown>(colName?: string): Promise<T>;
1217
+ run(): Promise<D1Result>;
1218
+ all<T = unknown>(): Promise<D1Result<T>>;
1219
+ raw<T = unknown>(): Promise<T[]>;
1220
+ }
1221
+
1222
+ interface D1Result<T = unknown> {
1223
+ results?: T[];
1224
+ success: boolean;
1225
+ error?: string;
1226
+ meta: {
1227
+ changed_db?: boolean;
1228
+ changes?: number;
1229
+ last_row_id?: number;
1230
+ duration?: number;
1231
+ rows_read?: number;
1232
+ rows_written?: number;
1233
+ };
1234
+ }
1235
+
1236
+ interface D1ExecResult {
1237
+ count: number;
1238
+ duration: number;
1239
+ }
1240
+ ```
1241
+
1242
+ ---
1243
+
1244
+ ## Troubleshooting / FAQ
1245
+
1246
+ ### Connection Issues
1247
+
1248
+ **Q: I'm getting "Failed to connect to database" errors.**
1249
+
1250
+ A: Check the following:
1251
+ 1. Verify your `DATABASE_URL` is correct and accessible
1252
+ 2. For PostgreSQL, ensure SSL settings match your database requirements
1253
+ 3. For serverless databases (Neon, PlanetScale), verify your API keys/tokens
1254
+ 4. Check firewall rules if connecting to a remote database
1255
+
1256
+ ```typescript
1257
+ // Enable debug logging to see connection details
1258
+ const config = definePostgresConfig({
1259
+ url: process.env.DATABASE_URL!,
1260
+ debug: true,
1261
+ });
1262
+ ```
1263
+
1264
+ ### Edge Runtime Errors
1265
+
1266
+ **Q: My database queries fail on Vercel Edge or Cloudflare Workers.**
1267
+
1268
+ A: Ensure you're using an edge-compatible driver:
1269
+
1270
+ ```typescript
1271
+ // Check edge compatibility
1272
+ import { EDGE_COMPATIBLE_DRIVERS } from '@ereo/db-drizzle';
1273
+
1274
+ console.log(EDGE_COMPATIBLE_DRIVERS);
1275
+ // {
1276
+ // 'postgres-js': false, // NOT edge compatible
1277
+ // 'neon-http': true, // Edge compatible
1278
+ // 'neon-websocket': true, // Edge compatible
1279
+ // 'planetscale': true, // Edge compatible
1280
+ // 'libsql': true, // Edge compatible
1281
+ // 'bun-sqlite': false, // NOT edge compatible
1282
+ // 'better-sqlite3': false, // NOT edge compatible
1283
+ // 'd1': true, // Edge compatible
1284
+ // }
1285
+ ```
1286
+
1287
+ ### Transaction Support
1288
+
1289
+ **Q: Transactions don't work with SQLite drivers.**
1290
+
1291
+ A: For `bun-sqlite` and `better-sqlite3`, use the Drizzle client's transaction method directly:
1292
+
1293
+ ```typescript
1294
+ const db = adapter.getClient();
1295
+
1296
+ // Use Drizzle's built-in transaction support
1297
+ await db.transaction(async (tx) => {
1298
+ // Your transaction code
1299
+ });
1300
+ ```
1301
+
1302
+ ### Query Deduplication
1303
+
1304
+ **Q: How do I disable query deduplication?**
1305
+
1306
+ A: Query deduplication only applies to request-scoped clients. Use `adapter.getClient()` for non-deduplicated queries:
1307
+
1308
+ ```typescript
1309
+ // Deduplicated (via useDb/getRequestClient)
1310
+ const db = useDb(context);
1311
+ await db.query('SELECT * FROM users', []);
1312
+
1313
+ // Not deduplicated (direct client)
1314
+ const client = adapter.getClient();
1315
+ await client.select().from(users);
1316
+ ```
1317
+
1318
+ **Q: Why isn't my cache invalidating after mutations?**
1319
+
1320
+ A: Call `invalidate()` or `clearDedup()` after mutations:
1321
+
1322
+ ```typescript
1323
+ const db = useDb(context);
1324
+
1325
+ // Perform mutation
1326
+ await db.client.insert(users).values({ name: 'Alice' });
1327
+
1328
+ // Invalidate cache for specific tables
1329
+ db.invalidate(['users']);
1330
+
1331
+ // Or clear entire cache
1332
+ db.clearDedup();
1333
+ ```
1334
+
1335
+ ### Driver Selection
1336
+
1337
+ **Q: Which driver should I use?**
1338
+
1339
+ A: Use `suggestDrivers()` to get recommendations based on your runtime:
1340
+
1341
+ ```typescript
1342
+ import { suggestDrivers, detectRuntime } from '@ereo/db-drizzle';
1343
+
1344
+ const runtime = detectRuntime();
1345
+ const recommended = suggestDrivers();
1346
+
1347
+ console.log(`Runtime: ${runtime}`);
1348
+ console.log(`Recommended drivers: ${recommended.join(', ')}`);
1349
+ ```
1350
+
1351
+ General guidelines:
1352
+ - **Local development**: `bun-sqlite` (Bun) or `better-sqlite3` (Node.js)
1353
+ - **Traditional servers**: `postgres-js` for PostgreSQL
1354
+ - **Serverless/Edge**: `neon-http`, `planetscale`, or `libsql`
1355
+ - **Cloudflare Workers**: `d1` or `neon-http`
1356
+
1357
+ ### Schema Not Working
1358
+
1359
+ **Q: Relational queries return undefined.**
1360
+
1361
+ A: Ensure you pass the schema to your configuration:
1362
+
1363
+ ```typescript
1364
+ import * as schema from './db/schema';
1365
+
1366
+ const config = definePostgresConfig({
1367
+ url: process.env.DATABASE_URL!,
1368
+ schema, // Required for relational queries
1369
+ });
1370
+ ```
1371
+
1372
+ ### Performance
1373
+
1374
+ **Q: How can I improve query performance?**
1375
+
1376
+ A: Consider these optimizations:
1377
+
1378
+ 1. **Enable prepared statements** (default for postgres-js):
1379
+ ```typescript
1380
+ connection: { prepare: true }
1381
+ ```
1382
+
1383
+ 2. **Tune connection pool** for your workload:
1384
+ ```typescript
1385
+ connection: { max: 20, idle_timeout: 30 }
1386
+ ```
1387
+
1388
+ 3. **Use WAL mode for SQLite**:
1389
+ ```typescript
1390
+ pragma: { journal_mode: 'WAL' }
1391
+ ```
1392
+
1393
+ 4. **Leverage query deduplication** for read-heavy routes
1394
+
1395
+ 5. **Use appropriate indexes** in your schema
1396
+
1397
+ ---
1398
+
1399
+ ## License
1400
+
1401
+ MIT