pgserve 2.3.0 → 2.5.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 +9 -4
- package/bin/postgres-server.js +170 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +3 -2
- package/scripts/audit-redaction-lint.js +349 -0
- package/scripts/test-npx.sh +32 -10
- package/src/audit/audit.js +134 -0
- package/src/cli-install.cjs +340 -100
- package/src/commands/uninstall.js +241 -0
- package/src/commands/verify.js +360 -0
- package/src/cosign/cache-token.js +328 -0
- package/src/cosign/schema.js +97 -0
- package/src/cosign/trust-list.js +81 -0
- package/src/cosign/verify-binary.js +277 -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/runtime-json.js +181 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/upgrade/index.js +5 -0
- package/src/upgrade/steps/cosign-meta-migration.js +123 -0
- 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/restore.js
DELETED
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RestoreManager - Automatic restore from external PostgreSQL on startup
|
|
3
|
-
*
|
|
4
|
-
* High-performance restore using:
|
|
5
|
-
* - Parallel database restore (Promise.all)
|
|
6
|
-
* - Native wire protocol for bulk COPY transfer (pg-wire.js)
|
|
7
|
-
* - Unix sockets for local connections (~30% faster)
|
|
8
|
-
* - Binary format COPY (~2x faster than text)
|
|
9
|
-
*
|
|
10
|
-
* 100% Bun-native: Uses PgWirePool for all database operations.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { PgWirePool } from './pg-wire.js';
|
|
14
|
-
import { createLogger } from './logger.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Match database name against patterns (supports wildcards)
|
|
18
|
-
* Reused from sync.js for consistency
|
|
19
|
-
* @param {string} dbName - Database name to check
|
|
20
|
-
* @param {string[]} patterns - Array of patterns (supports * wildcard)
|
|
21
|
-
* @returns {boolean}
|
|
22
|
-
*/
|
|
23
|
-
function matchesPattern(dbName, patterns) {
|
|
24
|
-
if (!patterns || patterns.length === 0) return true; // No filter = restore all
|
|
25
|
-
|
|
26
|
-
return patterns.some(pattern => {
|
|
27
|
-
if (pattern.includes('*')) {
|
|
28
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
29
|
-
return regex.test(dbName);
|
|
30
|
-
}
|
|
31
|
-
return dbName === pattern;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* RestoreManager - Handles automatic restore from external PostgreSQL
|
|
37
|
-
*/
|
|
38
|
-
export class RestoreManager {
|
|
39
|
-
constructor(options = {}) {
|
|
40
|
-
this.sourceUrl = options.sourceUrl; // External PostgreSQL URL
|
|
41
|
-
this.patterns = options.patterns || []; // Database patterns ["myapp", "tenant_*"]
|
|
42
|
-
this.targetPort = options.targetPort; // Local embedded PostgreSQL port
|
|
43
|
-
this.targetSocketPath = options.targetSocketPath; // Unix socket path (optional)
|
|
44
|
-
|
|
45
|
-
this.logger = options.logger || createLogger({ level: options.logLevel || 'info', component: 'restore' });
|
|
46
|
-
|
|
47
|
-
// Connection pools (lazy initialized)
|
|
48
|
-
this.sourcePool = null;
|
|
49
|
-
|
|
50
|
-
// Performance tuning - parallel restore limits
|
|
51
|
-
this.maxParallelDatabases = options.maxParallelDatabases || 4;
|
|
52
|
-
this.maxParallelTables = options.maxParallelTables || 8;
|
|
53
|
-
|
|
54
|
-
// Timeout handling
|
|
55
|
-
this.restoreTimeout = options.restoreTimeout || 60000; // 60s default
|
|
56
|
-
|
|
57
|
-
// Progress callback for dashboard
|
|
58
|
-
this.onProgress = options.onProgress || (() => {});
|
|
59
|
-
|
|
60
|
-
// Totals for progress tracking
|
|
61
|
-
this.totalDatabases = 0;
|
|
62
|
-
this.totalTables = 0;
|
|
63
|
-
this.totalBytes = 0;
|
|
64
|
-
|
|
65
|
-
// Metrics collection
|
|
66
|
-
this.metrics = {
|
|
67
|
-
startTime: 0,
|
|
68
|
-
endTime: 0,
|
|
69
|
-
databasesRestored: 0,
|
|
70
|
-
tablesRestored: 0,
|
|
71
|
-
rowsRestored: 0,
|
|
72
|
-
bytesTransferred: 0,
|
|
73
|
-
errors: []
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Main entry point - restore databases from external PostgreSQL
|
|
79
|
-
* Called from router.js after pgManager.start(), before SyncManager
|
|
80
|
-
*
|
|
81
|
-
* @param {PostgresManager} pgManager - Local PostgreSQL manager
|
|
82
|
-
* @returns {Promise<Object>} Restore result with metrics
|
|
83
|
-
*/
|
|
84
|
-
async restore(pgManager) {
|
|
85
|
-
if (!this.sourceUrl) {
|
|
86
|
-
return { skipped: true, reason: 'no sourceUrl configured' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.metrics.startTime = Date.now();
|
|
90
|
-
this.logger.info({ source: this.sourceUrl.replace(/:[^:@]+@/, ':***@') }, 'Starting automatic restore from external PostgreSQL');
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
// Initialize connection to external PostgreSQL
|
|
94
|
-
const connected = await this._initSourcePool();
|
|
95
|
-
if (!connected) {
|
|
96
|
-
return { skipped: true, reason: 'external PostgreSQL unreachable' };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Discover databases matching patterns
|
|
100
|
-
const databases = await this._discoverDatabases();
|
|
101
|
-
|
|
102
|
-
if (databases.length === 0) {
|
|
103
|
-
this.logger.info('No databases found matching sync patterns on external PostgreSQL');
|
|
104
|
-
return { skipped: true, reason: 'no matching databases found' };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
this.logger.info({ count: databases.length, databases }, 'Found databases to restore');
|
|
108
|
-
|
|
109
|
-
// Set totals for progress tracking
|
|
110
|
-
this.totalDatabases = databases.length;
|
|
111
|
-
|
|
112
|
-
// Restore databases in parallel (with controlled concurrency)
|
|
113
|
-
await this._restoreDatabasesParallel(databases, pgManager);
|
|
114
|
-
|
|
115
|
-
this.metrics.endTime = Date.now();
|
|
116
|
-
const duration = this.metrics.endTime - this.metrics.startTime;
|
|
117
|
-
|
|
118
|
-
this.logger.info({
|
|
119
|
-
databasesRestored: this.metrics.databasesRestored,
|
|
120
|
-
tablesRestored: this.metrics.tablesRestored,
|
|
121
|
-
rowsRestored: this.metrics.rowsRestored,
|
|
122
|
-
bytesTransferred: this.metrics.bytesTransferred,
|
|
123
|
-
throughputMBps: ((this.metrics.bytesTransferred / 1024 / 1024) / (duration / 1000)).toFixed(2),
|
|
124
|
-
durationMs: duration,
|
|
125
|
-
errors: this.metrics.errors.length
|
|
126
|
-
}, 'Restore completed');
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
success: true,
|
|
130
|
-
metrics: { ...this.metrics }
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
} catch (error) {
|
|
134
|
-
this.logger.error({ err: error }, 'Restore failed');
|
|
135
|
-
return { success: false, error: error.message };
|
|
136
|
-
} finally {
|
|
137
|
-
await this._closeSourcePool();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Initialize connection pool to external PostgreSQL
|
|
143
|
-
* @returns {Promise<boolean>} true if connected successfully
|
|
144
|
-
*/
|
|
145
|
-
async _initSourcePool() {
|
|
146
|
-
try {
|
|
147
|
-
const url = new URL(this.sourceUrl);
|
|
148
|
-
this.sourcePool = new PgWirePool({
|
|
149
|
-
hostname: url.hostname,
|
|
150
|
-
port: parseInt(url.port) || 5432,
|
|
151
|
-
database: url.pathname.slice(1) || 'postgres',
|
|
152
|
-
username: url.username || 'postgres',
|
|
153
|
-
password: url.password || 'postgres',
|
|
154
|
-
max: 3 // Small pool - just for discovery
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Test connection
|
|
158
|
-
await this.sourcePool.query('SELECT 1');
|
|
159
|
-
this.logger.debug('Connected to external PostgreSQL');
|
|
160
|
-
return true;
|
|
161
|
-
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
|
164
|
-
this.logger.warn({ err: error.message }, 'External PostgreSQL unreachable, skipping restore');
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Close source connection pool
|
|
173
|
-
*/
|
|
174
|
-
async _closeSourcePool() {
|
|
175
|
-
if (this.sourcePool) {
|
|
176
|
-
await this.sourcePool.end();
|
|
177
|
-
this.sourcePool = null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Discover databases on external PostgreSQL matching patterns
|
|
183
|
-
* @returns {Promise<string[]>} List of database names
|
|
184
|
-
*/
|
|
185
|
-
async _discoverDatabases() {
|
|
186
|
-
const result = await this.sourcePool.query(`
|
|
187
|
-
SELECT datname FROM pg_database
|
|
188
|
-
WHERE datistemplate = false
|
|
189
|
-
AND datname NOT IN ('postgres', 'template0', 'template1')
|
|
190
|
-
ORDER BY datname
|
|
191
|
-
`);
|
|
192
|
-
|
|
193
|
-
// Filter by patterns
|
|
194
|
-
return result.rows
|
|
195
|
-
.map(r => r.datname)
|
|
196
|
-
.filter(name => matchesPattern(name, this.patterns));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Restore databases in parallel with controlled concurrency
|
|
201
|
-
* @param {string[]} databases - Database names to restore
|
|
202
|
-
* @param {PostgresManager} pgManager - Local PostgreSQL manager
|
|
203
|
-
*/
|
|
204
|
-
async _restoreDatabasesParallel(databases, pgManager) {
|
|
205
|
-
// Batch databases to limit concurrency
|
|
206
|
-
const batches = [];
|
|
207
|
-
for (let i = 0; i < databases.length; i += this.maxParallelDatabases) {
|
|
208
|
-
batches.push(databases.slice(i, i + this.maxParallelDatabases));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
for (const batch of batches) {
|
|
212
|
-
const results = await Promise.allSettled(
|
|
213
|
-
batch.map(dbName => this._restoreDatabase(dbName, pgManager))
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// Track failures but continue
|
|
217
|
-
for (let i = 0; i < results.length; i++) {
|
|
218
|
-
if (results[i].status === 'rejected') {
|
|
219
|
-
this.metrics.errors.push({
|
|
220
|
-
database: batch[i],
|
|
221
|
-
error: results[i].reason.message
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (this.metrics.errors.length > 0) {
|
|
228
|
-
this.logger.warn({
|
|
229
|
-
failedCount: this.metrics.errors.length,
|
|
230
|
-
totalCount: databases.length
|
|
231
|
-
}, 'Some databases failed to restore');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Restore a single database: create DB, schema, data
|
|
237
|
-
* @param {string} dbName - Database name
|
|
238
|
-
* @param {PostgresManager} pgManager - Local PostgreSQL manager
|
|
239
|
-
*/
|
|
240
|
-
async _restoreDatabase(dbName, pgManager) {
|
|
241
|
-
this.logger.info({ dbName }, 'Restoring database');
|
|
242
|
-
const startTime = Date.now();
|
|
243
|
-
|
|
244
|
-
// Step 1: Create database locally
|
|
245
|
-
await pgManager.createDatabase(dbName);
|
|
246
|
-
|
|
247
|
-
// Step 2: Create connection pools for this specific database
|
|
248
|
-
const sourceDbPool = await this._createSourceDbPool(dbName);
|
|
249
|
-
const targetDbPool = await this._createTargetDbPool(dbName);
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
// Step 3: Restore schema (types, tables, indexes, FKs)
|
|
253
|
-
await this._restoreSchema(sourceDbPool, targetDbPool, dbName);
|
|
254
|
-
|
|
255
|
-
// Step 4: Discover tables and copy data in parallel
|
|
256
|
-
const tables = await this._discoverTables(sourceDbPool);
|
|
257
|
-
this.totalTables += tables.length; // Track total for progress
|
|
258
|
-
if (tables.length > 0) {
|
|
259
|
-
await this._restoreTablesParallel(sourceDbPool, targetDbPool, tables);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Step 5: Restore sequences (after data for correct values)
|
|
263
|
-
await this._restoreSequences(sourceDbPool, targetDbPool);
|
|
264
|
-
|
|
265
|
-
this.metrics.databasesRestored++;
|
|
266
|
-
const duration = Date.now() - startTime;
|
|
267
|
-
this.logger.info({ dbName, durationMs: duration }, 'Database restored successfully');
|
|
268
|
-
|
|
269
|
-
} finally {
|
|
270
|
-
await sourceDbPool.end();
|
|
271
|
-
await targetDbPool.end();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Create connection pool to specific database on external PostgreSQL
|
|
277
|
-
* @param {string} dbName - Database name
|
|
278
|
-
* @returns {Promise<PgWirePool>}
|
|
279
|
-
*/
|
|
280
|
-
async _createSourceDbPool(dbName) {
|
|
281
|
-
const url = new URL(this.sourceUrl);
|
|
282
|
-
|
|
283
|
-
return new PgWirePool({
|
|
284
|
-
hostname: url.hostname,
|
|
285
|
-
port: parseInt(url.port) || 5432,
|
|
286
|
-
database: dbName,
|
|
287
|
-
username: url.username || 'postgres',
|
|
288
|
-
password: url.password || 'postgres',
|
|
289
|
-
max: this.maxParallelTables
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Create connection pool to specific database on local embedded PostgreSQL
|
|
295
|
-
* Uses Unix socket when available for ~30% faster connections
|
|
296
|
-
* @param {string} dbName - Database name
|
|
297
|
-
* @returns {Promise<PgWirePool>}
|
|
298
|
-
*/
|
|
299
|
-
async _createTargetDbPool(dbName) {
|
|
300
|
-
const config = {
|
|
301
|
-
database: dbName,
|
|
302
|
-
username: 'postgres',
|
|
303
|
-
password: 'postgres',
|
|
304
|
-
max: this.maxParallelTables
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Prefer Unix socket for faster local connections
|
|
308
|
-
if (this.targetSocketPath) {
|
|
309
|
-
config.unix = this.targetSocketPath;
|
|
310
|
-
} else {
|
|
311
|
-
config.hostname = '127.0.0.1';
|
|
312
|
-
config.port = this.targetPort;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return new PgWirePool(config);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Restore schema: ENUMs, tables, indexes, foreign keys
|
|
320
|
-
* Order matters: types → tables → indexes → FKs
|
|
321
|
-
* @param {PgWirePool} sourcePool - External database pool
|
|
322
|
-
* @param {PgWirePool} targetPool - Local database pool
|
|
323
|
-
* @param {string} dbName - Database name (for logging)
|
|
324
|
-
*/
|
|
325
|
-
async _restoreSchema(sourcePool, targetPool, dbName) {
|
|
326
|
-
// 1. Restore ENUM types
|
|
327
|
-
await this._restoreEnums(sourcePool, targetPool);
|
|
328
|
-
|
|
329
|
-
// 2. Restore tables (structure only, no data yet)
|
|
330
|
-
await this._restoreTables(sourcePool, targetPool);
|
|
331
|
-
|
|
332
|
-
this.logger.debug({ dbName }, 'Schema restored');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Restore ENUM types from external database
|
|
337
|
-
*/
|
|
338
|
-
async _restoreEnums(sourcePool, targetPool) {
|
|
339
|
-
const result = await sourcePool.query(`
|
|
340
|
-
SELECT n.nspname as schema, t.typname as name,
|
|
341
|
-
array_agg(e.enumlabel ORDER BY e.enumsortorder) as values
|
|
342
|
-
FROM pg_type t
|
|
343
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
344
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
345
|
-
WHERE n.nspname = 'public'
|
|
346
|
-
GROUP BY n.nspname, t.typname
|
|
347
|
-
`);
|
|
348
|
-
|
|
349
|
-
for (const enumType of result.rows) {
|
|
350
|
-
const values = enumType.values.map(v => `'${v.replace(/'/g, "''")}'`).join(', ');
|
|
351
|
-
const createSql = `CREATE TYPE "${enumType.name}" AS ENUM (${values})`;
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
await targetPool.query(createSql);
|
|
355
|
-
} catch (err) {
|
|
356
|
-
if (err.code !== '42710') throw err; // 42710 = type already exists
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Restore table structures from external database
|
|
363
|
-
*/
|
|
364
|
-
async _restoreTables(sourcePool, targetPool) {
|
|
365
|
-
// Get table list
|
|
366
|
-
const tablesResult = await sourcePool.query(`
|
|
367
|
-
SELECT table_name FROM information_schema.tables
|
|
368
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
369
|
-
ORDER BY table_name
|
|
370
|
-
`);
|
|
371
|
-
|
|
372
|
-
for (const row of tablesResult.rows) {
|
|
373
|
-
const tableName = row.table_name;
|
|
374
|
-
const createSql = await this._getTableCreateStatement(sourcePool, tableName);
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
await targetPool.query(createSql);
|
|
378
|
-
} catch (err) {
|
|
379
|
-
if (err.code !== '42P07') throw err; // 42P07 = table already exists
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Generate CREATE TABLE statement from information_schema
|
|
386
|
-
* @param {PgWirePool} sourcePool - Source database pool
|
|
387
|
-
* @param {string} tableName - Table name
|
|
388
|
-
* @returns {Promise<string>} CREATE TABLE SQL
|
|
389
|
-
*/
|
|
390
|
-
async _getTableCreateStatement(sourcePool, tableName) {
|
|
391
|
-
// Get columns
|
|
392
|
-
const columnsResult = await sourcePool.query(`
|
|
393
|
-
SELECT column_name, data_type, udt_name, character_maximum_length,
|
|
394
|
-
column_default, is_nullable, numeric_precision, numeric_scale
|
|
395
|
-
FROM information_schema.columns
|
|
396
|
-
WHERE table_schema = 'public' AND table_name = $1
|
|
397
|
-
ORDER BY ordinal_position
|
|
398
|
-
`, [tableName]);
|
|
399
|
-
|
|
400
|
-
const columns = columnsResult.rows.map(col => {
|
|
401
|
-
let type = col.data_type;
|
|
402
|
-
|
|
403
|
-
// Handle special types
|
|
404
|
-
if (type === 'USER-DEFINED') {
|
|
405
|
-
type = `"${col.udt_name}"`; // ENUM or custom type
|
|
406
|
-
} else if (type === 'character varying' && col.character_maximum_length) {
|
|
407
|
-
type = `varchar(${col.character_maximum_length})`;
|
|
408
|
-
} else if (type === 'character' && col.character_maximum_length) {
|
|
409
|
-
type = `char(${col.character_maximum_length})`;
|
|
410
|
-
} else if (type === 'numeric' && col.numeric_precision) {
|
|
411
|
-
type = col.numeric_scale
|
|
412
|
-
? `numeric(${col.numeric_precision},${col.numeric_scale})`
|
|
413
|
-
: `numeric(${col.numeric_precision})`;
|
|
414
|
-
} else if (type === 'ARRAY') {
|
|
415
|
-
type = `${col.udt_name.replace(/^_/, '')}[]`;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
let colDef = `"${col.column_name}" ${type}`;
|
|
419
|
-
|
|
420
|
-
if (col.column_default) {
|
|
421
|
-
colDef += ` DEFAULT ${col.column_default}`;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (col.is_nullable === 'NO') {
|
|
425
|
-
colDef += ' NOT NULL';
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return colDef;
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Get primary key
|
|
432
|
-
const pkResult = await sourcePool.query(`
|
|
433
|
-
SELECT a.attname
|
|
434
|
-
FROM pg_index i
|
|
435
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
436
|
-
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
|
437
|
-
ORDER BY array_position(i.indkey, a.attnum)
|
|
438
|
-
`, [tableName]);
|
|
439
|
-
|
|
440
|
-
if (pkResult.rows.length > 0) {
|
|
441
|
-
const pkCols = pkResult.rows.map(r => `"${r.attname}"`).join(', ');
|
|
442
|
-
columns.push(`PRIMARY KEY (${pkCols})`);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return `CREATE TABLE "${tableName}" (\n ${columns.join(',\n ')}\n)`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Discover tables in the database
|
|
450
|
-
* @param {PgWirePool} sourcePool - Source database pool
|
|
451
|
-
* @returns {Promise<string[]>} Table names
|
|
452
|
-
*/
|
|
453
|
-
async _discoverTables(sourcePool) {
|
|
454
|
-
const result = await sourcePool.query(`
|
|
455
|
-
SELECT table_name FROM information_schema.tables
|
|
456
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
457
|
-
ORDER BY table_name
|
|
458
|
-
`);
|
|
459
|
-
|
|
460
|
-
return result.rows.map(r => r.table_name);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Restore table data in parallel using COPY protocol
|
|
465
|
-
* @param {PgWirePool} sourcePool - Source database pool
|
|
466
|
-
* @param {PgWirePool} targetPool - Target database pool
|
|
467
|
-
* @param {string[]} tables - Table names
|
|
468
|
-
*/
|
|
469
|
-
async _restoreTablesParallel(sourcePool, targetPool, tables) {
|
|
470
|
-
// Batch tables to limit concurrency
|
|
471
|
-
const batches = [];
|
|
472
|
-
for (let i = 0; i < tables.length; i += this.maxParallelTables) {
|
|
473
|
-
batches.push(tables.slice(i, i + this.maxParallelTables));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
for (const batch of batches) {
|
|
477
|
-
await Promise.all(
|
|
478
|
-
batch.map(table => this._copyTableData(sourcePool, targetPool, table))
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Copy table data using binary COPY protocol (high performance)
|
|
485
|
-
* Uses async generator pattern for streaming data between connections.
|
|
486
|
-
* @param {PgWirePool} sourcePool - Source database pool
|
|
487
|
-
* @param {PgWirePool} targetPool - Target database pool
|
|
488
|
-
* @param {string} tableName - Table name
|
|
489
|
-
*/
|
|
490
|
-
async _copyTableData(sourcePool, targetPool, tableName) {
|
|
491
|
-
// Get row count first (for metrics)
|
|
492
|
-
const countResult = await sourcePool.query(
|
|
493
|
-
`SELECT COUNT(*)::int as count FROM "${tableName}"`
|
|
494
|
-
);
|
|
495
|
-
const rowCount = countResult.rows[0].count;
|
|
496
|
-
|
|
497
|
-
if (rowCount === 0) {
|
|
498
|
-
this.logger.debug({ tableName, rows: 0 }, 'Skipping empty table');
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Get connections for COPY streaming
|
|
503
|
-
const sourceClient = await sourcePool.connect();
|
|
504
|
-
const targetClient = await targetPool.connect();
|
|
505
|
-
|
|
506
|
-
let bytesTransferred = 0;
|
|
507
|
-
|
|
508
|
-
try {
|
|
509
|
-
// Create byte-tracking wrapper for the async generator
|
|
510
|
-
async function* trackBytes(source) {
|
|
511
|
-
for await (const chunk of source) {
|
|
512
|
-
bytesTransferred += chunk.length;
|
|
513
|
-
yield chunk;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Stream COPY: source → target using async generators
|
|
518
|
-
const copyToSql = `COPY "${tableName}" TO STDOUT WITH (FORMAT binary)`;
|
|
519
|
-
const copyFromSql = `COPY "${tableName}" FROM STDIN WITH (FORMAT binary)`;
|
|
520
|
-
|
|
521
|
-
const sourceStream = sourceClient.copyTo(copyToSql);
|
|
522
|
-
await targetClient.copyFrom(copyFromSql, trackBytes(sourceStream));
|
|
523
|
-
|
|
524
|
-
// Update metrics
|
|
525
|
-
this.metrics.bytesTransferred += bytesTransferred;
|
|
526
|
-
this.metrics.rowsRestored += rowCount;
|
|
527
|
-
this.metrics.tablesRestored++;
|
|
528
|
-
|
|
529
|
-
this.logger.debug({ tableName, rows: rowCount, bytes: bytesTransferred }, 'Table data copied');
|
|
530
|
-
|
|
531
|
-
// Emit progress for dashboard
|
|
532
|
-
this.onProgress({
|
|
533
|
-
databasesRestored: this.metrics.databasesRestored,
|
|
534
|
-
totalDatabases: this.totalDatabases,
|
|
535
|
-
tablesRestored: this.metrics.tablesRestored,
|
|
536
|
-
totalTables: this.totalTables,
|
|
537
|
-
bytesTransferred: this.metrics.bytesTransferred,
|
|
538
|
-
totalBytes: this.totalBytes
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
} finally {
|
|
542
|
-
sourcePool.release(sourceClient);
|
|
543
|
-
targetPool.release(targetClient);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Restore sequences to correct values (after data restore)
|
|
549
|
-
* @param {PgWirePool} sourcePool - Source database pool
|
|
550
|
-
* @param {PgWirePool} targetPool - Target database pool
|
|
551
|
-
*/
|
|
552
|
-
async _restoreSequences(sourcePool, targetPool) {
|
|
553
|
-
// Get all sequences
|
|
554
|
-
const seqResult = await sourcePool.query(`
|
|
555
|
-
SELECT sequence_name FROM information_schema.sequences
|
|
556
|
-
WHERE sequence_schema = 'public'
|
|
557
|
-
`);
|
|
558
|
-
|
|
559
|
-
for (const seq of seqResult.rows) {
|
|
560
|
-
const seqName = seq.sequence_name;
|
|
561
|
-
|
|
562
|
-
// Get current value from source
|
|
563
|
-
const valueResult = await sourcePool.query(`SELECT last_value FROM "${seqName}"`);
|
|
564
|
-
const lastValue = valueResult.rows[0].last_value;
|
|
565
|
-
|
|
566
|
-
// Set on target
|
|
567
|
-
try {
|
|
568
|
-
await targetPool.query(`SELECT setval($1, $2, true)`, [seqName, lastValue]);
|
|
569
|
-
} catch (err) {
|
|
570
|
-
this.logger.warn({ sequence: seqName, err: err.message }, 'Failed to restore sequence');
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|