@ereo/db 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,1282 @@
1
+ # @ereo/db
2
+
3
+ Database adapter abstractions for the EreoJS framework. This package provides ORM-agnostic database abstractions including adapter interfaces, query deduplication, connection pooling primitives, and comprehensive type utilities for end-to-end type safety.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [API Reference](#api-reference)
11
+ - [Plugin Factory](#plugin-factory)
12
+ - [Context Helpers](#context-helpers)
13
+ - [Adapter Interface](#adapter-interface)
14
+ - [Query Deduplication](#query-deduplication)
15
+ - [Connection Pool](#connection-pool)
16
+ - [Retry Utilities](#retry-utilities)
17
+ - [Error Classes](#error-classes)
18
+ - [Types](#types)
19
+ - [Configuration Options](#configuration-options)
20
+ - [Use Cases](#use-cases)
21
+ - [Integration with Other Packages](#integration-with-other-packages)
22
+ - [Troubleshooting](#troubleshooting)
23
+ - [TypeScript Types Reference](#typescript-types-reference)
24
+
25
+ ## Overview
26
+
27
+ `@ereo/db` is the core database abstraction layer for EreoJS applications. It provides:
28
+
29
+ - **Adapter Interface**: A standardized interface that ORM-specific adapters (Drizzle, Prisma, Kysely) implement for consistent database access patterns.
30
+ - **Query Deduplication**: Request-scoped caching that automatically deduplicates identical queries within a single request, eliminating N+1 query problems.
31
+ - **Connection Pooling**: Abstract connection pool primitives with presets for server, edge, and serverless environments.
32
+ - **Type Utilities**: End-to-end type safety with inference utilities compatible with Drizzle ORM.
33
+ - **Plugin Integration**: Seamless integration with EreoJS's plugin system for automatic lifecycle management.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ # Using bun
39
+ bun add @ereo/db
40
+
41
+ # Using npm
42
+ npm install @ereo/db
43
+
44
+ # Using pnpm
45
+ pnpm add @ereo/db
46
+ ```
47
+
48
+ You will also need an ORM-specific adapter package:
49
+
50
+ ```bash
51
+ # For Drizzle ORM
52
+ bun add @ereo/db-drizzle drizzle-orm
53
+
54
+ # With a database driver (e.g., postgres)
55
+ bun add postgres
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ### 1. Configure the Database Plugin
61
+
62
+ ```typescript
63
+ // ereo.config.ts
64
+ import { defineConfig } from '@ereo/core';
65
+ import { createDatabasePlugin } from '@ereo/db';
66
+ import { createDrizzleAdapter } from '@ereo/db-drizzle';
67
+ import * as schema from './db/schema';
68
+
69
+ const adapter = createDrizzleAdapter({
70
+ driver: 'postgres-js',
71
+ url: process.env.DATABASE_URL,
72
+ schema,
73
+ });
74
+
75
+ export default defineConfig({
76
+ plugins: [
77
+ createDatabasePlugin(adapter),
78
+ ],
79
+ });
80
+ ```
81
+
82
+ ### 2. Use in Route Loaders
83
+
84
+ ```typescript
85
+ // routes/users.tsx
86
+ import { createLoader } from '@ereo/core';
87
+ import { useDb } from '@ereo/db';
88
+ import { users } from '../db/schema';
89
+ import { eq } from 'drizzle-orm';
90
+
91
+ export const loader = createLoader({
92
+ load: async ({ context, params }) => {
93
+ const db = useDb(context);
94
+
95
+ // Queries are automatically deduplicated within the same request
96
+ const user = await db.client
97
+ .select()
98
+ .from(users)
99
+ .where(eq(users.id, params.id));
100
+
101
+ return { user };
102
+ },
103
+ });
104
+ ```
105
+
106
+ ### 3. Use Transactions in Actions
107
+
108
+ ```typescript
109
+ // routes/users.tsx
110
+ import { createAction } from '@ereo/core';
111
+ import { withTransaction } from '@ereo/db';
112
+ import { users, profiles } from '../db/schema';
113
+
114
+ export const action = createAction({
115
+ async run({ context, formData }) {
116
+ return withTransaction(context, async (tx) => {
117
+ const [user] = await tx.insert(users).values({
118
+ name: formData.get('name'),
119
+ email: formData.get('email'),
120
+ }).returning();
121
+
122
+ await tx.insert(profiles).values({
123
+ userId: user.id,
124
+ bio: formData.get('bio'),
125
+ });
126
+
127
+ return { success: true, userId: user.id };
128
+ });
129
+ },
130
+ });
131
+ ```
132
+
133
+ ## API Reference
134
+
135
+ ### Plugin Factory
136
+
137
+ #### `createDatabasePlugin(adapter, options?)`
138
+
139
+ Creates an EreoJS plugin that integrates a database adapter with the framework's request lifecycle.
140
+
141
+ ```typescript
142
+ import { createDatabasePlugin } from '@ereo/db';
143
+
144
+ const plugin = createDatabasePlugin(adapter, {
145
+ registerDefault: true, // Register as the default adapter (default: true)
146
+ registrationName: 'main', // Name for adapter registration (default: adapter.name)
147
+ debug: false, // Enable debug logging (default: false)
148
+ });
149
+ ```
150
+
151
+ **Parameters:**
152
+
153
+ | Parameter | Type | Description |
154
+ |-----------|------|-------------|
155
+ | `adapter` | `DatabaseAdapter<TSchema>` | The database adapter instance |
156
+ | `options` | `DatabasePluginOptions` | Optional plugin configuration |
157
+
158
+ **Options:**
159
+
160
+ | Option | Type | Default | Description |
161
+ |--------|------|---------|-------------|
162
+ | `registerDefault` | `boolean` | `true` | Register this adapter as the default |
163
+ | `registrationName` | `string` | `adapter.name` | Name to register the adapter under |
164
+ | `debug` | `boolean` | `false` | Enable debug logging |
165
+
166
+ **Lifecycle:**
167
+
168
+ 1. **Setup**: Registers the adapter globally and performs a health check
169
+ 2. **Middleware**: Attaches request-scoped clients to context for each request
170
+ 3. **Shutdown**: Handles cleanup when the application stops
171
+
172
+ ---
173
+
174
+ ### Context Helpers
175
+
176
+ #### `useDb(context)`
177
+
178
+ Get the database client from request context. Use this in loaders, actions, and middleware.
179
+
180
+ ```typescript
181
+ import { useDb } from '@ereo/db';
182
+
183
+ export const loader = createLoader({
184
+ load: async ({ context }) => {
185
+ const db = useDb(context);
186
+
187
+ // Access the underlying ORM client
188
+ const users = await db.client.select().from(usersTable);
189
+
190
+ // Execute raw SQL with deduplication
191
+ const result = await db.query('SELECT * FROM users WHERE id = ?', [1]);
192
+
193
+ // Get deduplication statistics
194
+ const stats = db.getDedupStats();
195
+ console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
196
+
197
+ return { users };
198
+ },
199
+ });
200
+ ```
201
+
202
+ **Returns:** `RequestScopedClient<TSchema>`
203
+
204
+ **Throws:** `Error` if the database plugin is not configured
205
+
206
+ ---
207
+
208
+ #### `useAdapter(context)`
209
+
210
+ Get the raw database adapter from context. Use this when you need direct adapter access.
211
+
212
+ ```typescript
213
+ import { useAdapter } from '@ereo/db';
214
+
215
+ export const action = createAction({
216
+ async run({ context }) {
217
+ const adapter = useAdapter(context);
218
+
219
+ // Manual transaction control
220
+ const tx = await adapter.beginTransaction();
221
+ try {
222
+ // ... perform operations
223
+ await tx.commit();
224
+ } catch (error) {
225
+ await tx.rollback();
226
+ throw error;
227
+ }
228
+ },
229
+ });
230
+ ```
231
+
232
+ **Returns:** `DatabaseAdapter<TSchema>`
233
+
234
+ **Throws:** `Error` if the database plugin is not configured
235
+
236
+ ---
237
+
238
+ #### `getDb()`
239
+
240
+ Get the default registered database adapter. Use this outside of request context (e.g., in scripts or background jobs).
241
+
242
+ ```typescript
243
+ import { getDb } from '@ereo/db';
244
+
245
+ // In a background job or script
246
+ async function cleanupOldRecords() {
247
+ const adapter = getDb();
248
+ if (!adapter) {
249
+ throw new Error('Database not configured');
250
+ }
251
+
252
+ await adapter.execute(
253
+ 'DELETE FROM sessions WHERE expires_at < NOW()'
254
+ );
255
+ }
256
+ ```
257
+
258
+ **Returns:** `DatabaseAdapter<TSchema> | undefined`
259
+
260
+ ---
261
+
262
+ #### `withTransaction(context, fn)`
263
+
264
+ Run a function within a database transaction using request context. Automatically clears the deduplication cache after the transaction completes.
265
+
266
+ ```typescript
267
+ import { withTransaction } from '@ereo/db';
268
+
269
+ export const action = createAction({
270
+ async run({ context }) {
271
+ const result = await withTransaction(context, async (tx) => {
272
+ // All operations use the same transaction
273
+ const [order] = await tx.insert(orders).values({
274
+ userId: 1,
275
+ total: 99.99,
276
+ }).returning();
277
+
278
+ await tx.insert(orderItems).values([
279
+ { orderId: order.id, productId: 1, quantity: 2 },
280
+ { orderId: order.id, productId: 3, quantity: 1 },
281
+ ]);
282
+
283
+ return order;
284
+ });
285
+
286
+ return { orderId: result.id };
287
+ },
288
+ });
289
+ ```
290
+
291
+ **Parameters:**
292
+
293
+ | Parameter | Type | Description |
294
+ |-----------|------|-------------|
295
+ | `context` | `AppContext` | The request context |
296
+ | `fn` | `(tx: TSchema) => Promise<TResult>` | Function to run within the transaction |
297
+
298
+ **Returns:** `Promise<TResult>`
299
+
300
+ ---
301
+
302
+ ### Adapter Interface
303
+
304
+ #### `DatabaseAdapter<TSchema>`
305
+
306
+ The core interface that all ORM adapters must implement.
307
+
308
+ ```typescript
309
+ interface DatabaseAdapter<TSchema = unknown> {
310
+ /** Human-readable adapter name */
311
+ readonly name: string;
312
+
313
+ /** Whether this adapter is compatible with edge runtimes */
314
+ readonly edgeCompatible: boolean;
315
+
316
+ /** Get the underlying database client */
317
+ getClient(): TSchema;
318
+
319
+ /** Get a request-scoped client with query deduplication */
320
+ getRequestClient(context: AppContext): RequestScopedClient<TSchema>;
321
+
322
+ /** Execute a raw SQL query */
323
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
324
+
325
+ /** Execute a raw SQL statement that modifies data */
326
+ execute(sql: string, params?: unknown[]): Promise<MutationResult>;
327
+
328
+ /** Run operations within a transaction */
329
+ transaction<T>(
330
+ fn: (tx: TSchema) => Promise<T>,
331
+ options?: TransactionOptions
332
+ ): Promise<T>;
333
+
334
+ /** Begin a manual transaction */
335
+ beginTransaction(options?: TransactionOptions): Promise<Transaction<TSchema>>;
336
+
337
+ /** Check database connectivity and health */
338
+ healthCheck(): Promise<HealthCheckResult>;
339
+
340
+ /** Disconnect from the database */
341
+ disconnect(): Promise<void>;
342
+ }
343
+ ```
344
+
345
+ ---
346
+
347
+ #### `RequestScopedClient<TSchema>`
348
+
349
+ Request-scoped database client with automatic query deduplication.
350
+
351
+ ```typescript
352
+ interface RequestScopedClient<TSchema> {
353
+ /** The underlying database client (e.g., Drizzle instance) */
354
+ readonly client: TSchema;
355
+
356
+ /** Execute a raw SQL query with automatic deduplication */
357
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<DedupResult<QueryResult<T>>>;
358
+
359
+ /** Get deduplication statistics for this request */
360
+ getDedupStats(): DedupStats;
361
+
362
+ /** Clear the deduplication cache */
363
+ clearDedup(): void;
364
+
365
+ /** Mark that a mutation has occurred, clearing relevant cache entries */
366
+ invalidate(tables?: string[]): void;
367
+ }
368
+ ```
369
+
370
+ ---
371
+
372
+ #### `Transaction<TSchema>`
373
+
374
+ Manual transaction handle for explicit control.
375
+
376
+ ```typescript
377
+ interface Transaction<TSchema> {
378
+ /** The transaction-scoped database client */
379
+ readonly client: TSchema;
380
+
381
+ /** Commit the transaction */
382
+ commit(): Promise<void>;
383
+
384
+ /** Rollback the transaction */
385
+ rollback(): Promise<void>;
386
+
387
+ /** Whether the transaction is still active */
388
+ readonly isActive: boolean;
389
+ }
390
+ ```
391
+
392
+ ---
393
+
394
+ #### Adapter Registry Functions
395
+
396
+ ##### `registerAdapter(name, adapter)`
397
+
398
+ Register an adapter instance globally for access from anywhere in the application.
399
+
400
+ ```typescript
401
+ import { registerAdapter } from '@ereo/db';
402
+
403
+ registerAdapter('primary', primaryAdapter);
404
+ registerAdapter('readonly', readonlyAdapter);
405
+ ```
406
+
407
+ ##### `getAdapter(name)`
408
+
409
+ Get a registered adapter by name.
410
+
411
+ ```typescript
412
+ import { getAdapter } from '@ereo/db';
413
+
414
+ const primary = getAdapter('primary');
415
+ const readonly = getAdapter('readonly');
416
+ ```
417
+
418
+ ##### `getDefaultAdapter()`
419
+
420
+ Get the first registered adapter (useful when only one adapter is configured).
421
+
422
+ ```typescript
423
+ import { getDefaultAdapter } from '@ereo/db';
424
+
425
+ const adapter = getDefaultAdapter();
426
+ ```
427
+
428
+ ##### `clearAdapterRegistry()`
429
+
430
+ Clear all registered adapters. Useful for testing.
431
+
432
+ ```typescript
433
+ import { clearAdapterRegistry } from '@ereo/db';
434
+
435
+ afterEach(() => {
436
+ clearAdapterRegistry();
437
+ });
438
+ ```
439
+
440
+ ---
441
+
442
+ ### Query Deduplication
443
+
444
+ Query deduplication automatically caches identical queries within a single request, eliminating redundant database calls.
445
+
446
+ #### `generateFingerprint(query, params?)`
447
+
448
+ Generate a deterministic cache key for a query and its parameters.
449
+
450
+ ```typescript
451
+ import { generateFingerprint } from '@ereo/db';
452
+
453
+ const key1 = generateFingerprint('SELECT * FROM users WHERE id = ?', [1]);
454
+ const key2 = generateFingerprint('SELECT * FROM users WHERE id = ?', [1]);
455
+ // key1 === key2
456
+
457
+ const key3 = generateFingerprint('SELECT * FROM users WHERE id = ?', [2]);
458
+ // key1 !== key3
459
+ ```
460
+
461
+ **Features:**
462
+ - Normalizes whitespace in SQL queries
463
+ - Handles special types: `Date`, `bigint`, `undefined`
464
+ - Uses djb2 hashing algorithm for fast, consistent fingerprints
465
+
466
+ ---
467
+
468
+ #### `dedupQuery(context, query, params, executor, options?)`
469
+
470
+ Execute a query with automatic deduplication. Returns cached results for identical queries within the same request.
471
+
472
+ ```typescript
473
+ import { dedupQuery } from '@ereo/db';
474
+
475
+ const result = await dedupQuery(
476
+ context,
477
+ 'SELECT * FROM users WHERE id = ?',
478
+ [userId],
479
+ async () => {
480
+ // This only executes on cache miss
481
+ return db.query('SELECT * FROM users WHERE id = ?', [userId]);
482
+ },
483
+ {
484
+ tables: ['users'], // Tables affected (for selective invalidation)
485
+ noCache: false, // Skip caching for this query
486
+ }
487
+ );
488
+
489
+ console.log(result.fromCache); // true if served from cache
490
+ console.log(result.cacheKey); // The fingerprint used
491
+ console.log(result.result); // The query result
492
+ ```
493
+
494
+ **Options:**
495
+
496
+ | Option | Type | Description |
497
+ |--------|------|-------------|
498
+ | `tables` | `string[]` | Tables affected by this query (for selective invalidation) |
499
+ | `noCache` | `boolean` | Skip caching for this query |
500
+
501
+ ---
502
+
503
+ #### `clearDedupCache(context)`
504
+
505
+ Clear the entire deduplication cache for a request. Call this after mutations to ensure subsequent reads get fresh data.
506
+
507
+ ```typescript
508
+ import { clearDedupCache } from '@ereo/db';
509
+
510
+ // After a mutation
511
+ await db.execute('UPDATE users SET name = ? WHERE id = ?', ['Alice', 1]);
512
+ clearDedupCache(context);
513
+ ```
514
+
515
+ ---
516
+
517
+ #### `invalidateTables(context, tables)`
518
+
519
+ Selectively invalidate cache entries related to specific tables.
520
+
521
+ ```typescript
522
+ import { invalidateTables } from '@ereo/db';
523
+
524
+ // Only invalidate user-related cached queries
525
+ await db.execute('UPDATE users SET name = ? WHERE id = ?', ['Alice', 1]);
526
+ invalidateTables(context, ['users']);
527
+
528
+ // Queries for 'posts' table are still cached
529
+ ```
530
+
531
+ **Note:** Table name matching is case-insensitive.
532
+
533
+ ---
534
+
535
+ #### `getRequestDedupStats(context)`
536
+
537
+ Get deduplication statistics for the current request.
538
+
539
+ ```typescript
540
+ import { getRequestDedupStats } from '@ereo/db';
541
+
542
+ const stats = getRequestDedupStats(context);
543
+ console.log({
544
+ total: stats.total, // Total queries attempted
545
+ deduplicated: stats.deduplicated, // Queries served from cache
546
+ unique: stats.unique, // Unique queries executed
547
+ hitRate: stats.hitRate, // Cache hit rate (0-1)
548
+ });
549
+ ```
550
+
551
+ ---
552
+
553
+ #### `debugGetCacheContents(context)`
554
+
555
+ Get the current cache contents for debugging. Only use in development.
556
+
557
+ ```typescript
558
+ import { debugGetCacheContents } from '@ereo/db';
559
+
560
+ if (process.env.NODE_ENV === 'development') {
561
+ const contents = debugGetCacheContents(context);
562
+ contents.forEach(entry => {
563
+ console.log({
564
+ key: entry.key,
565
+ tables: entry.tables,
566
+ age: entry.age, // milliseconds since cached
567
+ });
568
+ });
569
+ }
570
+ ```
571
+
572
+ ---
573
+
574
+ ### Connection Pool
575
+
576
+ Abstract connection pooling utilities that adapters can extend.
577
+
578
+ #### `ConnectionPool<T>`
579
+
580
+ Abstract base class for connection pools. Extend this to create ORM-specific pools.
581
+
582
+ ```typescript
583
+ import { ConnectionPool } from '@ereo/db';
584
+
585
+ class PostgresPool extends ConnectionPool<pg.Client> {
586
+ protected async createConnection(): Promise<pg.Client> {
587
+ const client = new pg.Client(this.connectionString);
588
+ await client.connect();
589
+ return client;
590
+ }
591
+
592
+ protected async closeConnection(connection: pg.Client): Promise<void> {
593
+ await connection.end();
594
+ }
595
+
596
+ protected async validateConnection(connection: pg.Client): Promise<boolean> {
597
+ try {
598
+ await connection.query('SELECT 1');
599
+ return true;
600
+ } catch {
601
+ return false;
602
+ }
603
+ }
604
+ }
605
+ ```
606
+
607
+ **Methods:**
608
+
609
+ | Method | Description |
610
+ |--------|-------------|
611
+ | `acquire()` | Acquire a connection from the pool |
612
+ | `release(connection)` | Release a connection back to the pool |
613
+ | `close()` | Close the pool and all connections |
614
+ | `getStats()` | Get pool statistics |
615
+ | `healthCheck()` | Check if the pool is healthy |
616
+
617
+ ---
618
+
619
+ #### `DEFAULT_POOL_CONFIG`
620
+
621
+ Default pool configuration for server environments.
622
+
623
+ ```typescript
624
+ import { DEFAULT_POOL_CONFIG } from '@ereo/db';
625
+
626
+ // {
627
+ // min: 2,
628
+ // max: 10,
629
+ // idleTimeoutMs: 30000,
630
+ // acquireTimeoutMs: 10000,
631
+ // acquireRetries: 3,
632
+ // }
633
+ ```
634
+
635
+ ---
636
+
637
+ #### `createEdgePoolConfig(overrides?)`
638
+
639
+ Create pool configuration optimized for edge environments (Cloudflare Workers, Vercel Edge).
640
+
641
+ ```typescript
642
+ import { createEdgePoolConfig } from '@ereo/db';
643
+
644
+ const config = createEdgePoolConfig({
645
+ max: 2, // Override specific settings
646
+ });
647
+
648
+ // {
649
+ // min: 0,
650
+ // max: 2,
651
+ // idleTimeoutMs: 0,
652
+ // acquireTimeoutMs: 5000,
653
+ // acquireRetries: 2,
654
+ // }
655
+ ```
656
+
657
+ ---
658
+
659
+ #### `createServerlessPoolConfig(overrides?)`
660
+
661
+ Create pool configuration for serverless environments (AWS Lambda, Vercel Functions).
662
+
663
+ ```typescript
664
+ import { createServerlessPoolConfig } from '@ereo/db';
665
+
666
+ const config = createServerlessPoolConfig();
667
+
668
+ // {
669
+ // min: 0,
670
+ // max: 5,
671
+ // idleTimeoutMs: 10000,
672
+ // acquireTimeoutMs: 8000,
673
+ // acquireRetries: 2,
674
+ // }
675
+ ```
676
+
677
+ ---
678
+
679
+ #### `PoolStats`
680
+
681
+ Statistics about connection pool state.
682
+
683
+ ```typescript
684
+ interface PoolStats {
685
+ active: number; // Connections currently in use
686
+ idle: number; // Idle connections available
687
+ total: number; // Total connections (active + idle)
688
+ waiting: number; // Requests waiting for a connection
689
+ totalCreated: number; // Total connections created over lifetime
690
+ totalClosed: number; // Total connections closed over lifetime
691
+ }
692
+ ```
693
+
694
+ ---
695
+
696
+ ### Retry Utilities
697
+
698
+ #### `withRetry(operation, config?)`
699
+
700
+ Execute an operation with automatic retry logic and exponential backoff.
701
+
702
+ ```typescript
703
+ import { withRetry, isCommonRetryableError } from '@ereo/db';
704
+
705
+ const result = await withRetry(
706
+ async () => {
707
+ return db.query('SELECT * FROM users');
708
+ },
709
+ {
710
+ maxAttempts: 3,
711
+ baseDelayMs: 100,
712
+ maxDelayMs: 5000,
713
+ exponential: true,
714
+ isRetryable: isCommonRetryableError,
715
+ }
716
+ );
717
+ ```
718
+
719
+ **Configuration:**
720
+
721
+ | Option | Type | Default | Description |
722
+ |--------|------|---------|-------------|
723
+ | `maxAttempts` | `number` | `3` | Maximum retry attempts |
724
+ | `baseDelayMs` | `number` | `100` | Base delay between retries |
725
+ | `maxDelayMs` | `number` | `5000` | Maximum delay between retries |
726
+ | `exponential` | `boolean` | `true` | Use exponential backoff |
727
+ | `isRetryable` | `(error: Error) => boolean` | - | Custom retry condition |
728
+
729
+ ---
730
+
731
+ #### `isCommonRetryableError(error)`
732
+
733
+ Check if an error is commonly retryable (connection issues, deadlocks, etc.).
734
+
735
+ ```typescript
736
+ import { isCommonRetryableError } from '@ereo/db';
737
+
738
+ // Returns true for:
739
+ // - Connection refused/reset/closed/timeout
740
+ // - Deadlock detected
741
+ // - Serialization failure
742
+ // - Too many connections
743
+
744
+ isCommonRetryableError(new Error('Connection refused')); // true
745
+ isCommonRetryableError(new Error('Deadlock detected')); // true
746
+ isCommonRetryableError(new Error('Syntax error')); // false
747
+ ```
748
+
749
+ ---
750
+
751
+ #### `DEFAULT_RETRY_CONFIG`
752
+
753
+ Default retry configuration.
754
+
755
+ ```typescript
756
+ import { DEFAULT_RETRY_CONFIG } from '@ereo/db';
757
+
758
+ // {
759
+ // maxAttempts: 3,
760
+ // baseDelayMs: 100,
761
+ // maxDelayMs: 5000,
762
+ // exponential: true,
763
+ // }
764
+ ```
765
+
766
+ ---
767
+
768
+ ### Error Classes
769
+
770
+ All error classes extend `DatabaseError` and include error codes for programmatic handling.
771
+
772
+ #### `DatabaseError`
773
+
774
+ Base database error class.
775
+
776
+ ```typescript
777
+ import { DatabaseError } from '@ereo/db';
778
+
779
+ const error = new DatabaseError('Something went wrong', 'CUSTOM_CODE', cause);
780
+ console.log(error.name); // 'DatabaseError'
781
+ console.log(error.code); // 'CUSTOM_CODE'
782
+ console.log(error.cause); // Original error
783
+ ```
784
+
785
+ ---
786
+
787
+ #### `ConnectionError`
788
+
789
+ Connection-related errors (code: `CONNECTION_ERROR`).
790
+
791
+ ```typescript
792
+ import { ConnectionError } from '@ereo/db';
793
+
794
+ try {
795
+ await adapter.connect();
796
+ } catch (error) {
797
+ if (error instanceof ConnectionError) {
798
+ console.log('Failed to connect:', error.message);
799
+ }
800
+ }
801
+ ```
802
+
803
+ ---
804
+
805
+ #### `QueryError`
806
+
807
+ Query execution errors (code: `QUERY_ERROR`).
808
+
809
+ ```typescript
810
+ import { QueryError } from '@ereo/db';
811
+
812
+ const error = new QueryError(
813
+ 'Syntax error near "FORM"',
814
+ 'SELECT * FORM users', // The problematic query
815
+ [1, 2, 3], // Parameters
816
+ originalError
817
+ );
818
+
819
+ console.log(error.query); // 'SELECT * FORM users'
820
+ console.log(error.params); // [1, 2, 3]
821
+ ```
822
+
823
+ ---
824
+
825
+ #### `TransactionError`
826
+
827
+ Transaction errors (code: `TRANSACTION_ERROR`).
828
+
829
+ ```typescript
830
+ import { TransactionError } from '@ereo/db';
831
+
832
+ try {
833
+ await adapter.transaction(async (tx) => {
834
+ // ...
835
+ });
836
+ } catch (error) {
837
+ if (error instanceof TransactionError) {
838
+ console.log('Transaction failed:', error.message);
839
+ }
840
+ }
841
+ ```
842
+
843
+ ---
844
+
845
+ #### `TimeoutError`
846
+
847
+ Timeout errors (code: `TIMEOUT_ERROR`).
848
+
849
+ ```typescript
850
+ import { TimeoutError } from '@ereo/db';
851
+
852
+ const error = new TimeoutError('Query timed out', 5000);
853
+ console.log(error.timeoutMs); // 5000
854
+ ```
855
+
856
+ ---
857
+
858
+ ### Types
859
+
860
+ #### Query Result Types
861
+
862
+ ```typescript
863
+ /** Result of a SELECT query */
864
+ interface QueryResult<T = unknown> {
865
+ rows: T[];
866
+ rowCount: number;
867
+ }
868
+
869
+ /** Result of an INSERT/UPDATE/DELETE mutation */
870
+ interface MutationResult {
871
+ rowsAffected: number;
872
+ lastInsertId?: number | bigint;
873
+ }
874
+
875
+ /** Result wrapper with deduplication metadata */
876
+ interface DedupResult<T> {
877
+ result: T;
878
+ fromCache: boolean;
879
+ cacheKey: string;
880
+ }
881
+
882
+ /** Deduplication statistics */
883
+ interface DedupStats {
884
+ total: number; // Total queries attempted
885
+ deduplicated: number; // Queries served from cache
886
+ unique: number; // Unique queries executed
887
+ hitRate: number; // Cache hit rate (0-1)
888
+ }
889
+ ```
890
+
891
+ ---
892
+
893
+ #### Configuration Types
894
+
895
+ ```typescript
896
+ /** Connection pool configuration */
897
+ interface PoolConfig {
898
+ min?: number; // Minimum connections (default: 2)
899
+ max?: number; // Maximum connections (default: 10)
900
+ idleTimeoutMs?: number; // Idle timeout in ms (default: 30000)
901
+ acquireTimeoutMs?: number; // Acquire timeout in ms (default: 10000)
902
+ acquireRetries?: number; // Max acquire retries (default: 3)
903
+ }
904
+
905
+ /** Transaction isolation levels */
906
+ type IsolationLevel =
907
+ | 'read uncommitted'
908
+ | 'read committed'
909
+ | 'repeatable read'
910
+ | 'serializable';
911
+
912
+ /** Transaction configuration */
913
+ interface TransactionOptions {
914
+ isolationLevel?: IsolationLevel;
915
+ readOnly?: boolean;
916
+ timeout?: number; // Timeout in milliseconds
917
+ }
918
+
919
+ /** Base adapter configuration */
920
+ interface AdapterConfig {
921
+ url: string;
922
+ debug?: boolean;
923
+ pool?: PoolConfig;
924
+ edgeCompatible?: boolean;
925
+ }
926
+ ```
927
+
928
+ ---
929
+
930
+ #### Type Inference Utilities
931
+
932
+ ```typescript
933
+ /** Infer the select type from a Drizzle table */
934
+ type InferSelect<T extends { $inferSelect: unknown }> = T['$inferSelect'];
935
+
936
+ /** Infer the insert type from a Drizzle table */
937
+ type InferInsert<T extends { $inferInsert: unknown }> = T['$inferInsert'];
938
+
939
+ // Usage:
940
+ type User = InferSelect<typeof users>;
941
+ type NewUser = InferInsert<typeof users>;
942
+ ```
943
+
944
+ ---
945
+
946
+ #### Module Augmentation for Typed Tables
947
+
948
+ ```typescript
949
+ // In your project's types file
950
+ declare module '@ereo/db' {
951
+ interface DatabaseTables {
952
+ users: typeof import('./schema').users;
953
+ posts: typeof import('./schema').posts;
954
+ }
955
+ }
956
+
957
+ // Now you get typed table access
958
+ type UserTable = TableType<'users'>;
959
+ type AllTableNames = TableNames; // 'users' | 'posts'
960
+ ```
961
+
962
+ ---
963
+
964
+ #### Query Builder Types
965
+
966
+ ```typescript
967
+ /** Typed WHERE clause conditions */
968
+ type TypedWhere<T> = {
969
+ [K in keyof T]?: T[K] | WhereOperator<T[K]>;
970
+ } & {
971
+ AND?: TypedWhere<T>[];
972
+ OR?: TypedWhere<T>[];
973
+ NOT?: TypedWhere<T>;
974
+ };
975
+
976
+ /** WHERE clause operators */
977
+ interface WhereOperator<T> {
978
+ eq?: T;
979
+ ne?: T;
980
+ gt?: T;
981
+ gte?: T;
982
+ lt?: T;
983
+ lte?: T;
984
+ in?: T[];
985
+ notIn?: T[];
986
+ like?: string;
987
+ ilike?: string;
988
+ isNull?: boolean;
989
+ isNotNull?: boolean;
990
+ between?: [T, T];
991
+ }
992
+
993
+ /** Typed ORDER BY clause */
994
+ type TypedOrderBy<T> = {
995
+ [K in keyof T]?: 'asc' | 'desc';
996
+ };
997
+
998
+ /** Typed SELECT fields */
999
+ type TypedSelect<T> = (keyof T)[] | '*';
1000
+ ```
1001
+
1002
+ ---
1003
+
1004
+ #### Health Check Result
1005
+
1006
+ ```typescript
1007
+ interface HealthCheckResult {
1008
+ healthy: boolean;
1009
+ latencyMs: number;
1010
+ error?: string;
1011
+ metadata?: Record<string, unknown>;
1012
+ }
1013
+ ```
1014
+
1015
+ ---
1016
+
1017
+ ## Configuration Options
1018
+
1019
+ ### Pool Configuration Presets
1020
+
1021
+ | Environment | `min` | `max` | `idleTimeoutMs` | `acquireTimeoutMs` | `acquireRetries` |
1022
+ |-------------|-------|-------|-----------------|--------------------|--------------------|
1023
+ | Server (default) | 2 | 10 | 30000 | 10000 | 3 |
1024
+ | Edge | 0 | 1 | 0 | 5000 | 2 |
1025
+ | Serverless | 0 | 5 | 10000 | 8000 | 2 |
1026
+
1027
+ ---
1028
+
1029
+ ## Use Cases
1030
+
1031
+ ### Avoiding N+1 Queries
1032
+
1033
+ Query deduplication automatically prevents N+1 queries in nested loaders:
1034
+
1035
+ ```typescript
1036
+ // Parent loader
1037
+ export const loader = createLoader({
1038
+ load: async ({ context }) => {
1039
+ const db = useDb(context);
1040
+ const posts = await db.client.select().from(postsTable);
1041
+ return { posts };
1042
+ },
1043
+ });
1044
+
1045
+ // Child component that runs for each post
1046
+ function PostAuthor({ postId }) {
1047
+ // Even if this runs 100 times, the query only executes once
1048
+ const author = useLoaderData(async (context) => {
1049
+ const db = useDb(context);
1050
+ return db.client
1051
+ .select()
1052
+ .from(usersTable)
1053
+ .where(eq(usersTable.id, postId));
1054
+ });
1055
+ }
1056
+ ```
1057
+
1058
+ ### Optimistic Updates with Cache Invalidation
1059
+
1060
+ ```typescript
1061
+ export const action = createAction({
1062
+ async run({ context, formData }) {
1063
+ const db = useDb(context);
1064
+
1065
+ // Perform the update
1066
+ await db.client
1067
+ .update(usersTable)
1068
+ .set({ name: formData.get('name') })
1069
+ .where(eq(usersTable.id, formData.get('id')));
1070
+
1071
+ // Invalidate only user-related cached queries
1072
+ db.invalidate(['users']);
1073
+
1074
+ return { success: true };
1075
+ },
1076
+ });
1077
+ ```
1078
+
1079
+ ### Background Jobs with Direct Adapter Access
1080
+
1081
+ ```typescript
1082
+ import { getDb } from '@ereo/db';
1083
+
1084
+ async function processExpiredSessions() {
1085
+ const adapter = getDb();
1086
+ if (!adapter) {
1087
+ throw new Error('Database not configured');
1088
+ }
1089
+
1090
+ const result = await adapter.execute(
1091
+ 'DELETE FROM sessions WHERE expires_at < NOW()'
1092
+ );
1093
+
1094
+ console.log(`Cleaned up ${result.rowsAffected} expired sessions`);
1095
+ }
1096
+ ```
1097
+
1098
+ ### Custom Connection Pool
1099
+
1100
+ ```typescript
1101
+ import { ConnectionPool, createServerlessPoolConfig } from '@ereo/db';
1102
+
1103
+ class CustomPool extends ConnectionPool<MyConnection> {
1104
+ constructor() {
1105
+ super(createServerlessPoolConfig({
1106
+ max: 3, // Limit for serverless
1107
+ }));
1108
+ }
1109
+
1110
+ protected async createConnection() {
1111
+ return new MyConnection(process.env.DATABASE_URL);
1112
+ }
1113
+
1114
+ protected async closeConnection(conn: MyConnection) {
1115
+ await conn.close();
1116
+ }
1117
+
1118
+ protected async validateConnection(conn: MyConnection) {
1119
+ return conn.isAlive();
1120
+ }
1121
+ }
1122
+ ```
1123
+
1124
+ ---
1125
+
1126
+ ## Integration with Other Packages
1127
+
1128
+ ### @ereo/db-drizzle
1129
+
1130
+ The official Drizzle ORM adapter for `@ereo/db`.
1131
+
1132
+ ```typescript
1133
+ import { createDrizzleAdapter, definePostgresConfig } from '@ereo/db-drizzle';
1134
+ import { createDatabasePlugin } from '@ereo/db';
1135
+
1136
+ const adapter = createDrizzleAdapter(definePostgresConfig({
1137
+ url: process.env.DATABASE_URL,
1138
+ schema,
1139
+ }));
1140
+
1141
+ export default defineConfig({
1142
+ plugins: [createDatabasePlugin(adapter)],
1143
+ });
1144
+ ```
1145
+
1146
+ ### @ereo/core
1147
+
1148
+ The database plugin integrates with EreoJS's plugin system:
1149
+
1150
+ ```typescript
1151
+ import { defineConfig } from '@ereo/core';
1152
+ import { createDatabasePlugin } from '@ereo/db';
1153
+
1154
+ export default defineConfig({
1155
+ plugins: [
1156
+ createDatabasePlugin(adapter),
1157
+ // Other plugins...
1158
+ ],
1159
+ });
1160
+ ```
1161
+
1162
+ The plugin automatically:
1163
+ - Registers the adapter during setup
1164
+ - Attaches request-scoped clients via middleware
1165
+ - Performs health checks on startup
1166
+
1167
+ ---
1168
+
1169
+ ## Troubleshooting
1170
+
1171
+ ### "Database not available in context"
1172
+
1173
+ **Cause:** `useDb()` was called before the database plugin middleware ran.
1174
+
1175
+ **Solution:** Ensure `createDatabasePlugin` is registered in your config:
1176
+
1177
+ ```typescript
1178
+ export default defineConfig({
1179
+ plugins: [createDatabasePlugin(adapter)],
1180
+ });
1181
+ ```
1182
+
1183
+ ### "Database not connected"
1184
+
1185
+ **Cause:** Attempting to use `getClient()` before the adapter connected.
1186
+
1187
+ **Solution:** Use `useDb()` in request context, or call `healthCheck()` first:
1188
+
1189
+ ```typescript
1190
+ const adapter = getDb();
1191
+ const health = await adapter.healthCheck();
1192
+ if (health.healthy) {
1193
+ const client = adapter.getClient();
1194
+ }
1195
+ ```
1196
+
1197
+ ### High Memory Usage from Deduplication
1198
+
1199
+ **Cause:** Very large result sets being cached.
1200
+
1201
+ **Solution:** Skip caching for large queries:
1202
+
1203
+ ```typescript
1204
+ await dedupQuery(context, sql, params, executor, { noCache: true });
1205
+ ```
1206
+
1207
+ ### Connection Pool Exhaustion
1208
+
1209
+ **Symptoms:** `TimeoutError: Timed out waiting for connection`
1210
+
1211
+ **Solutions:**
1212
+ 1. Increase `max` connections
1213
+ 2. Ensure connections are properly released
1214
+ 3. Check for connection leaks in your code
1215
+ 4. Use appropriate pool presets for your environment
1216
+
1217
+ ```typescript
1218
+ const config = createServerlessPoolConfig({
1219
+ max: 10,
1220
+ acquireTimeoutMs: 15000,
1221
+ });
1222
+ ```
1223
+
1224
+ ---
1225
+
1226
+ ## TypeScript Types Reference
1227
+
1228
+ All types are exported from the main package entry point:
1229
+
1230
+ ```typescript
1231
+ import {
1232
+ // Adapter types
1233
+ type DatabaseAdapter,
1234
+ type RequestScopedClient,
1235
+ type Transaction,
1236
+ type AdapterFactory,
1237
+
1238
+ // Result types
1239
+ type QueryResult,
1240
+ type MutationResult,
1241
+ type DedupResult,
1242
+ type DedupStats,
1243
+
1244
+ // Configuration types
1245
+ type PoolConfig,
1246
+ type AdapterConfig,
1247
+ type TransactionOptions,
1248
+ type IsolationLevel,
1249
+ type PoolStats,
1250
+ type RetryConfig,
1251
+ type HealthCheckResult,
1252
+
1253
+ // Type inference utilities
1254
+ type InferSelect,
1255
+ type InferInsert,
1256
+ type DatabaseTables,
1257
+ type TableNames,
1258
+ type TableType,
1259
+
1260
+ // Query builder types
1261
+ type TypedWhere,
1262
+ type WhereOperator,
1263
+ type TypedOrderBy,
1264
+ type TypedSelect,
1265
+
1266
+ // Plugin types
1267
+ type DatabasePluginOptions,
1268
+
1269
+ // Error classes
1270
+ DatabaseError,
1271
+ ConnectionError,
1272
+ QueryError,
1273
+ TransactionError,
1274
+ TimeoutError,
1275
+ } from '@ereo/db';
1276
+ ```
1277
+
1278
+ ---
1279
+
1280
+ ## License
1281
+
1282
+ MIT