@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
@@ -0,0 +1,92 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const SawitDB = require('../WowoEngine');
4
+
5
+ class DatabaseRegistry {
6
+ constructor(dataDir, config) {
7
+ this.dataDir = dataDir;
8
+ this.config = config;
9
+ this.databases = new Map(); // name -> SawitDB instance
10
+ this.walConfig = config.wal || { enabled: false };
11
+
12
+ // Ensure data directory exists
13
+ if (!fs.existsSync(this.dataDir)) {
14
+ fs.mkdirSync(this.dataDir, { recursive: true });
15
+ }
16
+ }
17
+
18
+ validateName(name) {
19
+ if (!name || typeof name !== 'string') {
20
+ throw new Error("Database name required");
21
+ }
22
+ // Prevent Path Traversal and illegal chars
23
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
24
+ throw new Error("Invalid database name. Only alphanumeric, hyphen, and underscore allowed.");
25
+ }
26
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
27
+ throw new Error("Invalid database name (Path Traversal attempt).");
28
+ }
29
+ return true;
30
+ }
31
+
32
+ get(name) {
33
+ this.validateName(name);
34
+ return this.getOrCreate(name);
35
+ }
36
+
37
+ getOrCreate(name) {
38
+ if (!this.databases.has(name)) {
39
+ const dbPath = path.join(this.dataDir, `${name}.sawit`);
40
+ const db = new SawitDB(dbPath, { wal: this.walConfig });
41
+ this.databases.set(name, db);
42
+ }
43
+ return this.databases.get(name);
44
+ }
45
+
46
+ exists(name) {
47
+ const dbPath = path.join(this.dataDir, `${name}.sawit`);
48
+ return fs.existsSync(dbPath);
49
+ }
50
+
51
+ create(name) {
52
+ this.validateName(name);
53
+ return this.getOrCreate(name);
54
+ }
55
+
56
+ drop(name) {
57
+ this.validateName(name);
58
+ const dbPath = path.join(this.dataDir, `${name}.sawit`);
59
+ if (!fs.existsSync(dbPath)) {
60
+ throw new Error(`Database '${name}' does not exist`);
61
+ }
62
+
63
+ // Close if open
64
+ if (this.databases.has(name)) {
65
+ this.databases.get(name).close();
66
+ this.databases.delete(name);
67
+ }
68
+
69
+ // Delete file
70
+ fs.unlinkSync(dbPath);
71
+ }
72
+
73
+ list() {
74
+ return fs.readdirSync(this.dataDir)
75
+ .filter((file) => file.endsWith('.sawit'))
76
+ .map((file) => file.replace('.sawit', ''));
77
+ }
78
+
79
+ closeAll() {
80
+ for (const [name, db] of this.databases) {
81
+ try {
82
+ console.log(`[Server] Closing database: ${name}`);
83
+ db.close();
84
+ } catch (e) {
85
+ console.error(`[Server] Error closing database ${name}:`, e.message);
86
+ }
87
+ }
88
+ this.databases.clear();
89
+ }
90
+ }
91
+
92
+ module.exports = DatabaseRegistry;
@@ -0,0 +1,92 @@
1
+ const crypto = require('crypto');
2
+
3
+ class AuthManager {
4
+ constructor(server) {
5
+ this.server = server;
6
+ this.authConfig = server.config.auth || null;
7
+ }
8
+
9
+ isEnabled() {
10
+ return !!this.authConfig;
11
+ }
12
+
13
+ /**
14
+ * Hash a password using SHA-256 with salt
15
+ * @param {string} password - Plain text password
16
+ * @param {string} salt - Optional salt (generated if not provided)
17
+ * @returns {string} - Format: salt:hash
18
+ */
19
+ static hashPassword(password, salt = null) {
20
+ salt = salt || crypto.randomBytes(16).toString('hex');
21
+ const hash = crypto
22
+ .createHash('sha256')
23
+ .update(salt + password)
24
+ .digest('hex');
25
+ return `${salt}:${hash}`;
26
+ }
27
+
28
+ /**
29
+ * Verify a password against a stored hash using timing-safe comparison
30
+ * @param {string} password - Plain text password to verify
31
+ * @param {string} storedHash - Stored hash in format "salt:hash" or plain text
32
+ * @returns {boolean}
33
+ */
34
+ verifyPassword(password, storedHash) {
35
+ // Support both hashed (salt:hash) and legacy plaintext passwords
36
+ if (storedHash.includes(':')) {
37
+ const [salt, hash] = storedHash.split(':');
38
+ const computedHash = crypto
39
+ .createHash('sha256')
40
+ .update(salt + password)
41
+ .digest('hex');
42
+ // Timing-safe comparison to prevent timing attacks
43
+ try {
44
+ return crypto.timingSafeEqual(
45
+ Buffer.from(hash, 'hex'),
46
+ Buffer.from(computedHash, 'hex')
47
+ );
48
+ } catch (e) {
49
+ return false;
50
+ }
51
+ } else {
52
+ // Legacy plaintext comparison with timing-safe method
53
+ // Pad both strings to same length to prevent length-based timing attacks
54
+ const maxLen = Math.max(password.length, storedHash.length);
55
+ const paddedInput = password.padEnd(maxLen, '\0');
56
+ const paddedStored = storedHash.padEnd(maxLen, '\0');
57
+ try {
58
+ return crypto.timingSafeEqual(
59
+ Buffer.from(paddedInput),
60
+ Buffer.from(paddedStored)
61
+ );
62
+ } catch (e) {
63
+ return false;
64
+ }
65
+ }
66
+ }
67
+
68
+ handleAuth(socket, payload, session) {
69
+ const { username, password } = payload;
70
+
71
+ if (!this.isEnabled()) {
72
+ session.setAuth(true);
73
+ return this.server.sendResponse(socket, {
74
+ type: 'auth_success',
75
+ message: 'No authentication required'
76
+ });
77
+ }
78
+
79
+ const storedPassword = this.authConfig[username];
80
+ if (storedPassword && this.verifyPassword(password, storedPassword)) {
81
+ session.setAuth(true);
82
+ this.server.sendResponse(socket, {
83
+ type: 'auth_success',
84
+ message: 'Authentication successful'
85
+ });
86
+ } else {
87
+ this.server.sendError(socket, 'Invalid credentials');
88
+ }
89
+ }
90
+ }
91
+
92
+ module.exports = AuthManager;
@@ -0,0 +1,278 @@
1
+ class RequestRouter {
2
+ constructor(server) {
3
+ this.server = server;
4
+ this.dbRegistry = server.dbRegistry;
5
+ this.authManager = server.authManager;
6
+ }
7
+
8
+ handle(socket, request, session) {
9
+ const { type, payload } = request;
10
+
11
+ // Authentication check
12
+ if (this.authManager.isEnabled() && !session.authenticated && type !== 'auth') {
13
+ return this.server.sendError(socket, 'Authentication required');
14
+ }
15
+
16
+ switch (type) {
17
+ case 'auth':
18
+ this.authManager.handleAuth(socket, payload, session);
19
+ break;
20
+
21
+ case 'use':
22
+ this.handleUseDatabase(socket, payload, session);
23
+ break;
24
+
25
+ case 'query':
26
+ this.handleQuery(socket, payload, session);
27
+ break;
28
+
29
+ case 'ping':
30
+ this.server.sendResponse(socket, { type: 'pong', timestamp: Date.now() });
31
+ break;
32
+
33
+ case 'list_databases':
34
+ this.handleListDatabases(socket);
35
+ break;
36
+
37
+ case 'drop_database':
38
+ this.handleDropDatabase(socket, payload, session);
39
+ break;
40
+
41
+ case 'stats':
42
+ this.handleStats(socket);
43
+ break;
44
+
45
+ default:
46
+ this.server.sendError(socket, `Unknown request type: ${type}`);
47
+ }
48
+ }
49
+
50
+ handleUseDatabase(socket, payload, session) {
51
+ const { database } = payload;
52
+
53
+ if (!database || typeof database !== 'string') {
54
+ return this.server.sendError(socket, 'Invalid database name');
55
+ }
56
+
57
+ // Validate database name (alphanumeric, underscore, dash)
58
+ if (!/^[a-zA-Z0-9_-]+$/.test(database)) {
59
+ return this.server.sendError(
60
+ socket,
61
+ 'Database name can only contain letters, numbers, underscore, and dash'
62
+ );
63
+ }
64
+
65
+ try {
66
+ this.dbRegistry.getOrCreate(database);
67
+ session.setDatabase(database);
68
+ this.server.sendResponse(socket, {
69
+ type: 'use_success',
70
+ database,
71
+ message: `Switched to database '${database}'`
72
+ });
73
+ } catch (err) {
74
+ this.server.sendError(socket, `Failed to use database: ${err.message}`);
75
+ }
76
+ }
77
+
78
+ async handleQuery(socket, payload, session) {
79
+ const { query, params } = payload;
80
+ const startTime = Date.now();
81
+
82
+ // --- Intercept Server-Level Commands (Wilayah Management) ---
83
+ const qUpper = query.trim().toUpperCase();
84
+
85
+ // 1. LIHAT WILAYAH
86
+ if (qUpper === 'LIHAT WILAYAH' || qUpper === 'SHOW DATABASES') {
87
+ try {
88
+ const list = this.dbRegistry.list()
89
+ .map((f) => `- ${f}`)
90
+ .join('\n');
91
+ const result = `Daftar Wilayah:\n${list}`;
92
+
93
+ return this.server.sendResponse(socket, {
94
+ type: 'query_result',
95
+ result,
96
+ query,
97
+ executionTime: Date.now() - startTime
98
+ });
99
+ } catch (err) {
100
+ return this.server.sendError(socket, `Gagal melihat wilayah: ${err.message}`);
101
+ }
102
+ }
103
+
104
+ // 2. BUKA WILAYAH / CREATE DATABASE
105
+ if (qUpper.startsWith('BUKA WILAYAH') || qUpper.startsWith('CREATE DATABASE')) {
106
+ const parts = query.trim().split(/\s+/);
107
+ // Index 2 is name (BUKA WILAYAH name OR CREATE DATABASE name)
108
+ if (parts.length < 3) {
109
+ return this.server.sendError(socket, 'Syntax: BUKA WILAYAH [nama]');
110
+ }
111
+ const dbName = parts[2];
112
+
113
+ try {
114
+ if (!/^[a-zA-Z0-9_-]+$/.test(dbName)) {
115
+ return this.server.sendError(socket, 'Nama wilayah hanya boleh huruf, angka, _ dan -');
116
+ }
117
+
118
+ if (this.dbRegistry.exists(dbName)) {
119
+ return this.server.sendResponse(socket, {
120
+ type: 'query_result',
121
+ result: `Wilayah '${dbName}' sudah ada.`,
122
+ query,
123
+ executionTime: Date.now() - startTime
124
+ });
125
+ }
126
+
127
+ this.dbRegistry.create(dbName);
128
+ return this.server.sendResponse(socket, {
129
+ type: 'query_result',
130
+ result: `Wilayah '${dbName}' berhasil dibuka.`,
131
+ query,
132
+ executionTime: Date.now() - startTime
133
+ });
134
+ } catch (err) {
135
+ return this.server.sendError(socket, `Gagal membuka wilayah: ${err.message}`);
136
+ }
137
+ }
138
+
139
+ // 3. MASUK WILAYAH / USE
140
+ if (qUpper.startsWith('MASUK WILAYAH') || qUpper.startsWith('USE ')) {
141
+ const parts = query.trim().split(/\s+/);
142
+ const dbName = parts[1] === 'WILAYAH' ? parts[2] : parts[1]; // Handle MASUK WILAYAH vs USE
143
+
144
+ if (!dbName) return this.server.sendError(socket, 'Syntax: MASUK WILAYAH [nama]');
145
+
146
+ if (!this.dbRegistry.exists(dbName)) {
147
+ return this.server.sendError(socket, `Wilayah '${dbName}' tidak ditemukan.`);
148
+ }
149
+
150
+ session.setDatabase(dbName);
151
+ return this.server.sendResponse(socket, {
152
+ type: 'query_result',
153
+ result: `Selamat datang di wilayah '${dbName}'.`,
154
+ query,
155
+ executionTime: Date.now() - startTime
156
+ });
157
+ }
158
+
159
+ // 4. BAKAR WILAYAH / DROP DATABASE
160
+ if (qUpper.startsWith('BAKAR WILAYAH') || qUpper.startsWith('DROP DATABASE')) {
161
+ const parts = query.trim().split(/\s+/);
162
+ const dbName = parts[1] === 'WILAYAH' ? parts[2] : parts[parts.length - 1]; // BAKAR WILAYAH name vs DROP DATABASE name
163
+ // Simple split logic might fail on extra spaces, but good enough for now.
164
+ // Strict parsing:
165
+ const nameIndex = qUpper.startsWith('BAKAR') ? 2 : 2;
166
+ const targetName = parts[nameIndex];
167
+
168
+ if (!targetName) return this.server.sendError(socket, 'Syntax: BAKAR WILAYAH [nama]');
169
+
170
+ try {
171
+ this.dbRegistry.drop(targetName);
172
+ if (session.currentDatabase === targetName) {
173
+ session.setDatabase(null);
174
+ }
175
+ return this.server.sendResponse(socket, {
176
+ type: 'query_result',
177
+ result: `Wilayah '${targetName}' telah hangus terbakar.`,
178
+ query,
179
+ executionTime: Date.now() - startTime
180
+ });
181
+ } catch (err) {
182
+ return this.server.sendError(socket, `Gagal membakar wilayah: ${err.message}`);
183
+ }
184
+ }
185
+
186
+ // --- End Intercept ---
187
+
188
+ if (!session.currentDatabase) {
189
+ return this.server.sendError(
190
+ socket,
191
+ 'Anda belum masuk wilayah manapun. Gunakan: MASUK WILAYAH [nama]'
192
+ );
193
+ }
194
+
195
+ try {
196
+ let result;
197
+ if (this.server.threadPool) {
198
+ // Offload to Worker Thread (assuming ThreadPool interface matches)
199
+ const dbPath = this.dbRegistry.get(session.currentDatabase).pager.filePath; // Hack to get path or use Registry
200
+ // But Registry creates path.
201
+ // Let's rely on Registry.get(name) returning db instance.
202
+ // But ThreadPool needs path.
203
+ const fullPath = require('path').join(this.dbRegistry.dataDir, `${session.currentDatabase}.sawit`);
204
+ result = await this.server.threadPool.execute(fullPath, query, this.server.dbRegistry.walConfig);
205
+ } else {
206
+ // Local Execution
207
+ const db = this.dbRegistry.get(session.currentDatabase);
208
+ result = await Promise.resolve(db.query(query, params));
209
+ }
210
+
211
+ const duration = Date.now() - startTime;
212
+ this.server.stats.totalQueries++;
213
+ this.server.log('debug', `Query: ${query}`, { duration });
214
+
215
+ this.server.sendResponse(socket, {
216
+ type: 'query_result',
217
+ result,
218
+ query,
219
+ executionTime: duration
220
+ });
221
+ } catch (err) {
222
+ this.server.log('error', `Query failed: ${err.message}`);
223
+ this.server.stats.errors++;
224
+ this.server.sendError(socket, `Query error: ${err.message}`);
225
+ }
226
+ }
227
+
228
+ handleListDatabases(socket) {
229
+ try {
230
+ const databases = this.dbRegistry.list();
231
+ this.server.sendResponse(socket, {
232
+ type: 'database_list',
233
+ databases,
234
+ count: databases.length
235
+ });
236
+ } catch (err) {
237
+ this.server.sendError(socket, `Failed to list databases: ${err.message}`);
238
+ }
239
+ }
240
+
241
+ handleDropDatabase(socket, payload, session) {
242
+ const { database } = payload;
243
+ if (!database) return this.server.sendError(socket, 'Database name required');
244
+
245
+ try {
246
+ this.dbRegistry.drop(database);
247
+ if (session.currentDatabase === database) {
248
+ session.setDatabase(null);
249
+ }
250
+ this.server.sendResponse(socket, {
251
+ type: 'drop_success',
252
+ database,
253
+ message: `Database '${database}' has been burned (dropped)`
254
+ });
255
+ } catch (err) {
256
+ this.server.sendError(socket, `Failed to drop database: ${err.message}`);
257
+ }
258
+ }
259
+
260
+ handleStats(socket) {
261
+ const uptime = Date.now() - this.server.stats.startTime;
262
+ const stats = {
263
+ ...this.server.stats,
264
+ uptime,
265
+ uptimeFormatted: this.server.formatUptime(uptime),
266
+ databases: this.dbRegistry.databases.size,
267
+ memoryUsage: process.memoryUsage(),
268
+ workers: this.server.threadPool ? this.server.threadPool.getStats() : null,
269
+ };
270
+
271
+ this.server.sendResponse(socket, {
272
+ type: 'stats',
273
+ stats
274
+ });
275
+ }
276
+ }
277
+
278
+ module.exports = RequestRouter;
@@ -0,0 +1,19 @@
1
+ class ClientSession {
2
+ constructor(socket, clientId) {
3
+ this.socket = socket;
4
+ this.clientId = clientId;
5
+ this.authenticated = false;
6
+ this.currentDatabase = null;
7
+ this.connectedAt = Date.now();
8
+ }
9
+
10
+ setAuth(isAuthenticated) {
11
+ this.authenticated = isAuthenticated;
12
+ }
13
+
14
+ setDatabase(databaseName) {
15
+ this.currentDatabase = databaseName;
16
+ }
17
+ }
18
+
19
+ module.exports = ClientSession;
@@ -0,0 +1,183 @@
1
+ const BTreeIndex = require('../modules/BTreeIndex');
2
+
3
+ class IndexManager {
4
+ constructor(db) {
5
+ this.db = db;
6
+ // Indexes are stored in db.indexes for now to maintain state
7
+ // access via db.indexes
8
+ }
9
+
10
+ get indexes() {
11
+ return this.db.indexes;
12
+ }
13
+
14
+ createIndex(table, field) {
15
+ const entry = this.db.tableManager
16
+ ? this.db.tableManager.findTableEntry(table)
17
+ : this.db._findTableEntry(table);
18
+
19
+ if (!entry) throw new Error(`Kebun '${table}' tidak ditemukan.`);
20
+
21
+ const indexKey = `${table}.${field}`;
22
+ if (this.indexes.has(indexKey)) {
23
+ return `Indeks pada '${table}.${field}' sudah ada.`;
24
+ }
25
+
26
+ // Create index
27
+ const index = new BTreeIndex();
28
+ index.name = indexKey;
29
+ index.keyField = field;
30
+
31
+ // Build index from existing data
32
+ // We need a way to scan the table.
33
+ // If TableManager has no scan capability yet, use db._select or db._scanTable fallback
34
+ // Ideally this logic belongs here or uses a Scanner service.
35
+ // For now, assuming db._select or similar is available or we replicate scan logic.
36
+ // Actually, db._select depends on QueryExecutor now?
37
+ // Let's rely on db._select being available (maybe delegating to SelectExecutor!)
38
+ // Circular dependency risk: SelectExecutor needs IndexManager, IndexManager needs SelectExecutor logic?
39
+ // Index building needs a simple table scan.
40
+ // Let's imply we can use db._scanTable directly if available.
41
+
42
+ let allRecords = [];
43
+ if (this.db._scanTable) {
44
+ // Use low-level scan if available, WITH HINTS to capture _pageId
45
+ allRecords = this.db._scanTable(entry, null, null, true);
46
+ } else if (this.db.query) {
47
+ // Use high level query? Risky.
48
+ // Assume _scanTable is preserved or moved to a Scanner.
49
+ // For now, let's assume WowoEngine keeps _scanTable or we move it to TableScanner.
50
+ // Let's use `db._scanTable` assuming it exists.
51
+ allRecords = this.db._scanTable(entry, null, null, true);
52
+ }
53
+
54
+ for (const record of allRecords) {
55
+ if (record.hasOwnProperty(field)) {
56
+ index.insert(record[field], record);
57
+ }
58
+ }
59
+
60
+ this.indexes.set(indexKey, index);
61
+
62
+ // PERSISTENCE: Save to _indexes table
63
+ try {
64
+ // Use db._insert to persist request.
65
+ // If InsertExecutor delegates to db._insert, or db._insert uses InsertExecutor?
66
+ // Safer to allow db._insert if it resolves to the executor.
67
+ this.db._insert('_indexes', { table, field });
68
+ } catch (e) {
69
+ console.error("Failed to persist index definition", e);
70
+ }
71
+
72
+ return `Indeks dibuat pada '${table}.${field}' (${allRecords.length} records indexed)`;
73
+ }
74
+
75
+ updateIndexes(table, newObj, oldObj) {
76
+ // If oldObj is null, it's an INSERT. If newObj is null, it's a DELETE. Both? Update.
77
+
78
+ for (const [indexKey, index] of this.indexes) {
79
+ const [tbl, field] = indexKey.split('.');
80
+ if (tbl !== table) continue; // Wrong table
81
+
82
+ // 1. Remove old value from index (if exists and changed)
83
+ if (oldObj && oldObj.hasOwnProperty(field)) {
84
+ // Only remove if value changed OR it's a delete (newObj is null)
85
+ // If update, check if value diff
86
+ if (!newObj || newObj[field] !== oldObj[field]) {
87
+ index.delete(oldObj[field]);
88
+ }
89
+ }
90
+
91
+ // 2. Insert new value (if exists)
92
+ if (newObj && newObj.hasOwnProperty(field)) {
93
+ // Only insert if it's new OR value changed
94
+ if (!oldObj || newObj[field] !== oldObj[field]) {
95
+ index.insert(newObj[field], newObj);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ removeFromIndexes(table, data) {
102
+ for (const [indexKey, index] of this.indexes) {
103
+ const [tbl, field] = indexKey.split('.');
104
+ if (tbl === table && data.hasOwnProperty(field)) {
105
+ index.delete(data[field]); // Basic deletion from B-Tree
106
+ }
107
+ }
108
+ }
109
+
110
+ showIndexes(table) {
111
+ if (table) {
112
+ const indexes = [];
113
+ for (const [key, index] of this.indexes) {
114
+ if (key.startsWith(table + '.')) {
115
+ indexes.push(index.stats());
116
+ }
117
+ }
118
+ return indexes.length > 0 ? indexes : `Tidak ada indeks pada '${table}'`;
119
+ } else {
120
+ const allIndexes = [];
121
+ for (const index of this.indexes.values()) {
122
+ allIndexes.push(index.stats());
123
+ }
124
+ return allIndexes;
125
+ }
126
+ }
127
+
128
+ // Initial loader
129
+ loadIndexes() {
130
+ if (!this.db._select) return; // Wait until ready?
131
+
132
+ // Re-implement load indexes to include Hints
133
+ // We need to read _indexes.
134
+ // db._select might use SelectExecutor, which uses IndexManager...
135
+ // Bootstrapping issue.
136
+ // We should use low-level scan for bootstrapping.
137
+
138
+ let indexRecords = [];
139
+ try {
140
+ // Manual scan of _indexes
141
+ const entry = this.db.tableManager
142
+ ? this.db.tableManager.findTableEntry('_indexes')
143
+ : this.db._findTableEntry('_indexes');
144
+
145
+ if (entry && this.db._scanTable) {
146
+ indexRecords = this.db._scanTable(entry, null);
147
+ }
148
+ } catch (e) { return; }
149
+
150
+ for (const rec of indexRecords) {
151
+ const table = rec.table;
152
+ const field = rec.field;
153
+ const indexKey = `${table}.${field}`;
154
+
155
+ if (!this.indexes.has(indexKey)) {
156
+ const index = new BTreeIndex();
157
+ index.name = indexKey;
158
+ index.keyField = field;
159
+
160
+ try {
161
+ // Fetch all records with Hints
162
+ const entry = this.db.tableManager
163
+ ? this.db.tableManager.findTableEntry(table)
164
+ : this.db._findTableEntry(table);
165
+
166
+ if (entry && this.db._scanTable) {
167
+ const allRecords = this.db._scanTable(entry, null, null, true); // true for Hints
168
+ for (const record of allRecords) {
169
+ if (record.hasOwnProperty(field)) {
170
+ index.insert(record[field], record);
171
+ }
172
+ }
173
+ this.indexes.set(indexKey, index);
174
+ }
175
+ } catch (e) {
176
+ console.error(`Failed to rebuild index ${indexKey}: ${e.message}`);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ module.exports = IndexManager;
@@ -0,0 +1,11 @@
1
+ class QueryExecutor {
2
+ constructor(db) {
3
+ this.db = db;
4
+ }
5
+
6
+ execute(cmd) {
7
+ throw new Error("Method 'execute' must be implemented");
8
+ }
9
+ }
10
+
11
+ module.exports = QueryExecutor;