@wowoengine/sawitdb 2.4.0 → 2.6.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 (38) hide show
  1. package/README.md +86 -25
  2. package/bin/sawit-server.js +12 -1
  3. package/cli/benchmark.js +318 -0
  4. package/cli/local.js +98 -20
  5. package/cli/remote.js +65 -11
  6. package/cli/test.js +203 -0
  7. package/cli/test_security.js +140 -0
  8. package/docs/DB Event.md +32 -0
  9. package/docs/index.html +699 -336
  10. package/package.json +10 -7
  11. package/src/SawitClient.js +122 -98
  12. package/src/SawitServer.js +78 -450
  13. package/src/SawitWorker.js +55 -0
  14. package/src/WowoEngine.js +360 -449
  15. package/src/modules/BTreeIndex.js +114 -43
  16. package/src/modules/ClusterManager.js +78 -0
  17. package/src/modules/Env.js +33 -0
  18. package/src/modules/Pager.js +215 -6
  19. package/src/modules/QueryParser.js +310 -82
  20. package/src/modules/ThreadManager.js +84 -0
  21. package/src/modules/ThreadPool.js +154 -0
  22. package/src/modules/WAL.js +340 -0
  23. package/src/server/DatabaseRegistry.js +92 -0
  24. package/src/server/auth/AuthManager.js +92 -0
  25. package/src/server/router/RequestRouter.js +278 -0
  26. package/src/server/session/ClientSession.js +19 -0
  27. package/src/services/IndexManager.js +183 -0
  28. package/src/services/QueryExecutor.js +11 -0
  29. package/src/services/TableManager.js +162 -0
  30. package/src/services/event/DBEvent.js +61 -0
  31. package/src/services/event/DBEventHandler.js +39 -0
  32. package/src/services/executors/AggregateExecutor.js +153 -0
  33. package/src/services/executors/DeleteExecutor.js +134 -0
  34. package/src/services/executors/InsertExecutor.js +113 -0
  35. package/src/services/executors/SelectExecutor.js +130 -0
  36. package/src/services/executors/UpdateExecutor.js +156 -0
  37. package/src/services/logic/ConditionEvaluator.js +75 -0
  38. package/src/services/logic/JoinProcessor.js +230 -0
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  <div align="center">
6
6
 
