pgserve 0.1.5 → 1.0.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.
@@ -2,13 +2,12 @@
2
2
 
3
3
  /**
4
4
  * Benchmark Runner
5
- * Compares SQLite, PGlite, and PostgreSQL performance
5
+ * Compares SQLite, PGlite, PostgreSQL Server, and pgserve performance
6
6
  */
7
7
 
8
8
  import Database from 'better-sqlite3';
9
9
  import { PGlite } from '@electric-sql/pglite';
10
- import { startServer, cleanup } from '../../src/index.js';
11
- import { execSync } from 'child_process';
10
+ import { startMultiTenantServer } from '../../src/index.js';
12
11
  import fs from 'fs';
13
12
  import path from 'path';
14
13
  import pg from 'pg';
@@ -17,38 +16,41 @@ const { Pool } = pg;
17
16
 
18
17
  // Global error handlers (suppress expected PGlite WASM ExitStatus errors)
19
18
  process.on('unhandledRejection', (reason, promise) => {
20
- // ExitStatus errors are expected from PGlite WASM cleanup - ignore them
21
- if (reason && reason.name === 'ExitStatus') {
22
- return;
23
- }
24
- console.error('❌ Unhandled Promise Rejection:', reason);
19
+ if (reason && reason.name === 'ExitStatus') return;
20
+ console.error('Unhandled Promise Rejection:', reason);
25
21
  });
26
22
 
27
23
  process.on('uncaughtException', (error) => {
28
- // ExitStatus errors are expected from PGlite WASM cleanup - ignore them
29
- if (error && error.name === 'ExitStatus') {
30
- return;
31
- }
32
- console.error('❌ Uncaught Exception:', error);
24
+ if (error && error.name === 'ExitStatus') return;
25
+ console.error('Uncaught Exception:', error);
33
26
  process.exit(1);
34
27
  });
35
28
 
36
29
  const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
37
30
 
31
+ // PostgreSQL Server configuration (Docker with tmpfs for fair RAM-to-RAM comparison)
32
+ const POSTGRES_CONFIG = {
33
+ host: 'localhost',
34
+ port: 15432,
35
+ user: 'postgres',
36
+ password: 'benchpass',
37
+ database: 'bench'
38
+ };
39
+
38
40
  /**
39
41
  * Benchmark scenario configuration
40
42
  */
41
43
  const scenarios = [
42
44
  {
43
45
  name: 'Concurrent Writes (10 agents)',
44
- description: 'Simulates Hive agent sessions writing simultaneously',
46
+ description: 'Simulates 10 concurrent agents writing simultaneously',
45
47
  operations: [
46
48
  { type: 'INSERT', count: 100, concurrent: 10 }
47
49
  ]
48
50
  },
49
51
  {
50
52
  name: 'Mixed Workload (messages)',
51
- description: 'Simulates Evolution API message operations',
53
+ description: 'Simulates typical API message operations',
52
54
  operations: [
53
55
  { type: 'INSERT', count: 500 },
54
56
  { type: 'SELECT', count: 2000 },
@@ -57,7 +59,7 @@ const scenarios = [
57
59
  },
58
60
  {
59
61
  name: 'Write Lock Contention',
60
- description: 'Stress test for lock handling',
62
+ description: 'Stress test for lock handling with 50 concurrent writers',
61
63
  operations: [
62
64
  { type: 'INSERT', count: 100, concurrent: 50 }
63
65
  ]
@@ -163,6 +165,29 @@ async function benchmarkSQLite(scenario) {
163
165
  }
164
166
  }
165
167
  }
168
+ } else if (op.type === 'SELECT') {
169
+ for (let i = 0; i < op.count; i++) {
170
+ const start = Date.now();
171
+ try {
172
+ db.prepare('SELECT * FROM messages LIMIT 10').all();
173
+ metrics.addLatency(Date.now() - start);
174
+ } catch (error) {
175
+ metrics.addError(error);
176
+ }
177
+ }
178
+ } else if (op.type === 'UPDATE') {
179
+ for (let i = 0; i < op.count; i++) {
180
+ const start = Date.now();
181
+ try {
182
+ db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(
183
+ `Updated ${i}`,
184
+ (i % 100) + 1
185
+ );
186
+ metrics.addLatency(Date.now() - start);
187
+ } catch (error) {
188
+ metrics.addError(error);
189
+ }
190
+ }
166
191
  }
167
192
  }
