singulio-postgres 1.1.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 ADDED
@@ -0,0 +1,866 @@
1
+ # @singulio/postgres
2
+
3
+ High-performance PostgreSQL 18.x client for Bun with Row-Level Security (RLS), connection pooling, transactions, and pgvector support for AI/ML workloads.
4
+
5
+ ## Overview
6
+
7
+ `@singulio/postgres` is a thin, type-safe wrapper around Bun's native `bun:postgres` driver that provides enterprise-grade features for modern PostgreSQL applications:
8
+
9
+ - **Row-Level Security (RLS)**: Native multi-tenant isolation using PostgreSQL session GUCs
10
+ - **Connection Pooling**: Configurable pooling with idle timeout and max lifetime management
11
+ - **Transaction Support**: ACID transactions with auto-rollback, isolation levels, and savepoints
12
+ - **pgvector Integration**: First-class support for AI/ML embeddings and similarity search
13
+ - **Health Checks**: Kubernetes-ready readiness/liveness probes
14
+ - **Zero Dependencies**: Leverages Bun's native PostgreSQL driver for maximum performance
15
+ - **Full TypeScript**: Complete type safety with intelligent type inference
16
+
17
+ ## Features
18
+
19
+ ### Core Capabilities
20
+
21
+ - **Simple Client API**: Easy-to-use query methods (query, queryOne, queryAll, execute)
22
+ - **Connection Pooling**: Automatic connection lifecycle management with configurable limits
23
+ - **RLS Helpers**: Multi-tenant data isolation with session-scoped context
24
+ - **Transaction Management**: Nested transactions, savepoints, and configurable isolation levels
25
+ - **Vector Operations**: Full pgvector support for embeddings, similarity search, and batch operations
26
+ - **Health Monitoring**: Built-in health check endpoints for Kubernetes deployments
27
+ - **Performance**: Native Bun driver with minimal overhead
28
+
29
+ ### Database Compatibility
30
+
31
+ - PostgreSQL 18.x (recommended)
32
+ - PostgreSQL 16+ with pgvector extension
33
+
34
+ ## Installation
35
+
36
+ ### npm
37
+
38
+ ```bash
39
+ npm install @singulio/postgres
40
+ ```
41
+
42
+ ### yarn
43
+
44
+ ```bash
45
+ yarn add @singulio/postgres
46
+ ```
47
+
48
+ ### bun
49
+
50
+ ```bash
51
+ bun add @singulio/postgres
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ### Basic Usage
57
+
58
+ ```typescript
59
+ import { createClient } from '@singulio/postgres';
60
+
61
+ // Create a client
62
+ const client = createClient(process.env.DATABASE_URL);
63
+
64
+ // Simple query
65
+ const users = await client.queryAll('SELECT * FROM users WHERE active = $1', [true]);
66
+
67
+ // Insert with returning
68
+ const newUser = await client.queryOne(
69
+ 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
70
+ ['Alice', 'alice@example.com']
71
+ );
72
+
73
+ // Execute command (returns affected row count)
74
+ const deleted = await client.execute('DELETE FROM users WHERE id = $1', [123]);
75
+
76
+ console.log(`Deleted ${deleted} user(s)`);
77
+ ```
78
+
79
+ ### Connection Pooling
80
+
81
+ ```typescript
82
+ import { createPool } from '@singulio/postgres';
83
+
84
+ // Create and initialize pool
85
+ const pool = await createPool({
86
+ connectionString: process.env.DATABASE_URL,
87
+ min: 5, // Minimum connections
88
+ max: 20, // Maximum connections
89
+ idleTimeout: 30000, // Close idle connections after 30s
90
+ connectionTimeout: 5000, // Timeout acquiring connection
91
+ maxLifetime: 3600000, // Recycle connections after 1 hour
92
+ });
93
+
94
+ // Query through pool
95
+ const result = await pool.query('SELECT * FROM products WHERE price < $1', [100]);
96
+
97
+ // Use exclusive connection for multiple queries
98
+ const stats = await pool.withConnection(async (query) => {
99
+ const total = await query('SELECT COUNT(*) FROM orders');
100
+ const pending = await query('SELECT COUNT(*) FROM orders WHERE status = $1', ['pending']);
101
+ return { total: total.rows[0].count, pending: pending.rows[0].count };
102
+ });
103
+
104
+ // Get pool statistics
105
+ const poolStats = pool.stats();
106
+ console.log(`Pool: ${poolStats.active} active, ${poolStats.idle} idle, ${poolStats.pending} waiting`);
107
+
108
+ // Close pool when done
109
+ await pool.close();
110
+ ```
111
+
112
+ ### Row-Level Security (RLS)
113
+
114
+ ```typescript
115
+ import { createClient, withTenant, createRLSClient } from '@singulio/postgres';
116
+
117
+ const client = createClient(process.env.DATABASE_URL);
118
+
119
+ // Execute queries within tenant context
120
+ const tenantData = await withTenant(
121
+ { tenantId: 'tenant-abc', userId: 'user-123', isAdmin: false },
122
+ async () => {
123
+ // All queries here automatically filtered by tenant
124
+ return await client.queryAll('SELECT * FROM contacts');
125
+ }
126
+ );
127
+
128
+ // RLS-aware client
129
+ const rlsClient = createRLSClient();
130
+
131
+ // Query with explicit tenant context
132
+ const orders = await rlsClient.queryAll(
133
+ { tenantId: 'tenant-xyz' },
134
+ 'SELECT * FROM orders WHERE status = $1',
135
+ ['shipped']
136
+ );
137
+
138
+ // Admin bypass
139
+ const allData = await rlsClient.queryAll(
140
+ { tenantId: 'tenant-xyz', isAdmin: true },
141
+ 'SELECT * FROM sensitive_data'
142
+ );
143
+ ```
144
+
145
+ ### Transactions
146
+
147
+ ```typescript
148
+ import { transaction, serializableTransaction, savepoint } from '@singulio/postgres';
149
+
150
+ // Basic transaction with auto-rollback
151
+ await transaction(async (tx) => {
152
+ await tx.execute('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]);
153
+ await tx.execute('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]);
154
+ // Auto-commits on success, auto-rollbacks on error
155
+ });
156
+
157
+ // Serializable transaction for strong consistency
158
+ await serializableTransaction(async (tx) => {
159
+ const inventory = await tx.queryOne('SELECT quantity FROM inventory WHERE sku = $1', ['ABC']);
160
+ if (inventory.quantity > 0) {
161
+ await tx.execute('UPDATE inventory SET quantity = quantity - 1 WHERE sku = $1', ['ABC']);
162
+ await tx.execute('INSERT INTO orders (sku, quantity) VALUES ($1, 1)', ['ABC']);
163
+ }
164
+ });
165
+
166
+ // Transaction with RLS context
167
+ await transaction(
168
+ async (tx) => {
169
+ const contact = await tx.queryOne('SELECT * FROM contacts WHERE id = $1', [456]);
170
+ await tx.execute('UPDATE contacts SET last_viewed = NOW() WHERE id = $1', [456]);
171
+ return contact;
172
+ },
173
+ {
174
+ isolationLevel: 'REPEATABLE READ',
175
+ rlsContext: { tenantId: 'tenant-123' }
176
+ }
177
+ );
178
+
179
+ // Savepoints for nested rollback
180
+ await transaction(async (tx) => {
181
+ await tx.execute('INSERT INTO logs (message) VALUES ($1)', ['Started']);
182
+
183
+ await savepoint('before_update');
184
+ try {
185
+ await tx.execute('UPDATE critical_table SET value = $1', [newValue]);
186
+ } catch (error) {
187
+ await rollbackToSavepoint('before_update');
188
+ // Continue transaction with fallback
189
+ }
190
+
191
+ await tx.execute('INSERT INTO logs (message) VALUES ($1)', ['Completed']);
192
+ });
193
+ ```
194
+
195
+ ### Vector Search (pgvector)
196
+
197
+ ```typescript
198
+ import {
199
+ ensureVectorExtension,
200
+ createVectorColumn,
201
+ createVectorIndex,
202
+ vectorSearch,
203
+ insertWithVector,
204
+ formatVector,
205
+ } from '@singulio/postgres';
206
+
207
+ // Setup pgvector extension
208
+ await ensureVectorExtension();
209
+
210
+ // Add vector column to existing table
211
+ await createVectorColumn('documents', 'embedding', 1536); // OpenAI ada-002 dimension
212
+
213
+ // Create HNSW index for fast similarity search
214
+ await createVectorIndex('documents', 'embedding', 'hnsw', '<=>', {
215
+ m: 16, // Max connections per layer
216
+ efConstruction: 64 // Build quality
217
+ });
218
+
219
+ // Insert document with embedding
220
+ const embedding = [0.1, 0.2, 0.3, /* ... 1536 dimensions */];
221
+ await insertWithVector(
222
+ 'documents',
223
+ { title: 'AI Report', content: 'Lorem ipsum...' },
224
+ 'embedding',
225
+ embedding
226
+ );
227
+
228
+ // Similarity search using cosine distance
229
+ const queryEmbedding = [0.15, 0.25, 0.35, /* ... */];
230
+ const results = await vectorSearch('documents', 'embedding', queryEmbedding, {
231
+ operator: '<=>', // Cosine similarity
232
+ limit: 10,
233
+ threshold: 0.3, // Max distance threshold
234
+ filter: 'created_at > $1',
235
+ filterParams: [new Date('2024-01-01')],
236
+ });
237
+
238
+ for (const doc of results.rows) {
239
+ console.log(`${doc.title} - similarity: ${1 - doc.distance}`);
240
+ }
241
+
242
+ // Batch insert with vectors
243
+ await batchInsertWithVectors(
244
+ 'documents',
245
+ ['title', 'content'],
246
+ 'embedding',
247
+ [
248
+ { data: ['Doc 1', 'Content 1'], vector: embedding1 },
249
+ { data: ['Doc 2', 'Content 2'], vector: embedding2 },
250
+ ]
251
+ );
252
+
253
+ // Supported distance operators:
254
+ // '<->' - L2 (Euclidean) distance
255
+ // '<=>' - Cosine distance (1 - cosine similarity)
256
+ // '<#>' - Inner product (negative)
257
+ // '<+>' - L1 (Manhattan) distance
258
+ ```
259
+
260
+ ## API Reference
261
+
262
+ ### Client
263
+
264
+ #### `createClient(config, logger?)`
265
+
266
+ Creates a new PostgreSQL client.
267
+
268
+ **Parameters:**
269
+ - `config`: Connection string or `PostgresConfig` object
270
+ - `logger`: Optional logger instance (matches `@singulio/logger` interface)
271
+
272
+ **Returns:** `PostgresClient`
273
+
274
+ #### PostgresClient Methods
275
+
276
+ ##### `query<T>(queryText, params?): Promise<QueryResult<T>>`
277
+
278
+ Execute a SQL query with parameters.
279
+
280
+ ##### `queryOne<T>(queryText, params?): Promise<T | null>`
281
+
282
+ Execute query and return first row or null.
283
+
284
+ ##### `queryAll<T>(queryText, params?): Promise<T[]>`
285
+
286
+ Execute query and return all rows.
287
+
288
+ ##### `execute(queryText, params?): Promise<number>`
289
+
290
+ Execute command and return affected row count.
291
+
292
+ ##### `ping(): Promise<boolean>`
293
+
294
+ Check database connectivity.
295
+
296
+ ##### `version(): Promise<string>`
297
+
298
+ Get PostgreSQL server version.
299
+
300
+ ##### `close(): Promise<void>`
301
+
302
+ Close client connection.
303
+
304
+ ### Pool
305
+
306
+ #### `createPool(config, logger?): Promise<PostgresPool>`
307
+
308
+ Create and initialize a connection pool.
309
+
310
+ **Parameters:**
311
+ - `config`: `PoolConfig` object with connection and pool settings
312
+ - `logger`: Optional logger instance
313
+
314
+ **Returns:** `Promise<PostgresPool>`
315
+
316
+ #### PostgresPool Methods
317
+
318
+ ##### `initialize(): Promise<void>`
319
+
320
+ Initialize pool with minimum connections (called automatically by `createPool`).
321
+
322
+ ##### `query<T>(queryText, params?): Promise<QueryResult<T>>`
323
+
324
+ Execute query using pooled connection.
325
+
326
+ ##### `withConnection<T>(fn): Promise<T>`
327
+
328
+ Execute function with exclusive connection.
329
+
330
+ **Example:**
331
+ ```typescript
332
+ const result = await pool.withConnection(async (query) => {
333
+ const r1 = await query('SELECT ...');
334
+ const r2 = await query('UPDATE ...');
335
+ return { r1, r2 };
336
+ });
337
+ ```
338
+
339
+ ##### `stats(): PoolStats`
340
+
341
+ Get current pool statistics (total, idle, active, pending).
342
+
343
+ ##### `close(): Promise<void>`
344
+
345
+ Close pool and all connections.
346
+
347
+ ### Row-Level Security
348
+
349
+ #### `setRLSContext(context): Promise<void>`
350
+
351
+ Set RLS context variables for current session.
352
+
353
+ **Parameters:**
354
+ - `context.tenantId`: Tenant identifier (required)
355
+ - `context.userId`: User identifier (optional)
356
+ - `context.isAdmin`: Admin flag (optional, default: false)
357
+ - `context.extraGUCs`: Additional session variables (optional)
358
+
359
+ #### `clearRLSContext(): Promise<void>`
360
+
361
+ Clear RLS context variables.
362
+
363
+ #### `getRLSContext(): Promise<RLSContext | null>`
364
+
365
+ Get current RLS context.
366
+
367
+ #### `withTenant<T>(context, fn): Promise<T>`
368
+
369
+ Execute function with RLS context, auto-cleanup.
370
+
371
+ #### `queryWithTenant<T>(context, queryText, params?): Promise<QueryResult<T>>`
372
+
373
+ Execute single query with RLS context.
374
+
375
+ #### `createRLSClient(logger?): RLSClient`
376
+
377
+ Create RLS-aware client wrapper.
378
+
379
+ **RLSClient Methods:**
380
+ - `query<T>(context, queryText, params?): Promise<QueryResult<T>>`
381
+ - `queryOne<T>(context, queryText, params?): Promise<T | null>`
382
+ - `queryAll<T>(context, queryText, params?): Promise<T[]>`
383
+ - `withTenant<T>(context, fn): Promise<T>`
384
+
385
+ ### Transactions
386
+
387
+ #### `transaction<T>(fn, options?, logger?): Promise<T>`
388
+
389
+ Execute function within transaction with auto-commit/rollback.
390
+
391
+ **Parameters:**
392
+ - `fn`: Function receiving `TransactionQuery` interface
393
+ - `options.isolationLevel`: 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'
394
+ - `options.readOnly`: Read-only transaction (default: false)
395
+ - `options.deferrable`: Deferrable (requires SERIALIZABLE + readOnly)
396
+ - `options.rlsContext`: RLS context to apply
397
+ - `logger`: Optional logger
398
+
399
+ **TransactionQuery Methods:**
400
+ - `query<T>(queryText, params?): Promise<QueryResult<T>>`
401
+ - `queryOne<T>(queryText, params?): Promise<T | null>`
402
+ - `queryAll<T>(queryText, params?): Promise<T[]>`
403
+ - `execute(queryText, params?): Promise<number>`
404
+
405
+ #### `serializableTransaction<T>(fn, rlsContext?, logger?): Promise<T>`
406
+
407
+ Execute with SERIALIZABLE isolation level.
408
+
409
+ #### `readOnlyTransaction<T>(fn, rlsContext?, logger?): Promise<T>`
410
+
411
+ Execute read-only transaction (optimized for reporting).
412
+
413
+ #### `savepoint(name): Promise<void>`
414
+
415
+ Create savepoint within transaction.
416
+
417
+ #### `rollbackToSavepoint(name): Promise<void>`
418
+
419
+ Rollback to savepoint.
420
+
421
+ #### `releaseSavepoint(name): Promise<void>`
422
+
423
+ Release savepoint.
424
+
425
+ ### Vector (pgvector)
426
+
427
+ #### `ensureVectorExtension(): Promise<void>`
428
+
429
+ Install pgvector extension if not exists.
430
+
431
+ #### `createVectorColumn(table, column, dimensions): Promise<void>`
432
+
433
+ Add vector column to table.
434
+
435
+ #### `createVectorIndex(table, column, indexType, operator, options?): Promise<void>`
436
+
437
+ Create vector index for similarity search.
438
+
439
+ **Parameters:**
440
+ - `indexType`: 'hnsw' | 'ivfflat'
441
+ - `operator`: '<->' | '<=>' | '<#>' | '<+>'
442
+ - `options.m`: HNSW max connections (default: 16)
443
+ - `options.efConstruction`: HNSW build quality (default: 64)
444
+ - `options.lists`: IVFFlat lists (default: 100)
445
+
446
+ #### `vectorSearch<T>(table, column, queryVector, options?): Promise<QueryResult<T & { distance }>>`
447
+
448
+ Search for similar vectors.
449
+
450
+ **Options:**
451
+ - `operator`: Distance operator (default: '<->')
452
+ - `limit`: Max results (default: 10)
453
+ - `threshold`: Max distance threshold
454
+ - `filter`: Additional WHERE clause
455
+ - `filterParams`: Filter parameters
456
+
457
+ #### `insertWithVector(table, data, vectorColumn, vector): Promise<QueryResult>`
458
+
459
+ Insert row with vector.
460
+
461
+ #### `updateVector(table, idColumn, idValue, vectorColumn, vector): Promise<QueryResult>`
462
+
463
+ Update vector for existing row.
464
+
465
+ #### `batchInsertWithVectors(table, columns, vectorColumn, rows): Promise<QueryResult>`
466
+
467
+ Batch insert rows with vectors.
468
+
469
+ **Parameters:**
470
+ - `rows`: Array of `{ data: unknown[], vector: Vector }`
471
+
472
+ #### Vector Utilities
473
+
474
+ - `formatVector(vector): string` - Format for PostgreSQL
475
+ - `parseVector(value): number[] | null` - Parse from PostgreSQL
476
+ - `vectorDimension(vector): number` - Get dimension count
477
+ - `normalizeVector(vector): number[]` - Normalize to unit length
478
+ - `getDistanceOperator(op): string` - Get operator SQL
479
+ - `getDistanceOperatorName(op): string` - Get human-readable name
480
+
481
+ ### Health Checks
482
+
483
+ #### `healthCheck(logger?): Promise<HealthCheckResult>`
484
+
485
+ Simple connectivity check.
486
+
487
+ #### `healthCheckWithPool(pool, logger?): Promise<HealthCheckResult>`
488
+
489
+ Health check with pool statistics.
490
+
491
+ #### `deepHealthCheck(logger?): Promise<HealthCheckResult>`
492
+
493
+ Deep check verifying read/write capability.
494
+
495
+ #### `getVersion(): Promise<string>`
496
+
497
+ Get PostgreSQL version.
498
+
499
+ #### `isInRecovery(): Promise<boolean>`
500
+
501
+ Check if database is in recovery mode (replica).
502
+
503
+ #### `getDatabaseSize(dbName?): Promise<string>`
504
+
505
+ Get database size (human-readable).
506
+
507
+ #### `getConnectionCount(): Promise<{ active, idle, total, maxConnections }>`
508
+
509
+ Get connection statistics.
510
+
511
+ #### `createHealthHandler(pool?): (req: Request) => Promise<Response>`
512
+
513
+ Create HTTP health check handler for Bun.serve.
514
+
515
+ **Endpoints:**
516
+ - `/health`, `/healthz` - Simple health check
517
+ - `/ready`, `/readyz` - Deep health check
518
+
519
+ **Example:**
520
+ ```typescript
521
+ import { createPool, createHealthHandler } from '@singulio/postgres';
522
+
523
+ const pool = await createPool({ connectionString: process.env.DATABASE_URL });
524
+ const healthHandler = createHealthHandler(pool);
525
+
526
+ Bun.serve({
527
+ port: 3000,
528
+ fetch: healthHandler,
529
+ });
530
+ ```
531
+
532
+ ## Configuration
533
+
534
+ ### PostgresConfig
535
+
536
+ ```typescript
537
+ interface PostgresConfig {
538
+ connectionString?: string; // Full connection URL
539
+ host?: string; // Default: 'localhost'
540
+ port?: number; // Default: 5432
541
+ database?: string; // Default: 'postgres'
542
+ user?: string; // Default: 'postgres'
543
+ password?: string; // Default: ''
544
+ ssl?: boolean | 'require' | 'prefer' | 'disable';
545
+ applicationName?: string; // For pg_stat_activity
546
+ }
547
+ ```
548
+
549
+ ### PoolConfig
550
+
551
+ Extends `PostgresConfig` with:
552
+
553
+ ```typescript
554
+ interface PoolConfig extends PostgresConfig {
555
+ min?: number; // Minimum connections (default: 2)
556
+ max?: number; // Maximum connections (default: 20)
557
+ idleTimeout?: number; // Idle timeout ms (default: 30000)
558
+ connectionTimeout?: number; // Acquire timeout ms (default: 10000)
559
+ maxLifetime?: number; // Max lifetime ms (default: 3600000)
560
+ }
561
+ ```
562
+
563
+ ### RLSContext
564
+
565
+ ```typescript
566
+ interface RLSContext {
567
+ tenantId: string; // Required tenant ID
568
+ userId?: string; // Optional user ID
569
+ isAdmin?: boolean; // Admin bypass flag
570
+ extraGUCs?: Record<string, string>; // Additional session vars
571
+ }
572
+ ```
573
+
574
+ ### Environment Variables
575
+
576
+ ```bash
577
+ # Connection string format
578
+ DATABASE_URL="postgres://user:password@localhost:5432/mydb"
579
+
580
+ # Or individual components
581
+ PGHOST="localhost"
582
+ PGPORT="5432"
583
+ PGDATABASE="mydb"
584
+ PGUSER="postgres"
585
+ PGPASSWORD="secret"
586
+ PGSSLMODE="require"
587
+ ```
588
+
589
+ ## Cross-Platform Compatibility
590
+
591
+ ### Runtime Requirements
592
+
593
+ - **Node.js**: >= 18.0.0
594
+ - **Bun**: >= 1.0.0 (recommended for best performance)
595
+
596
+ ### Operating Systems
597
+
598
+ - **Linux** (x64, arm64, arm, ia32)
599
+ - **macOS** (x64, arm64)
600
+ - **Windows** (x64, ia32, arm64)
601
+
602
+ ### PostgreSQL Versions
603
+
604
+ - **PostgreSQL 18.x** (recommended)
605
+ - PostgreSQL 16+ (with pgvector 0.5.0+ for vector features)
606
+
607
+ ### Architecture Support
608
+
609
+ - **x64** (x86_64, amd64)
610
+ - **arm64** (aarch64, Apple Silicon)
611
+ - **ia32** (x86)
612
+ - **arm** (armv7)
613
+
614
+ ### Best Practices
615
+
616
+ 1. **Use Bun Runtime**: This package is optimized for Bun's native PostgreSQL driver
617
+ 2. **Connection Pooling**: Always use pools in production for concurrent workloads
618
+ 3. **RLS Setup**: Enable RLS on tables before using RLS helpers:
619
+ ```sql
620
+ ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
621
+ CREATE POLICY tenant_isolation ON contacts
622
+ USING (tenant_ref = current_setting('app.tenant_ref')::uuid);
623
+ ```
624
+ 4. **Vector Indexes**: Create appropriate indexes based on your distance metric:
625
+ - HNSW for better recall and speed (recommended)
626
+ - IVFFlat for larger datasets with memory constraints
627
+ 5. **Health Checks**: Implement both `/health` (liveness) and `/ready` (readiness) endpoints
628
+ 6. **Error Handling**: Always wrap database operations in try-catch blocks
629
+ 7. **Parameterized Queries**: Never interpolate user input directly into SQL
630
+
631
+ ## Performance Tips
632
+
633
+ 1. **Connection Pooling**: Configure pool size based on CPU cores (typically 2-4x core count)
634
+ 2. **Vector Indexes**: Tune HNSW `m` and `efConstruction` parameters for your dataset
635
+ 3. **Batch Operations**: Use `batchInsertWithVectors` for bulk inserts
636
+ 4. **Read Replicas**: Use `readOnlyTransaction` for analytics on replicas
637
+ 5. **Transaction Isolation**: Use lowest isolation level that meets your consistency needs
638
+ 6. **Connection Lifetime**: Set `maxLifetime` to handle PostgreSQL connection limits
639
+
640
+ ## Examples
641
+
642
+ ### Multi-Tenant SaaS Application
643
+
644
+ ```typescript
645
+ import { createPool, createRLSClient, transaction } from '@singulio/postgres';
646
+
647
+ const pool = await createPool({
648
+ connectionString: process.env.DATABASE_URL,
649
+ max: 20,
650
+ applicationName: 'saas-api',
651
+ });
652
+
653
+ const rlsClient = createRLSClient();
654
+
655
+ // API endpoint
656
+ async function getContacts(tenantId: string, userId: string) {
657
+ return await rlsClient.queryAll(
658
+ { tenantId, userId },
659
+ 'SELECT * FROM contacts ORDER BY created_at DESC'
660
+ );
661
+ }
662
+
663
+ // Create contact with audit log
664
+ async function createContact(tenantId: string, userId: string, data: any) {
665
+ return await transaction(
666
+ async (tx) => {
667
+ const contact = await tx.queryOne(
668
+ 'INSERT INTO contacts (name, email) VALUES ($1, $2) RETURNING *',
669
+ [data.name, data.email]
670
+ );
671
+
672
+ await tx.execute(
673
+ 'INSERT INTO audit_logs (action, entity_id, user_id) VALUES ($1, $2, $3)',
674
+ ['contact.created', contact.id, userId]
675
+ );
676
+
677
+ return contact;
678
+ },
679
+ { rlsContext: { tenantId, userId } }
680
+ );
681
+ }
682
+ ```
683
+
684
+ ### AI-Powered Document Search
685
+
686
+ ```typescript
687
+ import { createClient, vectorSearch, insertWithVector } from '@singulio/postgres';
688
+ import { embed } from './embeddings'; // Your embedding function
689
+
690
+ const client = createClient(process.env.DATABASE_URL);
691
+
692
+ // Index document
693
+ async function indexDocument(title: string, content: string) {
694
+ const embedding = await embed(content);
695
+ return await insertWithVector(
696
+ 'documents',
697
+ { title, content, indexed_at: new Date() },
698
+ 'embedding',
699
+ embedding
700
+ );
701
+ }
702
+
703
+ // Semantic search
704
+ async function searchDocuments(query: string, limit = 10) {
705
+ const queryEmbedding = await embed(query);
706
+
707
+ const results = await vectorSearch('documents', 'embedding', queryEmbedding, {
708
+ operator: '<=>', // Cosine similarity
709
+ limit,
710
+ filter: 'published = true',
711
+ });
712
+
713
+ return results.rows.map(doc => ({
714
+ title: doc.title,
715
+ content: doc.content,
716
+ similarity: 1 - doc.distance, // Convert distance to similarity
717
+ }));
718
+ }
719
+ ```
720
+
721
+ ### Kubernetes Health Checks
722
+
723
+ ```typescript
724
+ import { createPool, createHealthHandler } from '@singulio/postgres';
725
+
726
+ const pool = await createPool({
727
+ connectionString: process.env.DATABASE_URL,
728
+ min: 2,
729
+ max: 10,
730
+ });
731
+
732
+ const healthHandler = createHealthHandler(pool);
733
+
734
+ Bun.serve({
735
+ port: 3000,
736
+ async fetch(req) {
737
+ // Health checks
738
+ const response = await healthHandler(req);
739
+ if (response.status !== 404) return response;
740
+
741
+ // Your application routes
742
+ return new Response('Not Found', { status: 404 });
743
+ },
744
+ });
745
+ ```
746
+
747
+ ## Testing
748
+
749
+ ### Unit Tests
750
+
751
+ ```typescript
752
+ import { createClient, transaction } from '@singulio/postgres';
753
+ import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
754
+
755
+ describe('Database Operations', () => {
756
+ let client;
757
+
758
+ beforeAll(async () => {
759
+ client = createClient(process.env.TEST_DATABASE_URL);
760
+ });
761
+
762
+ afterAll(async () => {
763
+ await client.close();
764
+ });
765
+
766
+ it('should insert and retrieve user', async () => {
767
+ const user = await client.queryOne(
768
+ 'INSERT INTO users (name) VALUES ($1) RETURNING *',
769
+ ['Test User']
770
+ );
771
+
772
+ expect(user.name).toBe('Test User');
773
+ });
774
+
775
+ it('should rollback on error', async () => {
776
+ await expect(async () => {
777
+ await transaction(async (tx) => {
778
+ await tx.execute('INSERT INTO users (name) VALUES ($1)', ['User 1']);
779
+ throw new Error('Rollback test');
780
+ });
781
+ }).toThrow();
782
+
783
+ // Verify rollback
784
+ const count = await client.queryOne('SELECT COUNT(*) FROM users WHERE name = $1', ['User 1']);
785
+ expect(count.count).toBe(0);
786
+ });
787
+ });
788
+ ```
789
+
790
+ ## Troubleshooting
791
+
792
+ ### Connection Issues
793
+
794
+ ```typescript
795
+ // Test basic connectivity
796
+ const client = createClient(process.env.DATABASE_URL);
797
+ const canConnect = await client.ping();
798
+ console.log('Connected:', canConnect);
799
+
800
+ // Check version
801
+ const version = await client.version();
802
+ console.log('PostgreSQL version:', version);
803
+ ```
804
+
805
+ ### Pool Exhaustion
806
+
807
+ ```typescript
808
+ // Monitor pool statistics
809
+ const stats = pool.stats();
810
+ if (stats.pending > 5) {
811
+ console.warn('Pool under pressure:', stats);
812
+ }
813
+
814
+ // Increase pool size or investigate slow queries
815
+ ```
816
+
817
+ ### RLS Not Working
818
+
819
+ ```sql
820
+ -- Verify RLS is enabled
821
+ SELECT tablename, rowsecurity
822
+ FROM pg_tables
823
+ WHERE schemaname = 'public';
824
+
825
+ -- Check policies
826
+ SELECT * FROM pg_policies WHERE tablename = 'your_table';
827
+
828
+ -- Test current settings
829
+ SELECT current_setting('app.tenant_ref', true);
830
+ ```
831
+
832
+ ### Vector Search Performance
833
+
834
+ ```sql
835
+ -- Check index usage
836
+ EXPLAIN ANALYZE
837
+ SELECT * FROM documents
838
+ ORDER BY embedding <=> '[...]'
839
+ LIMIT 10;
840
+
841
+ -- Should show "Index Scan using idx_documents_embedding_hnsw"
842
+ ```
843
+
844
+ ## License
845
+
846
+ MIT
847
+
848
+ ## Contributing
849
+
850
+ Contributions welcome! Please open an issue or pull request.
851
+
852
+ ## Support
853
+
854
+ For issues and questions:
855
+ - GitHub Issues: https://github.com/singuliodev/postgres/issues
856
+ - Documentation: https://github.com/singuliodev/postgres#readme
857
+
858
+ ## Related Packages
859
+
860
+ - `@singulio/logger` - Structured logging (compatible Logger interface)
861
+ - `@singulio/types` - Shared TypeScript types
862
+ - `@singulio/validation` - Request/response validation
863
+
864
+ ---
865
+
866
+ Built with Bun for maximum performance. Powered by PostgreSQL 18.