@wowoengine/sawitdb 2.5.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 +72 -16
- package/bin/sawit-server.js +5 -1
- package/cli/benchmark.js +292 -119
- package/cli/local.js +26 -11
- package/cli/remote.js +26 -11
- package/cli/test.js +110 -72
- package/cli/test_security.js +140 -0
- package/docs/DB Event.md +32 -0
- package/docs/index.html +125 -17
- package/package.json +10 -7
- package/src/SawitClient.js +122 -98
- package/src/SawitServer.js +77 -464
- package/src/SawitWorker.js +55 -0
- package/src/WowoEngine.js +245 -824
- package/src/modules/BTreeIndex.js +54 -24
- package/src/modules/ClusterManager.js +78 -0
- package/src/modules/Env.js +33 -0
- package/src/modules/Pager.js +12 -9
- package/src/modules/QueryParser.js +233 -48
- package/src/modules/ThreadManager.js +84 -0
- package/src/modules/ThreadPool.js +154 -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
|
@@ -36,6 +36,8 @@ Please support our brothers and sisters in Aceh.
|
|
|
36
36
|
- **Transparansi**: Query language is clear. No "Pasal Karet" (Ambiguous Laws) or "Rapat Tertutup" in 5-star hotels.
|
|
37
37
|
- **Speed**: Faster than printing an e-KTP at the Kelurahan.
|
|
38
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.
|
|
39
41
|
- **NPM Support (NEW)**: Install via `npm install @wowoengine/sawitdb`.
|
|
40
42
|
|
|
41
43
|
## Filosofi
|
|
@@ -48,14 +50,18 @@ SawitDB is built with the spirit of "Data Sovereignty". We believe a reliable da
|
|
|
48
50
|
|
|
49
51
|
## File List
|
|
50
52
|
|
|
51
|
-
- `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).
|
|
52
61
|
- `bin/sawit-server.js`: Server executable.
|
|
53
|
-
- `cli
|
|
54
|
-
-
|
|
55
|
-
- [
|
|
56
|
-
- `cli/test.js`: Unit Test Suite.
|
|
57
|
-
- `cli/benchmark.js`: Performance Benchmark Tool.
|
|
58
|
-
- `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.
|
|
59
65
|
|
|
60
66
|
## Installation
|
|
61
67
|
|
|
@@ -65,16 +71,21 @@ Install via NPM:
|
|
|
65
71
|
npm install @wowoengine/sawitdb
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
## Quick Start
|
|
74
|
+
## Quick Start
|
|
69
75
|
|
|
70
76
|
### 1. Start the Server
|
|
71
77
|
```bash
|
|
72
|
-
node
|
|
78
|
+
node bin/sawit-server.js
|
|
79
|
+
# Or with Cluster Mode enabled in .env
|
|
73
80
|
```
|
|
74
81
|
The server will start on `0.0.0.0:7878` by default.
|
|
75
82
|
|
|
76
83
|
### 2. Connect with Client
|
|
77
|
-
|
|
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.
|
|
78
89
|
|
|
79
90
|
---
|
|
80
91
|
|
|
@@ -181,15 +192,48 @@ CREATE INDEX ON [table] ([field])
|
|
|
181
192
|
```sql
|
|
182
193
|
HITUNG COUNT(*) DARI [table]
|
|
183
194
|
HITUNG AVG(price) DARI [products] KELOMPOK [category]
|
|
184
|
-
--
|
|
185
|
-
|
|
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
|
|
186
224
|
```
|
|
187
225
|
|
|
188
226
|
## Architecture Details
|
|
189
227
|
|
|
190
|
-
- **
|
|
191
|
-
- **
|
|
192
|
-
- **
|
|
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`).
|
|
193
237
|
|
|
194
238
|
## Benchmark Performance
|
|
195
239
|
Test Environment: Single Thread, Windows Node.js (Local NVMe)
|
|
@@ -202,7 +246,7 @@ Test Environment: Single Thread, Windows Node.js (Local NVMe)
|
|
|
202
246
|
| **UPDATE (Indexed)** | ~11,000 | 0.090 ms |
|
|
203
247
|
| **DELETE (Indexed)** | ~19,000 | 0.052 ms |
|
|
204
248
|
|
|
205
|
-
*Note: Hasil
|
|
249
|
+
*Note: Hasil diatas adalah benchmark Internal (Engine-only). Untuk Network Benchmark (Cluster Mode), TPS berkisar ~20.000 (overhead jaringan).*
|
|
206
250
|
|
|
207
251
|
## Full Feature Comparison
|
|
208
252
|
|
|
@@ -217,12 +261,23 @@ Test Environment: Single Thread, Windows Node.js (Local NVMe)
|
|
|
217
261
|
| **Drop Table** | `BAKAR LAHAN [table]` | `DROP TABLE [table]` | Deletes table & data |
|
|
218
262
|
| **Insert** | `TANAM KE [table] ... BIBIT (...)` | `INSERT INTO [table] (...) VALUES (...)` | Auto-ID if omitted |
|
|
219
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 |
|
|
220
267
|
| **Update** | `PUPUK [table] DENGAN ... DIMANA ...` | `UPDATE [table] SET ... WHERE ...` | Atomic update |
|
|
221
268
|
| **Delete** | `GUSUR DARI [table] DIMANA ...` | `DELETE FROM [table] WHERE ...` | Row-level deletion |
|
|
222
269
|
| **Index** | `INDEKS [table] PADA [field]` | `CREATE INDEX ON [table] (field)` | B-Tree Indexing |
|
|
223
270
|
| **Count** | `HITUNG COUNT(*) DARI [table]` | `SELECT COUNT(*) FROM [table]` (via HITUNG) | Aggregation |
|
|
224
271
|
| **Sum** | `HITUNG SUM(col) DARI [table]` | `SELECT SUM(col) FROM [table]` (via HITUNG) | Aggregation |
|
|
225
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 |
|
|
226
281
|
|
|
227
282
|
### Supported Operators Table
|
|
228
283
|
|
|
@@ -236,6 +291,7 @@ Test Environment: Single Thread, Windows Node.js (Local NVMe)
|
|
|
236
291
|
| **Range** | `BETWEEN 1000 AND 5000` | Inclusive range check |
|
|
237
292
|
| **Null** | `IS NULL` | Check if field is empty/null |
|
|
238
293
|
| **Not Null** | `IS NOT NULL` | Check if field has value |
|
|
294
|
+
| **Distinct** | `SELECT DISTINCT col` | Remove duplicate rows |
|
|
239
295
|
| **Limit** | `LIMIT 10` | Restrict number of rows |
|
|
240
296
|
| **Offset** | `OFFSET 5` | Skip first N rows (Pagination) |
|
|
241
297
|
| **Order** | `ORDER BY price DESC` | Sort by field (ASC/DESC) |
|
package/bin/sawit-server.js
CHANGED
|
@@ -26,7 +26,10 @@ const config = {
|
|
|
26
26
|
enabled: process.env.SAWIT_WAL_ENABLED !== 'false', // Default true if not explicitly false
|
|
27
27
|
syncMode: process.env.SAWIT_WAL_SYNC_MODE || 'normal',
|
|
28
28
|
checkpointInterval: parseInt(process.env.SAWIT_WAL_CHECKPOINT_INTERVAL) || 10000
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
// Multi-Threading Configuration
|
|
31
|
+
clusterMode: process.env.SAWIT_CLUSTER_MODE === 'true',
|
|
32
|
+
workerCount: parseInt(process.env.SAWIT_CLUSTER_WORKERS) || 0
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
// Parse authentication if provided
|
|
@@ -44,6 +47,7 @@ console.log(` - Host: ${config.host}`);
|
|
|
44
47
|
console.log(` - Data Directory: ${config.dataDir}`);
|
|
45
48
|
console.log(` - Auth: ${config.auth ? 'Enabled' : 'Disabled'}`);
|
|
46
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'}`);
|
|
47
51
|
console.log('');
|
|
48
52
|
|
|
49
53
|
// Create and start server
|
package/cli/benchmark.js
CHANGED
|
@@ -1,145 +1,318 @@
|
|
|
1
1
|
const SawitDB = require('../src/WowoEngine');
|
|
2
|
+
const SawitClient = require('../src/SawitClient');
|
|
2
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');
|
|
3
29
|
|
|
4
30
|
console.log("=".repeat(80));
|
|
5
|
-
console.log(
|
|
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)`);
|
|
6
34
|
console.log("=".repeat(80));
|
|
7
|
-
console.log("\nTarget: Exceed v2.4 baseline");
|
|
8
|
-
console.log("- INSERT: >= 3,000 TPS");
|
|
9
|
-
console.log("- SELECT: >= 3,000 TPS");
|
|
10
|
-
console.log("- UPDATE: >= 3,000 TPS");
|
|
11
|
-
console.log("- DELETE: >= 3,000 TPS");
|
|
12
|
-
console.log("- WAL : " + (process.env.WAL !== 'false' ? 'ENABLED (' + (process.env.WAL_MODE || 'normal') + ')' : 'DISABLED') + "\n");
|
|
13
35
|
|
|
14
|
-
const
|
|
15
|
-
if (fs.existsSync(DB_PATH)) fs.unlinkSync(DB_PATH);
|
|
16
|
-
if (fs.existsSync(DB_PATH + '.wal')) fs.unlinkSync(DB_PATH + '.wal');
|
|
17
|
-
|
|
18
|
-
// WAL Configuration (Default enabled for benchmark unless overridden)
|
|
19
|
-
const walEnabled = process.env.WAL !== 'false';
|
|
20
|
-
const walMode = process.env.WAL_MODE || 'normal';
|
|
21
|
-
|
|
22
|
-
const db = new SawitDB(DB_PATH, {
|
|
23
|
-
wal: {
|
|
24
|
-
enabled: walEnabled,
|
|
25
|
-
syncMode: walMode,
|
|
26
|
-
checkpointInterval: 10000
|
|
27
|
-
}
|
|
28
|
-
});
|
|
36
|
+
const TARGET_TPS = 3000;
|
|
29
37
|
const N = 10000;
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const price = Math.floor(Math.random() * 1000) + 1;
|
|
36
|
-
db.query(`INSERT INTO products (id, price) VALUES (${i}, ${price})`);
|
|
37
|
-
}
|
|
38
|
-
db.query('CREATE INDEX ON products (id)');
|
|
39
|
-
console.log("✓ Setup complete\n");
|
|
40
|
-
|
|
41
|
-
// Pre-generate queries to eliminate random() overhead
|
|
42
|
-
const selectQueries = [];
|
|
43
|
-
const updateQueries = [];
|
|
44
|
-
for (let i = 0; i < 1000; i++) {
|
|
45
|
-
const id = Math.floor(Math.random() * N);
|
|
46
|
-
selectQueries.push(`SELECT * FROM products WHERE id = ${id}`);
|
|
47
|
-
updateQueries.push(`UPDATE products SET price = ${Math.floor(Math.random() * 1000)} WHERE id = ${id}`);
|
|
39
|
+
let serverProcess = null;
|
|
40
|
+
|
|
41
|
+
async function sleep(ms) {
|
|
42
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let max = -Infinity;
|
|
57
|
-
|
|
58
|
-
for (const query of queries) {
|
|
59
|
-
const t0 = process.hrtime.bigint();
|
|
60
|
-
db.query(query);
|
|
61
|
-
const t1 = process.hrtime.bigint();
|
|
62
|
-
const duration = Number(t1 - t0) / 1e6; // ms
|
|
63
|
-
if (duration < min) min = duration;
|
|
64
|
-
if (duration > max) max = duration;
|
|
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 });
|
|
65
51
|
}
|
|
66
|
-
|
|
67
|
-
const tps = Math.round(queries.length / (time / 1000));
|
|
68
|
-
const avg = (time / queries.length).toFixed(3);
|
|
69
|
-
const status = tps >= target ? '✅ PASS' : '❌ FAIL';
|
|
70
|
-
const pct = Math.round((tps / target) * 100);
|
|
52
|
+
fs.mkdirSync(SPAWN_DATA_DIR, { recursive: true });
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
}
|
|
54
|
+
const serverPath = path.join(__dirname, '../src/SawitServer.js');
|
|
74
55
|
|
|
75
|
-
|
|
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
|
+
});
|
|
76
69
|
|
|
77
|
-
|
|
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
|
+
});
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
const insertQueries = [];
|
|
81
|
-
for (let i = 0; i < 1000; i++) {
|
|
82
|
-
insertQueries.push(`INSERT INTO products (id, price) VALUES (${N + i}, 999)`);
|
|
83
|
-
}
|
|
84
|
-
results.push(benchmark('INSERT', insertQueries, 3000));
|
|
83
|
+
serverProcess.on('error', (err) => reject(err));
|
|
85
84
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
});
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
async function main() {
|
|
97
|
+
let db;
|
|
98
|
+
let clients = [];
|
|
93
99
|
|
|
94
|
-
//
|
|
95
|
-
|
|
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
|
+
}
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
const deleteQueries = [];
|
|
99
|
-
for (let i = 0; i < 500; i++) {
|
|
100
|
-
db.query(`INSERT INTO products (id, price) VALUES (${N + i}, 1)`);
|
|
101
|
-
deleteQueries.push(`DELETE FROM products WHERE id = ${N + i}`);
|
|
102
|
-
}
|
|
103
|
-
results.push(benchmark('DELETE (indexed)', deleteQueries, 3000));
|
|
104
|
-
|
|
105
|
-
console.log("=".repeat(100));
|
|
106
|
-
console.log("RESULTS");
|
|
107
|
-
console.log("=".repeat(100));
|
|
108
|
-
console.log("┌────────────────────────────┬──────────┬──────────┬──────────┬──────────┬────────┬─────────┬────────┐");
|
|
109
|
-
console.log("│ Operation │ TPS │ Avg (ms) │ Min (ms) │ Max (ms) │ Target │ % │ Status │");
|
|
110
|
-
console.log("├────────────────────────────┼──────────┼──────────┼──────────┼──────────┼────────┼─────────┼────────┤");
|
|
111
|
-
|
|
112
|
-
let passCount = 0;
|
|
113
|
-
for (const r of results) {
|
|
114
|
-
const name = r.name.padEnd(26);
|
|
115
|
-
const tps = r.tps.toString().padStart(8);
|
|
116
|
-
const avg = r.avg.padStart(8);
|
|
117
|
-
const min = r.min.padStart(8);
|
|
118
|
-
const max = r.max.padStart(8);
|
|
119
|
-
const target = r.target.toString().padStart(6);
|
|
120
|
-
const pct = (r.pct + '%').padStart(7);
|
|
121
|
-
const status = r.status.padEnd(6);
|
|
122
|
-
|
|
123
|
-
if (r.status.includes('PASS')) passCount++;
|
|
124
|
-
console.log(`│ ${name} │ ${tps} │ ${avg} │ ${min} │ ${max} │ ${target} │ ${pct} │ ${status} │`);
|
|
125
|
-
}
|
|
107
|
+
console.log("Setting up...");
|
|
126
108
|
|
|
127
|
-
|
|
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');
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 = [];
|
|
137
172
|
|
|
173
|
+
// Helper to distribute work
|
|
174
|
+
async function runParallel(name, queryList, targetTps) {
|
|
175
|
+
// Warmup
|
|
176
|
+
await query(db, queryList[0]);
|
|
138
177
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
if (fs.existsSync(DB_PATH + '.wal')) fs.unlinkSync(DB_PATH + '.wal');
|
|
143
|
-
} catch (e) { }
|
|
178
|
+
const start = Date.now();
|
|
179
|
+
const totalOps = queryList.length;
|
|
144
180
|
|
|
145
|
-
|
|
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
|
+
});
|
package/cli/local.js
CHANGED
|
@@ -17,18 +17,33 @@ console.log("--- SawitDB (Local Mode) ---");
|
|
|
17
17
|
console.log("Perintah (Tani / SQL):");
|
|
18
18
|
console.log(" LAHAN [nama] | CREATE TABLE [name]");
|
|
19
19
|
console.log(" LIHAT LAHAN | SHOW TABLES");
|
|
20
|
-
console.log("
|
|
21
|
-
console.log("
|
|
22
|
-
console.log("
|
|
23
|
-
console.log("
|
|
24
|
-
console.log("
|
|
25
|
-
console.log("
|
|
26
|
-
console.log("
|
|
27
|
-
console.log("
|
|
20
|
+
console.log("\n MANIPULASI DATA (DML):");
|
|
21
|
+
console.log(" TANAM KE [table] ... BIBIT ... | INSERT INTO ... VALUES ...");
|
|
22
|
+
console.log(" PANEN [cols] DARI [table] | SELECT [cols] FROM [table]");
|
|
23
|
+
console.log(" ... DIMANA [cond] | ... WHERE [cond]");
|
|
24
|
+
console.log(" ... URUTKAN BERDASARKAN [col] | ... ORDER BY [col] [ASC/DESC]");
|
|
25
|
+
console.log(" ... HANYA [n] MULAI DARI [m] | ... LIMIT [n] OFFSET [m]");
|
|
26
|
+
console.log(" ... KELOMPOK [col] | ... GROUP BY [col]");
|
|
27
|
+
console.log(" ... DENGAN SYARAT [cond] | ... HAVING [cond]");
|
|
28
|
+
console.log(" PANEN UNIK [col] DARI ... | SELECT DISTINCT [col] FROM ...");
|
|
29
|
+
console.log(" PUPUK [table] DENGAN ... | UPDATE [table] SET ...");
|
|
30
|
+
console.log(" GUSUR DARI [table] | DELETE FROM [table]");
|
|
31
|
+
console.log("\n RELASI & GABUNGAN (JOINS):");
|
|
32
|
+
console.log(" GABUNG [table] PADA [cond] | INNER JOIN [table] ON [cond]");
|
|
33
|
+
console.log(" GABUNG KIRI [table] PADA ... | LEFT JOIN [table] ON ...");
|
|
34
|
+
console.log(" GABUNG KANAN [table] PADA ... | RIGHT JOIN [table] ON ...");
|
|
35
|
+
console.log(" GABUNG SILANG [table] | CROSS JOIN [table]");
|
|
36
|
+
console.log("\n LAIN-LAIN (MISC):");
|
|
37
|
+
console.log(" INDEKS [table] PADA [field] | CREATE INDEX ON [table]([field])");
|
|
38
|
+
console.log(" HITUNG FUNC(field) DARI ... | SELECT AGGREGATE(...) FROM ...");
|
|
39
|
+
console.log(" JELASKAN PANEN ... | EXPLAIN SELECT ...");
|
|
40
|
+
console.log("\n OPERATOR:");
|
|
41
|
+
console.log(" =, !=, >, <, >=, <=, LIKE, IN, NOT IN, BETWEEN, IS NULL");
|
|
28
42
|
console.log("\nManajemen Wilayah:");
|
|
29
|
-
console.log(" MASUK WILAYAH [nama] - Pindah Database");
|
|
30
|
-
console.log(" BUKA WILAYAH [nama] - Buat Database Baru");
|
|
31
|
-
console.log(" LIHAT WILAYAH - List Database");
|
|
43
|
+
console.log(" MASUK WILAYAH [nama] - Pindah Database (USE)");
|
|
44
|
+
console.log(" BUKA WILAYAH [nama] - Buat Database Baru (CREATE DATABASE)");
|
|
45
|
+
console.log(" LIHAT WILAYAH - List Database (SHOW DATABASES)");
|
|
46
|
+
console.log(" BAKAR WILAYAH [nama] - Hapus Database (DROP DATABASE)");
|
|
32
47
|
console.log("\nContoh:");
|
|
33
48
|
console.log(" TANAM KE sawit (id, bibit) BIBIT (1, 'Dura')");
|
|
34
49
|
console.log(" PANEN * DARI sawit DIMANA id > 0");
|