@wowoengine/sawitdb 2.4.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/LICENSE +21 -0
- package/README.md +245 -0
- package/bin/sawit-server.js +68 -0
- package/cli/local.js +49 -0
- package/cli/remote.js +165 -0
- package/docs/index.html +727 -0
- package/docs/sawitdb.jpg +0 -0
- package/package.json +63 -0
- package/src/SawitClient.js +265 -0
- package/src/SawitServer.js +602 -0
- package/src/WowoEngine.js +539 -0
- package/src/modules/BTreeIndex.js +282 -0
- package/src/modules/Pager.js +70 -0
- package/src/modules/QueryParser.js +569 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
const Pager = require('./modules/Pager');
|
|
2
|
+
const QueryParser = require('./modules/QueryParser');
|
|
3
|
+
const BTreeIndex = require('./modules/BTreeIndex');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SawitDB implements the Logic over the Pager
|
|
7
|
+
*/
|
|
8
|
+
class SawitDB {
|
|
9
|
+
constructor(filePath) {
|
|
10
|
+
this.pager = new Pager(filePath);
|
|
11
|
+
this.indexes = new Map(); // Map of 'tableName.fieldName' -> BTreeIndex
|
|
12
|
+
this.parser = new QueryParser();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
query(queryString, params) {
|
|
16
|
+
// Parse the query into a command object
|
|
17
|
+
const cmd = this.parser.parse(queryString, params);
|
|
18
|
+
|
|
19
|
+
if (cmd.type === 'EMPTY') return "";
|
|
20
|
+
if (cmd.type === 'ERROR') return `Error: ${cmd.message}`;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
switch (cmd.type) {
|
|
24
|
+
case 'CREATE_TABLE':
|
|
25
|
+
return this._createTable(cmd.table);
|
|
26
|
+
|
|
27
|
+
case 'SHOW_TABLES':
|
|
28
|
+
return this._showTables();
|
|
29
|
+
|
|
30
|
+
case 'SHOW_INDEXES':
|
|
31
|
+
return this._showIndexes(cmd.table); // cmd.table can be null
|
|
32
|
+
|
|
33
|
+
case 'INSERT':
|
|
34
|
+
return this._insert(cmd.table, cmd.data);
|
|
35
|
+
|
|
36
|
+
case 'SELECT':
|
|
37
|
+
// Map generic generic Select Logic
|
|
38
|
+
const rows = this._select(cmd.table, cmd.criteria, cmd.sort, cmd.limit, cmd.offset);
|
|
39
|
+
// Projection handled inside _select or here?
|
|
40
|
+
// _select now handles SORT/LIMIT/OFFSET which acts on All Rows (mostly).
|
|
41
|
+
// Projection should be last.
|
|
42
|
+
|
|
43
|
+
if (cmd.cols.length === 1 && cmd.cols[0] === '*') return rows;
|
|
44
|
+
|
|
45
|
+
return rows.map(r => {
|
|
46
|
+
const newRow = {};
|
|
47
|
+
cmd.cols.forEach(c => newRow[c] = r[c]);
|
|
48
|
+
return newRow;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
case 'DELETE':
|
|
52
|
+
return this._delete(cmd.table, cmd.criteria);
|
|
53
|
+
|
|
54
|
+
case 'UPDATE':
|
|
55
|
+
return this._update(cmd.table, cmd.updates, cmd.criteria);
|
|
56
|
+
|
|
57
|
+
case 'DROP_TABLE':
|
|
58
|
+
return this._dropTable(cmd.table);
|
|
59
|
+
|
|
60
|
+
case 'CREATE_INDEX':
|
|
61
|
+
return this._createIndex(cmd.table, cmd.field);
|
|
62
|
+
|
|
63
|
+
case 'AGGREGATE':
|
|
64
|
+
return this._aggregate(cmd.table, cmd.func, cmd.field, cmd.criteria, cmd.groupBy);
|
|
65
|
+
|
|
66
|
+
default:
|
|
67
|
+
return `Perintah tidak dikenal atau belum diimplementasikan di Engine Refactor.`;
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return `Error: ${e.message}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Core Logic ---
|
|
75
|
+
|
|
76
|
+
_findTableEntry(name) {
|
|
77
|
+
const p0 = this.pager.readPage(0);
|
|
78
|
+
const numTables = p0.readUInt32LE(8);
|
|
79
|
+
let offset = 12;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < numTables; i++) {
|
|
82
|
+
const tName = p0.toString('utf8', offset, offset + 32).replace(/\0/g, '');
|
|
83
|
+
if (tName === name) {
|
|
84
|
+
return {
|
|
85
|
+
index: i,
|
|
86
|
+
offset: offset,
|
|
87
|
+
startPage: p0.readUInt32LE(offset + 32),
|
|
88
|
+
lastPage: p0.readUInt32LE(offset + 36)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
offset += 40;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_showTables() {
|
|
97
|
+
const p0 = this.pager.readPage(0);
|
|
98
|
+
const numTables = p0.readUInt32LE(8);
|
|
99
|
+
const tables = [];
|
|
100
|
+
let offset = 12;
|
|
101
|
+
for (let i = 0; i < numTables; i++) {
|
|
102
|
+
const tName = p0.toString('utf8', offset, offset + 32).replace(/\0/g, '');
|
|
103
|
+
tables.push(tName);
|
|
104
|
+
offset += 40;
|
|
105
|
+
}
|
|
106
|
+
return tables;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_createTable(name) {
|
|
110
|
+
if (!name) throw new Error("Nama kebun tidak boleh kosong");
|
|
111
|
+
if (name.length > 32) throw new Error("Nama kebun max 32 karakter");
|
|
112
|
+
if (this._findTableEntry(name)) return `Kebun '${name}' sudah ada.`;
|
|
113
|
+
|
|
114
|
+
const p0 = this.pager.readPage(0);
|
|
115
|
+
const numTables = p0.readUInt32LE(8);
|
|
116
|
+
let offset = 12 + (numTables * 40);
|
|
117
|
+
if (offset + 40 > Pager.PAGE_SIZE) throw new Error("Lahan penuh (Page 0 full)");
|
|
118
|
+
|
|
119
|
+
const newPageId = this.pager.allocPage();
|
|
120
|
+
|
|
121
|
+
const nameBuf = Buffer.alloc(32);
|
|
122
|
+
nameBuf.write(name);
|
|
123
|
+
nameBuf.copy(p0, offset);
|
|
124
|
+
|
|
125
|
+
p0.writeUInt32LE(newPageId, offset + 32);
|
|
126
|
+
p0.writeUInt32LE(newPageId, offset + 36);
|
|
127
|
+
p0.writeUInt32LE(numTables + 1, 8);
|
|
128
|
+
|
|
129
|
+
this.pager.writePage(0, p0);
|
|
130
|
+
return `Kebun '${name}' telah dibuka.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_dropTable(name) {
|
|
134
|
+
// Simple Drop: Remove from directory. Pages leak (fragmentation) but that's typical for simple heap files.
|
|
135
|
+
const entry = this._findTableEntry(name);
|
|
136
|
+
if (!entry) return `Kebun '${name}' tidak ditemukan.`;
|
|
137
|
+
|
|
138
|
+
const p0 = this.pager.readPage(0);
|
|
139
|
+
const numTables = p0.readUInt32LE(8);
|
|
140
|
+
|
|
141
|
+
// Move last entry to this spot to fill gap
|
|
142
|
+
if (numTables > 1 && entry.index < numTables - 1) {
|
|
143
|
+
const lastOffset = 12 + ((numTables - 1) * 40);
|
|
144
|
+
const lastEntryBuf = p0.slice(lastOffset, lastOffset + 40);
|
|
145
|
+
lastEntryBuf.copy(p0, entry.offset);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Clear last spot
|
|
149
|
+
const lastOffset = 12 + ((numTables - 1) * 40);
|
|
150
|
+
p0.fill(0, lastOffset, lastOffset + 40);
|
|
151
|
+
|
|
152
|
+
p0.writeUInt32LE(numTables - 1, 8);
|
|
153
|
+
this.pager.writePage(0, p0);
|
|
154
|
+
|
|
155
|
+
return `Kebun '${name}' telah dibakar (Drop).`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_updateTableLastPage(name, newLastPageId) {
|
|
159
|
+
const entry = this._findTableEntry(name);
|
|
160
|
+
if (!entry) throw new Error("Internal Error: Table missing for update");
|
|
161
|
+
const p0 = this.pager.readPage(0);
|
|
162
|
+
p0.writeUInt32LE(newLastPageId, entry.offset + 36);
|
|
163
|
+
this.pager.writePage(0, p0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_insert(table, data) {
|
|
167
|
+
if (!data || Object.keys(data).length === 0) {
|
|
168
|
+
throw new Error("Data kosong / fiktif? Ini melanggar integritas (Korupsi Data).");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const entry = this._findTableEntry(table);
|
|
172
|
+
if (!entry) throw new Error(`Kebun '${table}' tidak ditemukan.`);
|
|
173
|
+
|
|
174
|
+
const dataStr = JSON.stringify(data);
|
|
175
|
+
const dataBuf = Buffer.from(dataStr, 'utf8');
|
|
176
|
+
const recordLen = dataBuf.length;
|
|
177
|
+
const totalLen = 2 + recordLen;
|
|
178
|
+
|
|
179
|
+
let currentPageId = entry.lastPage;
|
|
180
|
+
let pData = this.pager.readPage(currentPageId);
|
|
181
|
+
let freeOffset = pData.readUInt16LE(6);
|
|
182
|
+
|
|
183
|
+
if (freeOffset + totalLen > Pager.PAGE_SIZE) {
|
|
184
|
+
const newPageId = this.pager.allocPage();
|
|
185
|
+
pData.writeUInt32LE(newPageId, 0);
|
|
186
|
+
this.pager.writePage(currentPageId, pData);
|
|
187
|
+
|
|
188
|
+
currentPageId = newPageId;
|
|
189
|
+
pData = this.pager.readPage(currentPageId);
|
|
190
|
+
freeOffset = pData.readUInt16LE(6);
|
|
191
|
+
this._updateTableLastPage(table, currentPageId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pData.writeUInt16LE(recordLen, freeOffset);
|
|
195
|
+
dataBuf.copy(pData, freeOffset + 2);
|
|
196
|
+
|
|
197
|
+
const count = pData.readUInt16LE(4);
|
|
198
|
+
pData.writeUInt16LE(count + 1, 4);
|
|
199
|
+
pData.writeUInt16LE(freeOffset + totalLen, 6);
|
|
200
|
+
|
|
201
|
+
this.pager.writePage(currentPageId, pData);
|
|
202
|
+
|
|
203
|
+
// Update Indexes if any
|
|
204
|
+
this._updateIndexes(table, data);
|
|
205
|
+
|
|
206
|
+
return "Bibit tertanam.";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_updateIndexes(table, data) {
|
|
210
|
+
for (const [indexKey, index] of this.indexes) {
|
|
211
|
+
const [tbl, field] = indexKey.split('.');
|
|
212
|
+
if (tbl === table && data.hasOwnProperty(field)) {
|
|
213
|
+
index.insert(data[field], data);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_checkMatch(obj, criteria) {
|
|
219
|
+
if (!criteria) return true;
|
|
220
|
+
|
|
221
|
+
// Handle compound conditions (AND/OR)
|
|
222
|
+
if (criteria.type === 'compound') {
|
|
223
|
+
let result = true;
|
|
224
|
+
let currentLogic = 'AND'; // Initial logic is irrelevant for first item, but technically AND identity is true
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < criteria.conditions.length; i++) {
|
|
227
|
+
const cond = criteria.conditions[i];
|
|
228
|
+
const matches = this._checkSingleCondition(obj, cond);
|
|
229
|
+
|
|
230
|
+
if (i === 0) {
|
|
231
|
+
result = matches;
|
|
232
|
+
} else {
|
|
233
|
+
if (cond.logic === 'OR') {
|
|
234
|
+
result = result || matches;
|
|
235
|
+
} else {
|
|
236
|
+
result = result && matches;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Simple single condition
|
|
244
|
+
return this._checkSingleCondition(obj, criteria);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_checkSingleCondition(obj, criteria) {
|
|
248
|
+
const val = obj[criteria.key];
|
|
249
|
+
const target = criteria.val;
|
|
250
|
+
switch (criteria.op) {
|
|
251
|
+
case '=': return val == target;
|
|
252
|
+
case '!=': return val != target;
|
|
253
|
+
case '>': return val > target;
|
|
254
|
+
case '<': return val < target;
|
|
255
|
+
case '>=': return val >= target;
|
|
256
|
+
case '<=': return val <= target;
|
|
257
|
+
case 'IN': return Array.isArray(target) && target.includes(val);
|
|
258
|
+
case 'NOT IN': return Array.isArray(target) && !target.includes(val);
|
|
259
|
+
case 'LIKE':
|
|
260
|
+
// Simple regex-like match. Handle % for wildcards.
|
|
261
|
+
const regexStr = '^' + target.replace(/%/g, '.*') + '$';
|
|
262
|
+
const re = new RegExp(regexStr, 'i');
|
|
263
|
+
return re.test(String(val));
|
|
264
|
+
case 'BETWEEN':
|
|
265
|
+
return val >= target[0] && val <= target[1];
|
|
266
|
+
case 'IS NULL':
|
|
267
|
+
return val === null || val === undefined;
|
|
268
|
+
case 'IS NOT NULL':
|
|
269
|
+
return val !== null && val !== undefined;
|
|
270
|
+
default: return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_select(table, criteria, sort, limit, offsetCount) {
|
|
275
|
+
const entry = this._findTableEntry(table);
|
|
276
|
+
if (!entry) throw new Error(`Kebun '${table}' tidak ditemukan.`);
|
|
277
|
+
|
|
278
|
+
// Optimization: If Index exists and criteria is simple '='
|
|
279
|
+
// Only valid if NO sorting is needed, or if index matches sort (not implemented yet).
|
|
280
|
+
// For now, always do full scan if sort/fancy criteria involved
|
|
281
|
+
// OR rely on in-memory sort after index fetch.
|
|
282
|
+
|
|
283
|
+
let results = [];
|
|
284
|
+
|
|
285
|
+
// --- 1. Fetch Candidates ---
|
|
286
|
+
if (criteria && !criteria.type && criteria.op === '=' && !sort) {
|
|
287
|
+
// Index optimization path - only if no sort (for safety)
|
|
288
|
+
// ... (Index Logic) ...
|
|
289
|
+
const indexKey = `${table}.${criteria.key}`;
|
|
290
|
+
if (this.indexes.has(indexKey)) {
|
|
291
|
+
const index = this.indexes.get(indexKey);
|
|
292
|
+
results = index.search(criteria.val);
|
|
293
|
+
} else {
|
|
294
|
+
results = this._scanTable(entry, criteria);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
results = this._scanTable(entry, criteria);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- 2. Sorting (In-Memory) ---
|
|
301
|
+
if (sort) {
|
|
302
|
+
// sort = { key, dir: 'ASC' | 'DESC' }
|
|
303
|
+
results.sort((a, b) => {
|
|
304
|
+
const valA = a[sort.key];
|
|
305
|
+
const valB = b[sort.key];
|
|
306
|
+
if (valA < valB) return sort.dir === 'asc' ? -1 : 1;
|
|
307
|
+
if (valA > valB) return sort.dir === 'asc' ? 1 : -1;
|
|
308
|
+
return 0;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- 3. Limit & Offset ---
|
|
313
|
+
let start = 0;
|
|
314
|
+
let end = results.length;
|
|
315
|
+
|
|
316
|
+
if (offsetCount) start = offsetCount;
|
|
317
|
+
if (limit) end = start + limit;
|
|
318
|
+
|
|
319
|
+
return results.slice(start, end);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_scanTable(entry, criteria) {
|
|
323
|
+
let currentPageId = entry.startPage;
|
|
324
|
+
const results = [];
|
|
325
|
+
|
|
326
|
+
while (currentPageId !== 0) {
|
|
327
|
+
const pData = this.pager.readPage(currentPageId);
|
|
328
|
+
const count = pData.readUInt16LE(4);
|
|
329
|
+
let offset = 8;
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < count; i++) {
|
|
332
|
+
const len = pData.readUInt16LE(offset);
|
|
333
|
+
const jsonStr = pData.toString('utf8', offset + 2, offset + 2 + len);
|
|
334
|
+
try {
|
|
335
|
+
const obj = JSON.parse(jsonStr);
|
|
336
|
+
if (this._checkMatch(obj, criteria)) {
|
|
337
|
+
results.push(obj);
|
|
338
|
+
}
|
|
339
|
+
} catch (err) { }
|
|
340
|
+
offset += 2 + len;
|
|
341
|
+
}
|
|
342
|
+
currentPageId = pData.readUInt32LE(0);
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
_delete(table, criteria) {
|
|
348
|
+
const entry = this._findTableEntry(table);
|
|
349
|
+
if (!entry) throw new Error(`Kebun '${table}' tidak ditemukan.`);
|
|
350
|
+
|
|
351
|
+
let currentPageId = entry.startPage;
|
|
352
|
+
let deletedCount = 0;
|
|
353
|
+
|
|
354
|
+
while (currentPageId !== 0) {
|
|
355
|
+
let pData = this.pager.readPage(currentPageId);
|
|
356
|
+
const count = pData.readUInt16LE(4);
|
|
357
|
+
let offset = 8;
|
|
358
|
+
const recordsToKeep = [];
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < count; i++) {
|
|
361
|
+
const len = pData.readUInt16LE(offset);
|
|
362
|
+
const jsonStr = pData.toString('utf8', offset + 2, offset + 2 + len);
|
|
363
|
+
let shouldDelete = false;
|
|
364
|
+
try {
|
|
365
|
+
const obj = JSON.parse(jsonStr);
|
|
366
|
+
if (this._checkMatch(obj, criteria)) shouldDelete = true;
|
|
367
|
+
} catch (e) { }
|
|
368
|
+
|
|
369
|
+
if (shouldDelete) {
|
|
370
|
+
deletedCount++;
|
|
371
|
+
} else {
|
|
372
|
+
recordsToKeep.push({ len, data: pData.slice(offset + 2, offset + 2 + len) });
|
|
373
|
+
}
|
|
374
|
+
offset += 2 + len;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (recordsToKeep.length < count) {
|
|
378
|
+
let writeOffset = 8;
|
|
379
|
+
pData.writeUInt16LE(recordsToKeep.length, 4);
|
|
380
|
+
for (let rec of recordsToKeep) {
|
|
381
|
+
pData.writeUInt16LE(rec.len, writeOffset);
|
|
382
|
+
rec.data.copy(pData, writeOffset + 2);
|
|
383
|
+
writeOffset += 2 + rec.len;
|
|
384
|
+
}
|
|
385
|
+
pData.writeUInt16LE(writeOffset, 6);
|
|
386
|
+
pData.fill(0, writeOffset);
|
|
387
|
+
this.pager.writePage(currentPageId, pData);
|
|
388
|
+
}
|
|
389
|
+
currentPageId = pData.readUInt32LE(0);
|
|
390
|
+
}
|
|
391
|
+
return `Berhasil menggusur ${deletedCount} bibit.`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_update(table, updates, criteria) {
|
|
395
|
+
const records = this._select(table, criteria);
|
|
396
|
+
if (records.length === 0) return "Tidak ada bibit yang cocok untuk dipupuk.";
|
|
397
|
+
|
|
398
|
+
this._delete(table, criteria);
|
|
399
|
+
|
|
400
|
+
let count = 0;
|
|
401
|
+
for (const rec of records) {
|
|
402
|
+
for (const k in updates) {
|
|
403
|
+
rec[k] = updates[k];
|
|
404
|
+
}
|
|
405
|
+
this._insert(table, rec);
|
|
406
|
+
count++;
|
|
407
|
+
}
|
|
408
|
+
return `Berhasil memupuk ${count} bibit.`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- Index Management ---
|
|
412
|
+
|
|
413
|
+
_createIndex(table, field) {
|
|
414
|
+
const entry = this._findTableEntry(table);
|
|
415
|
+
if (!entry) throw new Error(`Kebun '${table}' tidak ditemukan.`);
|
|
416
|
+
|
|
417
|
+
const indexKey = `${table}.${field}`;
|
|
418
|
+
if (this.indexes.has(indexKey)) {
|
|
419
|
+
return `Indeks pada '${table}.${field}' sudah ada.`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Create index
|
|
423
|
+
const index = new BTreeIndex();
|
|
424
|
+
index.name = indexKey;
|
|
425
|
+
index.keyField = field;
|
|
426
|
+
|
|
427
|
+
// Build index from existing data
|
|
428
|
+
const allRecords = this._select(table, null);
|
|
429
|
+
for (const record of allRecords) {
|
|
430
|
+
if (record.hasOwnProperty(field)) {
|
|
431
|
+
index.insert(record[field], record);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.indexes.set(indexKey, index);
|
|
436
|
+
return `Indeks dibuat pada '${table}.${field}' (${allRecords.length} records indexed)`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
_showIndexes(table) {
|
|
440
|
+
if (table) {
|
|
441
|
+
const indexes = [];
|
|
442
|
+
for (const [key, index] of this.indexes) {
|
|
443
|
+
if (key.startsWith(table + '.')) {
|
|
444
|
+
indexes.push(index.stats());
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return indexes.length > 0 ? indexes : `Tidak ada indeks pada '${table}'`;
|
|
448
|
+
} else {
|
|
449
|
+
const allIndexes = [];
|
|
450
|
+
for (const index of this.indexes.values()) {
|
|
451
|
+
allIndexes.push(index.stats());
|
|
452
|
+
}
|
|
453
|
+
return allIndexes;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// --- Aggregation Support ---
|
|
458
|
+
|
|
459
|
+
_aggregate(table, func, field, criteria, groupBy) {
|
|
460
|
+
const records = this._select(table, criteria);
|
|
461
|
+
|
|
462
|
+
if (groupBy) {
|
|
463
|
+
return this._groupedAggregate(records, func, field, groupBy);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
switch (func.toUpperCase()) {
|
|
467
|
+
case 'COUNT':
|
|
468
|
+
return { count: records.length };
|
|
469
|
+
|
|
470
|
+
case 'SUM':
|
|
471
|
+
if (!field) throw new Error("SUM requires a field");
|
|
472
|
+
const sum = records.reduce((acc, r) => acc + (Number(r[field]) || 0), 0);
|
|
473
|
+
return { sum, field };
|
|
474
|
+
|
|
475
|
+
case 'AVG':
|
|
476
|
+
if (!field) throw new Error("AVG requires a field");
|
|
477
|
+
const avg = records.reduce((acc, r) => acc + (Number(r[field]) || 0), 0) / records.length;
|
|
478
|
+
return { avg, field, count: records.length };
|
|
479
|
+
|
|
480
|
+
case 'MIN':
|
|
481
|
+
if (!field) throw new Error("MIN requires a field");
|
|
482
|
+
const min = Math.min(...records.map(r => Number(r[field]) || Infinity));
|
|
483
|
+
return { min, field };
|
|
484
|
+
|
|
485
|
+
case 'MAX':
|
|
486
|
+
if (!field) throw new Error("MAX requires a field");
|
|
487
|
+
const max = Math.max(...records.map(r => Number(r[field]) || -Infinity));
|
|
488
|
+
return { max, field };
|
|
489
|
+
|
|
490
|
+
default:
|
|
491
|
+
throw new Error(`Unknown aggregate function: ${func}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
_groupedAggregate(records, func, field, groupBy) {
|
|
496
|
+
const groups = {};
|
|
497
|
+
|
|
498
|
+
// Group records
|
|
499
|
+
for (const record of records) {
|
|
500
|
+
const key = record[groupBy];
|
|
501
|
+
if (!groups[key]) {
|
|
502
|
+
groups[key] = [];
|
|
503
|
+
}
|
|
504
|
+
groups[key].push(record);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Apply aggregate to each group
|
|
508
|
+
const results = [];
|
|
509
|
+
for (const [key, groupRecords] of Object.entries(groups)) {
|
|
510
|
+
const result = { [groupBy]: key };
|
|
511
|
+
|
|
512
|
+
switch (func.toUpperCase()) {
|
|
513
|
+
case 'COUNT':
|
|
514
|
+
result.count = groupRecords.length;
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'SUM':
|
|
518
|
+
result.sum = groupRecords.reduce((acc, r) => acc + (Number(r[field]) || 0), 0);
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case 'AVG':
|
|
522
|
+
result.avg = groupRecords.reduce((acc, r) => acc + (Number(r[field]) || 0), 0) / groupRecords.length;
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'MIN':
|
|
526
|
+
result.min = Math.min(...groupRecords.map(r => Number(r[field]) || Infinity));
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'MAX':
|
|
530
|
+
result.max = Math.max(...groupRecords.map(r => Number(r[field]) || -Infinity));
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
results.push(result);
|
|
534
|
+
}
|
|
535
|
+
return results;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = SawitDB;
|