pgserve 0.1.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.
Files changed (158) hide show
  1. package/.genie/AGENTS.md +13 -0
  2. package/.genie/agents/README.md +110 -0
  3. package/.genie/agents/analyze.md +176 -0
  4. package/.genie/agents/forge.md +290 -0
  5. package/.genie/agents/garbage-cleaner.md +324 -0
  6. package/.genie/agents/garbage-collector.md +596 -0
  7. package/.genie/agents/github-issue-gc.md +618 -0
  8. package/.genie/agents/review.md +380 -0
  9. package/.genie/agents/semantic-analyzer/find-duplicates.md +90 -0
  10. package/.genie/agents/semantic-analyzer/find-orphans.md +99 -0
  11. package/.genie/agents/semantic-analyzer.md +101 -0
  12. package/.genie/agents/update.md +182 -0
  13. package/.genie/agents/wish.md +357 -0
  14. package/.genie/code/AGENTS.md +692 -0
  15. package/.genie/code/agents/audit/risk.md +173 -0
  16. package/.genie/code/agents/audit/security.md +189 -0
  17. package/.genie/code/agents/audit.md +145 -0
  18. package/.genie/code/agents/challenge.md +230 -0
  19. package/.genie/code/agents/change-reviewer.md +295 -0
  20. package/.genie/code/agents/code-garbage-collector.md +425 -0
  21. package/.genie/code/agents/code-quality.md +410 -0
  22. package/.genie/code/agents/commit-suggester.md +255 -0
  23. package/.genie/code/agents/commit.md +124 -0
  24. package/.genie/code/agents/consensus.md +204 -0
  25. package/.genie/code/agents/daily-standup.md +722 -0
  26. package/.genie/code/agents/docgen.md +48 -0
  27. package/.genie/code/agents/explore.md +79 -0
  28. package/.genie/code/agents/fix.md +100 -0
  29. package/.genie/code/agents/git/commit-advisory.md +219 -0
  30. package/.genie/code/agents/git/workflows/issue.md +244 -0
  31. package/.genie/code/agents/git/workflows/pr.md +179 -0
  32. package/.genie/code/agents/git/workflows/release.md +460 -0
  33. package/.genie/code/agents/git/workflows/report.md +342 -0
  34. package/.genie/code/agents/git.md +432 -0
  35. package/.genie/code/agents/implementor.md +161 -0
  36. package/.genie/code/agents/install.md +515 -0
  37. package/.genie/code/agents/issue-creator.md +344 -0
  38. package/.genie/code/agents/polish.md +116 -0
  39. package/.genie/code/agents/qa.md +653 -0
  40. package/.genie/code/agents/refactor.md +294 -0
  41. package/.genie/code/agents/release.md +1129 -0
  42. package/.genie/code/agents/roadmap.md +885 -0
  43. package/.genie/code/agents/tests.md +557 -0
  44. package/.genie/code/agents/tracer.md +50 -0
  45. package/.genie/code/agents/update/upstream-update.md +85 -0
  46. package/.genie/code/agents/update/versions/generic-update.md +305 -0
  47. package/.genie/code/agents/vibe.md +1317 -0
  48. package/.genie/code/spells/agent-configuration.md +58 -0
  49. package/.genie/code/spells/automated-rc-publishing.md +106 -0
  50. package/.genie/code/spells/branch-tracker-guidance.md +28 -0
  51. package/.genie/code/spells/debug.md +320 -0
  52. package/.genie/code/spells/emoji-naming-convention.md +303 -0
  53. package/.genie/code/spells/evidence-storage.md +26 -0
  54. package/.genie/code/spells/file-naming-rules.md +35 -0
  55. package/.genie/code/spells/forge-code-blueprints.md +195 -0
  56. package/.genie/code/spells/genie-integration.md +153 -0
  57. package/.genie/code/spells/publishing-protocol.md +61 -0
  58. package/.genie/code/spells/team-consultation-protocol.md +284 -0
  59. package/.genie/code/spells/tool-requirements.md +20 -0
  60. package/.genie/code/spells/triad-maintenance-protocol.md +154 -0
  61. package/.genie/code/teams/tech-council/council.md +328 -0
  62. package/.genie/code/teams/tech-council/jt.md +352 -0
  63. package/.genie/code/teams/tech-council/nayr.md +305 -0
  64. package/.genie/code/teams/tech-council/oettam.md +375 -0
  65. package/.genie/neurons/README.md +193 -0
  66. package/.genie/neurons/forge.md +106 -0
  67. package/.genie/neurons/genie.md +63 -0
  68. package/.genie/neurons/review.md +106 -0
  69. package/.genie/neurons/wish.md +104 -0
  70. package/.genie/product/README.md +20 -0
  71. package/.genie/product/cli-automation.md +359 -0
  72. package/.genie/product/environment.md +60 -0
  73. package/.genie/product/mission.md +60 -0
  74. package/.genie/product/roadmap.md +44 -0
  75. package/.genie/product/tech-stack.md +34 -0
  76. package/.genie/product/templates/context-template.md +218 -0
  77. package/.genie/product/templates/qa-done-report-template.md +68 -0
  78. package/.genie/product/templates/review-report-template.md +89 -0
  79. package/.genie/product/templates/wish-template.md +120 -0
  80. package/.genie/scripts/helpers/analyze-commit.js +195 -0
  81. package/.genie/scripts/helpers/bullet-counter.js +194 -0
  82. package/.genie/scripts/helpers/bullet-find.js +289 -0
  83. package/.genie/scripts/helpers/bullet-id.js +244 -0
  84. package/.genie/scripts/helpers/check-secrets.js +237 -0
  85. package/.genie/scripts/helpers/count-tokens.js +200 -0
  86. package/.genie/scripts/helpers/create-frontmatter.js +456 -0
  87. package/.genie/scripts/helpers/detect-markers.js +293 -0
  88. package/.genie/scripts/helpers/detect-todos.js +267 -0
  89. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +135 -0
  90. package/.genie/scripts/helpers/embeddings.js +344 -0
  91. package/.genie/scripts/helpers/find-empty-sections.js +158 -0
  92. package/.genie/scripts/helpers/index.js +319 -0
  93. package/.genie/scripts/helpers/validate-frontmatter.js +578 -0
  94. package/.genie/scripts/helpers/validate-links.js +207 -0
  95. package/.genie/scripts/helpers/validate-paths.js +373 -0
  96. package/.genie/spells/README.md +9 -0
  97. package/.genie/spells/ace-protocol.md +118 -0
  98. package/.genie/spells/ask-one-at-a-time.md +175 -0
  99. package/.genie/spells/backup-analyzer.md +542 -0
  100. package/.genie/spells/blocker.md +12 -0
  101. package/.genie/spells/break-things-move-fast.md +56 -0
  102. package/.genie/spells/context-candidates.md +72 -0
  103. package/.genie/spells/context-critic.md +51 -0
  104. package/.genie/spells/defer-to-expertise.md +278 -0
  105. package/.genie/spells/delegate-dont-do.md +292 -0
  106. package/.genie/spells/error-investigation-protocol.md +328 -0
  107. package/.genie/spells/evidence-based-completion.md +273 -0
  108. package/.genie/spells/experiment.md +65 -0
  109. package/.genie/spells/file-creation-protocol.md +229 -0
  110. package/.genie/spells/forge-integration.md +281 -0
  111. package/.genie/spells/forge-orchestration.md +514 -0
  112. package/.genie/spells/gather-context.md +18 -0
  113. package/.genie/spells/global-health-check.md +34 -0
  114. package/.genie/spells/global-noop-roundtrip.md +25 -0
  115. package/.genie/spells/install-genie.md +1232 -0
  116. package/.genie/spells/install.md +82 -0
  117. package/.genie/spells/investigate-before-commit.md +112 -0
  118. package/.genie/spells/know-yourself.md +288 -0
  119. package/.genie/spells/learn.md +828 -0
  120. package/.genie/spells/mcp-diagnostic-protocol.md +246 -0
  121. package/.genie/spells/mcp-first.md +124 -0
  122. package/.genie/spells/multi-step-execution.md +67 -0
  123. package/.genie/spells/orchestration-boundary-protocol.md +256 -0
  124. package/.genie/spells/orchestrator-not-implementor.md +189 -0
  125. package/.genie/spells/prompt.md +746 -0
  126. package/.genie/spells/reflect.md +404 -0
  127. package/.genie/spells/routing-decision-matrix.md +368 -0
  128. package/.genie/spells/run-in-parallel.md +12 -0
  129. package/.genie/spells/session-state-updater-example.md +196 -0
  130. package/.genie/spells/session-state-updater.md +220 -0
  131. package/.genie/spells/track-long-running-tasks.md +133 -0
  132. package/.genie/spells/troubleshoot-infrastructure.md +176 -0
  133. package/.genie/spells/upgrade-genie.md +415 -0
  134. package/.genie/spells/url-presentation-protocol.md +301 -0
  135. package/.genie/spells/wish-initiation.md +158 -0
  136. package/.genie/spells/wish-issue-linkage.md +410 -0
  137. package/.genie/spells/wish-lifecycle.md +100 -0
  138. package/.genie/state/provider-status.json +3 -0
  139. package/.genie/state/version.json +16 -0
  140. package/AGENTS.md +422 -0
  141. package/CLAUDE.md +1 -0
  142. package/LICENSE +21 -0
  143. package/Makefile +235 -0
  144. package/README.md +323 -0
  145. package/bin/pglite-server.js +457 -0
  146. package/ecosystem.config.cjs +23 -0
  147. package/examples/multi-tenant-demo.js +104 -0
  148. package/package.json +47 -0
  149. package/src/detector.js +105 -0
  150. package/src/index.js +177 -0
  151. package/src/pool.js +320 -0
  152. package/src/ports.js +114 -0
  153. package/src/protocol.js +216 -0
  154. package/src/registry.js +134 -0
  155. package/src/router.js +289 -0
  156. package/src/server.js +265 -0
  157. package/tests/benchmarks/runner.js +489 -0
  158. package/tests/multi-tenant.test.js +201 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Benchmark Runner
