@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.
Files changed (37) hide show
  1. package/README.md +72 -16
  2. package/bin/sawit-server.js +5 -1
  3. package/cli/benchmark.js +292 -119
  4. package/cli/local.js +26 -11
  5. package/cli/remote.js +26 -11
  6. package/cli/test.js +110 -72
  7. package/cli/test_security.js +140 -0
  8. package/docs/DB Event.md +32 -0
  9. package/docs/index.html +125 -17
  10. package/package.json +10 -7
  11. package/src/SawitClient.js +122 -98
  12. package/src/SawitServer.js +77 -464
  13. package/src/SawitWorker.js +55 -0
  14. package/src/WowoEngine.js +245 -824
  15. package/src/modules/BTreeIndex.js +54 -24
  16. package/src/modules/ClusterManager.js +78 -0
  17. package/src/modules/Env.js +33 -0
  18. package/src/modules/Pager.js +12 -9
  19. package/src/modules/QueryParser.js +233 -48
  20. package/src/modules/ThreadManager.js +84 -0
  21. package/src/modules/ThreadPool.js +154 -0
  22. package/src/server/DatabaseRegistry.js +92 -0
  23. package/src/server/auth/AuthManager.js +92 -0
  24. package/src/server/router/RequestRouter.js +278 -0
  25. package/src/server/session/ClientSession.js +19 -0
  26. package/src/services/IndexManager.js +183 -0
  27. package/src/services/QueryExecutor.js +11 -0
  28. package/src/services/TableManager.js +162 -0
  29. package/src/services/event/DBEvent.js +61 -0
  30. package/src/services/event/DBEventHandler.js +39 -0
  31. package/src/services/executors/AggregateExecutor.js +153 -0
  32. package/src/services/executors/DeleteExecutor.js +134 -0
  33. package/src/services/executors/InsertExecutor.js +113 -0
  34. package/src/services/executors/SelectExecutor.js +130 -0
  35. package/src/services/executors/UpdateExecutor.js +156 -0
  36. package/src/services/logic/ConditionEvaluator.js +75 -0
  37. 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 (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).
52
61
  - `bin/sawit-server.js`: Server executable.
53
- - `cli/local.js`: Interactive CLI tool (Local).
54
- - `cli/remote.js`: Interactive CLI tool (Network).
55
- - [CHANGELOG.md](CHANGELOG.md): Version history and release notes.
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 (Network Edition)
74
+ ## Quick Start
69
75
 
70
76
  ### 1. Start the Server
71
77
  ```bash
72
- node src/SawitServer.js
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
- 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.
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
- -- Generic Keyword Alias
185
- 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
186
224
  ```
187
225
 
188
226
  ## Architecture Details
189
227
 
190
- - **Modular Codebase**: Engine logic separated into `src/modules/` (`Pager.js`, `QueryParser.js`, `BTreeIndex.js`) for better maintainability.
191
- - **Page 0 (Master Page)**: Contains header and Table Directory.
192
- - **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`).
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 dapat bervariasi tergantung hardware.*
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) |
@@ -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("ACCURATE BENCHMARK - PRE-GENERATED QUERIES");
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 DB_PATH = './data/accurate_benchmark.sawit';
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
- // Setup
32
- console.log("Setting up...");
33
- db.query('CREATE TABLE products');
34
- for (let i = 0; i < N; i++) {
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 benchmark(name, queries, target) {
51
- // Warmup
52
- for (let i = 0; i < 10; i++) db.query(queries[i % queries.length]);
53
-
54
- const start = Date.now();
55
- let min = Infinity;
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
- const time = Date.now() - start;
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
- return { name, tps, avg, min: min.toFixed(3), max: max.toFixed(3), target, status, pct };
73
- }
54
+ const serverPath = path.join(__dirname, '../src/SawitServer.js');
74
55
 
75
- const results = [];
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
- console.log("Running benchmarks...\n");
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
- // INSERT
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
- // Cleanup inserts
87
- for (let i = 0; i < 1000; i++) {
88
- db.query(`DELETE FROM products WHERE id = ${N + i}`);
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
- // SELECT (indexed)
92
- results.push(benchmark('SELECT (indexed)', selectQueries, 3000));
96
+ async function main() {
97
+ let db;
98
+ let clients = [];
93
99
 
94
- // UPDATE
95
- results.push(benchmark('UPDATE (indexed)', updateQueries, 3000));
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
- // DELETE
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
- console.log("└────────────────────────────┴──────────┴──────────┴──────────┴──────────┴────────┴─────────┴────────┘");
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
- const passRate = Math.round((passCount / results.length) * 100);
130
- console.log(`\nPass Rate: ${passRate}% (${passCount}/${results.length})`);
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
- if (passRate === 100) {
133
- console.log("\n100% PASS");
134
- } else {
135
- console.log(`\n⚠️ ${results.length - passCount} operation(s) still below target`);
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
- db.close(); // Ensure handles are released
140
- fs.unlinkSync(DB_PATH);
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
- console.log("\nBenchmark Complete");
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(" TANAM KE [table] (cols) BIBIT (vals) | INSERT INTO ... VALUES ...");
21
- console.log(" PANEN ... DARI [table] | SELECT ... FROM ...");
22
- console.log(" ... DIMANA [cond] | ... WHERE [cond]");
23
- console.log(" PUPUK [table] DENGAN ... | UPDATE [table] SET ...");
24
- console.log(" GUSUR DARI [table] | DELETE FROM [table]");
25
- console.log(" BAKAR LAHAN [table] | DROP TABLE [table]");
26
- console.log(" INDEKS [table] PADA [field] | CREATE INDEX ON [table]([field])");
27
- console.log(" HITUNG FUNC(field) DARI ... | AGGREGATE support");
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");