pgserve 2.1.3 → 2.2.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.
Files changed (228) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +31 -6
  5. package/bin/postgres-server.js +56 -0
  6. package/console/README.md +131 -0
  7. package/console/api.js +173 -0
  8. package/console/app.jsx +483 -0
  9. package/console/colors_and_type.css +227 -0
  10. package/console/components.jsx +167 -0
  11. package/console/console.css +1666 -0
  12. package/console/data.jsx +350 -0
  13. package/console/index.html +31 -0
  14. package/console/screens/databases.jsx +5 -0
  15. package/console/screens/health.jsx +5 -0
  16. package/console/screens/ingress.jsx +5 -0
  17. package/console/screens/optimizer.jsx +5 -0
  18. package/console/screens/rlm-sim.jsx +5 -0
  19. package/console/screens/rlm-trace.jsx +5 -0
  20. package/console/screens/security.jsx +5 -0
  21. package/console/screens/settings.jsx +611 -0
  22. package/console/screens/sql.jsx +5 -0
  23. package/console/screens/sync.jsx +5 -0
  24. package/console/screens/tables.jsx +5 -0
  25. package/console/tweaks-panel.jsx +425 -0
  26. package/package.json +11 -1
  27. package/src/cli-config.cjs +310 -0
  28. package/src/cli-install.cjs +98 -11
  29. package/src/cli-restart.cjs +228 -0
  30. package/src/cli-ui.cjs +580 -0
  31. package/src/cluster.js +43 -38
  32. package/src/postgres.js +141 -19
  33. package/src/settings-loader.cjs +235 -0
  34. package/src/settings-migrate.cjs +212 -0
  35. package/src/settings-pg-args.cjs +146 -0
  36. package/src/settings-schema.cjs +422 -0
  37. package/src/settings-validator.cjs +416 -0
  38. package/src/settings-writer.cjs +288 -0
  39. package/.claude/context/windows-debug.md +0 -119
  40. package/.genie/AGENTS.md +0 -15
  41. package/.genie/agents/README.md +0 -110
  42. package/.genie/agents/analyze.md +0 -176
  43. package/.genie/agents/forge.md +0 -290
  44. package/.genie/agents/garbage-cleaner.md +0 -324
  45. package/.genie/agents/garbage-collector.md +0 -596
  46. package/.genie/agents/github-issue-gc.md +0 -618
  47. package/.genie/agents/review.md +0 -380
  48. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  49. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  50. package/.genie/agents/semantic-analyzer.md +0 -101
  51. package/.genie/agents/update.md +0 -182
  52. package/.genie/agents/wish.md +0 -357
  53. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  54. package/.genie/code/AGENTS.md +0 -694
  55. package/.genie/code/agents/audit/risk.md +0 -173
  56. package/.genie/code/agents/audit/security.md +0 -189
  57. package/.genie/code/agents/audit.md +0 -145
  58. package/.genie/code/agents/challenge.md +0 -230
  59. package/.genie/code/agents/change-reviewer.md +0 -295
  60. package/.genie/code/agents/code-garbage-collector.md +0 -425
  61. package/.genie/code/agents/code-quality.md +0 -410
  62. package/.genie/code/agents/commit-suggester.md +0 -255
  63. package/.genie/code/agents/commit.md +0 -124
  64. package/.genie/code/agents/consensus.md +0 -204
  65. package/.genie/code/agents/daily-standup.md +0 -722
  66. package/.genie/code/agents/docgen.md +0 -48
  67. package/.genie/code/agents/explore.md +0 -79
  68. package/.genie/code/agents/fix.md +0 -100
  69. package/.genie/code/agents/git/commit-advisory.md +0 -219
  70. package/.genie/code/agents/git/workflows/issue.md +0 -244
  71. package/.genie/code/agents/git/workflows/pr.md +0 -179
  72. package/.genie/code/agents/git/workflows/release.md +0 -460
  73. package/.genie/code/agents/git/workflows/report.md +0 -342
  74. package/.genie/code/agents/git.md +0 -432
  75. package/.genie/code/agents/implementor.md +0 -161
  76. package/.genie/code/agents/install.md +0 -515
  77. package/.genie/code/agents/issue-creator.md +0 -344
  78. package/.genie/code/agents/polish.md +0 -116
  79. package/.genie/code/agents/qa.md +0 -653
  80. package/.genie/code/agents/refactor.md +0 -294
  81. package/.genie/code/agents/release.md +0 -1129
  82. package/.genie/code/agents/roadmap.md +0 -885
  83. package/.genie/code/agents/tests.md +0 -557
  84. package/.genie/code/agents/tracer.md +0 -50
  85. package/.genie/code/agents/update/upstream-update.md +0 -85
  86. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  87. package/.genie/code/agents/vibe.md +0 -1317
  88. package/.genie/code/spells/agent-configuration.md +0 -58
  89. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  90. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  91. package/.genie/code/spells/debug.md +0 -320
  92. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  93. package/.genie/code/spells/evidence-storage.md +0 -26
  94. package/.genie/code/spells/file-naming-rules.md +0 -35
  95. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  96. package/.genie/code/spells/genie-integration.md +0 -153
  97. package/.genie/code/spells/publishing-protocol.md +0 -61
  98. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  99. package/.genie/code/spells/tool-requirements.md +0 -20
  100. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  101. package/.genie/code/teams/tech-council/council.md +0 -328
  102. package/.genie/code/teams/tech-council/jt.md +0 -352
  103. package/.genie/code/teams/tech-council/nayr.md +0 -305
  104. package/.genie/code/teams/tech-council/oettam.md +0 -375
  105. package/.genie/neurons/README.md +0 -193
  106. package/.genie/neurons/forge.md +0 -106
  107. package/.genie/neurons/genie.md +0 -63
  108. package/.genie/neurons/review.md +0 -106
  109. package/.genie/neurons/wish.md +0 -104
  110. package/.genie/product/README.md +0 -20
  111. package/.genie/product/cli-automation.md +0 -359
  112. package/.genie/product/environment.md +0 -60
  113. package/.genie/product/mission.md +0 -60
  114. package/.genie/product/roadmap.md +0 -44
  115. package/.genie/product/tech-stack.md +0 -34
  116. package/.genie/product/templates/context-template.md +0 -218
  117. package/.genie/product/templates/qa-done-report-template.md +0 -68
  118. package/.genie/product/templates/review-report-template.md +0 -89
  119. package/.genie/product/templates/wish-template.md +0 -120
  120. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  121. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  122. package/.genie/scripts/helpers/bullet-find.js +0 -289
  123. package/.genie/scripts/helpers/bullet-id.js +0 -244
  124. package/.genie/scripts/helpers/check-secrets.js +0 -237
  125. package/.genie/scripts/helpers/count-tokens.js +0 -200
  126. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  127. package/.genie/scripts/helpers/detect-markers.js +0 -293
  128. package/.genie/scripts/helpers/detect-todos.js +0 -267
  129. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  130. package/.genie/scripts/helpers/embeddings.js +0 -344
  131. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  132. package/.genie/scripts/helpers/index.js +0 -319
  133. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  134. package/.genie/scripts/helpers/validate-links.js +0 -207
  135. package/.genie/scripts/helpers/validate-paths.js +0 -373
  136. package/.genie/spells/README.md +0 -9
  137. package/.genie/spells/ace-protocol.md +0 -118
  138. package/.genie/spells/ask-one-at-a-time.md +0 -175
  139. package/.genie/spells/backup-analyzer.md +0 -542
  140. package/.genie/spells/blocker.md +0 -12
  141. package/.genie/spells/break-things-move-fast.md +0 -56
  142. package/.genie/spells/context-candidates.md +0 -72
  143. package/.genie/spells/context-critic.md +0 -51
  144. package/.genie/spells/defer-to-expertise.md +0 -278
  145. package/.genie/spells/delegate-dont-do.md +0 -292
  146. package/.genie/spells/error-investigation-protocol.md +0 -328
  147. package/.genie/spells/evidence-based-completion.md +0 -273
  148. package/.genie/spells/experiment.md +0 -65
  149. package/.genie/spells/file-creation-protocol.md +0 -229
  150. package/.genie/spells/forge-integration.md +0 -281
  151. package/.genie/spells/forge-orchestration.md +0 -514
  152. package/.genie/spells/gather-context.md +0 -18
  153. package/.genie/spells/global-health-check.md +0 -34
  154. package/.genie/spells/global-noop-roundtrip.md +0 -25
  155. package/.genie/spells/install-genie.md +0 -1232
  156. package/.genie/spells/install.md +0 -82
  157. package/.genie/spells/investigate-before-commit.md +0 -112
  158. package/.genie/spells/know-yourself.md +0 -288
  159. package/.genie/spells/learn.md +0 -828
  160. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  161. package/.genie/spells/mcp-first.md +0 -124
  162. package/.genie/spells/multi-step-execution.md +0 -67
  163. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  164. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  165. package/.genie/spells/prompt.md +0 -746
  166. package/.genie/spells/reflect.md +0 -404
  167. package/.genie/spells/routing-decision-matrix.md +0 -368
  168. package/.genie/spells/run-in-parallel.md +0 -12
  169. package/.genie/spells/session-state-updater-example.md +0 -196
  170. package/.genie/spells/session-state-updater.md +0 -220
  171. package/.genie/spells/track-long-running-tasks.md +0 -133
  172. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  173. package/.genie/spells/upgrade-genie.md +0 -415
  174. package/.genie/spells/url-presentation-protocol.md +0 -301
  175. package/.genie/spells/wish-initiation.md +0 -158
  176. package/.genie/spells/wish-issue-linkage.md +0 -410
  177. package/.genie/spells/wish-lifecycle.md +0 -100
  178. package/.genie/state/provider-status.json +0 -3
  179. package/.genie/state/version.json +0 -16
  180. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  181. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  182. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  183. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  184. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  185. package/.gitguardian.yaml +0 -29
  186. package/.gitguardianignore +0 -16
  187. package/.github/workflows/ci.yml +0 -122
  188. package/.github/workflows/release.yml +0 -289
  189. package/.github/workflows/version.yml +0 -228
  190. package/.husky/pre-commit +0 -2
  191. package/AGENTS.md +0 -433
  192. package/CLAUDE.md +0 -1
  193. package/Makefile +0 -285
  194. package/assets/icon.ico +0 -0
  195. package/bun.lock +0 -435
  196. package/bunfig.toml +0 -28
  197. package/ecosystem.config.cjs +0 -23
  198. package/eslint.config.js +0 -63
  199. package/examples/multi-tenant-demo.js +0 -104
  200. package/install.sh +0 -123
  201. package/knip.json +0 -9
  202. package/scripts/test-bun-self-heal.sh +0 -163
  203. package/scripts/test-npx.sh +0 -60
  204. package/tests/audit.test.js +0 -189
  205. package/tests/backpressure.test.js +0 -167
  206. package/tests/benchmarks/runner.js +0 -1197
  207. package/tests/benchmarks/vector-generator.js +0 -368
  208. package/tests/cli-install.test.js +0 -322
  209. package/tests/control-db.test.js +0 -285
  210. package/tests/daemon-args.test.js +0 -86
  211. package/tests/daemon-control.test.js +0 -171
  212. package/tests/daemon-fingerprint-integration.test.js +0 -111
  213. package/tests/daemon-pr24-regression.test.js +0 -198
  214. package/tests/fingerprint.test.js +0 -263
  215. package/tests/fixtures/240-orphan-seed.sql +0 -30
  216. package/tests/multi-tenant.test.js +0 -374
  217. package/tests/orphan-cleanup.test.js +0 -390
  218. package/tests/pg-version-regex.test.js +0 -129
  219. package/tests/quick-bench.js +0 -135
  220. package/tests/router-handshake-retry.test.js +0 -119
  221. package/tests/router-handshake-watchdog.test.js +0 -110
  222. package/tests/sdk.test.js +0 -71
  223. package/tests/stale-postmaster-pid.test.js +0 -85
  224. package/tests/stress-test.js +0 -439
  225. package/tests/sync-perf-test.js +0 -150
  226. package/tests/tcp-listen.test.js +0 -368
  227. package/tests/tenancy.test.js +0 -403
  228. package/tests/wrapper-supervision.test.js +0 -107