5
+ * Compares SQLite, PGlite, and PostgreSQL performance
6
+ */
7
+
8
+ import Database from 'better-sqlite3';
9
+ import { PGlite } from '@electric-sql/pglite';
10
+ import { startServer, cleanup } from '../../src/index.js';
11
+ import { execSync } from 'child_process';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import pg from 'pg';
15
+
16
+ const { Pool } = pg;
17
+
18
+ // Global error handlers (suppress expected PGlite WASM ExitStatus errors)
19
+ 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);
25
+ });
26
+
27
+ 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);
33
+ process.exit(1);
34
+ });
35
+
36
+ const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
37
+
38
+ /**
39
+ * Benchmark scenario configuration
40
+ */
41
+ const scenarios = [
42
+ {
43
+ name: 'Concurrent Writes (10 agents)',
44
+ description: 'Simulates Hive agent sessions writing simultaneously',
45
+ operations: [
46
+ { type: 'INSERT', count: 100, concurrent: 10 }
47
+ ]
48
+ },
49
+ {
50
+ name: 'Mixed Workload (messages)',
51
+ description: 'Simulates Evolution API message operations',
52
+ operations: [
53
+ { type: 'INSERT', count: 500 },
54
+ { type: 'SELECT', count: 2000 },
55
+ { type: 'UPDATE', count: 250 }
56
+ ]
57
+ },
58
+ {
59
+ name: 'Write Lock Contention',
60
+ description: 'Stress test for lock handling',
61
+ operations: [
62
+ { type: 'INSERT', count: 100, concurrent: 50 }
63
+ ]
64
+ }
65
+ ];
66
+
67
+ /**
68
+ * Performance metrics
69
+ */
70
+ class Metrics {
71
+ constructor() {
72
+ this.latencies = [];
73
+ this.errors = 0;
74
+ this.lockTimeouts = 0;
75
+ this.startTime = 0;
76
+ this.endTime = 0;
77
+ }
78
+
79
+ start() {
80
+ this.startTime = Date.now();
81
+ }
82
+
83
+ end() {
84
+ this.endTime = Date.now();
85
+ }
86
+
87
+ addLatency(ms) {
88
+ this.latencies.push(ms);
89
+ }
90
+
91
+ addError(error) {
92
+ this.errors++;
93
+ if (error.message && error.message.includes('SQLITE_BUSY')) {
94
+ this.lockTimeouts++;
95
+ }
96
+ }
97
+
98
+ getThroughput() {
99
+ const durationMs = this.endTime - this.startTime;
100
+ const durationS = durationMs / 1000;
101
+ return Math.round(this.latencies.length / durationS);
102
+ }
103
+
104
+ getPercentile(p) {
105
+ if (this.latencies.length === 0) return 0;
106
+ const sorted = [...this.latencies].sort((a, b) => a - b);
107
+ const index = Math.ceil((sorted.length * p) / 100) - 1;
108
+ return sorted[Math.max(0, index)];
109
+ }
110
+
111
+ getReport() {
112
+ return {
113
+ throughput: this.getThroughput(),
114
+ p50: this.getPercentile(50),
115
+ p99: this.getPercentile(99),
116
+ errors: this.errors,
117
+ lockTimeouts: this.lockTimeouts,
118
+ totalOps: this.latencies.length
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * SQLite Benchmark
125
+ */
126
+ async function benchmarkSQLite(scenario) {
127
+ console.log(' 🔸 Running SQLite benchmark...');
128
+
129
+ const dbPath = path.join(RESULTS_DIR, 'sqlite-bench.db');
130
+ if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
131
+
132
+ const db = new Database(dbPath);
133
+
134
+ // Setup schema
135
+ db.exec(`
136
+ CREATE TABLE IF NOT EXISTS messages (
137
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
138
+ content TEXT,
139
+ timestamp INTEGER
140
+ )
141
+ `);
142
+
143
+ const metrics = new Metrics();
144
+ metrics.start();
145
+
146
+ // Run operations
147
+ for (const op of scenario.operations) {
148
+ if (op.type === 'INSERT') {
149
+ const concurrent = op.concurrent || 1;
150
+ const perThread = Math.floor(op.count / concurrent);
151
+
152
+ for (let i = 0; i < concurrent; i++) {
153
+ for (let j = 0; j < perThread; j++) {
154
+ const start = Date.now();
155
+ try {
156
+ db.prepare('INSERT INTO messages (content, timestamp) VALUES (?, ?)').run(
157
+ `Message ${i}-${j}`,
158
+ Date.now()
159
+ );
160
+ metrics.addLatency(Date.now() - start);
161
+ } catch (error) {
162
+ metrics.addError(error);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ metrics.end();
170
+ db.close();
171
+
172
+ return metrics.getReport();
173
+ }
174
+
175
+ /**
176
+ * PGlite Benchmark
177
+ */
178
+ async function benchmarkPGlite(scenario) {
179
+ console.log(' 🔹 Running PGlite benchmark...');
180
+
181
+ // Clean up stale instances
182
+ cleanup();
183
+
184
+ const dataDir = path.join(RESULTS_DIR, 'pglite-bench');
185
+ if (fs.existsSync(dataDir)) {
186
+ fs.rmSync(dataDir, { recursive: true });
187
+ }
188
+
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
+ }
225
+
226
+ // Setup schema
227
+ await pool.query(`
228
+ CREATE TABLE IF NOT EXISTS messages (
229
+ id SERIAL PRIMARY KEY,
230
+ content TEXT,
231
+ timestamp BIGINT
232
+ )
233
+ `);
234
+
235
+ const metrics = new Metrics();
236
+ metrics.start();
237
+
238
+ // Run operations
239
+ for (const op of scenario.operations) {
240
+ 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
+ );
262
+ }
263
+
264
+ await Promise.all(promises);
265
+ }
266
+ }
267
+
268
+ metrics.end();
269
+
270
+ // Cleanup
271
+ await pool.end();
272
+
273
+ // Stop instance
274
+ 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
+ }
281
+ }
282
+
283
+ return metrics.getReport();
284
+ }
285
+
286
+ /**
287
+ * PostgreSQL Server Benchmark
288
+ */
289
+ async function benchmarkPostgreSQL(scenario) {
290
+ console.log(' 🔷 Running PostgreSQL Server benchmark...');
291
+
292
+ 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
299
+ });
300
+
301
+ try {
302
+ // Setup schema (use bench_ prefix to avoid conflicts)
303
+ await pool.query(`
304
+ DROP TABLE IF EXISTS bench_messages;
305
+ CREATE TABLE bench_messages (
306
+ id SERIAL PRIMARY KEY,
307
+ content TEXT,
308
+ timestamp BIGINT
309
+ )
310
+ `);
311
+
312
+ const metrics = new Metrics();
313
+ metrics.start();
314
+
315
+ // Run operations
316
+ for (const op of scenario.operations) {
317
+ if (op.type === 'INSERT') {
318
+ const concurrent = op.concurrent || 1;
319
+ const perThread = Math.floor(op.count / concurrent);
320
+
321
+ const promises = [];
322
+ for (let i = 0; i < concurrent; i++) {
323
+ promises.push(
324
+ (async () => {
325
+ for (let j = 0; j < perThread; j++) {
326
+ const start = Date.now();
327
+ try {
328
+ await pool.query(
329
+ 'INSERT INTO bench_messages (content, timestamp) VALUES ($1, $2)',
330
+ [`Message ${i}-${j}`, Date.now()]
331
+ );
332
+ metrics.addLatency(Date.now() - start);
333
+ } catch (error) {
334
+ metrics.addError(error);
335
+ }
336
+ }
337
+ })()
338
+ );
339
+ }
340
+
341
+ await Promise.all(promises);
342
+ }
343
+ }
344
+
345
+ metrics.end();
346
+
347
+ // Cleanup
348
+ await pool.query('DROP TABLE IF EXISTS bench_messages');
349
+ await pool.end();
350
+
351
+ return metrics.getReport();
352
+ } catch (error) {
353
+ console.error(' ❌ PostgreSQL benchmark failed:', error.message);
354
+ await pool.end();
355
+ throw error;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Generate comparison report
361
+ */
362
+ function generateReport(results) {
363
+ const report = {
364
+ timestamp: new Date().toISOString(),
365
+ scenarios: results
366
+ };
367
+
368
+ // Save JSON
369
+ const jsonPath = path.join(RESULTS_DIR, 'benchmark-results.json');
370
+ fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
371
+
372
+ // Generate markdown
373
+ let md = '# Benchmark Results\n\n';
374
+ md += `**Date:** ${new Date().toLocaleString()}\n\n`;
375
+
376
+ for (const scenario of results) {
377
+ md += `## ${scenario.name}\n\n`;
378
+ md += `${scenario.description}\n\n`;
379
+
380
+ 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
+ };
400
+
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
+ };
409
+
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';
416
+ md += '```\n\n';
417
+
418
+ // 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`;
428
+ } 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`;
432
+ }
433
+ }
434
+
435
+ const mdPath = path.join(RESULTS_DIR, 'benchmark-results.md');
436
+ fs.writeFileSync(mdPath, md);
437
+
438
+ console.log(`\n✅ Results saved to:`);
439
+ console.log(` JSON: ${jsonPath}`);
440
+ console.log(` Markdown: ${mdPath}\n`);
441
+
442
+ return report;
443
+ }
444
+
445
+ /**
446
+ * Main runner
447
+ */
448
+ async function main() {
449
+ console.log('╔═══════════════════════════════════════════════════════════════╗');
450
+ console.log('║ PGlite Embedded Server - Benchmark Suite ║');
451
+ console.log('╚═══════════════════════════════════════════════════════════════╝\n');
452
+
453
+ // Ensure results directory exists
454
+ if (!fs.existsSync(RESULTS_DIR)) {
455
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
456
+ }
457
+
458
+ const results = [];
459
+
460
+ for (const scenario of scenarios) {
461
+ console.log(`\n📊 Scenario: ${scenario.name}`);
462
+ console.log(` ${scenario.description}\n`);
463
+
464
+ const sqlite = await benchmarkSQLite(scenario);
465
+ const pglite = await benchmarkPGlite(scenario);
466
+ const postgres = await benchmarkPostgreSQL(scenario);
467
+
468
+ results.push({
469
+ name: scenario.name,
470
+ description: scenario.description,
471
+ sqlite,
472
+ pglite,
473
+ postgres
474
+ });
475
+
476
+ console.log(`\n SQLite: ${sqlite.throughput} qps, P50=${sqlite.p50}ms, errors=${sqlite.errors}`);
477
+ console.log(` PGlite: ${pglite.throughput} qps, P50=${pglite.p50}ms, errors=${pglite.errors}`);
478
+ console.log(` PostgreSQL: ${postgres.throughput} qps, P50=${postgres.p50}ms, errors=${postgres.errors}`);
479
+ }
480
+
481
+ console.log('\n📄 Generating report...\n');
482
+ const report = generateReport(results);
483
+
484
+ console.log('╔═══════════════════════════════════════════════════════════════╗');
485
+ console.log('║ ✅ Benchmarks Complete! ║');
486
+ console.log('╚═══════════════════════════════════════════════════════════════╝\n');
487
+ }
488
+
489
+ main().catch(console.error);
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Multi-Tenant Router Test
3
+ *
4
+ * Tests the new multi-tenant architecture:
5
+ * - Single port server
6
+ * - Multiple databases auto-provisioned
7
+ * - Database isolation
8
+ */
9
+
10
+ import { startMultiTenantServer } from '../src/index.js';
11
+ import pg from 'pg';
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+
17
+ const { Client } = pg;
18
+
19
+ // Test data directory
20
+ const testDataDir = './test-data-multitenant';
21
+
22
+ // Cleanup helper
23
+ function cleanup() {
24
+ if (fs.existsSync(testDataDir)) {
25
+ fs.rmSync(testDataDir, { recursive: true, force: true });
26
+ }
27
+ }
28
+
29
+ test('Multi-tenant router - basic setup', async (t) => {
30
+ cleanup();
31
+
32
+ const router = await startMultiTenantServer({
33
+ port: 15432, // Use non-standard port for testing
34
+ baseDir: testDataDir,
35
+ logLevel: 'info'
36
+ });
37
+
38
+ // Verify router started
39
+ const stats = router.getStats();
40
+ assert.equal(stats.port, 15432);
41
+ assert.equal(stats.pool.totalInstances, 0); // No instances yet
42
+
43
+ await router.stop();
44
+ cleanup();
45
+ });
46
+
47
+ test('Multi-tenant router - auto-provision database', async (t) => {
48
+ cleanup();
49
+
50
+ const router = await startMultiTenantServer({
51
+ port: 15432,
52
+ baseDir: testDataDir,
53
+ logLevel: 'info'
54
+ });
55
+
56
+ // Connect to database "testdb1" (should auto-create)
57
+ const client = new Client({
58
+ host: '127.0.0.1',
59
+ port: 15432,
60
+ database: 'testdb1',
61
+ user: 'postgres' // PGlite doesn't require auth
62
+ });
63
+
64
+ await client.connect();
65
+
66
+ // Verify instance was created
67
+ const stats = router.getStats();
68
+ assert.equal(stats.pool.totalInstances, 1);
69
+
70
+ const databases = router.listDatabases();
71
+ assert.equal(databases.length, 1);
72
+ assert.equal(databases[0].dbName, 'testdb1');
73
+ assert.equal(databases[0].locked, true); // Locked to connection
74
+
75
+ // Create table
76
+ await client.query('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)');
77
+ await client.query("INSERT INTO users (name) VALUES ('Alice')");
78
+
79
+ // Query
80
+ const result = await client.query('SELECT * FROM users');
81
+ assert.equal(result.rows.length, 1);
82
+ assert.equal(result.rows[0].name, 'Alice');
83
+
84
+ await client.end();
85
+ await router.stop();
86
+ cleanup();
87
+ });
88
+
89
+ test('Multi-tenant router - multiple databases isolated', async (t) => {
90
+ cleanup();
91
+
92
+ const router = await startMultiTenantServer({
93
+ port: 15432,
94
+ baseDir: testDataDir,
95
+ logLevel: 'info'
96
+ });
97
+
98
+ // Connect to database 1
99
+ const client1 = new Client({
100
+ host: '127.0.0.1',
101
+ port: 15432,
102
+ database: 'db1'
103
+ });
104
+
105
+ await client1.connect();
106
+ await client1.query('CREATE TABLE users (id INT, name TEXT)');
107
+ await client1.query("INSERT INTO users VALUES (1, 'Alice')");
108
+
109
+ // Verify db1 exists
110
+ let stats = router.getStats();
111
+ assert.equal(stats.pool.totalInstances, 1);
112
+
113
+ await client1.end();
114
+
115
+ // Connect to database 2
116
+ const client2 = new Client({
117
+ host: '127.0.0.1',
118
+ port: 15432,
119
+ database: 'db2'
120
+ });
121
+
122
+ await client2.connect();
123
+ await client2.query('CREATE TABLE posts (id INT, title TEXT)');
124
+ await client2.query("INSERT INTO posts VALUES (1, 'Hello World')");
125
+
126
+ // Verify db2 exists
127
+ stats = router.getStats();
128
+ assert.equal(stats.pool.totalInstances, 2);
129
+
130
+ await client2.end();
131
+
132
+ // Reconnect to db1 - verify data is isolated
133
+ const client1Again = new Client({
134
+ host: '127.0.0.1',
135
+ port: 15432,
136
+ database: 'db1'
137
+ });
138
+
139
+ await client1Again.connect();
140
+
141
+ // Should have users table, NOT posts table
142
+ const usersResult = await client1Again.query('SELECT * FROM users');
143
+ assert.equal(usersResult.rows.length, 1);
144
+ assert.equal(usersResult.rows[0].name, 'Alice');
145
+
146
+ // Posts table should NOT exist
147
+ try {
148
+ await client1Again.query('SELECT * FROM posts');
149
+ assert.fail('Should throw error - posts table does not exist in db1');
150
+ } catch (error) {
151
+ assert.ok(error.message.includes('does not exist'));
152
+ }
153
+
154
+ await client1Again.end();
155
+ await router.stop();
156
+ cleanup();
157
+ });
158
+
159
+ test('Multi-tenant router - instance reuse', async (t) => {
160
+ cleanup();
161
+
162
+ const router = await startMultiTenantServer({
163
+ port: 15432,
164
+ baseDir: testDataDir,
165
+ logLevel: 'info'
166
+ });
167
+
168
+ // First connection to "reuse-test"
169
+ const client1 = new Client({
170
+ host: '127.0.0.1',
171
+ port: 15432,
172
+ database: 'reuse-test'
173
+ });
174
+
175
+ await client1.connect();
176
+ await client1.query('CREATE TABLE test (value INT)');
177
+ await client1.query('INSERT INTO test VALUES (42)');
178
+ await client1.end();
179
+
180
+ // Second connection to same database (should reuse instance)
181
+ const client2 = new Client({
182
+ host: '127.0.0.1',
183
+ port: 15432,
184
+ database: 'reuse-test'
185
+ });
186
+
187
+ await client2.connect();
188
+
189
+ // Should still have the table from client1
190
+ const result = await client2.query('SELECT * FROM test');
191
+ assert.equal(result.rows.length, 1);
192
+ assert.equal(result.rows[0].value, 42);
193
+
194
+ // Still only 1 instance
195
+ const stats = router.getStats();
196
+ assert.equal(stats.pool.totalInstances, 1);
197
+
198
+ await client2.end();
199
+ await router.stop();
200
+ cleanup();
201
+ });