@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.
- package/README.md +86 -25
- package/bin/sawit-server.js +12 -1
- package/cli/benchmark.js +318 -0
- package/cli/local.js +98 -20
- package/cli/remote.js +65 -11
- package/cli/test.js +203 -0
- package/cli/test_security.js +140 -0
- package/docs/DB Event.md +32 -0
- package/docs/index.html +699 -336
- package/package.json +10 -7
- package/src/SawitClient.js +122 -98
- package/src/SawitServer.js +78 -450
- package/src/SawitWorker.js +55 -0
- package/src/WowoEngine.js +360 -449
- package/src/modules/BTreeIndex.js +114 -43
- package/src/modules/ClusterManager.js +78 -0
- package/src/modules/Env.js +33 -0
- package/src/modules/Pager.js +215 -6
- package/src/modules/QueryParser.js +310 -82
- package/src/modules/ThreadManager.js +84 -0
- package/src/modules/ThreadPool.js +154 -0
- package/src/modules/WAL.js +340 -0
- package/src/server/DatabaseRegistry.js +92 -0
- package/src/server/auth/AuthManager.js +92 -0
- package/src/server/router/RequestRouter.js +278 -0
- package/src/server/session/ClientSession.js +19 -0
- package/src/services/IndexManager.js +183 -0
- package/src/services/QueryExecutor.js +11 -0
- package/src/services/TableManager.js +162 -0
- package/src/services/event/DBEvent.js +61 -0
- package/src/services/event/DBEventHandler.js +39 -0
- package/src/services/executors/AggregateExecutor.js +153 -0
- package/src/services/executors/DeleteExecutor.js +134 -0
- package/src/services/executors/InsertExecutor.js +113 -0
- package/src/services/executors/SelectExecutor.js +130 -0
- package/src/services/executors/UpdateExecutor.js +156 -0
- package/src/services/logic/ConditionEvaluator.js +75 -0
- 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
|
[](https://wowoengine.github.io/SawitDB/)
|
|
8
|
+
[](https://www.npmjs.com/package/@wowoengine/sawitdb)
|
|
8
9
|
[](https://github.com/WowoEngine/SawitDB-Go)
|
|
9
10
|
[](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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
51
|
-
-
|
|
52
|
-
- [
|
|
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
|
-
|
|
68
|
+
Install via NPM:
|
|
58
69
|
|
|
59
70
|
```bash
|
|
60
|
-
|
|
61
|
-
git clone https://github.com/WowoEngine/SawitDB.git
|
|
71
|
+
npm install @wowoengine/sawitdb
|
|
62
72
|
```
|
|
63
73
|
|
|
64
|
-
## Quick Start
|
|
74
|
+
## Quick Start
|
|
65
75
|
|
|
66
76
|
### 1. Start the Server
|
|
67
77
|
```bash
|
|
68
|
-
node
|
|
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
|
-
|
|
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
|
|
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
|
-
--
|
|
181
|
-
|
|
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
|
-
- **
|
|
187
|
-
- **
|
|
188
|
-
- **
|
|
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** | ~
|
|
196
|
-
| **SELECT (PK Index)** |
|
|
197
|
-
| **SELECT (Scan)** | ~
|
|
198
|
-
| **UPDATE** | ~
|
|
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
|
|
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) |
|
package/bin/sawit-server.js
CHANGED
|
@@ -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
|
package/cli/benchmark.js
ADDED
|
@@ -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
|
+
});
|