@@ -1,1197 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * Benchmark Runner
5
- * Compares SQLite, PostgreSQL, and pgserve performance
6
- *
7
- * 100% Bun-native: Uses bun:sqlite instead of better-sqlite3
8
- */
9
-
10
- import { Database } from 'bun:sqlite';
11
- import { startMultiTenantServer } from '../../src/index.js';
12
- import { spawn } from 'child_process';
13
- import fs from 'fs';
14
- import path from 'path';
15
- import os from 'os';
16
- import pg from 'pg';
17
- import { loadEmbeddings, generateQueryVectors, formatPgVector, getEmbeddingsPath, getGroundTruth, calculateRecall } from './vector-generator.js';
18
-
19
- const { Pool } = pg;
20
- const LEGACY_PGSERVE_VERSION = '1.2.0';
21
- const LEGACY_PGSERVE_SPEC = `pgserve@${LEGACY_PGSERVE_VERSION}`;
22
- const NPX_BIN = process.platform === 'win32' ? 'npx.cmd' : 'npx';
23
-
24
- // ============================================================================
25
- // ANSI Colors and Visual Utilities (stress-test style)
26
- // ============================================================================
27
- const C = {
28
- reset: '\x1b[0m',
29
- bold: '\x1b[1m',
30
- dim: '\x1b[2m',
31
- green: '\x1b[32m',
32
- yellow: '\x1b[33m',
33
- cyan: '\x1b[36m',
34
- red: '\x1b[31m',
35
- magenta: '\x1b[35m',
36
- blue: '\x1b[34m',
37
- white: '\x1b[37m',
38
- };
39
-
40
- /**
41
- * Print benchmark banner
42
- */
43
- function banner() {
44
- console.log(`
45
- ${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
46
- ║ pgserve UNIFIED BENCHMARK SUITE ║
47
- ║ ║
48
- ║ Comparing: SQLite │ PostgreSQL │ pgserve 1.2.0 │ pgserve v2 ║
49
- ╚════════════════════════════════════════════════════════════════╝${C.reset}
50
- `);
51
- }
52
-
53
- /**
54
- * Print section header
55
- */
56
- function section(name, description) {
57
- console.log(`
58
- ${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
59
- ${C.bold}${C.cyan}▶ ${name}${C.reset}
60
- ${C.dim} ${description}${C.reset}
61
- ${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
62
- `);
63
- }
64
-
65
- /**
66
- * Progress bar
67
- */
68
- function progressBar(current, total, width = 30) {
69
- const pct = Math.min(1, Math.max(0, current / total || 0));
70
- // filled is clamped to [0, width], so (width - filled) is always non-negative
71
- const filled = Math.max(0, Math.min(width, Math.round(pct * width)));
72
- const empty = width - filled;
73
- return `[${C.green}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}] ${(pct * 100).toFixed(0)}%`;
74
- }
75
-
76
- /**
77
- * Calculate score for an engine
78
- * Higher is better - weighted combination of throughput and latency
79
- */
80
- function calculateScore(results) {
81
- if (results.skipped) return 0;
82
-
83
- // Score formula:
84
- // - Base: throughput QPS (main factor)
85
- // - Bonus: low latency (P99 < 10ms gets bonus)
86
- // - Penalty: errors
87
- const throughputScore = results.throughput || 0;
88
- const latencyBonus = results.p99 > 0 ? Math.max(0, (10 - results.p99) * 10) : 0;
89
- const errorPenalty = (results.errors || 0) * 100;
90
-
91
- return Math.round(throughputScore + latencyBonus - errorPenalty);
92
- }
93
-
94
- /**
95
- * Print final results table with scores
96
- */
97
- function printFinalResults(allResults, vectorResults, canUseRam) {
98
- console.log(`
99
- ${C.cyan}${C.bold}
100
- ╔════════════════════════════════════════════════════════════════╗
101
- ║ FINAL RESULTS ║
102
- ╚════════════════════════════════════════════════════════════════╝${C.reset}
103
- `);
104
-
105
- // Aggregate results per engine
106
- // Note: recallCount tracks only SEARCH scenarios (INSERT has 'N/A' recall)
107
- const engines = {
108
- sqlite: { name: 'SQLite', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
109
- postgres: { name: 'PostgreSQL', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0, skipped: false },
110
- pgserveV1: { name: 'pgserve 1.2.0', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
111
- pgserve: { name: 'pgserve v2', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
112
- pgserveRam: { name: 'pgserve v2 RAM', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
113
- };
114
-
115
- // Aggregate CRUD results
116
- for (const r of allResults) {
117
- for (const [key, eng] of Object.entries(engines)) {
118
- const data = r[key];
119
- if (data && !data.skipped) {
120
- eng.crudQps += data.throughput || 0;
121
- eng.p50 += data.p50 || 0;
122
- eng.p99 += data.p99 || 0;
123
- eng.errors += data.errors || 0;
124
- eng.count++;
125
- } else if (data?.skipped) {
126
- eng.skipped = true;
127
- }
128
- }
129
- }
130
-
131
- // Aggregate Vector results (with recall)
132
- // Note: INSERT scenarios have recall='N/A', only SEARCH scenarios have numeric recall
133
- for (const r of vectorResults) {
134
- for (const [key, eng] of Object.entries(engines)) {
135
- const data = r[key];
136
- if (data && !data.skipped) {
137
- eng.vecQps += data.throughput || 0;
138
- eng.vecCount++;
139
- // Only count recall for SEARCH scenarios (not INSERT which has 'N/A')
140
- const recallValue = parseFloat(data.recall);
141
- if (!isNaN(recallValue)) {
142
- eng.vecRecall += recallValue;
143
- eng.recallCount++;
144
- }
145
- }
146
- }
147
- }
148
-
149
- // Average the results
150
- for (const eng of Object.values(engines)) {
151
- if (eng.count > 0) {
152
- eng.crudQps = Math.round(eng.crudQps / eng.count);
153
- eng.p50 = (eng.p50 / eng.count).toFixed(1);
154
- eng.p99 = (eng.p99 / eng.count).toFixed(1);
155
- }
156
- if (eng.vecCount > 0) {
157
- eng.vecQps = Math.round(eng.vecQps / eng.vecCount);
158
- } else {
159
- eng.vecQps = 0;
160
- }
161
- // Average recall only from SEARCH scenarios (recallCount), not INSERT scenarios
162
- if (eng.recallCount > 0) {
163
- eng.vecRecall = (eng.vecRecall / eng.recallCount).toFixed(1);
164
- } else {
165
- eng.vecRecall = 'N/A';
166
- }
167
- eng.score = Math.round(eng.crudQps * 0.6 + eng.vecQps * 0.4 - eng.errors * 10);
168
- }
169
-
170
- // Print table header (with Recall column)
171
- const hasVec = vectorResults.length > 0;
172
- if (hasVec) {
173
- console.log(`${C.bold}Engine │ CRUD QPS │ Vec QPS │ Recall │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
174
- console.log(`${'─'.repeat(90)}`);
175
- } else {
176
- console.log(`${C.bold}Engine │ CRUD QPS │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
177
- console.log(`${'─'.repeat(70)}`);
178
- }
179
-
180
- // Print each engine row
181
- const engineOrder = ['sqlite', 'postgres', 'pgserveV1', 'pgserve'];
182
- if (canUseRam) engineOrder.push('pgserveRam');
183
-
184
- let maxScore = 0;
185
- let winner = '';
186
-
187
- for (const key of engineOrder) {
188
- const eng = engines[key];
189
- if (eng.skipped) {
190
- if (hasVec) {
191
- console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
192
- } else {
193
- console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
194
- }
195
- continue;
196
- }
197
-
198
- const color = eng.errors > 0 ? C.yellow : C.green;
199
- const scoreColor = eng.score > maxScore ? C.magenta + C.bold : color;
200
-
201
- if (eng.score > maxScore) {
202
- maxScore = eng.score;
203
- winner = eng.name;
204
- }
205
-
206
- if (hasVec) {
207
- // vecRecall is 'N/A' for INSERT-only scenarios, or a numeric string like '100.0' for SEARCH
208
- const recallStr = eng.vecRecall !== 'N/A' ? `${eng.vecRecall}%` : 'N/A';
209
- const vecQpsStr = eng.vecCount > 0 ? eng.vecQps.toLocaleString() : 'N/A';
210
- console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${vecQpsStr.padStart(7)} │ ${recallStr.padStart(6)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
211
- } else {
212
- console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
213
- }
214
- }
215
-
216
- console.log(`${'─'.repeat(hasVec ? 90 : 70)}`);
217
-
218
- // Winner announcement
219
- console.log(`
220
- ${C.magenta}${C.bold}╔═══════════════════════════════════════════════════╗
221
- ║ 🏆 WINNER: ${winner.padEnd(20)} SCORE: ${String(maxScore).padStart(7)} ║
222
- ╚═══════════════════════════════════════════════════╝${C.reset}
223
- `);
224
- }
225
-
226
- // Global error handlers (suppress expected PostgreSQL WASM ExitStatus errors)
227
- process.on('unhandledRejection', (reason, promise) => {
228
- if (reason && reason.name === 'ExitStatus') return;
229
- console.error('Unhandled Promise Rejection:', reason);
230
- });
231
-
232
- process.on('uncaughtException', (error) => {
233
- if (error && error.name === 'ExitStatus') return;
234
- console.error('Uncaught Exception:', error);
235
- process.exit(1);
236
- });
237
-
238
- const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
239
-
240
- // PostgreSQL Server configuration (Docker with tmpfs for fair RAM-to-RAM comparison)
241
- const POSTGRES_CONFIG = {
242
- host: 'localhost',
243
- port: 5433, // pgvector/pgvector:pg17 Docker container
244
- user: 'postgres',
245
- password: 'postgres',
246
- database: 'postgres'
247
- };
248
-
249
- /**
250
- * Benchmark scenario configuration
251
- */
252
- const scenarios = [
253
- {
254
- name: 'Concurrent Writes (10 agents)',
255
- description: 'Simulates 10 concurrent agents writing simultaneously',
256
- operations: [
257
- { type: 'INSERT', count: 100, concurrent: 10 }
258
- ]
259
- },
260
- {
261
- name: 'Mixed Workload (messages)',
262
- description: 'Simulates typical API message operations',
263
- operations: [
264
- { type: 'INSERT', count: 500 },
265
- { type: 'SELECT', count: 2000 },
266
- { type: 'UPDATE', count: 250 }
267
- ]
268
- },
269
- {
270
- name: 'Write Lock Contention',
271
- description: 'Stress test for lock handling with 50 concurrent writers',
272
- operations: [
273
- { type: 'INSERT', count: 100, concurrent: 50 }
274
- ]
275
- }
276
- ];
277
-
278
- /**
279
- * Vector benchmark scenarios (pgvector)
280
- * Requires: --include-vector flag
281
- */
282
- /**
283
- * Vector benchmark scenarios
284
- * Following industry-standard methodology (ANN-Benchmarks, Qdrant, VectorDBBench):
285
- * - Measure Recall@k alongside QPS
286
- * - Compare approximate results to brute-force ground truth
287
- * - Report both metrics together (can't compare QPS without knowing recall)
288
- */
289
- const vectorScenarios = [
290
- {
291
- name: 'Vector INSERT (1000 vectors)',
292
- description: 'Bulk insert performance - where RAM mode shows benefits',
293
- type: 'INSERT',
294
- dimension: 1536,
295
- insertCount: 1000, // Insert 1000 vectors to measure write speed
296
- },
297
- {
298
- name: 'k-NN Search (k=10)',
299
- description: 'Recall@10 and QPS on 10k vectors, 100 queries',
300
- type: 'SEARCH',
301
- dimension: 1536,
302
- corpusSize: 10000,
303
- queryCount: 100,
304
- k: 10,
305
- warmupQueries: 20 // Warm-up before measuring
306
- },
307
- {
308
- name: 'k-NN Search (k=100)',
309
- description: 'Recall@100 and QPS on 10k vectors - harder recall target',
310
- type: 'SEARCH',
311
- dimension: 1536,
312
- corpusSize: 10000,
313
- queryCount: 100,
314
- k: 100,
315
- warmupQueries: 20
316
- }
317
- ];
318
-
319
- /**
320
- * Performance metrics
321
- */
322
- class Metrics {
323
- constructor() {
324
- this.latencies = [];
325
- this.errors = 0;
326
- this.lockTimeouts = 0;
327
- this.startTime = 0;
328
- this.endTime = 0;
329
- }
330
-
331
- start() {
332
- this.startTime = Date.now();
333
- }
334
-
335
- end() {
336
- this.endTime = Date.now();
337
- }
338
-
339
- addLatency(ms) {
340
- this.latencies.push(ms);
341
- }
342
-
343
- addError(error) {
344
- this.errors++;
345
- if (error.message && error.message.includes('SQLITE_BUSY')) {
346
- this.lockTimeouts++;
347
- }
348
- }
349
-
350
- getThroughput() {
351
- const durationMs = this.endTime - this.startTime;
352
- const durationS = durationMs / 1000;
353
- return Math.round(this.latencies.length / durationS);
354
- }
355
-
356
- getPercentile(p) {
357
- if (this.latencies.length === 0) return 0;
358
- const sorted = [...this.latencies].sort((a, b) => a - b);
359
- const index = Math.ceil((sorted.length * p) / 100) - 1;
360
- return sorted[Math.max(0, index)];
361
- }
362
-
363
- getReport() {
364
- return {
365
- throughput: this.getThroughput(),
366
- p50: this.getPercentile(50),
367
- p99: this.getPercentile(99),
368
- errors: this.errors,
369
- lockTimeouts: this.lockTimeouts,
370
- totalOps: this.latencies.length
371
- };
372
- }
373
- }
374
-
375
- function skippedCrud(reason, errors = 1) {
376
- return { throughput: 0, p50: 0, p99: 0, errors, lockTimeouts: 0, totalOps: 0, skipped: true, reason };
377
- }
378
-
379
- function skippedVector(reason, errors = 1) {
380
- return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors, totalOps: 0, skipped: true, reason };
381
- }
382
-
383
- async function openPgPool({ port, database, max = 20, timeoutMs = 30_000 }) {
384
- const pool = new Pool({
385
- host: '127.0.0.1',
386
- port,
387
- database,
388
- user: 'postgres',
389
- password: 'postgres',
390
- max,
391
- connectionTimeoutMillis: 1000
392
- });
393
-
394
- const deadline = Date.now() + timeoutMs;
395
- let lastError;
396
- while (Date.now() < deadline) {
397
- try {
398
- await pool.query('SELECT 1');
399
- return pool;
400
- } catch (error) {
401
- lastError = error;
402
- await new Promise(resolve => setTimeout(resolve, 500));
403
- }
404
- }
405
-
406
- await pool.end().catch(() => {});
407
- throw lastError || new Error(`PostgreSQL did not become ready on port ${port}`);
408
- }
409
-
410
- async function runCrudScenarioOnPool(pool, scenario) {
411
- await pool.query(`
412
- DROP TABLE IF EXISTS bench_messages;
413
- CREATE TABLE bench_messages (
414
- id SERIAL PRIMARY KEY,
415
- content TEXT,
416
- timestamp BIGINT
417
- )
418
- `);
419
-
420
- const metrics = new Metrics();
421
- metrics.start();
422
-
423
- for (const op of scenario.operations) {
424
- if (op.type === 'INSERT') {
425
- const concurrent = op.concurrent || 1;
426
- const perThread = Math.floor(op.count / concurrent);
427
-
428
- const promises = [];
429
- for (let i = 0; i < concurrent; i++) {
430
- promises.push(
431
- (async () => {
432
- for (let j = 0; j < perThread; j++) {
433
- const start = Date.now();
434
- try {
435
- await pool.query(
436
- 'INSERT INTO bench_messages (content, timestamp) VALUES ($1, $2)',
437
- [`Message ${i}-${j}`, Date.now()]
438
- );
439
- metrics.addLatency(Date.now() - start);
440
- } catch (error) {
441
- metrics.addError(error);
442
- }
443
- }
444
- })()
445
- );
446
- }
447
-
448
- await Promise.all(promises);
449
- } else if (op.type === 'SELECT') {
450
- for (let i = 0; i < op.count; i++) {
451
- const start = Date.now();
452
- try {
453
- await pool.query('SELECT * FROM bench_messages LIMIT 10');
454
- metrics.addLatency(Date.now() - start);
455
- } catch (error) {
456
- metrics.addError(error);
457
- }
458
- }
459
- } else if (op.type === 'UPDATE') {
460
- for (let i = 0; i < op.count; i++) {
461
- const start = Date.now();
462
- try {
463
- await pool.query(
464
- 'UPDATE bench_messages SET content = $1 WHERE id = $2',
465
- [`Updated ${i}`, (i % 100) + 1]
466
- );
467
- metrics.addLatency(Date.now() - start);
468
- } catch (error) {
469
- metrics.addError(error);
470
- }
471
- }
472
- }
473
- }
474
-
475
- metrics.end();
476
- await pool.query('DROP TABLE IF EXISTS bench_messages');
477
- return metrics.getReport();
478
- }
479
-
480
- async function runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth) {
481
- await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
482
-
483
- await pool.query(`
484
- DROP TABLE IF EXISTS embeddings;
485
- CREATE TABLE embeddings (
486
- id INTEGER PRIMARY KEY,
487
- vector vector(${scenario.dimension})
488
- )
489
- `);
490
-
491
- if (scenario.type === 'INSERT') {
492
- console.log(` Inserting ${scenario.insertCount} vectors (measured)...`);
493
- const latencies = [];
494
-
495
- for (let i = 0; i < scenario.insertCount; i++) {
496
- const vec = formatPgVector(embeddings.vectors[i]);
497
- const start = performance.now();
498
- await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
499
- latencies.push(performance.now() - start);
500
- }
501
-
502
- const totalTime = latencies.reduce((a, b) => a + b, 0);
503
- const qps = Math.round((scenario.insertCount / totalTime) * 1000);
504
- latencies.sort((a, b) => a - b);
505
- const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
506
- const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
507
-
508
- await pool.query('DROP TABLE IF EXISTS embeddings');
509
-
510
- return {
511
- throughput: qps,
512
- recall: 'N/A',
513
- p50: parseFloat(p50),
514
- p99: parseFloat(p99),
515
- errors: 0,
516
- totalOps: scenario.insertCount,
517
- skipped: false
518
- };
519
- }
520
-
521
- console.log(' Inserting vectors...');
522
- for (let i = 0; i < embeddings.vectors.length; i++) {
523
- const vec = formatPgVector(embeddings.vectors[i]);
524
- await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
525
- }
526
-
527
- console.log(' Warming up...');
528
- for (let i = 0; i < scenario.warmupQueries; i++) {
529
- const queryVec = formatPgVector(queryVectors[i % queryVectors.length]);
530
- await pool.query('SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2', [queryVec, scenario.k]);
531
- }
532
-
533
- console.log(' Running measured queries...');
534
- const latencies = [];
535
- const approximateResults = [];
536
-
537
- for (let i = 0; i < scenario.queryCount; i++) {
538
- const queryVec = formatPgVector(queryVectors[i]);
539
- const start = performance.now();
540
- const result = await pool.query(
541
- 'SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2',
542
- [queryVec, scenario.k]
543
- );
544
- latencies.push(performance.now() - start);
545
- approximateResults.push(result.rows.map(r => r.id));
546
- }
547
-
548
- const { recall } = calculateRecall(approximateResults, groundTruth, scenario.k);
549
- const totalTime = latencies.reduce((a, b) => a + b, 0);
550
- const qps = Math.round((scenario.queryCount / totalTime) * 1000);
551
- latencies.sort((a, b) => a - b);
552
- const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
553
- const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
554
-
555
- await pool.query('DROP TABLE IF EXISTS embeddings');
556
-
557
- return {
558
- throughput: qps,
559
- recall: (recall * 100).toFixed(1),
560
- p50: parseFloat(p50),
561
- p99: parseFloat(p99),
562
- errors: 0,
563
- totalOps: scenario.queryCount,
564
- skipped: false
565
- };
566
- }
567
-
568
- async function startLegacyPgserve({ port, enablePgvector = false }) {
569
- const dataDir = path.join(RESULTS_DIR, `pgserve-${LEGACY_PGSERVE_VERSION}-port-${port}`);
570
- fs.rmSync(dataDir, { recursive: true, force: true });
571
-
572
- const args = [
573
- '-y',
574
- LEGACY_PGSERVE_SPEC,
575
- '--port',
576
- String(port),
577
- '--host',
578
- '127.0.0.1',
579
- '--data',
580
- dataDir,
581
- '--log',
582
- 'error',
583
- '--no-stats',
584
- '--no-cluster',
585
- ];
586
- if (enablePgvector) args.push('--pgvector');
587
-
588
- const tail = [];
589
- const child = spawn(NPX_BIN, args, {
590
- stdio: ['ignore', 'pipe', 'pipe'],
591
- env: { ...process.env, NO_COLOR: '1' },
592
- });
593
-
594
- const append = (chunk) => {
595
- tail.push(String(chunk));
596
- while (tail.join('').length > 4000) tail.shift();
597
- };
598
- child.stdout.on('data', append);
599
- child.stderr.on('data', append);
600
-
601
- let exited = false;
602
- child.once('exit', () => {
603
- exited = true;
604
- });
605
-
606
- try {
607
- const pool = await openPgPool({ port, database: 'bench_test', timeoutMs: 60_000 });
608
- await pool.end();
609
- } catch (error) {
610
- await stopChildProcess(child);
611
- const output = tail.join('').trim();
612
- const detail = output ? `; output: ${output}` : '';
613
- throw new Error(`${LEGACY_PGSERVE_SPEC} failed to become ready: ${error.message}${detail}`);
614
- }
615
-
616
- return {
617
- async stop() {
618
- if (!exited) await stopChildProcess(child);
619
- fs.rmSync(dataDir, { recursive: true, force: true });
620
- },
621
- };
622
- }
623
-
624
- async function stopChildProcess(child) {
625
- if (child.exitCode !== null || child.signalCode !== null) return;
626
-
627
- child.kill('SIGTERM');
628
- const exited = await new Promise((resolve) => {
629
- const timer = setTimeout(() => resolve(false), 3000);
630
- child.once('exit', () => {
631
- clearTimeout(timer);
632
- resolve(true);
633
- });
634
- });
635
-
636
- if (!exited) {
637
- child.kill('SIGKILL');
638
- await new Promise((resolve) => child.once('exit', resolve));
639
- }
640
- }
641
-
642
- /**
643
- * SQLite Benchmark
644
- */
645
- async function benchmarkSQLite(scenario) {
646
- console.log(' 🔸 Running SQLite benchmark...');
647
-
648
- const dbPath = path.join(RESULTS_DIR, 'sqlite-bench.db');
649
- if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
650
-
651
- const db = new Database(dbPath);
652
-
653
- // Setup schema
654
- db.exec(`
655
- CREATE TABLE IF NOT EXISTS messages (
656
- id INTEGER PRIMARY KEY AUTOINCREMENT,
657
- content TEXT,
658
- timestamp INTEGER
659
- )
660
- `);
661
-
662
- const metrics = new Metrics();
663
- metrics.start();
664
-
665
- // Run operations
666
- for (const op of scenario.operations) {
667
- if (op.type === 'INSERT') {
668
- const concurrent = op.concurrent || 1;
669
- const perThread = Math.floor(op.count / concurrent);
670
-
671
- for (let i = 0; i < concurrent; i++) {
672
- for (let j = 0; j < perThread; j++) {
673
- const start = Date.now();
674
- try {
675
- db.prepare('INSERT INTO messages (content, timestamp) VALUES (?, ?)').run(
676
- `Message ${i}-${j}`,
677
- Date.now()
678
- );
679
- metrics.addLatency(Date.now() - start);
680
- } catch (error) {
681
- metrics.addError(error);
682
- }
683
- }
684
- }
685
- } else if (op.type === 'SELECT') {
686
- for (let i = 0; i < op.count; i++) {
687
- const start = Date.now();
688
- try {
689
- db.prepare('SELECT * FROM messages LIMIT 10').all();
690
- metrics.addLatency(Date.now() - start);
691
- } catch (error) {
692
- metrics.addError(error);
693
- }
694
- }
695
- } else if (op.type === 'UPDATE') {
696
- for (let i = 0; i < op.count; i++) {
697
- const start = Date.now();
698
- try {
699
- db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(
700
- `Updated ${i}`,
701
- (i % 100) + 1
702
- );
703
- metrics.addLatency(Date.now() - start);
704
- } catch (error) {
705
- metrics.addError(error);
706
- }
707
- }
708
- }
709
- }
710
-
711
- metrics.end();
712
- db.close();
713
-
714
- return metrics.getReport();
715
- }
716
-
717
- /**
718
- * PostgreSQL Server Benchmark (remote real PostgreSQL)
719
- */
720
- async function benchmarkPostgreSQL(scenario) {
721
- console.log(' 🔷 Running PostgreSQL Server benchmark...');
722
-
723
- let pool;
724
- try {
725
- pool = await openPgPool({ ...POSTGRES_CONFIG });
726
- return await runCrudScenarioOnPool(pool, scenario);
727
- } catch (error) {
728
- console.error(' PostgreSQL benchmark skipped:', error.message);
729
- return skippedCrud(error.message, 0);
730
- } finally {
731
- await pool?.end().catch(() => {});
732
- }
733
- }
734
-
735
- /**
736
- * pgserve 1.2.0 Benchmark (published npm package)
737
- */
738
- async function benchmarkPgserveV1(scenario) {
739
- console.log(` 🧭 Running ${LEGACY_PGSERVE_SPEC} benchmark...`);
740
-
741
- let legacy;
742
- let pool;
743
- try {
744
- legacy = await startLegacyPgserve({ port: 18431 });
745
- pool = await openPgPool({ port: 18431, database: 'bench_test' });
746
- return await runCrudScenarioOnPool(pool, scenario);
747
- } catch (error) {
748
- console.error(` ${LEGACY_PGSERVE_SPEC} benchmark skipped:`, error.message);
749
- return skippedCrud(error.message);
750
- } finally {
751
- await pool?.end().catch(() => {});
752
- await legacy?.stop().catch(() => {});
753
- }
754
- }
755
-
756
- /**
757
- * pgserve Benchmark (our solution - embedded PostgreSQL with TRUE concurrency)
758
- * @param {Object} scenario - Benchmark scenario
759
- * @param {boolean} useRam - Use /dev/shm RAM storage (Linux only)
760
- */
761
- async function benchmarkPgserve(scenario, useRam = false) {
762
- const mode = useRam ? 'RAM' : 'disk';
763
- console.log(` 🚀 Running pgserve v2 (${mode}) benchmark...`);
764
-
765
- let server;
766
- let pool;
767
- try {
768
- // Start pgserve in memory mode (optionally with RAM storage)
769
- const port = useRam ? 18433 : 18432;
770
- server = await startMultiTenantServer({
771
- port,
772
- logLevel: 'error',
773
- useRam
774
- });
775
-
776
- pool = await openPgPool({ port, database: 'bench_test' });
777
- return await runCrudScenarioOnPool(pool, scenario);
778
- } catch (error) {
779
- console.error(` pgserve v2 (${mode}) benchmark failed:`, error.message);
780
- return skippedCrud(error.message);
781
- } finally {
782
- await pool?.end().catch(() => {});
783
- await server?.stop().catch(() => {});
784
- }
785
- }
786
-
787
- // ============================================================================
788
- // VECTOR BENCHMARKS (pgvector)
789
- // ============================================================================
790
-
791
- /**
792
- * PostgreSQL Server Vector Benchmark
793
- * Supports both INSERT and SEARCH scenarios
794
- */
795
- async function benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth) {
796
- console.log(' 🔷 Running PostgreSQL vector benchmark...');
797
-
798
- let pool;
799
- try {
800
- pool = await openPgPool({ ...POSTGRES_CONFIG });
801
- return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
802
- } catch (error) {
803
- console.error(' PostgreSQL vector benchmark skipped:', error.message);
804
- return skippedVector(error.message, 0);
805
- } finally {
806
- await pool?.end().catch(() => {});
807
- }
808
- }
809
-
810
- /**
811
- * pgserve 1.2.0 Vector Benchmark (published npm package)
812
- */
813
- async function benchmarkPgserveV1Vector(scenario, embeddings, queryVectors, groundTruth) {
814
- console.log(` 🧭 Running ${LEGACY_PGSERVE_SPEC} vector benchmark...`);
815
-
816
- let legacy;
817
- let pool;
818
- try {
819
- legacy = await startLegacyPgserve({ port: 18434, enablePgvector: true });
820
- pool = await openPgPool({ port: 18434, database: 'vector_bench' });
821
- return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
822
- } catch (error) {
823
- console.error(` ${LEGACY_PGSERVE_SPEC} vector benchmark skipped:`, error.message);
824
- return skippedVector(error.message);
825
- } finally {
826
- await pool?.end().catch(() => {});
827
- await legacy?.stop().catch(() => {});
828
- }
829
- }
830
-
831
- /**
832
- * pgserve Vector Benchmark
833
- * Supports both INSERT and SEARCH scenarios
834
- */
835
- async function benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, useRam = false) {
836
- const mode = useRam ? 'RAM' : 'disk';
837
- console.log(` 🚀 Running pgserve v2 (${mode}) vector benchmark...`);
838
-
839
- let server;
840
- let pool;
841
- try {
842
- // Start pgserve (use different ports for vector benchmarks to avoid conflicts)
843
- const port = useRam ? 18436 : 18435;
844
- server = await startMultiTenantServer({
845
- port,
846
- logLevel: 'error',
847
- useRam,
848
- enablePgvector: true
849
- });
850
-
851
- pool = await openPgPool({ port, database: 'vector_bench' });
852
- return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
853
- } catch (error) {
854
- console.error(` pgserve v2 (${mode}) vector benchmark failed:`, error.message);
855
- return skippedVector(error.message, 0);
856
- } finally {
857
- await pool?.end().catch(() => {});
858
- await server?.stop().catch(() => {});
859
- }
860
- }
861
-
862
- /**
863
- * Generate comparison report
864
- */
865
- function generateReport(results, vectorResults = []) {
866
- const report = {
867
- timestamp: new Date().toISOString(),
868
- scenarios: results,
869
- vectorScenarios: vectorResults
870
- };
871
-
872
- // Save JSON
873
- const jsonPath = path.join(RESULTS_DIR, 'benchmark-results.json');
874
- fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
875
-
876
- // Generate markdown
877
- let md = '# Benchmark Results\n\n';
878
- md += `**Date:** ${new Date().toLocaleString()}\n\n`;
879
- md += '## Quick Start\n\n';
880
- md += '```bash\n';
881
- md += '# Zero install - just run!\n';
882
- md += 'npx pgserve\n\n';
883
- md += '# Connect from any PostgreSQL client\n';
884
- md += 'psql postgresql://localhost:5432/mydb\n';
885
- md += '```\n\n';
886
-
887
- const metricValue = (data, key) => {
888
- if (!data || data.skipped) return 'N/A';
889
- const value = data[key];
890
- return value === undefined || value === null ? 'N/A' : value;
891
- };
892
- const metricNumber = (data, key) => {
893
- if (!data || data.skipped) return null;
894
- const value = Number.parseFloat(data[key]);
895
- return Number.isFinite(value) ? value : null;
896
- };
897
- const winnerName = (rows, key, direction) => {
898
- let winner = null;
899
- for (const row of rows) {
900
- const value = metricNumber(row.data, key);
901
- if (value === null) continue;
902
- if (!winner || (direction === 'max' ? value > winner.value : value < winner.value)) {
903
- winner = { name: row.name, value };
904
- }
905
- }
906
- return winner?.name || 'N/A';
907
- };
908
- const pctDelta = (current, baseline) => {
909
- if (!current || !baseline || current.skipped || baseline.skipped || baseline.throughput <= 0) return null;
910
- return ((current.throughput / baseline.throughput - 1) * 100).toFixed(1);
911
- };
912
- const renderMetricTable = (rows, metrics) => {
913
- let table = `| Metric | ${rows.map(r => r.name).join(' | ')} | Winner |\n`;
914
- table += `| --- | ${rows.map(() => '---:').join(' | ')} | --- |\n`;
915
- for (const metric of metrics) {
916
- table += `| ${metric.label} | ${rows.map(r => metric.format(metricValue(r.data, metric.key))).join(' | ')} | ${winnerName(rows, metric.key, metric.direction)} |\n`;
917
- }
918
- return `${table}\n`;
919
- };
920
- const plain = (value) => String(value);
921
- const percent = (value) => value === 'N/A' ? value : `${value}%`;
922
-
923
- for (const scenario of results) {
924
- md += `## ${scenario.name}\n\n`;
925
- md += `${scenario.description}\n\n`;
926
-
927
- const rows = [
928
- { name: 'SQLite', data: scenario.sqlite },
929
- { name: 'PostgreSQL', data: scenario.postgres },
930
- { name: 'pgserve 1.2.0', data: scenario.pgserveV1 },
931
- { name: 'pgserve v2', data: scenario.pgserve },
932
- ];
933
- if (scenario.pgserveRam && !scenario.pgserveRam.skipped) {
934
- rows.push({ name: 'pgserve v2 RAM', data: scenario.pgserveRam });
935
- }
936
-
937
- md += renderMetricTable(rows, [
938
- { label: 'Throughput (qps)', key: 'throughput', direction: 'max', format: plain },
939
- { label: 'P50 latency (ms)', key: 'p50', direction: 'min', format: plain },
940
- { label: 'P99 latency (ms)', key: 'p99', direction: 'min', format: plain },
941
- { label: 'Errors', key: 'errors', direction: 'min', format: plain },
942
- ]);
943
-
944
- const delta = pctDelta(scenario.pgserve, scenario.pgserveV1);
945
- if (delta !== null) {
946
- md += `**pgserve v2 vs 1.2.0:** ${delta}% throughput delta.\n\n`;
947
- }
948
- }
949
-
950
- // Vector benchmark results (with Recall@k)
951
- if (vectorResults && vectorResults.length > 0) {
952
- md += '---\n\n';
953
- md += '## Vector Benchmarks (pgvector) - Recall@k Methodology\n\n';
954
- md += '*Following industry-standard ANN-Benchmarks methodology: comparing approximate results to brute-force ground truth.*\n\n';
955
-
956
- for (const scenario of vectorResults) {
957
- md += `### ${scenario.name}\n\n`;
958
- md += `${scenario.description}\n\n`;
959
-
960
- const rows = [
961
- { name: 'PostgreSQL', data: scenario.postgres },
962
- { name: 'pgserve 1.2.0', data: scenario.pgserveV1 },
963
- { name: 'pgserve v2', data: scenario.pgserve },
964
- ];
965
- if (scenario.pgserveRam && !scenario.pgserveRam.skipped) {
966
- rows.push({ name: 'pgserve v2 RAM', data: scenario.pgserveRam });
967
- }
968
-
969
- md += renderMetricTable(rows, [
970
- { label: `Recall@${scenario.k || 10}`, key: 'recall', direction: 'max', format: percent },
971
- { label: 'Throughput (qps)', key: 'throughput', direction: 'max', format: plain },
972
- { label: 'P50 latency (ms)', key: 'p50', direction: 'min', format: plain },
973
- { label: 'P99 latency (ms)', key: 'p99', direction: 'min', format: plain },
974
- { label: 'Errors', key: 'errors', direction: 'min', format: plain },
975
- ]);
976
-
977
- // Find winner among non-skipped (considering both recall and throughput)
978
- const candidates = {};
979
- if (!scenario.postgres.skipped) candidates.postgres = { recall: parseFloat(scenario.postgres.recall), qps: scenario.postgres.throughput };
980
- if (!scenario.pgserveV1.skipped) candidates.pgserveV1 = { recall: parseFloat(scenario.pgserveV1.recall), qps: scenario.pgserveV1.throughput };
981
- if (!scenario.pgserve.skipped) candidates.pgserve = { recall: parseFloat(scenario.pgserve.recall), qps: scenario.pgserve.throughput };
982
- if (scenario.pgserveRam && !scenario.pgserveRam.skipped) candidates.pgserveRam = { recall: parseFloat(scenario.pgserveRam.recall), qps: scenario.pgserveRam.throughput };
983
-
984
- if (Object.keys(candidates).length > 0) {
985
- const nameMap = { postgres: 'PostgreSQL', pgserveV1: 'pgserve 1.2.0', pgserve: 'pgserve v2', pgserveRam: 'pgserve v2 RAM' };
986
- // Winner = highest QPS among those with 100% recall, otherwise highest recall
987
- const perfect = Object.entries(candidates).filter(([, v]) => v.recall === 100);
988
- let winnerKey;
989
- if (perfect.length > 0) {
990
- winnerKey = perfect.reduce((a, b) => a[1].qps > b[1].qps ? a : b)[0];
991
- md += `**${nameMap[winnerKey]} wins** (100% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
992
- } else {
993
- winnerKey = Object.entries(candidates).reduce((a, b) => a[1].recall > b[1].recall ? a : b)[0];
994
- md += `**${nameMap[winnerKey]} wins** (${candidates[winnerKey].recall}% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
995
- }
996
- }
997
-
998
- const delta = pctDelta(scenario.pgserve, scenario.pgserveV1);
999
- if (delta !== null) {
1000
- md += `**pgserve v2 vs 1.2.0:** ${delta}% throughput delta.\n\n`;
1001
- }
1002
- }
1003
- }
1004
-
1005
- md += '---\n\n';
1006
- md += '## Why pgserve?\n\n';
1007
- md += '- **TRUE Concurrency**: Native PostgreSQL process forking\n';
1008
- md += '- **RAM Mode**: `--ram` flag for /dev/shm storage (Linux)\n';
1009
- md += '- **Zero Config**: Just run `npx pgserve`\n';
1010
- md += '- **Auto-Provision**: Databases created on first connection\n';
1011
- md += '- **PostgreSQL 17.7**: Latest stable, native binaries\n';
1012
-
1013
- const mdPath = path.join(RESULTS_DIR, 'benchmark-results.md');
1014
- fs.writeFileSync(mdPath, md);
1015
-
1016
- console.log(`\nResults saved to:`);
1017
- console.log(` JSON: ${jsonPath}`);
1018
- console.log(` Markdown: ${mdPath}\n`);
1019
-
1020
- return report;
1021
- }
1022
-
1023
- /**
1024
- * Main runner
1025
- */
1026
- async function main() {
1027
- // Parse CLI args
1028
- const args = process.argv.slice(2);
1029
- const includeVector = args.includes('--include-vector') || args.includes('--vector');
1030
- const vectorOnly = args.includes('--vector-only');
1031
-
1032
- if (args.includes('--help') || args.includes('-h')) {
1033
- console.log(`
1034
- pgserve Benchmark Suite
1035
-
1036
- Usage:
1037
- bun tests/benchmarks/runner.js [options]
1038
-
1039
- Options:
1040
- --include-vector Include vector (pgvector) benchmarks
1041
- --vector-only Run only vector benchmarks
1042
- --help, -h Show this help
1043
-
1044
- Vector benchmarks require:
1045
- - PostgreSQL: Built-in pgvector support
1046
- - PostgreSQL: Docker image pgvector/pgvector:pg17
1047
- - pgserve 1.2.0 and v2: --pgvector support
1048
- `);
1049
- process.exit(0);
1050
- }
1051
-
1052
- // Print banner
1053
- banner();
1054
- if (includeVector || vectorOnly) {
1055
- console.log(`${C.dim} + Vector benchmarks (pgvector) enabled${C.reset}\n`);
1056
- }
1057
-
1058
- // Ensure results directory exists
1059
- if (!fs.existsSync(RESULTS_DIR)) {
1060
- fs.mkdirSync(RESULTS_DIR, { recursive: true });
1061
- }
1062
-
1063
- const results = [];
1064
- const vectorResults = [];
1065
-
1066
- // Check if RAM mode is available (Linux only with /dev/shm)
1067
- const canUseRam = os.platform() === 'linux' && fs.existsSync('/dev/shm');
1068
- if (canUseRam) {
1069
- console.log(`${C.green}💾 RAM mode available (/dev/shm detected)${C.reset}\n`);
1070
- } else {
1071
- console.log(`${C.yellow}⚠️ RAM mode not available (Linux /dev/shm required)${C.reset}\n`);
1072
- }
1073
-
1074
- // Run CRUD benchmarks (unless --vector-only)
1075
- if (!vectorOnly) {
1076
- for (const scenario of scenarios) {
1077
- section(scenario.name, scenario.description);
1078
-
1079
- const sqlite = await benchmarkSQLite(scenario);
1080
- const postgres = await benchmarkPostgreSQL(scenario);
1081
- const pgserveV1 = await benchmarkPgserveV1(scenario);
1082
- const pgserve = await benchmarkPgserve(scenario, false); // disk mode
1083
- const pgserveRam = canUseRam
1084
- ? await benchmarkPgserve(scenario, true) // RAM mode
1085
- : { throughput: 0, p50: 0, p99: 0, errors: 0, lockTimeouts: 0, totalOps: 0, skipped: true };
1086
-
1087
- results.push({
1088
- name: scenario.name,
1089
- description: scenario.description,
1090
- sqlite,
1091
- postgres,
1092
- pgserveV1,
1093
- pgserve,
1094
- pgserveRam
1095
- });
1096
-
1097
- // Scenario results summary
1098
- console.log(`\n ${C.bold}Results:${C.reset}`);
1099
- console.log(` ${C.dim}──────────────────────────────────────────${C.reset}`);
1100
- console.log(` ${C.yellow}🔸${C.reset} SQLite: ${C.bold}${sqlite.throughput}${C.reset} qps, P50=${sqlite.p50}ms, P99=${sqlite.p99}ms`);
1101
- console.log(` ${C.cyan}🔷${C.reset} PostgreSQL: ${postgres.skipped ? `${C.dim}SKIPPED${C.reset}` : `${C.bold}${postgres.throughput}${C.reset} qps, P50=${postgres.p50}ms, P99=${postgres.p99}ms`}`);
1102
- console.log(` ${C.blue}🧭${C.reset} pgserve 1.2.0: ${pgserveV1.skipped ? `${C.dim}SKIPPED${C.reset}` : `${C.bold}${pgserveV1.throughput}${C.reset} qps, P50=${pgserveV1.p50}ms, P99=${pgserveV1.p99}ms`}`);
1103
- console.log(` ${C.green}🚀${C.reset} pgserve v2: ${C.bold}${pgserve.throughput}${C.reset} qps, P50=${pgserve.p50}ms, P99=${pgserve.p99}ms`);
1104
- if (canUseRam) {
1105
- console.log(` ${C.magenta}⚡${C.reset} pgserve v2 RAM: ${C.bold}${pgserveRam.throughput}${C.reset} qps, P50=${pgserveRam.p50}ms, P99=${pgserveRam.p99}ms`);
1106
- }
1107
- }
1108
- }
1109
-
1110
- // Run vector benchmarks (if --include-vector or --vector-only)
1111
- if (includeVector || vectorOnly) {
1112
- console.log(`\n${C.cyan}${C.bold}
1113
- ╔════════════════════════════════════════════════════════════════╗
1114
- ║ Vector Benchmarks (pgvector) - Recall@k Methodology ║
1115
- ╚════════════════════════════════════════════════════════════════╝${C.reset}\n`);
1116
-
1117
- // Load or generate embeddings
1118
- console.log(`${C.dim}📦 Loading embeddings...${C.reset}`);
1119
- const dimension = 1536;
1120
- const corpusSize = 10000; // 10k vectors (~60MB) to exceed buffer cache and show RAM vs disk difference
1121
-
1122
- // Ensure embeddings file exists
1123
- getEmbeddingsPath(corpusSize, dimension);
1124
- const embeddings = loadEmbeddings(`embeddings-${corpusSize}-${dimension}.json`);
1125
- const queryVectors = generateQueryVectors(100, dimension);
1126
- console.log(`${C.dim} Loaded ${embeddings.vectors.length} corpus vectors, ${queryVectors.length} query vectors${C.reset}\n`);
1127
-
1128
- for (const scenario of vectorScenarios) {
1129
- section(`Vector: ${scenario.name}`, scenario.description);
1130
-
1131
- let groundTruth = null;
1132
-
1133
- // Only compute ground truth for SEARCH scenarios
1134
- if (scenario.type === 'SEARCH') {
1135
- console.log(`${C.dim} 📐 Computing ground truth (brute-force k=${scenario.k})...${C.reset}`);
1136
- groundTruth = getGroundTruth(
1137
- embeddings.vectors,
1138
- queryVectors.slice(0, scenario.queryCount),
1139
- scenario.k,
1140
- `corpus-${corpusSize}-dim-${dimension}-queries-${scenario.queryCount}`
1141
- );
1142
- }
1143
-
1144
- // Run benchmarks
1145
- const postgres = await benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth);
1146
- const pgserveV1 = await benchmarkPgserveV1Vector(scenario, embeddings, queryVectors, groundTruth);
1147
- const pgserve = await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, false);
1148
- const pgserveRam = canUseRam
1149
- ? await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, true)
1150
- : { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true };
1151
-
1152
- vectorResults.push({
1153
- name: scenario.name,
1154
- description: scenario.description,
1155
- type: scenario.type,
1156
- k: scenario.k,
1157
- postgres,
1158
- pgserveV1,
1159
- pgserve,
1160
- pgserveRam
1161
- });
1162
-
1163
- // Vector scenario results summary
1164
- const isInsert = scenario.type === 'INSERT';
1165
- console.log(`\n ${C.bold}Results (${isInsert ? 'INSERT QPS' : `Recall@${scenario.k} + QPS`}):${C.reset}`);
1166
- console.log(` ${C.dim}──────────────────────────────────────────────────────${C.reset}`);
1167
- const formatResult = (r, icon, color, name) => {
1168
- if (r.skipped) return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.dim}SKIPPED${r.reason ? ` (${r.reason})` : ''}${C.reset}`;
1169
- if (isInsert) {
1170
- return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.bold}${r.throughput}${C.reset} inserts/sec, P50=${r.p50}ms, P99=${r.p99}ms`;
1171
- }
1172
- return ` ${color}${icon}${C.reset} ${name.padEnd(14)} Recall: ${C.bold}${r.recall}%${C.reset}, ${C.bold}${r.throughput}${C.reset} qps, P50=${r.p50}ms`;
1173
- };
1174
- console.log(formatResult(postgres, '🔷', C.cyan, 'PostgreSQL:'));
1175
- console.log(formatResult(pgserveV1, '🧭', C.blue, 'pgserve 1.2.0:'));
1176
- console.log(formatResult(pgserve, '🚀', C.green, 'pgserve v2:'));
1177
- if (canUseRam) {
1178
- console.log(formatResult(pgserveRam, '⚡', C.magenta, 'pgserve v2 RAM:'));
1179
- }
1180
- }
1181
- }
1182
-
1183
- // Save detailed JSON report
1184
- generateReport(results, vectorResults);
1185
-
1186
- // Print final results table with scores
1187
- printFinalResults(results, vectorResults, canUseRam);
1188
-
1189
- console.log(`${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
1190
- ║ Benchmarks Complete! ║
1191
- ║ ║
1192
- ║ Try it yourself: npx pgserve ║
1193
- ╚════════════════════════════════════════════════════════════════╝${C.reset}
1194
- `);
1195
- }
1196
-
1197
- main().catch(console.error);