pgserve 2.3.0 → 2.4.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/bin/pgserve-wrapper.cjs +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
package/src/stats-collector.js
DELETED
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stats Collector - Centralized statistics gathering for pgserve
|
|
3
|
-
*
|
|
4
|
-
* Aggregates stats from:
|
|
5
|
-
* - Router (active connections)
|
|
6
|
-
* - PostgreSQL Manager (databases, storage)
|
|
7
|
-
* - PostgreSQL internals (pg_stat_activity, pg_stat_database)
|
|
8
|
-
* - Process (memory, uptime)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// CPU sampling threshold - avoid noise from too-frequent calls
|
|
12
|
-
const CPU_SAMPLE_MIN_INTERVAL_MS = 100;
|
|
13
|
-
|
|
14
|
-
// /proc/diskstats minimum fields per device line
|
|
15
|
-
// Fields: major minor device reads rd_merged rd_sectors rd_ms writes wr_merged wr_sectors wr_ms io_in_progress io_ms weighted_io_ms
|
|
16
|
-
const PROC_DISKSTATS_MIN_FIELDS = 14;
|
|
17
|
-
|
|
18
|
-
export class StatsCollector {
|
|
19
|
-
constructor(options = {}) {
|
|
20
|
-
this.pgManager = options.pgManager;
|
|
21
|
-
this.router = options.router;
|
|
22
|
-
this.clusterStats = options.clusterStats; // Function that returns cluster stats
|
|
23
|
-
this.logger = options.logger;
|
|
24
|
-
|
|
25
|
-
// Override values for cluster mode where router is null
|
|
26
|
-
this.serverPort = options.port;
|
|
27
|
-
this.serverHost = options.host;
|
|
28
|
-
|
|
29
|
-
// Cache to avoid over-querying (configurable for different monitoring needs)
|
|
30
|
-
this.cache = null;
|
|
31
|
-
this.cacheTime = 0;
|
|
32
|
-
this.cacheTTL = options.cacheTTL || 1000; // Default 1s cache
|
|
33
|
-
|
|
34
|
-
// CPU tracking
|
|
35
|
-
this.lastCpuUsage = process.cpuUsage();
|
|
36
|
-
this.lastCpuTime = Date.now();
|
|
37
|
-
|
|
38
|
-
// Disk I/O tracking (Linux)
|
|
39
|
-
this.lastDiskStats = null;
|
|
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)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Collect all available stats
|
|
54
|
-
* @returns {Promise<StatsSnapshot>}
|
|
55
|
-
*/
|
|
56
|
-
async collect() {
|
|
57
|
-
// Return cached if recent
|
|
58
|
-
if (this.cache && Date.now() - this.cacheTime < this.cacheTTL) {
|
|
59
|
-
return this.cache;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const pgStats = this.pgManager?.getStats?.() || {};
|
|
63
|
-
const routerStats = this.router?.getStats?.() || {};
|
|
64
|
-
// Prefer clusterStats (cluster mode aggregates from all workers)
|
|
65
|
-
// Fall back to routerStats (single-process mode)
|
|
66
|
-
const clusterStats = this.clusterStats?.() || null;
|
|
67
|
-
|
|
68
|
-
const snapshot = {
|
|
69
|
-
timestamp: Date.now(),
|
|
70
|
-
uptime: process.uptime(),
|
|
71
|
-
|
|
72
|
-
// Connection stats - cluster mode has aggregated stats, single-process uses router directly
|
|
73
|
-
connections: {
|
|
74
|
-
active: clusterStats?.connections?.active ?? routerStats.activeConnections ?? 0,
|
|
75
|
-
totalConnected: clusterStats?.connections?.totalConnected ?? 0,
|
|
76
|
-
totalDisconnected: clusterStats?.connections?.totalDisconnected ?? 0,
|
|
77
|
-
max: this.router?.maxConnections || 1000
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
// Server config
|
|
81
|
-
server: {
|
|
82
|
-
port: this.serverPort || routerStats.port || this.router?.port || 8432,
|
|
83
|
-
host: this.serverHost || routerStats.host || this.router?.host || '127.0.0.1',
|
|
84
|
-
pgPort: routerStats.pgPort || pgStats.port || 0,
|
|
85
|
-
memoryMode: this.router?.memoryMode ?? !pgStats.persistent,
|
|
86
|
-
useRam: this.pgManager?.useRam || false
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
// PostgreSQL manager stats
|
|
90
|
-
postgres: {
|
|
91
|
-
port: pgStats.port,
|
|
92
|
-
databases: pgStats.databases || [],
|
|
93
|
-
databaseDir: pgStats.databaseDir,
|
|
94
|
-
socketDir: pgStats.socketDir,
|
|
95
|
-
socketPath: pgStats.socketPath,
|
|
96
|
-
persistent: pgStats.persistent
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
// Cluster stats (if in cluster mode)
|
|
100
|
-
cluster: clusterStats ? {
|
|
101
|
-
workers: clusterStats.workers,
|
|
102
|
-
pids: clusterStats.pids,
|
|
103
|
-
workerStats: clusterStats.workerStats || {}
|
|
104
|
-
} : null,
|
|
105
|
-
|
|
106
|
-
// PostgreSQL internals (pg_stat_*)
|
|
107
|
-
internals: await this.collectPgStats(),
|
|
108
|
-
|
|
109
|
-
// pgvector stats (if enabled)
|
|
110
|
-
pgvector: await this.collectPgvectorStats(),
|
|
111
|
-
|
|
112
|
-
// Process stats
|
|
113
|
-
process: {
|
|
114
|
-
pid: process.pid,
|
|
115
|
-
memory: process.memoryUsage(),
|
|
116
|
-
cpu: this.getCpuUsage()
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
// System stats (Linux)
|
|
120
|
-
system: await this.getSystemStats()
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
this.cache = snapshot;
|
|
124
|
-
this.cacheTime = Date.now();
|
|
125
|
-
return snapshot;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get CPU usage percentage
|
|
130
|
-
*/
|
|
131
|
-
getCpuUsage() {
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
const elapsed = now - this.lastCpuTime;
|
|
134
|
-
if (elapsed < CPU_SAMPLE_MIN_INTERVAL_MS) return this.lastCpuPercent || 0;
|
|
135
|
-
|
|
136
|
-
const cpuUsage = process.cpuUsage(this.lastCpuUsage);
|
|
137
|
-
const totalMicros = cpuUsage.user + cpuUsage.system;
|
|
138
|
-
const elapsedMicros = elapsed * 1000; // Convert ms to microseconds
|
|
139
|
-
|
|
140
|
-
// CPU percentage (can be > 100% on multi-core)
|
|
141
|
-
const percent = (totalMicros / elapsedMicros) * 100;
|
|
142
|
-
|
|
143
|
-
this.lastCpuUsage = process.cpuUsage();
|
|
144
|
-
this.lastCpuTime = now;
|
|
145
|
-
this.lastCpuPercent = percent;
|
|
146
|
-
|
|
147
|
-
// Return raw percentage - can exceed 100% on multi-core systems
|
|
148
|
-
// Consumers (dashboard, etc.) can format/cap as needed for display
|
|
149
|
-
return percent;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get system stats (Linux-specific)
|
|
154
|
-
*/
|
|
155
|
-
async getSystemStats() {
|
|
156
|
-
const stats = {
|
|
157
|
-
loadAvg: null,
|
|
158
|
-
diskIO: null
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
// Load average (works on Linux/macOS)
|
|
163
|
-
const os = await import('os');
|
|
164
|
-
const loadAvg = os.loadavg();
|
|
165
|
-
stats.loadAvg = {
|
|
166
|
-
'1m': loadAvg[0],
|
|
167
|
-
'5m': loadAvg[1],
|
|
168
|
-
'15m': loadAvg[2]
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// Disk I/O stats (Linux only via /proc/diskstats)
|
|
172
|
-
if (process.platform === 'linux') {
|
|
173
|
-
const fs = await import('fs/promises');
|
|
174
|
-
try {
|
|
175
|
-
const diskstats = await fs.readFile('/proc/diskstats', 'utf8');
|
|
176
|
-
const now = Date.now();
|
|
177
|
-
|
|
178
|
-
// Parse diskstats - find main disk (sda, nvme0n1, vda, etc.)
|
|
179
|
-
let readSectors = 0;
|
|
180
|
-
let writeSectors = 0;
|
|
181
|
-
let readOps = 0;
|
|
182
|
-
let writeOps = 0;
|
|
183
|
-
|
|
184
|
-
for (const line of diskstats.split('\n')) {
|
|
185
|
-
const parts = line.trim().split(/\s+/);
|
|
186
|
-
if (parts.length < PROC_DISKSTATS_MIN_FIELDS) continue;
|
|
187
|
-
|
|
188
|
-
const device = parts[2];
|
|
189
|
-
// Match main disks (sda, sdb, nvme0n1, vda, etc.) but not partitions
|
|
190
|
-
if (/^(sd[a-z]|nvme\d+n\d+|vd[a-z])$/.test(device)) {
|
|
191
|
-
readOps += parseInt(parts[3]) || 0;
|
|
192
|
-
readSectors += parseInt(parts[5]) || 0;
|
|
193
|
-
writeOps += parseInt(parts[7]) || 0;
|
|
194
|
-
writeSectors += parseInt(parts[9]) || 0;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (this.lastDiskStats && this.lastDiskTime) {
|
|
199
|
-
const elapsed = (now - this.lastDiskTime) / 1000; // seconds
|
|
200
|
-
if (elapsed > 0) {
|
|
201
|
-
const readDiff = readSectors - this.lastDiskStats.readSectors;
|
|
202
|
-
const writeDiff = writeSectors - this.lastDiskStats.writeSectors;
|
|
203
|
-
const readOpsDiff = readOps - this.lastDiskStats.readOps;
|
|
204
|
-
const writeOpsDiff = writeOps - this.lastDiskStats.writeOps;
|
|
205
|
-
|
|
206
|
-
// Sectors are typically 512 bytes
|
|
207
|
-
stats.diskIO = {
|
|
208
|
-
readMBps: ((readDiff * 512) / (1024 * 1024)) / elapsed,
|
|
209
|
-
writeMBps: ((writeDiff * 512) / (1024 * 1024)) / elapsed,
|
|
210
|
-
readIOPS: readOpsDiff / elapsed,
|
|
211
|
-
writeIOPS: writeOpsDiff / elapsed
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
this.lastDiskStats = { readSectors, writeSectors, readOps, writeOps };
|
|
217
|
-
this.lastDiskTime = now;
|
|
218
|
-
} catch (err) {
|
|
219
|
-
// /proc/diskstats not available (normal on non-Linux or restricted environments)
|
|
220
|
-
this.logger?.debug?.({ err: err.message }, 'Could not read /proc/diskstats');
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
// OS module or stats not available (normal on some platforms)
|
|
225
|
-
this.logger?.debug?.({ err: err.message }, 'Could not collect system stats');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return stats;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Query PostgreSQL internal statistics
|
|
233
|
-
*/
|
|
234
|
-
async collectPgStats() {
|
|
235
|
-
// Get admin pool from pgManager
|
|
236
|
-
const adminPool = this.pgManager?.adminPool;
|
|
237
|
-
if (!adminPool) return null;
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
// Query pg_stat_activity for connection details
|
|
241
|
-
const activity = await adminPool`
|
|
242
|
-
SELECT
|
|
243
|
-
count(*) FILTER (WHERE state = 'active') as active_queries,
|
|
244
|
-
count(*) FILTER (WHERE state = 'idle') as idle_connections,
|
|
245
|
-
count(*) as total_connections
|
|
246
|
-
FROM pg_stat_activity
|
|
247
|
-
WHERE datname IS NOT NULL
|
|
248
|
-
`;
|
|
249
|
-
|
|
250
|
-
// Query pg_stat_database for DB-level stats
|
|
251
|
-
const dbStats = await adminPool`
|
|
252
|
-
SELECT
|
|
253
|
-
datname,
|
|
254
|
-
numbackends,
|
|
255
|
-
xact_commit,
|
|
256
|
-
xact_rollback,
|
|
257
|
-
blks_read,
|
|
258
|
-
blks_hit,
|
|
259
|
-
tup_returned,
|
|
260
|
-
tup_fetched,
|
|
261
|
-
tup_inserted,
|
|
262
|
-
tup_updated,
|
|
263
|
-
tup_deleted
|
|
264
|
-
FROM pg_stat_database
|
|
265
|
-
WHERE datname NOT IN ('template0', 'template1')
|
|
266
|
-
ORDER BY numbackends DESC
|
|
267
|
-
LIMIT 10
|
|
268
|
-
`;
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
activity: activity[0] || {},
|
|
272
|
-
databases: dbStats || []
|
|
273
|
-
};
|
|
274
|
-
} catch (err) {
|
|
275
|
-
this.logger?.debug?.({ err: err.message }, 'Failed to collect pg_stat_*');
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
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
|
-
}
|
|
453
|
-
}
|