pgserve 1.1.4 → 1.1.5-rc.1
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 +59 -0
- package/bin/pglite-server.js +13 -3
- package/package.json +1 -1
- package/src/cluster.js +49 -4
- package/src/postgres.js +44 -0
- package/src/router.js +2 -1
- package/src/stats-collector.js +186 -0
- package/src/stats-dashboard.js +19 -0
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ psql postgresql://localhost:8432/myapp
|
|
|
72
72
|
<td><b>Async Replication</b></td>
|
|
73
73
|
<td>Sync to real PostgreSQL with minimal overhead</td>
|
|
74
74
|
</tr>
|
|
75
|
+
<tr>
|
|
76
|
+
<td><b>pgvector Built-in</b></td>
|
|
77
|
+
<td>Use <code>--pgvector</code> for auto-enabled vector similarity search</td>
|
|
78
|
+
</tr>
|
|
75
79
|
<tr>
|
|
76
80
|
<td><b>Cross-Platform</b></td>
|
|
77
81
|
<td>Linux x64, macOS ARM64/x64, Windows x64</td>
|
|
@@ -129,6 +133,8 @@ Options:
|
|
|
129
133
|
--no-provision Disable auto-provisioning of databases
|
|
130
134
|
--sync-to <url> Sync to real PostgreSQL (async replication)
|
|
131
135
|
--sync-databases <p> Database patterns to sync (comma-separated)
|
|
136
|
+
--pgvector Auto-enable pgvector extension on new databases
|
|
137
|
+
--max-connections <n> Max concurrent connections (default: 1000)
|
|
132
138
|
--help Show help message
|
|
133
139
|
```
|
|
134
140
|
|
|
@@ -148,6 +154,12 @@ pgserve --data /var/lib/pgserve
|
|
|
148
154
|
# Custom port
|
|
149
155
|
pgserve --port 5433
|
|
150
156
|
|
|
157
|
+
# Enable pgvector for AI/RAG applications
|
|
158
|
+
pgserve --pgvector
|
|
159
|
+
|
|
160
|
+
# RAM mode + pgvector (fastest for AI workloads)
|
|
161
|
+
pgserve --ram --pgvector
|
|
162
|
+
|
|
151
163
|
# Sync to production PostgreSQL
|
|
152
164
|
pgserve --sync-to "postgresql://user:pass@db.example.com:5432/prod"
|
|
153
165
|
```
|
|
@@ -167,6 +179,7 @@ const server = await startMultiTenantServer({
|
|
|
167
179
|
baseDir: null, // null = memory mode
|
|
168
180
|
logLevel: 'info',
|
|
169
181
|
autoProvision: true,
|
|
182
|
+
enablePgvector: true, // Auto-enable pgvector on new databases
|
|
170
183
|
syncTo: null, // Optional: PostgreSQL URL for replication
|
|
171
184
|
syncDatabases: null // Optional: patterns like "myapp,tenant_*"
|
|
172
185
|
});
|
|
@@ -258,6 +271,52 @@ pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
|
|
|
258
271
|
|
|
259
272
|
<br>
|
|
260
273
|
|
|
274
|
+
## pgvector (Vector Search)
|
|
275
|
+
|
|
276
|
+
pgvector is **built-in** — no separate installation required. Just enable it:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Auto-enable pgvector on all new databases
|
|
280
|
+
pgserve --pgvector
|
|
281
|
+
|
|
282
|
+
# Combined with RAM mode for fastest vector operations
|
|
283
|
+
pgserve --ram --pgvector
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
When `--pgvector` is enabled, every new database automatically has the vector extension installed. No SQL setup required.
|
|
287
|
+
|
|
288
|
+
<details>
|
|
289
|
+
<summary><b>Using pgvector</b></summary>
|
|
290
|
+
|
|
291
|
+
```sql
|
|
292
|
+
-- Create table with vector column (1536 = OpenAI embedding size)
|
|
293
|
+
CREATE TABLE documents (id SERIAL, content TEXT, embedding vector(1536));
|
|
294
|
+
|
|
295
|
+
-- Insert with embedding
|
|
296
|
+
INSERT INTO documents (content, embedding) VALUES ('Hello', '[0.1, 0.2, ...]');
|
|
297
|
+
|
|
298
|
+
-- k-NN similarity search (L2 distance)
|
|
299
|
+
SELECT content FROM documents ORDER BY embedding <-> $1 LIMIT 10;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
See [pgvector documentation](https://github.com/pgvector/pgvector) for full API reference.
|
|
303
|
+
</details>
|
|
304
|
+
|
|
305
|
+
<details>
|
|
306
|
+
<summary><b>Without --pgvector flag</b></summary>
|
|
307
|
+
|
|
308
|
+
If you don't use `--pgvector`, you can still enable pgvector manually per database:
|
|
309
|
+
|
|
310
|
+
```sql
|
|
311
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
</details>
|
|
315
|
+
|
|
316
|
+
> pgvector 0.8.1 is bundled with the PostgreSQL binaries. Supports L2 distance (`<->`), inner product (`<#>`), and cosine distance (`<=>`).
|
|
317
|
+
|
|
318
|
+
<br>
|
|
319
|
+
|
|
261
320
|
## Performance
|
|
262
321
|
|
|
263
322
|
### CRUD Benchmarks
|
package/bin/pglite-server.js
CHANGED
|
@@ -55,6 +55,7 @@ OPTIONS:
|
|
|
55
55
|
--sync-databases Database patterns to sync (comma-separated, e.g. "myapp,tenant_*")
|
|
56
56
|
--no-stats Disable real-time stats dashboard (enabled by default)
|
|
57
57
|
--max-connections Max concurrent connections (default: 1000)
|
|
58
|
+
--pgvector Auto-enable pgvector extension on new databases
|
|
58
59
|
--help Show this help message
|
|
59
60
|
|
|
60
61
|
MODES:
|
|
@@ -112,7 +113,8 @@ function parseArgs() {
|
|
|
112
113
|
syncTo: null, // Sync target PostgreSQL URL
|
|
113
114
|
syncDatabases: null, // Database patterns to sync (comma-separated)
|
|
114
115
|
showStats: true, // Show real-time stats dashboard (default: enabled)
|
|
115
|
-
maxConnections: 1000 // Max concurrent connections (high default for multi-tenant)
|
|
116
|
+
maxConnections: 1000, // Max concurrent connections (high default for multi-tenant)
|
|
117
|
+
enablePgvector: false // Auto-enable pgvector extension on new databases
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -179,6 +181,10 @@ function parseArgs() {
|
|
|
179
181
|
options.maxConnections = parseInt(args[++i], 10);
|
|
180
182
|
break;
|
|
181
183
|
|
|
184
|
+
case '--pgvector':
|
|
185
|
+
options.enablePgvector = true;
|
|
186
|
+
break;
|
|
187
|
+
|
|
182
188
|
case '--help':
|
|
183
189
|
case 'help':
|
|
184
190
|
printHelp();
|
|
@@ -228,7 +234,8 @@ pgserve - Embedded PostgreSQL Server
|
|
|
228
234
|
logLevel: options.logLevel,
|
|
229
235
|
autoProvision: options.autoProvision,
|
|
230
236
|
workers: options.workers,
|
|
231
|
-
maxConnections: options.maxConnections
|
|
237
|
+
maxConnections: options.maxConnections,
|
|
238
|
+
enablePgvector: options.enablePgvector
|
|
232
239
|
});
|
|
233
240
|
|
|
234
241
|
// Only primary process shows full startup message
|
|
@@ -243,6 +250,7 @@ Cluster started successfully!
|
|
|
243
250
|
Workers: ${stats.workers} processes
|
|
244
251
|
Data: ${storageType}
|
|
245
252
|
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
253
|
+
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
246
254
|
|
|
247
255
|
Examples:
|
|
248
256
|
postgresql://${options.host}:${options.port}/myapp
|
|
@@ -262,7 +270,8 @@ Press Ctrl+C to stop
|
|
|
262
270
|
autoProvision: options.autoProvision,
|
|
263
271
|
syncTo: options.syncTo,
|
|
264
272
|
syncDatabases: options.syncDatabases,
|
|
265
|
-
maxConnections: options.maxConnections
|
|
273
|
+
maxConnections: options.maxConnections,
|
|
274
|
+
enablePgvector: options.enablePgvector
|
|
266
275
|
});
|
|
267
276
|
|
|
268
277
|
server = router;
|
|
@@ -280,6 +289,7 @@ Server started successfully!
|
|
|
280
289
|
Data: ${storageType}
|
|
281
290
|
PostgreSQL: Port ${router.pgPort} (internal)
|
|
282
291
|
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
292
|
+
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
283
293
|
Sync: ${syncStatus}${options.syncDatabases ? ` (${options.syncDatabases})` : ''}
|
|
284
294
|
|
|
285
295
|
Examples:
|
package/package.json
CHANGED
package/src/cluster.js
CHANGED
|
@@ -42,6 +42,7 @@ class ClusterRouter extends EventEmitter {
|
|
|
42
42
|
this.pgPassword = options.pgPassword || 'postgres';
|
|
43
43
|
this.autoProvision = options.autoProvision !== false;
|
|
44
44
|
this.maxConnections = options.maxConnections || 1000;
|
|
45
|
+
this.enablePgvector = options.enablePgvector || false;
|
|
45
46
|
|
|
46
47
|
this.logger = createLogger({ level: options.logLevel || 'info' });
|
|
47
48
|
this.sql = null; // Bun.sql for admin queries
|
|
@@ -124,6 +125,11 @@ class ClusterRouter extends EventEmitter {
|
|
|
124
125
|
if (result.length === 0) {
|
|
125
126
|
// Use sql() helper for safe identifier escaping (like CREATE DATABASE)
|
|
126
127
|
await this.sql.unsafe(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`);
|
|
128
|
+
|
|
129
|
+
// Auto-enable pgvector extension if configured
|
|
130
|
+
if (this.enablePgvector) {
|
|
131
|
+
await this.enablePgvectorExtension(dbName);
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
} catch (error) {
|
|
129
135
|
// Ignore "already exists" (race condition between workers)
|
|
@@ -133,6 +139,41 @@ class ClusterRouter extends EventEmitter {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Enable pgvector extension on a database
|
|
144
|
+
* Creates a temporary connection to the specific database to run CREATE EXTENSION
|
|
145
|
+
* @param {string} dbName - Database name to enable pgvector on
|
|
146
|
+
*/
|
|
147
|
+
async enablePgvectorExtension(dbName) {
|
|
148
|
+
let dbPool = null;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Create temporary connection to the specific database
|
|
152
|
+
dbPool = new SQL({
|
|
153
|
+
hostname: '127.0.0.1',
|
|
154
|
+
port: this.pgPort,
|
|
155
|
+
database: dbName,
|
|
156
|
+
username: this.pgUser,
|
|
157
|
+
password: this.pgPassword,
|
|
158
|
+
max: 1,
|
|
159
|
+
idleTimeout: 5,
|
|
160
|
+
connectionTimeout: 5,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Enable pgvector extension
|
|
164
|
+
await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
|
|
165
|
+
this.logger.info({ dbName }, 'pgvector extension enabled');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Log but don't fail database creation - pgvector might not be available
|
|
168
|
+
this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
|
|
169
|
+
} finally {
|
|
170
|
+
// Always close the temporary connection
|
|
171
|
+
if (dbPool) {
|
|
172
|
+
await dbPool.close().catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
136
177
|
/**
|
|
137
178
|
* Handle socket open (Bun TCP handler)
|
|
138
179
|
*/
|
|
@@ -342,7 +383,8 @@ export async function startClusterServer(options = {}) {
|
|
|
342
383
|
dataDir: options.baseDir,
|
|
343
384
|
port: pgPort,
|
|
344
385
|
logger: logger.child({ component: 'postgres' }),
|
|
345
|
-
useRam: options.useRam // Use /dev/shm for true RAM storage (Linux only)
|
|
386
|
+
useRam: options.useRam, // Use /dev/shm for true RAM storage (Linux only)
|
|
387
|
+
enablePgvector: options.enablePgvector // Auto-enable pgvector extension on new databases
|
|
346
388
|
});
|
|
347
389
|
|
|
348
390
|
await pgManager.start();
|
|
@@ -366,7 +408,8 @@ export async function startClusterServer(options = {}) {
|
|
|
366
408
|
PGSERVE_PG_PASSWORD: 'postgres',
|
|
367
409
|
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
368
410
|
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
369
|
-
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000)
|
|
411
|
+
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
412
|
+
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
370
413
|
});
|
|
371
414
|
workers.set(worker.id, worker);
|
|
372
415
|
}
|
|
@@ -393,7 +436,8 @@ export async function startClusterServer(options = {}) {
|
|
|
393
436
|
PGSERVE_PG_PASSWORD: 'postgres',
|
|
394
437
|
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
395
438
|
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
396
|
-
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000)
|
|
439
|
+
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
440
|
+
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
397
441
|
});
|
|
398
442
|
workers.set(newWorker.id, newWorker);
|
|
399
443
|
});
|
|
@@ -480,7 +524,8 @@ export async function startClusterServer(options = {}) {
|
|
|
480
524
|
pgPassword: process.env.PGSERVE_PG_PASSWORD || 'postgres',
|
|
481
525
|
logLevel: process.env.PGSERVE_LOG_LEVEL || 'info',
|
|
482
526
|
autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true',
|
|
483
|
-
maxConnections: parseInt(process.env.PGSERVE_MAX_CONNECTIONS) || 1000
|
|
527
|
+
maxConnections: parseInt(process.env.PGSERVE_MAX_CONNECTIONS) || 1000,
|
|
528
|
+
enablePgvector: process.env.PGSERVE_ENABLE_PGVECTOR === 'true'
|
|
484
529
|
});
|
|
485
530
|
|
|
486
531
|
await router.start();
|
package/src/postgres.js
CHANGED
|
@@ -406,6 +406,9 @@ export class PostgresManager {
|
|
|
406
406
|
// Sync/Replication options (for async sync to real PostgreSQL)
|
|
407
407
|
this.syncEnabled = options.syncEnabled || false;
|
|
408
408
|
this.syncManager = null; // Will be set via setSyncManager()
|
|
409
|
+
|
|
410
|
+
// pgvector extension auto-enable
|
|
411
|
+
this.enablePgvector = options.enablePgvector || false;
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
/**
|
|
@@ -880,6 +883,11 @@ export class PostgresManager {
|
|
|
880
883
|
this.createdDatabases.add(dbName);
|
|
881
884
|
this.logger.info({ dbName }, 'Database created');
|
|
882
885
|
|
|
886
|
+
// Auto-enable pgvector extension if configured
|
|
887
|
+
if (this.enablePgvector) {
|
|
888
|
+
await this.enablePgvectorExtension(dbName);
|
|
889
|
+
}
|
|
890
|
+
|
|
883
891
|
// Trigger async sync setup (non-blocking, doesn't affect hot path)
|
|
884
892
|
if (this.syncManager) {
|
|
885
893
|
this.syncManager.setupDatabaseSync(dbName)
|
|
@@ -909,6 +917,42 @@ export class PostgresManager {
|
|
|
909
917
|
}
|
|
910
918
|
}
|
|
911
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Enable pgvector extension on a database
|
|
922
|
+
* Creates a temporary connection to the specific database to run CREATE EXTENSION
|
|
923
|
+
* @param {string} dbName - Database name to enable pgvector on
|
|
924
|
+
*/
|
|
925
|
+
async enablePgvectorExtension(dbName) {
|
|
926
|
+
const { SQL } = await import('bun');
|
|
927
|
+
let dbPool = null;
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
// Create temporary connection to the specific database
|
|
931
|
+
dbPool = new SQL({
|
|
932
|
+
hostname: '127.0.0.1',
|
|
933
|
+
port: this.port,
|
|
934
|
+
database: dbName,
|
|
935
|
+
username: this.user,
|
|
936
|
+
password: this.password,
|
|
937
|
+
max: 1,
|
|
938
|
+
idleTimeout: 5,
|
|
939
|
+
connectionTimeout: 5,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Enable pgvector extension
|
|
943
|
+
await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
|
|
944
|
+
this.logger.info({ dbName }, 'pgvector extension enabled');
|
|
945
|
+
} catch (error) {
|
|
946
|
+
// Log but don't fail database creation - pgvector might not be available
|
|
947
|
+
this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
|
|
948
|
+
} finally {
|
|
949
|
+
// Always close the temporary connection
|
|
950
|
+
if (dbPool) {
|
|
951
|
+
await dbPool.close().catch(() => {});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
912
956
|
/**
|
|
913
957
|
* Check if a database exists
|
|
914
958
|
* @param {string} dbName - Database name to check
|
package/src/router.js
CHANGED
|
@@ -61,7 +61,8 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
61
61
|
port: this.pgPort,
|
|
62
62
|
logger: this.logger.child({ component: 'postgres' }),
|
|
63
63
|
syncEnabled: !!this.syncTo, // Enable logical replication if sync is configured
|
|
64
|
-
useRam: options.useRam // Use /dev/shm for true RAM storage (Linux only)
|
|
64
|
+
useRam: options.useRam, // Use /dev/shm for true RAM storage (Linux only)
|
|
65
|
+
enablePgvector: options.enablePgvector // Auto-enable pgvector extension on new databases
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
// TCP server
|
package/src/stats-collector.js
CHANGED
|
@@ -38,6 +38,15 @@ export class StatsCollector {
|
|
|
38
38
|
// Disk I/O tracking (Linux)
|
|
39
39
|
this.lastDiskStats = null;
|
|
40
40
|
this.lastDiskTime = 0;
|
|
41
|
+
|
|
42
|
+
// pgvector stats cache (longer TTL since it requires cross-DB queries)
|
|
43
|
+
this.pgvectorCache = null;
|
|
44
|
+
this.pgvectorCacheTime = 0;
|
|
45
|
+
this.pgvectorCacheTTL = 5000; // 5s cache for pgvector stats
|
|
46
|
+
|
|
47
|
+
// Connection pool reuse for pgvector stats (avoids TCP handshake overhead per collection)
|
|
48
|
+
this.pgvectorDbPools = new Map(); // dbName -> SQL pool
|
|
49
|
+
this.pgvectorExtCache = new Map(); // dbName -> boolean (vector extension exists)
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -97,6 +106,9 @@ export class StatsCollector {
|
|
|
97
106
|
// PostgreSQL internals (pg_stat_*)
|
|
98
107
|
internals: await this.collectPgStats(),
|
|
99
108
|
|
|
109
|
+
// pgvector stats (if enabled)
|
|
110
|
+
pgvector: await this.collectPgvectorStats(),
|
|
111
|
+
|
|
100
112
|
// Process stats
|
|
101
113
|
process: {
|
|
102
114
|
pid: process.pid,
|
|
@@ -264,4 +276,178 @@ export class StatsCollector {
|
|
|
264
276
|
return null;
|
|
265
277
|
}
|
|
266
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Collect pgvector statistics across all databases
|
|
282
|
+
* Uses separate cache with longer TTL due to cross-DB query overhead
|
|
283
|
+
*/
|
|
284
|
+
async collectPgvectorStats() {
|
|
285
|
+
// Return cached if recent
|
|
286
|
+
if (this.pgvectorCache && Date.now() - this.pgvectorCacheTime < this.pgvectorCacheTTL) {
|
|
287
|
+
return this.pgvectorCache;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const adminPool = this.pgManager?.adminPool;
|
|
291
|
+
if (!adminPool) return null;
|
|
292
|
+
|
|
293
|
+
// Query PostgreSQL directly for databases (pgManager.getStats().databases may be empty in cluster mode)
|
|
294
|
+
let databases = [];
|
|
295
|
+
try {
|
|
296
|
+
const dbResult = await adminPool`
|
|
297
|
+
SELECT datname FROM pg_database
|
|
298
|
+
WHERE datname NOT IN ('template0', 'template1', 'postgres')
|
|
299
|
+
`;
|
|
300
|
+
databases = dbResult.map(row => row.datname);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
this.logger?.debug?.({ err: err.message }, 'Failed to query databases for pgvector stats');
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (databases.length === 0) return null;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
let totalTables = 0;
|
|
310
|
+
let totalRows = 0;
|
|
311
|
+
const dimensions = new Set();
|
|
312
|
+
let dbsWithVectors = 0;
|
|
313
|
+
|
|
314
|
+
// Query each database for vector columns (parallel for performance)
|
|
315
|
+
const allDbStats = await Promise.all(
|
|
316
|
+
databases.map(dbName => this.queryDatabaseVectorStats(dbName))
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
for (const dbStats of allDbStats) {
|
|
320
|
+
if (dbStats && dbStats.tableCount > 0) {
|
|
321
|
+
dbsWithVectors++;
|
|
322
|
+
totalTables += dbStats.tableCount;
|
|
323
|
+
totalRows += dbStats.rowCount;
|
|
324
|
+
dbStats.dimensions.forEach(d => dimensions.add(d));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Only return stats if vectors exist
|
|
329
|
+
if (totalTables === 0) {
|
|
330
|
+
this.pgvectorCache = null;
|
|
331
|
+
this.pgvectorCacheTime = Date.now();
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const stats = {
|
|
336
|
+
enabled: true,
|
|
337
|
+
databases: dbsWithVectors,
|
|
338
|
+
tableCount: totalTables,
|
|
339
|
+
totalRows: totalRows,
|
|
340
|
+
dimensions: [...dimensions].sort((a, b) => b - a).join(', ') || null
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
this.pgvectorCache = stats;
|
|
344
|
+
this.pgvectorCacheTime = Date.now();
|
|
345
|
+
return stats;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
this.logger?.debug?.({ err: err.message }, 'Failed to collect pgvector stats');
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Query vector column stats for a specific database
|
|
354
|
+
* @param {string} dbName - Database name to query
|
|
355
|
+
* @returns {Promise<{tableCount: number, rowCount: number, dimensions: number[]}>}
|
|
356
|
+
*/
|
|
357
|
+
async queryDatabaseVectorStats(dbName) {
|
|
358
|
+
try {
|
|
359
|
+
// Reuse connection pool (avoids TCP handshake + auth overhead per collection)
|
|
360
|
+
let dbPool = this.pgvectorDbPools.get(dbName);
|
|
361
|
+
if (!dbPool) {
|
|
362
|
+
const { SQL } = await import('bun');
|
|
363
|
+
dbPool = new SQL({
|
|
364
|
+
hostname: '127.0.0.1',
|
|
365
|
+
port: this.pgManager?.port || 5433,
|
|
366
|
+
database: dbName,
|
|
367
|
+
username: 'postgres',
|
|
368
|
+
password: 'postgres',
|
|
369
|
+
max: 2, // Allow some concurrency
|
|
370
|
+
idleTimeout: 30, // Keep alive longer for reuse
|
|
371
|
+
connectionTimeout: 3,
|
|
372
|
+
});
|
|
373
|
+
this.pgvectorDbPools.set(dbName, dbPool);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check cached extension status (only query once per DB)
|
|
377
|
+
if (!this.pgvectorExtCache.has(dbName)) {
|
|
378
|
+
const extCheck = await dbPool`
|
|
379
|
+
SELECT 1 FROM pg_extension WHERE extname = 'vector'
|
|
380
|
+
`;
|
|
381
|
+
this.pgvectorExtCache.set(dbName, extCheck.length > 0);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!this.pgvectorExtCache.get(dbName)) {
|
|
385
|
+
return { tableCount: 0, rowCount: 0, dimensions: [] };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Single query: get tables, dimensions, and row counts via pg_class.reltuples
|
|
389
|
+
// Note: reltuples is an estimate (updated by ANALYZE/VACUUM), not exact COUNT(*)
|
|
390
|
+
// This is acceptable for dashboard stats with 5s cache - avoids expensive full table scans
|
|
391
|
+
// Trade-off: ~50-100x faster, but counts may be stale after bulk INSERT/DELETE until ANALYZE
|
|
392
|
+
const vectorInfo = await dbPool`
|
|
393
|
+
SELECT
|
|
394
|
+
c.relname as table_name,
|
|
395
|
+
a.atttypmod as dimensions,
|
|
396
|
+
GREATEST(c.reltuples, 0)::bigint AS row_count
|
|
397
|
+
FROM pg_attribute a
|
|
398
|
+
JOIN pg_class c ON a.attrelid = c.oid
|
|
399
|
+
JOIN pg_type t ON a.atttypid = t.oid
|
|
400
|
+
WHERE t.typname = 'vector'
|
|
401
|
+
AND c.relkind = 'r'
|
|
402
|
+
AND a.attnum > 0
|
|
403
|
+
AND NOT a.attisdropped
|
|
404
|
+
`;
|
|
405
|
+
|
|
406
|
+
if (vectorInfo.length === 0) {
|
|
407
|
+
return { tableCount: 0, rowCount: 0, dimensions: [] };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Aggregate results from single query
|
|
411
|
+
const tableRows = new Map();
|
|
412
|
+
const dims = [];
|
|
413
|
+
|
|
414
|
+
for (const row of vectorInfo) {
|
|
415
|
+
if (!tableRows.has(row.table_name)) {
|
|
416
|
+
tableRows.set(row.table_name, Number(row.row_count || 0));
|
|
417
|
+
}
|
|
418
|
+
if (row.dimensions > 0) {
|
|
419
|
+
dims.push(row.dimensions);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const totalRows = Array.from(tableRows.values()).reduce((sum, count) => sum + count, 0);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
tableCount: tableRows.size,
|
|
427
|
+
rowCount: totalRows,
|
|
428
|
+
dimensions: dims
|
|
429
|
+
};
|
|
430
|
+
} catch (err) {
|
|
431
|
+
this.logger?.debug?.({ dbName, err: err.message }, 'Failed to query database vector stats');
|
|
432
|
+
// Invalidate caches on error (DB might have been dropped)
|
|
433
|
+
this.pgvectorExtCache.delete(dbName);
|
|
434
|
+
const pool = this.pgvectorDbPools.get(dbName);
|
|
435
|
+
if (pool) {
|
|
436
|
+
this.pgvectorDbPools.delete(dbName);
|
|
437
|
+
await pool.close().catch(() => {});
|
|
438
|
+
}
|
|
439
|
+
return { tableCount: 0, rowCount: 0, dimensions: [] };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Close all pgvector database pools (called on shutdown)
|
|
445
|
+
*/
|
|
446
|
+
async closePgvectorPools() {
|
|
447
|
+
for (const [_dbName, pool] of this.pgvectorDbPools) {
|
|
448
|
+
await pool.close().catch(() => {});
|
|
449
|
+
}
|
|
450
|
+
this.pgvectorDbPools.clear();
|
|
451
|
+
this.pgvectorExtCache.clear();
|
|
452
|
+
}
|
|
267
453
|
}
|
package/src/stats-dashboard.js
CHANGED
|
@@ -261,6 +261,25 @@ export class StatsDashboard {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// pgvector section (only if vector data exists)
|
|
265
|
+
if (stats.pgvector?.enabled && stats.pgvector.tableCount > 0) {
|
|
266
|
+
const vecLines = [
|
|
267
|
+
`${ANSI.DIM}Databases:${ANSI.RESET} ${stats.pgvector.databases} with vectors`,
|
|
268
|
+
`${ANSI.DIM}Tables:${ANSI.RESET} ${stats.pgvector.tableCount} total`
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
if (stats.pgvector.totalRows > 0) {
|
|
272
|
+
vecLines.push(`${ANSI.DIM}Vectors:${ANSI.RESET} ${this.formatNumber(stats.pgvector.totalRows)} rows`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (stats.pgvector.dimensions) {
|
|
276
|
+
vecLines.push(`${ANSI.DIM}Dimensions:${ANSI.RESET} ${stats.pgvector.dimensions}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
lines.push(this.section('PGVECTOR', vecLines));
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
282
|
+
|
|
264
283
|
// System resources section
|
|
265
284
|
const resourceLines = [];
|
|
266
285
|
|