7
7
  [![Docs](https://img.shields.io/badge/Docs-Read%20Now-blue?style=for-the-badge&logo=googledocs)](https://wowoengine.github.io/SawitDB/)
8
+ [![NPM](https://img.shields.io/npm/v/@wowoengine/sawitdb?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@wowoengine/sawitdb)
8
9
  [![Go Version](https://img.shields.io/badge/Go%20Version-Visit%20Repo-cyan?style=for-the-badge&logo=go)](https://github.com/WowoEngine/SawitDB-Go)
9
10
  [![Changelog](https://img.shields.io/badge/Changelog-Read%20Updates-orange?style=for-the-badge&logo=github)](CHANGELOG.md)
10
11
 
@@ -13,9 +14,9 @@
13
14
 
14
15
  **SawitDB** is a unique database solution stored in `.sawit` binary files.
15
16
 
16
- The system features a custom **Paged Heap File** architecture similar to SQLite, using fixed-size 4KB pages to ensure efficient memory usage. What differentiates SawitDB is its unique **Agricultural Query Language (AQL)**, which replaces standard SQL keywords with Indonesian farming terminology.
17
+ The system features a custom **Hybrid Paged Architecture** similar to SQLite but supercharged with **Object Caching**, using fixed-size 4KB pages to ensure efficient memory usage and near-instant access. What differentiates SawitDB is its unique **Agricultural Query Language (AQL)**, which replaces standard SQL keywords with Indonesian farming terminology.
17
18
 
18
- **Now with Network Edition!** Connect via TCP using `sawitdb://` protocol similar to MongoDB.
19
+ **Now available on NPM!** Connect via TCP using `sawitdb://` protocol.
19
20
 
20
21
  **🚨 Emergency: Aceh Flood Relief**
21
22
  Please support our brothers and sisters in Aceh.
@@ -26,14 +27,18 @@ Please support our brothers and sisters in Aceh.
26
27
 
27
28
  ## Features
28
29
 
29
- - **Paged Architecture**: Data is stored in 4096-byte binary pages. The engine does not load the entire database into memory.
30
+ - **Hybrid Paged Architecture**: Data is stored in 4096-byte binary pages, but hot data is cached as native Objects for zero-copy reads.
30
31
  - **Single File Storage**: All data, schema, and indexes are stored in a single `.sawit` file.
31
32
  - **High Stability**: Uses 4KB atomic pages. More stable than a coalition government.
32
33
  - **Data Integrity (Anti-Korupsi)**: Implements strict `fsync` protocols. Data cannot be "corrupted" or "disappear" mysteriously like social aid funds (Bansos). No "Sunat Massal" here.
34
+ - **Crash Recovery**: Uses **Write-Ahead Logging (WAL)**. Guarantees data always returns after a crash. Unlike a fugitive (Buronan) who is "hard to find".
33
35
  - **Zero Bureaucracy (Zero Deps)**: Built entirely with standard Node.js. No unnecessary "Vendor Pengadaan" or "Mark-up Anggaran".
34
36
  - **Transparansi**: Query language is clear. No "Pasal Karet" (Ambiguous Laws) or "Rapat Tertutup" in 5-star hotels.
35
37
  - **Speed**: Faster than printing an e-KTP at the Kelurahan.
36
38
  - **Network Support (NEW)**: Client-Server architecture with Multi-database support and Authentication.
39
+ - **True Multi-Threading (NEW)**: Worker Pool architecture separates IO (Main Thread) from CPU (Worker Threads).
40
+ - **Advanced SQL (NEW)**: Support for `JOIN` (Left/Right/Full/Cross), `JAVING`, `DISTINCT`, and more.
41
+ - **NPM Support (NEW)**: Install via `npm install @wowoengine/sawitdb`.
37
42
 
38
43
  ## Filosofi
39
44
 
@@ -45,38 +50,48 @@ SawitDB is built with the spirit of "Data Sovereignty". We believe a reliable da
45
50
 
46
51
  ## File List
47
52
 
48
- - `src/WowoEngine.js`: Core Database Engine (Class: `SawitDB`).
53
+ - `src/WowoEngine.js`: Core Database Engine Entry Point.
54
+ - `src/SawitServer.js`: Server Class.
55
+ - `src/SawitClient.js`: Client Class.
56
+ - `src/modules/`: Core modules (QueryParser, BTreeIndex, WAL, Pager).
57
+ - `src/services/`: Logic services (TableManager, IndexManager, QueryExecutor).
58
+ - `src/services/executors/`: Specific query executors (Select, Insert, Update, Delete, Aggregate).
59
+ - `src/services/logic/`: Complex logic handlers (JoinProcessor, ConditionEvaluator).
60
+ - `src/server/`: Server components (AuthManager, RequestRouter, DatabaseRegistry).
49
61
  - `bin/sawit-server.js`: Server executable.
50
- - `cli/local.js`: Interactive CLI tool (Local).
51
- - `cli/remote.js`: Interactive CLI tool (Network).
52
- - [CHANGELOG.md](CHANGELOG.md): Version history and release notes.
53
- - `examples/`: Sample scripts.
62
+ - `cli/`: Command Line Interface tools (local, remote, test, bench).
63
+ - [CHANGELOG.md](CHANGELOG.md): Version history.
64
+ - [docs/DB Event](docs/DB Event.md): Database Event Documentation.
54
65
 
55
66
  ## Installation
56
67
 
57
- Ensure you have Node.js installed. Clone the repository.
68
+ Install via NPM:
58
69
 
59
70
  ```bash
60
- # Clone
61
- git clone https://github.com/WowoEngine/SawitDB.git
71
+ npm install @wowoengine/sawitdb
62
72
  ```
63
73
 
64
- ## Quick Start (Network Edition)
74
+ ## Quick Start
65
75
 
66
76
  ### 1. Start the Server
67
77
  ```bash
68
- node src/SawitServer.js
78
+ node bin/sawit-server.js
79
+ # Or with Cluster Mode enabled in .env
69
80
  ```
70
81
  The server will start on `0.0.0.0:7878` by default.
71
82
 
72
83
  ### 2. Connect with Client
73
- Use [SawitClient](#client-api) or any interactive session.
84
+ You can use the built-in CLI tool:
85
+ ```bash
86
+ node cli/remote.js
87
+ ```
88
+ Or use the `SawitClient` class in your Node.js application.
74
89
 
75
90
  ---
76
91
 
77
92
  ## Dual Syntax Support
78
93
 
79
- SawitDB 2.3 introduces the **Generic Syntax** alongside the classic **Agricultural Query Language (AQL)**, making it easier for developers familiar with standard SQL to adopt.
94
+ SawitDB introduces the **Generic Syntax** alongside the classic **Agricultural Query Language (AQL)**, making it easier for developers familiar with standard SQL to adopt.
80
95
 
81
96
  | Operation | Agricultural Query Language (AQL) | Generic SQL (Standard) |
82
97
  | :--- | :--- | :--- |
@@ -177,27 +192,61 @@ CREATE INDEX ON [table] ([field])
177
192
  ```sql
178
193
  HITUNG COUNT(*) DARI [table]
179
194
  HITUNG AVG(price) DARI [products] KELOMPOK [category]
180
- -- Generic Keyword Alias
181
- SELECT AVG(price) FROM [products] GROUP BY [category] (Coming Soon)
195
+ -- With HAVING clause
196
+ HITUNG COUNT(*) DARI sales GROUP BY region HAVING count > 5
197
+ ```
198
+
199
+ #### DISTINCT
200
+ ```sql
201
+ SELECT DISTINCT category FROM products
202
+ -- Returns only unique values
203
+ ```
204
+
205
+ #### JOIN Types
206
+ ```sql
207
+ -- INNER JOIN (default)
208
+ SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id
209
+
210
+ -- LEFT OUTER JOIN
211
+ SELECT * FROM employees LEFT JOIN departments ON employees.dept_id = departments.id
212
+
213
+ -- RIGHT OUTER JOIN
214
+ SELECT * FROM employees RIGHT JOIN departments ON employees.dept_id = departments.id
215
+
216
+ -- CROSS JOIN (Cartesian product)
217
+ SELECT * FROM colors CROSS JOIN sizes
218
+ ```
219
+
220
+ #### EXPLAIN Query Plan
221
+ ```sql
222
+ EXPLAIN SELECT * FROM users WHERE id = 5
223
+ -- Returns execution plan: scan type, index usage, join methods
182
224
  ```
183
225
 
184
226
  ## Architecture Details
185
227
 
186
- - **Modular Codebase**: Engine logic separated into `src/modules/` (`Pager.js`, `QueryParser.js`, `BTreeIndex.js`) for better maintainability.
187
- - **Page 0 (Master Page)**: Contains header and Table Directory.
188
- - **Data & Indexes**: Stored in 4KB atomic pages.
228
+ - **Worker Pool (Multi-threaded)**:
229
+ - **Main Thread**: Handles strictly I/O (Networking, Protocol Parsing).
230
+ - **Worker Threads**: Execute queries in parallel using `Least-Busy` Load Balancing.
231
+ - **Fault Tolerance**: Automatic worker healing and stuck-query rejection.
232
+ - **Storage Engine**:
233
+ - **Hybrid Paging**: 4KB binary pages with In-Memory Object Cache for hot data.
234
+ - **WAL (Write-Ahead Log)**: Ensures ACID properties and crash recovery.
235
+ - **B-Tree Indexing**: O(log n) lookups for high performance.
236
+ - **Modular Codebase**: Core logic separated into `src/modules/` (`Pager.js`, `ThreadPool.js`, `BTreeIndex.js`).
189
237
 
190
238
  ## Benchmark Performance
191
239
  Test Environment: Single Thread, Windows Node.js (Local NVMe)
192
240
 
193
241
  | Operation | Ops/Sec | Latency (avg) |
194
242
  |-----------|---------|---------------|
195
- | **INSERT** | ~3,125 | 0.32 ms |
196
- | **SELECT (PK Index)** | ~3,846 | 0.26 ms |
197
- | **SELECT (Scan)** | ~4,762 | 0.21 ms |
198
- | **UPDATE** | ~3,571 | 0.28 ms |
243
+ | **INSERT** | ~22,000 | 0.045 ms |
244
+ | **SELECT (PK Index)** | **~247,288** | 0.004 ms |
245
+ | **SELECT (Scan)** | ~13,200 (10k rows) | 0.075 ms |
246
+ | **UPDATE (Indexed)** | ~11,000 | 0.090 ms |
247
+ | **DELETE (Indexed)** | ~19,000 | 0.052 ms |
199
248
 
200
- *Note: Hasil dapat bervariasi tergantung hardware.*
249
+ *Note: Hasil diatas adalah benchmark Internal (Engine-only). Untuk Network Benchmark (Cluster Mode), TPS berkisar ~20.000 (overhead jaringan).*
201
250
 
202
251
  ## Full Feature Comparison
203
252
 
@@ -212,12 +261,23 @@ Test Environment: Single Thread, Windows Node.js (Local NVMe)
212
261
  | **Drop Table** | `BAKAR LAHAN [table]` | `DROP TABLE [table]` | Deletes table & data |
213
262
  | **Insert** | `TANAM KE [table] ... BIBIT (...)` | `INSERT INTO [table] (...) VALUES (...)` | Auto-ID if omitted |
214
263
  | **Select** | `PANEN ... DARI [table] DIMANA ...` | `SELECT ... FROM [table] WHERE ...` | Supports Projection |
264
+ | **Ordering** | `URUTKAN BERDASARKAN [col] [ASC/DESC/NAIK/TURUN]` | `ORDER BY [col] [ASC/DESC]` | Sort results |
265
+ | **Limit** | `HANYA [n]` | `LIMIT [n]` | Limit rows |
266
+ | **Offset** | `MULAI DARI [n]` | `OFFSET [n]` | Skip rows |
215
267
  | **Update** | `PUPUK [table] DENGAN ... DIMANA ...` | `UPDATE [table] SET ... WHERE ...` | Atomic update |
216
268
  | **Delete** | `GUSUR DARI [table] DIMANA ...` | `DELETE FROM [table] WHERE ...` | Row-level deletion |
217
269
  | **Index** | `INDEKS [table] PADA [field]` | `CREATE INDEX ON [table] (field)` | B-Tree Indexing |
218
270
  | **Count** | `HITUNG COUNT(*) DARI [table]` | `SELECT COUNT(*) FROM [table]` (via HITUNG) | Aggregation |
219
271
  | **Sum** | `HITUNG SUM(col) DARI [table]` | `SELECT SUM(col) FROM [table]` (via HITUNG) | Aggregation |
220
272
  | **Average** | `HITUNG AVG(col) DARI [table]` | `SELECT AVG(col) FROM [table]` (via HITUNG) | Aggregation |
273
+ | **Min/Max** | `HITUNG MIN(col) DARI [table]` | `SELECT MIN(col) FROM [table]` (via HITUNG) | Aggregation |
274
+ | **Grouping**| `KELOMPOK [col]` | `GROUP BY [col]` | Group results |
275
+ | **DISTINCT** | `PANEN DISTINCT col DARI [table]` | `SELECT DISTINCT col FROM [table]` | Unique rows |
276
+ | **LEFT JOIN** | `GABUNG KIRI [table] PADA ...` | `LEFT JOIN [table] ON ...` | Outer join |
277
+ | **RIGHT JOIN** | `GABUNG KANAN [table] PADA ...` | `RIGHT JOIN [table] ON ...` | Outer join |
278
+ | **CROSS JOIN** | `GABUNG SILANG [table]` | `CROSS JOIN [table]` | Cartesian product |
279
+ | **HAVING** | `DENGAN SYARAT count > 5` | `HAVING count > 5` | Filter groups |
280
+ | **EXPLAIN** | `JELASKAN SELECT ...` | `EXPLAIN SELECT ...` | Query plan |
221
281
 
222
282
  ### Supported Operators Table
223
283
 
@@ -231,6 +291,7 @@ Test Environment: Single Thread, Windows Node.js (Local NVMe)
231
291
  | **Range** | `BETWEEN 1000 AND 5000` | Inclusive range check |
232
292
  | **Null** | `IS NULL` | Check if field is empty/null |
233
293
  | **Not Null** | `IS NOT NULL` | Check if field has value |
294
+ | **Distinct** | `SELECT DISTINCT col` | Remove duplicate rows |
234
295
  | **Limit** | `LIMIT 10` | Restrict number of rows |
235
296
  | **Offset** | `OFFSET 5` | Skip first N rows (Pagination) |
236
297
  | **Order** | `ORDER BY price DESC` | Sort by field (ASC/DESC) |
@@ -20,7 +20,16 @@ const path = require('path');
20
20
  const config = {
21
21
  port: process.env.SAWIT_PORT || 7878,
22
22
  host: process.env.SAWIT_HOST || '0.0.0.0',
23
- dataDir: process.env.SAWIT_DATA_DIR || path.join(__dirname, '../data')
23
+ dataDir: process.env.SAWIT_DATA_DIR || path.join(__dirname, '../data'),
24
+ // WAL Configuration
25
+ wal: {
26
+ enabled: process.env.SAWIT_WAL_ENABLED !== 'false', // Default true if not explicitly false
27
+ syncMode: process.env.SAWIT_WAL_SYNC_MODE || 'normal',
28
+ checkpointInterval: parseInt(process.env.SAWIT_WAL_CHECKPOINT_INTERVAL) || 10000
29
+ },
30
+ // Multi-Threading Configuration
31
+ clusterMode: process.env.SAWIT_CLUSTER_MODE === 'true',
32
+ workerCount: parseInt(process.env.SAWIT_CLUSTER_WORKERS) || 0
24
33
  };
25
34
 
26
35
  // Parse authentication if provided
@@ -37,6 +46,8 @@ console.log(` - Port: ${config.port}`);
37
46
  console.log(` - Host: ${config.host}`);
38
47
  console.log(` - Data Directory: ${config.dataDir}`);
39
48
  console.log(` - Auth: ${config.auth ? 'Enabled' : 'Disabled'}`);
49
+ console.log(` - WAL: ${config.wal.enabled ? 'Enabled (' + config.wal.syncMode + ')' : 'Disabled'}`);
50
+ console.log(` - Mode: ${config.clusterMode ? 'CLUSTER (Workers: ' + (config.workerCount || 'Auto') + ')' : 'SINGLE THREAD'}`);
40
51
  console.log('');
41
52
 
42
53
  // Create and start server
@@ -0,0 +1,318 @@
1
+ const SawitDB = require('../src/WowoEngine');
2
+ const SawitClient = require('../src/SawitClient');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ // Configuration
8
+ const args = process.argv.slice(2);
9
+ const IS_SPAWN = args.includes('--spawn');
10
+ const IS_REMOTE = args.includes('--remote') || IS_SPAWN; // Spawn implies remote access
11
+ const IS_CLUSTER = args.includes('--cluster');
12
+
13
+ // Parsing Arguments
14
+ const getArg = (name, def) => {
15
+ const found = args.find(a => a.startsWith(name + '='));
16
+ return found ? parseInt(found.split('=')[1]) : def;
17
+ };
18
+
19
+ const WORKER_COUNT = getArg('--workers', 1); // Server Workers
20
+ const CLIENT_COUNT = getArg('--clients', 1); // Benchmark Clients (Concurrency)
21
+
22
+ // Connection Settings
23
+ const REMOTE_HOST = process.env.SAWIT_HOST || '127.0.0.1';
24
+ const REMOTE_PORT = IS_SPAWN ? 7979 : (process.env.SAWIT_PORT || 7878); // Use custom port if spawning to avoid conflict
25
+
26
+ // Local/Temp Paths
27
+ const DB_PATH = './data/accurate_benchmark.sawit';
28
+ const SPAWN_DATA_DIR = path.join(__dirname, '../data_bench_temp');
29
+
30
+ console.log("=".repeat(80));
31
+ console.log(`BENCHMARK - ${IS_SPAWN ? 'AUTO-SPAWNED SERVER' : (IS_REMOTE ? 'REMOTE MODE' : 'LOCAL EMBEDDED MODE')}`);
32
+ if (IS_SPAWN) console.log(`Server Mode: ${IS_CLUSTER ? 'CLUSTER' : 'SINGLE THREAD'} (Workers: ${WORKER_COUNT})`);
33
+ console.log(`Load: ${CLIENT_COUNT} concurrent client(s)`);
34
+ console.log("=".repeat(80));
35
+
36
+ const TARGET_TPS = 3000;
37
+ const N = 10000;
38
+
39
+ let serverProcess = null;
40
+
41
+ async function sleep(ms) {
42
+ return new Promise(resolve => setTimeout(resolve, ms));
43
+ }
44
+
45
+ async function startServer() {
46
+ console.log("[Setup] Spawning temporary server...");
47
+
48
+ // Cleanup temp dir
49
+ if (fs.existsSync(SPAWN_DATA_DIR)) {
50
+ fs.rmSync(SPAWN_DATA_DIR, { recursive: true, force: true });
51
+ }
52
+ fs.mkdirSync(SPAWN_DATA_DIR, { recursive: true });
53
+
54
+ const serverPath = path.join(__dirname, '../src/SawitServer.js');
55
+
56
+ return new Promise((resolve, reject) => {
57
+ serverProcess = spawn('node', [serverPath], {
58
+ env: {
59
+ ...process.env,
60
+ SAWIT_PORT: REMOTE_PORT,
61
+ SAWIT_HOST: REMOTE_HOST,
62
+ SAWIT_CLUSTER_MODE: IS_CLUSTER.toString(),
63
+ SAWIT_CLUSTER_WORKERS: WORKER_COUNT.toString(),
64
+ SAWIT_DATA_DIR: SPAWN_DATA_DIR,
65
+ SAWIT_LOG_LEVEL: 'info'
66
+ },
67
+ stdio: ['ignore', 'pipe', 'inherit'] // pipe stdout to check readiness
68
+ });
69
+
70
+ let ready = false;
71
+ serverProcess.stdout.on('data', (data) => {
72
+ const out = data.toString();
73
+ // console.log('[SERVER]', out.trim()); // Debug
74
+ if (out.includes('Ready to accept connections')) {
75
+ if (!ready) {
76
+ ready = true;
77
+ console.log("[Setup] Server Ready!");
78
+ resolve();
79
+ }
80
+ }
81
+ });
82
+
83
+ serverProcess.on('error', (err) => reject(err));
84
+
85
+ // Timeout safety
86
+ setTimeout(() => {
87
+ if (!ready) {
88
+ console.error("[Setup] Server timed out starting.");
89
+ if (serverProcess) serverProcess.kill();
90
+ reject(new Error("Server startup timeout"));
91
+ }
92
+ }, 10000);
93
+ });
94
+ }
95
+
96
+ async function main() {
97
+ let db;
98
+ let clients = [];
99
+
100
+ // --- SETUP ---
101
+ if (IS_SPAWN) {
102
+ await startServer();
103
+ // Give it a tiny bit more grace period for workers to settle
104
+ await sleep(1000);
105
+ }
106
+
107
+ console.log("Setting up...");
108
+
109
+ if (IS_REMOTE) {
110
+ // Init remote clients
111
+ for (let i = 0; i < CLIENT_COUNT; i++) {
112
+ const client = new SawitClient(`sawitdb://${REMOTE_HOST}:${REMOTE_PORT}/benchmark_db`);
113
+ await client.connect();
114
+ // Use same DB for standard benchmark
115
+ try {
116
+ await client.query(`BUKA WILAYAH benchmark_db`);
117
+ } catch (e) { } // Exists
118
+ await client.use('benchmark_db');
119
+ clients.push(client);
120
+ }
121
+ db = clients[0]; // Primary for setup
122
+ } else {
123
+ // Local Setup
124
+ if (fs.existsSync(DB_PATH)) fs.unlinkSync(DB_PATH);
125
+ if (fs.existsSync(DB_PATH + '.wal')) fs.unlinkSync(DB_PATH + '.wal');
126
+
127
+ db = new SawitDB(DB_PATH, {
128
+ wal: {
129
+ enabled: process.env.WAL !== 'false',
130
+ syncMode: process.env.WAL_MODE || 'normal',
131
+ checkpointInterval: 10000
132
+ }
133
+ });
134
+ }
135
+
136
+ // Cleanup & Init
137
+ try {
138
+ if (IS_REMOTE) await query(db, 'DROP TABLE products');
139
+ } catch (e) { }
140
+
141
+ await query(db, 'CREATE TABLE products');
142
+
143
+ console.log("Populating initial data...");
144
+ for (let i = 0; i < N; i++) {
145
+ const price = Math.floor(Math.random() * 1000) + 1;
146
+ await query(db, `INSERT INTO products (id, price) VALUES (${i}, ${price})`);
147
+ }
148
+ await query(db, 'CREATE INDEX ON products (id)');
149
+ console.log("✓ Setup complete\n");
150
+
151
+ // --- PREPARE DATA ---
152
+ const selectQueries = [];
153
+ const updateQueries = [];
154
+ for (let i = 0; i < 1000; i++) {
155
+ const id = Math.floor(Math.random() * N);
156
+ selectQueries.push(`SELECT * FROM products WHERE id = ${id}`);
157
+ updateQueries.push(`UPDATE products SET price = ${Math.floor(Math.random() * 1000)} WHERE id = ${id}`);
158
+ }
159
+
160
+ const insertQueries = [];
161
+ for (let i = 0; i < 1000; i++) {
162
+ insertQueries.push(`INSERT INTO products (id, price) VALUES (${N + i}, 999)`);
163
+ }
164
+
165
+ const deleteQueries = [];
166
+ for (let i = 0; i < 500; i++) {
167
+ deleteQueries.push(`DELETE FROM products WHERE id = ${N + i}`);
168
+ }
169
+
170
+ // --- RUN BENCHMARKS ---
171
+ const results = [];
172
+
173
+ // Helper to distribute work
174
+ async function runParallel(name, queryList, targetTps) {
175
+ // Warmup
176
+ await query(db, queryList[0]);
177
+
178
+ const start = Date.now();
179
+ const totalOps = queryList.length;
180
+
181
+ // Chunk queries for workers
182
+ const chunkSize = Math.ceil(totalOps / (IS_REMOTE ? CLIENT_COUNT : 1));
183
+ const promises = [];
184
+
185
+ if (IS_REMOTE) {
186
+ for (let i = 0; i < CLIENT_COUNT; i++) {
187
+ const client = clients[i];
188
+ const startIdx = i * chunkSize;
189
+ const endIdx = Math.min(startIdx + chunkSize, totalOps);
190
+ const workerQueries = queryList.slice(startIdx, endIdx);
191
+
192
+ if (workerQueries.length === 0) continue;
193
+
194
+ promises.push((async () => {
195
+ for (const q of workerQueries) await client.query(q);
196
+ })());
197
+ }
198
+ } else {
199
+ promises.push((async () => {
200
+ for (const q of queryList) await query(db, q);
201
+ })());
202
+ }
203
+
204
+ await Promise.all(promises);
205
+
206
+ const time = Date.now() - start;
207
+ const tps = Math.round(totalOps / (time / 1000));
208
+ const avg = (time / totalOps).toFixed(3);
209
+ const status = tps >= targetTps ? '✅ PASS' : '❌ FAIL';
210
+ const pct = Math.round((tps / targetTps) * 100);
211
+
212
+ return { name, tps, avg, target: targetTps, status, pct };
213
+ }
214
+
215
+ results.push(await runParallel('INSERT', insertQueries, TARGET_TPS));
216
+
217
+ // Cleanup inserts for delete test consistency?
218
+ // Actually we keep them for SELECT/UPDATE test?
219
+ // Benchmark sequence: INSERT -> SELECT -> UPDATE -> DELETE
220
+
221
+ results.push(await runParallel('SELECT (indexed)', selectQueries, TARGET_TPS));
222
+ results.push(await runParallel('UPDATE (indexed)', updateQueries, TARGET_TPS));
223
+
224
+ // Prepare data for delete (need to ensure rows exist)
225
+ // We inserted 1000 new rows in INSERT test (id N to N+999).
226
+ // DELETE test removes 500 of them.
227
+ results.push(await runParallel('DELETE (indexed)', deleteQueries, TARGET_TPS));
228
+
229
+
230
+ // --- REPORT ---
231
+ console.log("=".repeat(100));
232
+ console.log("RESULTS");
233
+ console.log("=".repeat(100));
234
+ console.log("┌────────────────────────────┬──────────┬──────────┬────────┬─────────┬────────┐");
235
+ console.log("│ Operation │ TPS │ Avg (ms) │ Target │ % │ Status │");
236
+ console.log("├────────────────────────────┼──────────┼──────────┼────────┼─────────┼────────┤");
237
+
238
+ let passCount = 0;
239
+ for (const r of results) {
240
+ const name = r.name.padEnd(26);
241
+ const tps = r.tps.toString().padStart(8);
242
+ const avg = r.avg.padStart(8);
243
+ const target = r.target.toString().padStart(6);
244
+ const pct = (r.pct + '%').padStart(7);
245
+ const status = r.status.padEnd(6);
246
+ if (r.status.includes('PASS')) passCount++;
247
+ console.log(`│ ${name} │ ${tps} │ ${avg} │ ${target} │ ${pct} │ ${status} │`);
248
+ }
249
+ console.log("└────────────────────────────┴──────────┴──────────┴────────┴─────────┴────────┘");
250
+
251
+ // TEARDOWN
252
+ if (IS_REMOTE) {
253
+ // Fetch Worker Stats
254
+ try {
255
+ const adminClient = clients[0];
256
+ const stats = await adminClient.stats();
257
+
258
+ if (stats && stats.workers) {
259
+ console.log("\nWORKER STATS (Load Balance Check)");
260
+ console.log("=".repeat(50));
261
+ console.log("┌───────────┬────────────────────┬──────────┐");
262
+ console.log("│ Worker ID │ Queries Processed │ Active │");
263
+ console.log("├───────────┼────────────────────┼──────────┤");
264
+
265
+ let total = 0;
266
+ for (const w of stats.workers) {
267
+ const id = w.id.toString().padEnd(9);
268
+ const count = w.queries.toString().padEnd(18);
269
+ const active = (w.active || 0).toString().padEnd(8);
270
+ console.log(`│ ${id} │ ${count} │ ${active} │`);
271
+ total += w.queries;
272
+ }
273
+ console.log("├───────────┼────────────────────┼──────────┤");
274
+ console.log(`│ TOTAL │ ${total.toString().padEnd(18)} │ - │`);
275
+ console.log("└───────────┴────────────────────┴──────────┘\n");
276
+ }
277
+ } catch (e) {
278
+ console.log("Failed to fetch worker stats:", e.message);
279
+ }
280
+
281
+ for (const c of clients) {
282
+ // Silence errors during teardown (prevents ECONNRESET logs when server is killed)
283
+ if (c.socket) {
284
+ c.socket.removeAllListeners('error');
285
+ c.socket.on('error', () => { }); // Catch-all for teardown errors
286
+ }
287
+ c.disconnect();
288
+ }
289
+ } else {
290
+ db.close();
291
+ if (fs.existsSync(DB_PATH)) fs.unlinkSync(DB_PATH);
292
+ }
293
+
294
+ if (serverProcess) {
295
+ console.log("Stopping server...");
296
+ serverProcess.kill();
297
+ await sleep(500);
298
+ // Cleanup temp dir
299
+ try { if (fs.existsSync(SPAWN_DATA_DIR)) fs.rmSync(SPAWN_DATA_DIR, { recursive: true, force: true }); } catch (e) { }
300
+ }
301
+ }
302
+
303
+ // Wrapper for async/sync agnostic call
304
+ async function query(dbInst, q) {
305
+ if (dbInst.query.constructor.name === "AsyncFunction") {
306
+ return await dbInst.query(q);
307
+ } else {
308
+ // Check if remote client (SawitClient has async query)
309
+ if (IS_REMOTE) return await dbInst.query(q);
310
+ return dbInst.query(q);
311
+ }
312
+ }
313
+
314
+ main().catch(err => {
315
+ console.error(err);
316
+ if (serverProcess) serverProcess.kill();
317
+ process.exit(1);
318
+ });