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.
- package/.genie/AGENTS.md +13 -0
- package/.genie/agents/README.md +110 -0
- package/.genie/agents/analyze.md +176 -0
- package/.genie/agents/forge.md +290 -0
- package/.genie/agents/garbage-cleaner.md +324 -0
- package/.genie/agents/garbage-collector.md +596 -0
- package/.genie/agents/github-issue-gc.md +618 -0
- package/.genie/agents/review.md +380 -0
- package/.genie/agents/semantic-analyzer/find-duplicates.md +90 -0
- package/.genie/agents/semantic-analyzer/find-orphans.md +99 -0
- package/.genie/agents/semantic-analyzer.md +101 -0
- package/.genie/agents/update.md +182 -0
- package/.genie/agents/wish.md +357 -0
- package/.genie/code/AGENTS.md +692 -0
- package/.genie/code/agents/audit/risk.md +173 -0
- package/.genie/code/agents/audit/security.md +189 -0
- package/.genie/code/agents/audit.md +145 -0
- package/.genie/code/agents/challenge.md +230 -0
- package/.genie/code/agents/change-reviewer.md +295 -0
- package/.genie/code/agents/code-garbage-collector.md +425 -0
- package/.genie/code/agents/code-quality.md +410 -0
- package/.genie/code/agents/commit-suggester.md +255 -0
- package/.genie/code/agents/commit.md +124 -0
- package/.genie/code/agents/consensus.md +204 -0
- package/.genie/code/agents/daily-standup.md +722 -0
- package/.genie/code/agents/docgen.md +48 -0
- package/.genie/code/agents/explore.md +79 -0
- package/.genie/code/agents/fix.md +100 -0
- package/.genie/code/agents/git/commit-advisory.md +219 -0
- package/.genie/code/agents/git/workflows/issue.md +244 -0
- package/.genie/code/agents/git/workflows/pr.md +179 -0
- package/.genie/code/agents/git/workflows/release.md +460 -0
- package/.genie/code/agents/git/workflows/report.md +342 -0
- package/.genie/code/agents/git.md +432 -0
- package/.genie/code/agents/implementor.md +161 -0
- package/.genie/code/agents/install.md +515 -0
- package/.genie/code/agents/issue-creator.md +344 -0
- package/.genie/code/agents/polish.md +116 -0
- package/.genie/code/agents/qa.md +653 -0
- package/.genie/code/agents/refactor.md +294 -0
- package/.genie/code/agents/release.md +1129 -0
- package/.genie/code/agents/roadmap.md +885 -0
- package/.genie/code/agents/tests.md +557 -0
- package/.genie/code/agents/tracer.md +50 -0
- package/.genie/code/agents/update/upstream-update.md +85 -0
- package/.genie/code/agents/update/versions/generic-update.md +305 -0
- package/.genie/code/agents/vibe.md +1317 -0
- package/.genie/code/spells/agent-configuration.md +58 -0
- package/.genie/code/spells/automated-rc-publishing.md +106 -0
- package/.genie/code/spells/branch-tracker-guidance.md +28 -0
- package/.genie/code/spells/debug.md +320 -0
- package/.genie/code/spells/emoji-naming-convention.md +303 -0
- package/.genie/code/spells/evidence-storage.md +26 -0
- package/.genie/code/spells/file-naming-rules.md +35 -0
- package/.genie/code/spells/forge-code-blueprints.md +195 -0
- package/.genie/code/spells/genie-integration.md +153 -0
- package/.genie/code/spells/publishing-protocol.md +61 -0
- package/.genie/code/spells/team-consultation-protocol.md +284 -0
- package/.genie/code/spells/tool-requirements.md +20 -0
- package/.genie/code/spells/triad-maintenance-protocol.md +154 -0
- package/.genie/code/teams/tech-council/council.md +328 -0
- package/.genie/code/teams/tech-council/jt.md +352 -0
- package/.genie/code/teams/tech-council/nayr.md +305 -0
- package/.genie/code/teams/tech-council/oettam.md +375 -0
- package/.genie/neurons/README.md +193 -0
- package/.genie/neurons/forge.md +106 -0
- package/.genie/neurons/genie.md +63 -0
- package/.genie/neurons/review.md +106 -0
- package/.genie/neurons/wish.md +104 -0
- package/.genie/product/README.md +20 -0
- package/.genie/product/cli-automation.md +359 -0
- package/.genie/product/environment.md +60 -0
- package/.genie/product/mission.md +60 -0
- package/.genie/product/roadmap.md +44 -0
- package/.genie/product/tech-stack.md +34 -0
- package/.genie/product/templates/context-template.md +218 -0
- package/.genie/product/templates/qa-done-report-template.md +68 -0
- package/.genie/product/templates/review-report-template.md +89 -0
- package/.genie/product/templates/wish-template.md +120 -0
- package/.genie/scripts/helpers/analyze-commit.js +195 -0
- package/.genie/scripts/helpers/bullet-counter.js +194 -0
- package/.genie/scripts/helpers/bullet-find.js +289 -0
- package/.genie/scripts/helpers/bullet-id.js +244 -0
- package/.genie/scripts/helpers/check-secrets.js +237 -0
- package/.genie/scripts/helpers/count-tokens.js +200 -0
- package/.genie/scripts/helpers/create-frontmatter.js +456 -0
- package/.genie/scripts/helpers/detect-markers.js +293 -0
- package/.genie/scripts/helpers/detect-todos.js +267 -0
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +135 -0
- package/.genie/scripts/helpers/embeddings.js +344 -0
- package/.genie/scripts/helpers/find-empty-sections.js +158 -0
- package/.genie/scripts/helpers/index.js +319 -0
- package/.genie/scripts/helpers/validate-frontmatter.js +578 -0
- package/.genie/scripts/helpers/validate-links.js +207 -0
- package/.genie/scripts/helpers/validate-paths.js +373 -0
- package/.genie/spells/README.md +9 -0
- package/.genie/spells/ace-protocol.md +118 -0
- package/.genie/spells/ask-one-at-a-time.md +175 -0
- package/.genie/spells/backup-analyzer.md +542 -0
- package/.genie/spells/blocker.md +12 -0
- package/.genie/spells/break-things-move-fast.md +56 -0
- package/.genie/spells/context-candidates.md +72 -0
- package/.genie/spells/context-critic.md +51 -0
- package/.genie/spells/defer-to-expertise.md +278 -0
- package/.genie/spells/delegate-dont-do.md +292 -0
- package/.genie/spells/error-investigation-protocol.md +328 -0
- package/.genie/spells/evidence-based-completion.md +273 -0
- package/.genie/spells/experiment.md +65 -0
- package/.genie/spells/file-creation-protocol.md +229 -0
- package/.genie/spells/forge-integration.md +281 -0
- package/.genie/spells/forge-orchestration.md +514 -0
- package/.genie/spells/gather-context.md +18 -0
- package/.genie/spells/global-health-check.md +34 -0
- package/.genie/spells/global-noop-roundtrip.md +25 -0
- package/.genie/spells/install-genie.md +1232 -0
- package/.genie/spells/install.md +82 -0
- package/.genie/spells/investigate-before-commit.md +112 -0
- package/.genie/spells/know-yourself.md +288 -0
- package/.genie/spells/learn.md +828 -0
- package/.genie/spells/mcp-diagnostic-protocol.md +246 -0
- package/.genie/spells/mcp-first.md +124 -0
- package/.genie/spells/multi-step-execution.md +67 -0
- package/.genie/spells/orchestration-boundary-protocol.md +256 -0
- package/.genie/spells/orchestrator-not-implementor.md +189 -0
- package/.genie/spells/prompt.md +746 -0
- package/.genie/spells/reflect.md +404 -0
- package/.genie/spells/routing-decision-matrix.md +368 -0
- package/.genie/spells/run-in-parallel.md +12 -0
- package/.genie/spells/session-state-updater-example.md +196 -0
- package/.genie/spells/session-state-updater.md +220 -0
- package/.genie/spells/track-long-running-tasks.md +133 -0
- package/.genie/spells/troubleshoot-infrastructure.md +176 -0
- package/.genie/spells/upgrade-genie.md +415 -0
- package/.genie/spells/url-presentation-protocol.md +301 -0
- package/.genie/spells/wish-initiation.md +158 -0
- package/.genie/spells/wish-issue-linkage.md +410 -0
- package/.genie/spells/wish-lifecycle.md +100 -0
- package/.genie/state/provider-status.json +3 -0
- package/.genie/state/version.json +16 -0
- package/AGENTS.md +422 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/Makefile +235 -0
- package/README.md +323 -0
- package/bin/pglite-server.js +457 -0
- package/ecosystem.config.cjs +23 -0
- package/examples/multi-tenant-demo.js +104 -0
- package/package.json +47 -0
- package/src/detector.js +105 -0
- package/src/index.js +177 -0
- package/src/pool.js +320 -0
- package/src/ports.js +114 -0
- package/src/protocol.js +216 -0
- package/src/registry.js +134 -0
- package/src/router.js +289 -0
- package/src/server.js +265 -0
- package/tests/benchmarks/runner.js +489 -0
- 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
|
+
});
|