168
193
 
@@ -173,58 +198,20 @@ async function benchmarkSQLite(scenario) {
173
198
  }
174
199
 
175
200
  /**
176
- * PGlite Benchmark
201
+ * PGlite Benchmark (in-process WASM PostgreSQL)
177
202
  */
178
203
  async function benchmarkPGlite(scenario) {
179
204
  console.log(' 🔹 Running PGlite benchmark...');
180
205
 
181
- // Clean up stale instances
182
- cleanup();
183
-
184
206
  const dataDir = path.join(RESULTS_DIR, 'pglite-bench');
185
207
  if (fs.existsSync(dataDir)) {
186
208
  fs.rmSync(dataDir, { recursive: true });
187
209
  }
188
210
 
189
- const instance = await startServer({
190
- dataDir,
191
- port: 12999,
192
- autoPort: true,
193
- logLevel: 'error'
194
- });
195
-
196
- // Connect via PostgreSQL pool (proper way to use the server)
197
- const pool = new Pool({
198
- host: 'localhost',
199
- port: instance.port,
200
- database: 'postgres',
201
- max: 20,
202
- connectionTimeoutMillis: 10000,
203
- ssl: false
204
- });
205
-
206
- // Give server a moment to be fully ready
207
- await new Promise(resolve => setTimeout(resolve, 1000));
208
-
209
- // Wait for server to be ready with retries
210
- let connected = false;
211
- for (let i = 0; i < 10; i++) {
212
- try {
213
- await pool.query('SELECT 1');
214
- connected = true;
215
- break;
216
- } catch (error) {
217
- if (i === 9) throw error;
218
- await new Promise(resolve => setTimeout(resolve, 500));
219
- }
220
- }
221
-
222
- if (!connected) {
223
- throw new Error('Failed to connect to PGlite server');
224
- }
211
+ const db = new PGlite(dataDir);
225
212
 
226
213
  // Setup schema
227
- await pool.query(`
214
+ await db.exec(`
228
215
  CREATE TABLE IF NOT EXISTS messages (
229
216
  id SERIAL PRIMARY KEY,
230
217
  content TEXT,
@@ -235,71 +222,75 @@ async function benchmarkPGlite(scenario) {
235
222
  const metrics = new Metrics();
236
223
  metrics.start();
237
224
 
238
- // Run operations
225
+ // Run operations (PGlite is single-threaded, so concurrent = sequential)
239
226
  for (const op of scenario.operations) {
240
227
  if (op.type === 'INSERT') {
241
- const concurrent = op.concurrent || 1;
242
- const perThread = Math.floor(op.count / concurrent);
243
-
244
- const promises = [];
245
- for (let i = 0; i < concurrent; i++) {
246
- promises.push(
247
- (async () => {
248
- for (let j = 0; j < perThread; j++) {
249
- const start = Date.now();
250
- try {
251
- await pool.query(
252
- 'INSERT INTO messages (content, timestamp) VALUES ($1, $2)',
253
- [`Message ${i}-${j}`, Date.now()]
254
- );
255
- metrics.addLatency(Date.now() - start);
256
- } catch (error) {
257
- metrics.addError(error);
258
- }
259
- }
260
- })()
261
- );
228
+ const total = op.count;
229
+ for (let i = 0; i < total; i++) {
230
+ const start = Date.now();
231
+ try {
232
+ await db.query(
233
+ 'INSERT INTO messages (content, timestamp) VALUES ($1, $2)',
234
+ [`Message ${i}`, Date.now()]
235
+ );
236
+ metrics.addLatency(Date.now() - start);
237
+ } catch (error) {
238
+ metrics.addError(error);
239
+ }
240
+ }
241
+ } else if (op.type === 'SELECT') {
242
+ for (let i = 0; i < op.count; i++) {
243
+ const start = Date.now();
244
+ try {
245
+ await db.query('SELECT * FROM messages LIMIT 10');
246
+ metrics.addLatency(Date.now() - start);
247
+ } catch (error) {
248
+ metrics.addError(error);
249
+ }
250
+ }
251
+ } else if (op.type === 'UPDATE') {
252
+ for (let i = 0; i < op.count; i++) {
253
+ const start = Date.now();
254
+ try {
255
+ await db.query(
256
+ 'UPDATE messages SET content = $1 WHERE id = $2',
257
+ [`Updated ${i}`, (i % 100) + 1]
258
+ );
259
+ metrics.addLatency(Date.now() - start);
260
+ } catch (error) {
261
+ metrics.addError(error);
262
+ }
262
263
  }
263
-
264
- await Promise.all(promises);
265
264
  }
266
265
  }
267
266
 
268
267
  metrics.end();
269
268
 
270
- // Cleanup
271
- await pool.end();
272
-
273
- // Stop instance
274
269
  try {
275
- await instance.stop();
276
- } catch (error) {
277
- // ExitStatus errors are expected during WASM cleanup - ignore them
278
- if (error.name !== 'ExitStatus') {
279
- console.error('⚠️ Error stopping instance:', error.message);
280
- }
270
+ await db.close();
271
+ } catch (e) {
272
+ // Ignore ExitStatus errors from WASM cleanup
281
273
  }
282
274
 
283
275
  return metrics.getReport();
284
276
  }
285
277
 
286
278
  /**
287
- * PostgreSQL Server Benchmark
279
+ * PostgreSQL Server Benchmark (remote real PostgreSQL)
288
280
  */
289
281
  async function benchmarkPostgreSQL(scenario) {
290
282
  console.log(' 🔷 Running PostgreSQL Server benchmark...');
291
283
 
292
284
  const pool = new Pool({
293
- host: '192.168.112.135',
294
- port: 5432,
295
- user: 'postgres',
296
- password: '#Duassenha#2024',
297
- database: 'genie_evolution',
298
- max: 20 // Connection pool size
285
+ ...POSTGRES_CONFIG,
286
+ max: 20
299
287
  });
300
288
 
301
289
  try {
302
- // Setup schema (use bench_ prefix to avoid conflicts)
290
+ // Test connection first
291
+ await pool.query('SELECT 1');
292
+
293
+ // Setup schema
303
294
  await pool.query(`
304
295
  DROP TABLE IF EXISTS bench_messages;
305
296
  CREATE TABLE bench_messages (
@@ -339,6 +330,29 @@ async function benchmarkPostgreSQL(scenario) {
339
330
  }
340
331
 
341
332
  await Promise.all(promises);
333
+ } else if (op.type === 'SELECT') {
334
+ for (let i = 0; i < op.count; i++) {
335
+ const start = Date.now();
336
+ try {
337
+ await pool.query('SELECT * FROM bench_messages LIMIT 10');
338
+ metrics.addLatency(Date.now() - start);
339
+ } catch (error) {
340
+ metrics.addError(error);
341
+ }
342
+ }
343
+ } else if (op.type === 'UPDATE') {
344
+ for (let i = 0; i < op.count; i++) {
345
+ const start = Date.now();
346
+ try {
347
+ await pool.query(
348
+ 'UPDATE bench_messages SET content = $1 WHERE id = $2',
349
+ [`Updated ${i}`, (i % 100) + 1]
350
+ );
351
+ metrics.addLatency(Date.now() - start);
352
+ } catch (error) {
353
+ metrics.addError(error);
354
+ }
355
+ }
342
356
  }
343
357
  }
344
358
 
@@ -350,9 +364,135 @@ async function benchmarkPostgreSQL(scenario) {
350
364
 
351
365
  return metrics.getReport();
352
366
  } catch (error) {
353
- console.error(' PostgreSQL benchmark failed:', error.message);
367
+ console.error(' PostgreSQL benchmark failed:', error.message);
368
+ await pool.end();
369
+ return { throughput: 0, p50: 0, p99: 0, errors: 1, lockTimeouts: 0, totalOps: 0, skipped: true };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * pgserve Benchmark (our solution - embedded PostgreSQL with TRUE concurrency)
375
+ */
376
+ async function benchmarkPgserve(scenario) {
377
+ console.log(' 🚀 Running pgserve benchmark...');
378
+
379
+ let server;
380
+ try {
381
+ // Start pgserve in memory mode
382
+ server = await startMultiTenantServer({
383
+ port: 18432,
384
+ logLevel: 'error'
385
+ });
386
+
387
+ // Wait for server to be fully ready
388
+ await new Promise(resolve => setTimeout(resolve, 2000));
389
+
390
+ const pool = new Pool({
391
+ host: 'localhost',
392
+ port: 18432,
393
+ database: 'bench_test',
394
+ user: 'postgres',
395
+ password: 'postgres',
396
+ max: 20,
397
+ connectionTimeoutMillis: 30000
398
+ });
399
+
400
+ // Wait for connection with retries
401
+ let connected = false;
402
+ for (let i = 0; i < 10; i++) {
403
+ try {
404
+ await pool.query('SELECT 1');
405
+ connected = true;
406
+ break;
407
+ } catch (error) {
408
+ if (i === 9) throw error;
409
+ await new Promise(resolve => setTimeout(resolve, 1000));
410
+ }
411
+ }
412
+
413
+ if (!connected) {
414
+ throw new Error('Failed to connect to pgserve');
415
+ }
416
+
417
+ // Setup schema
418
+ await pool.query(`
419
+ DROP TABLE IF EXISTS bench_messages;
420
+ CREATE TABLE bench_messages (
421
+ id SERIAL PRIMARY KEY,
422
+ content TEXT,
423
+ timestamp BIGINT
424
+ )
425
+ `);
426
+
427
+ const metrics = new Metrics();
428
+ metrics.start();
429
+
430
+ // Run operations (TRUE concurrent - pgserve handles this natively)
431
+ for (const op of scenario.operations) {
432
+ if (op.type === 'INSERT') {
433
+ const concurrent = op.concurrent || 1;
434
+ const perThread = Math.floor(op.count / concurrent);
435
+
436
+ const promises = [];
437
+ for (let i = 0; i < concurrent; i++) {
438
+ promises.push(
439
+ (async () => {
440
+ for (let j = 0; j < perThread; j++) {
441
+ const start = Date.now();
442
+ try {
443
+ await pool.query(
444
+ 'INSERT INTO bench_messages (content, timestamp) VALUES ($1, $2)',
445
+ [`Message ${i}-${j}`, Date.now()]
446
+ );
447
+ metrics.addLatency(Date.now() - start);
448
+ } catch (error) {
449
+ metrics.addError(error);
450
+ }
451
+ }
452
+ })()
453
+ );
454
+ }
455
+
456
+ await Promise.all(promises);
457
+ } else if (op.type === 'SELECT') {
458
+ for (let i = 0; i < op.count; i++) {
459
+ const start = Date.now();
460
+ try {
461
+ await pool.query('SELECT * FROM bench_messages LIMIT 10');
462
+ metrics.addLatency(Date.now() - start);
463
+ } catch (error) {
464
+ metrics.addError(error);
465
+ }
466
+ }
467
+ } else if (op.type === 'UPDATE') {
468
+ for (let i = 0; i < op.count; i++) {
469
+ const start = Date.now();
470
+ try {
471
+ await pool.query(
472
+ 'UPDATE bench_messages SET content = $1 WHERE id = $2',
473
+ [`Updated ${i}`, (i % 100) + 1]
474
+ );
475
+ metrics.addLatency(Date.now() - start);
476
+ } catch (error) {
477
+ metrics.addError(error);
478
+ }
479
+ }
480
+ }
481
+ }
482
+
483
+ metrics.end();
484
+
485
+ // Cleanup
354
486
  await pool.end();
355
- throw error;
487
+ await server.stop();
488
+
489
+ return metrics.getReport();
490
+ } catch (error) {
491
+ console.error(' pgserve benchmark failed:', error.message);
492
+ if (server) {
493
+ try { await server.stop(); } catch (e) {}
494
+ }
495
+ return { throughput: 0, p50: 0, p99: 0, errors: 1, lockTimeouts: 0, totalOps: 0, skipped: true };
356
496
  }
357
497
  }
358
498
 
@@ -372,70 +512,69 @@ function generateReport(results) {
372
512
  // Generate markdown
373
513
  let md = '# Benchmark Results\n\n';
374
514
  md += `**Date:** ${new Date().toLocaleString()}\n\n`;
515
+ md += '## Quick Start\n\n';
516
+ md += '```bash\n';
517
+ md += '# Zero install - just run!\n';
518
+ md += 'npx pgserve\n\n';
519
+ md += '# Connect from any PostgreSQL client\n';
520
+ md += 'psql postgresql://localhost:5432/mydb\n';
521
+ md += '```\n\n';
375
522
 
376
523
  for (const scenario of results) {
377
524
  md += `## ${scenario.name}\n\n`;
378
525
  md += `${scenario.description}\n\n`;
379
526
 
527
+ const { sqlite, pglite, postgres, pgserve } = scenario;
528
+
380
529
  md += '```\n';
381
- md += '┌─────────────────┬──────────┬──────────┬──────────┬──────────┐\n';
382
- md += '│ Database │ SQLite │ PGlite │ PostgreSQL│ Winner │\n';
383
- md += '├─────────────────┼──────────┼──────────┼──────────┼──────────┤\n';
384
-
385
- const sqlite = scenario.sqlite;
386
- const pglite = scenario.pglite;
387
- const postgres = scenario.postgres;
388
-
389
- // Find winner for each metric
390
- const maxThroughput = Math.max(sqlite.throughput, pglite.throughput, postgres.throughput);
391
- const minP50 = Math.min(sqlite.p50, pglite.p50, postgres.p50);
392
- const minP99 = Math.min(sqlite.p99, pglite.p99, postgres.p99);
393
- const minErrors = Math.min(sqlite.errors, pglite.errors, postgres.errors);
394
-
395
- const getThroughputWinner = () => {
396
- if (postgres.throughput === maxThroughput) return 'PostgreSQL';
397
- if (pglite.throughput === maxThroughput) return 'PGlite';
398
- return 'SQLite';
399
- };
530
+ md += '┌─────────────────┬──────────┬──────────┬──────────┬──────────┬──────────┐\n';
531
+ md += '│ Metric │ SQLite │ PGlite │ PostgreSQL│ pgserve │ Winner │\n';
532
+ md += '├─────────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤\n';
400
533
 
401
- const getLatencyWinner = (metric) => {
402
- const pg = postgres[metric];
403
- const pgl = pglite[metric];
404
- const sql = sqlite[metric];
405
- if (pg === Math.min(pg, pgl, sql)) return 'PostgreSQL';
406
- if (pgl === Math.min(pg, pgl, sql)) return 'PGlite';
407
- return 'SQLite';
408
- };
534
+ // Find winners
535
+ const throughputs = { sqlite: sqlite.throughput, pglite: pglite.throughput, postgres: postgres.throughput, pgserve: pgserve.throughput };
536
+ const p50s = { sqlite: sqlite.p50, pglite: pglite.p50, postgres: postgres.p50, pgserve: pgserve.p50 };
537
+ const p99s = { sqlite: sqlite.p99, pglite: pglite.p99, postgres: postgres.p99, pgserve: pgserve.p99 };
538
+ const errors = { sqlite: sqlite.errors, pglite: pglite.errors, postgres: postgres.errors, pgserve: pgserve.errors };
409
539
 
410
- md += `│ Throughput (qps) ${String(sqlite.throughput).padEnd(8)} ${String(pglite.throughput).padEnd(8)} ${String(postgres.throughput).padEnd(9)} ${getThroughputWinner().padEnd(8)} │\n`;
411
- md += `│ P50 latency (ms) ${String(sqlite.p50).padEnd(8)} ${String(pglite.p50).padEnd(8)} ${String(postgres.p50).padEnd(9)} ${getLatencyWinner('p50').padEnd(8)} │\n`;
412
- md += `│ P99 latency (ms) ${String(sqlite.p99).padEnd(8)} ${String(pglite.p99).padEnd(8)} ${String(postgres.p99).padEnd(9)} ${getLatencyWinner('p99').padEnd(8)} │\n`;
413
- md += `│ Errors │ ${String(sqlite.errors).padEnd(8)} │ ${String(pglite.errors).padEnd(8)} │ ${String(postgres.errors).padEnd(9)} │ ${postgres.errors === minErrors ? 'PostgreSQL' : (pglite.errors === minErrors ? 'PGlite' : 'SQLite').padEnd(8)} │\n`;
414
- md += `│ Lock timeouts │ ${String(sqlite.lockTimeouts).padEnd(8)} ${String(pglite.lockTimeouts).padEnd(8)} ${String(postgres.lockTimeouts).padEnd(9)} N/A │\n`;
415
- md += '└─────────────────┴──────────┴──────────┴──────────┴──────────┘\n';
540
+ const getMaxKey = (obj) => Object.entries(obj).reduce((a, b) => a[1] > b[1] ? a : b)[0];
541
+ const getMinKey = (obj) => Object.entries(obj).filter(([k,v]) => v > 0 || k === 'sqlite').reduce((a, b) => a[1] < b[1] ? a : b)[0];
542
+ const getMinErrorKey = (obj) => Object.entries(obj).reduce((a, b) => a[1] <= b[1] ? a : b)[0];
543
+
544
+ const nameMap = { sqlite: 'SQLite', pglite: 'PGlite', postgres: 'PostgreSQL', pgserve: 'pgserve' };
545
+
546
+ const pad = (s, n) => String(s).padEnd(n);
547
+
548
+ md += `│ Throughput (qps)│ ${pad(sqlite.throughput, 8)} │ ${pad(pglite.throughput, 8)} │ ${pad(postgres.throughput, 9)} │ ${pad(pgserve.throughput, 8)} │ ${pad(nameMap[getMaxKey(throughputs)], 8)} │\n`;
549
+ md += `│ P50 latency (ms)│ ${pad(sqlite.p50, 8)} │ ${pad(pglite.p50, 8)} │ ${pad(postgres.p50, 9)} │ ${pad(pgserve.p50, 8)} │ ${pad(nameMap[getMinKey(p50s)], 8)} │\n`;
550
+ md += `│ P99 latency (ms)│ ${pad(sqlite.p99, 8)} │ ${pad(pglite.p99, 8)} │ ${pad(postgres.p99, 9)} │ ${pad(pgserve.p99, 8)} │ ${pad(nameMap[getMinKey(p99s)], 8)} │\n`;
551
+ md += `│ Errors │ ${pad(sqlite.errors, 8)} │ ${pad(pglite.errors, 8)} │ ${pad(postgres.errors, 9)} │ ${pad(pgserve.errors, 8)} │ ${pad(nameMap[getMinErrorKey(errors)], 8)} │\n`;
552
+ md += '└─────────────────┴──────────┴──────────┴──────────┴──────────┴──────────┘\n';
416
553
  md += '```\n\n';
417
554
 
418
555
  // Analysis
419
- const winner = getThroughputWinner();
420
- if (winner === 'PostgreSQL') {
421
- const vsSQL = ((postgres.throughput / sqlite.throughput - 1) * 100).toFixed(1);
422
- const vsPGL = ((postgres.throughput / pglite.throughput - 1) * 100).toFixed(1);
423
- md += `💡 **PostgreSQL Server is ${vsSQL}% faster than SQLite and ${vsPGL}% faster than PGlite**\n\n`;
424
- } else if (winner === 'PGlite') {
425
- const vsSQL = ((pglite.throughput / sqlite.throughput - 1) * 100).toFixed(1);
426
- const vsPG = ((pglite.throughput / postgres.throughput - 1) * 100).toFixed(1);
427
- md += `💡 **PGlite is ${vsSQL}% faster than SQLite and ${vsPG}% faster than PostgreSQL Server**\n\n`;
556
+ const winner = nameMap[getMaxKey(throughputs)];
557
+ if (winner === 'pgserve') {
558
+ const vsSQLite = sqlite.throughput > 0 ? ((pgserve.throughput / sqlite.throughput - 1) * 100).toFixed(1) : 'N/A';
559
+ const vsPGlite = pglite.throughput > 0 ? ((pgserve.throughput / pglite.throughput - 1) * 100).toFixed(1) : 'N/A';
560
+ const vsPostgres = postgres.throughput > 0 ? ((pgserve.throughput / postgres.throughput - 1) * 100).toFixed(1) : 'N/A';
561
+ md += `**pgserve wins!** ${vsPGlite}% faster than PGlite for concurrent workloads.\n\n`;
428
562
  } else {
429
- const vsPGL = ((sqlite.throughput / pglite.throughput - 1) * 100).toFixed(1);
430
- const vsPG = ((sqlite.throughput / postgres.throughput - 1) * 100).toFixed(1);
431
- md += `💡 **SQLite is ${vsPGL}% faster than PGlite and ${vsPG}% faster than PostgreSQL Server**\n\n`;
563
+ md += `**${winner} wins** this scenario.\n\n`;
432
564
  }
433
565
  }
434
566
 
567
+ md += '---\n\n';
568
+ md += '## Why pgserve?\n\n';
569
+ md += '- **TRUE Concurrency**: Native PostgreSQL process forking\n';
570
+ md += '- **Zero Config**: Just run `npx pgserve`\n';
571
+ md += '- **Auto-Provision**: Databases created on first connection\n';
572
+ md += '- **PostgreSQL 17.7**: Latest stable, native binaries\n';
573
+
435
574
  const mdPath = path.join(RESULTS_DIR, 'benchmark-results.md');
436
575
  fs.writeFileSync(mdPath, md);
437
576
 
438
- console.log(`\n✅ Results saved to:`);
577
+ console.log(`\nResults saved to:`);
439
578
  console.log(` JSON: ${jsonPath}`);
440
579
  console.log(` Markdown: ${mdPath}\n`);
441
580
 
@@ -446,9 +585,10 @@ function generateReport(results) {
446
585
  * Main runner
447
586
  */
448
587
  async function main() {
449
- console.log('╔═══════════════════════════════════════════════════════════════╗');
450
- console.log('║ PGlite Embedded Server - Benchmark Suite ║');
451
- console.log('╚═══════════════════════════════════════════════════════════════╝\n');
588
+ console.log('╔════════════════════════════════════════════════════════════════╗');
589
+ console.log('║ pgserve Benchmark Suite ║');
590
+ console.log('║ Comparing: SQLite | PGlite | PostgreSQL | pgserve ║');
591
+ console.log('╚════════════════════════════════════════════════════════════════╝\n');
452
592
 
453
593
  // Ensure results directory exists
454
594
  if (!fs.existsSync(RESULTS_DIR)) {
@@ -464,26 +604,31 @@ async function main() {
464
604
  const sqlite = await benchmarkSQLite(scenario);
465
605
  const pglite = await benchmarkPGlite(scenario);
466
606
  const postgres = await benchmarkPostgreSQL(scenario);
607
+ const pgserve = await benchmarkPgserve(scenario);
467
608
 
468
609
  results.push({
469
610
  name: scenario.name,
470
611
  description: scenario.description,
471
612
  sqlite,
472
613
  pglite,
473
- postgres
614
+ postgres,
615
+ pgserve
474
616
  });
475
617
 
476
618
  console.log(`\n SQLite: ${sqlite.throughput} qps, P50=${sqlite.p50}ms, errors=${sqlite.errors}`);
477
619
  console.log(` PGlite: ${pglite.throughput} qps, P50=${pglite.p50}ms, errors=${pglite.errors}`);
478
620
  console.log(` PostgreSQL: ${postgres.throughput} qps, P50=${postgres.p50}ms, errors=${postgres.errors}`);
621
+ console.log(` pgserve: ${pgserve.throughput} qps, P50=${pgserve.p50}ms, errors=${pgserve.errors}`);
479
622
  }
480
623
 
481
624
  console.log('\n📄 Generating report...\n');
482
- const report = generateReport(results);
625
+ generateReport(results);
483
626
 
484
- console.log('╔═══════════════════════════════════════════════════════════════╗');
485
- console.log('║ Benchmarks Complete! ║');
486
- console.log('╚═══════════════════════════════════════════════════════════════╝\n');
627
+ console.log('╔════════════════════════════════════════════════════════════════╗');
628
+ console.log('║ Benchmarks Complete! ║');
629
+ console.log('║ ║');
630
+ console.log('║ Try it yourself: npx pgserve ║');
631
+ console.log('╚════════════════════════════════════════════════════════════════╝\n');
487
632
  }
488
633
 
489
634
  main().catch(console.error);