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 +866 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +127 -0
- package/dist/health.d.ts +44 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +182 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/pool.d.ts +52 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +216 -0
- package/dist/rls.d.ts +53 -0
- package/dist/rls.d.ts.map +1 -0
- package/dist/rls.js +134 -0
- package/dist/transaction.d.ts +54 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +138 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/vector.d.ts +74 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +223 -0
- package/package.json +97 -0
- package/src/client.ts +153 -0
- package/src/health.ts +226 -0
- package/src/index.ts +110 -0
- package/src/pool.ts +268 -0
- package/src/rls.ts +169 -0
- package/src/transaction.ts +207 -0
- package/src/types.ts +142 -0
- package/src/vector.ts +312 -0
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.
|