@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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple B-Tree Index for SawitDB
|
|
3
|
+
* Provides fast lookups by key
|
|
4
|
+
*/
|
|
5
|
+
class BTreeNode {
|
|
6
|
+
constructor(isLeaf = true) {
|
|
7
|
+
this.isLeaf = isLeaf;
|
|
8
|
+
this.keys = [];
|
|
9
|
+
this.values = []; // For leaf nodes: array of record references
|
|
10
|
+
this.children = []; // For internal nodes
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class BTreeIndex {
|
|
15
|
+
constructor(order = 32) {
|
|
16
|
+
this.order = order; // Maximum number of keys per node
|
|
17
|
+
this.root = new BTreeNode(true);
|
|
18
|
+
this.name = null;
|
|
19
|
+
this.keyField = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Insert a key-value pair into the index
|
|
24
|
+
* @param {*} key - The key to index
|
|
25
|
+
* @param {*} value - The value to store (usually record reference/data)
|
|
26
|
+
*/
|
|
27
|
+
insert(key, value) {
|
|
28
|
+
const root = this.root;
|
|
29
|
+
|
|
30
|
+
// If root is full, split it
|
|
31
|
+
if (root.keys.length >= this.order) {
|
|
32
|
+
const newRoot = new BTreeNode(false);
|
|
33
|
+
newRoot.children.push(this.root);
|
|
34
|
+
this._splitChild(newRoot, 0);
|
|
35
|
+
this.root = newRoot;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._insertNonFull(this.root, key, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_insertNonFull(node, key, value) {
|
|
42
|
+
let i = node.keys.length - 1;
|
|
43
|
+
|
|
44
|
+
if (node.isLeaf) {
|
|
45
|
+
// Insert key-value in sorted order
|
|
46
|
+
node.keys.push(null);
|
|
47
|
+
node.values.push(null);
|
|
48
|
+
|
|
49
|
+
while (i >= 0 && key < node.keys[i]) {
|
|
50
|
+
node.keys[i + 1] = node.keys[i];
|
|
51
|
+
node.values[i + 1] = node.values[i];
|
|
52
|
+
i--;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
node.keys[i + 1] = key;
|
|
56
|
+
node.values[i + 1] = value;
|
|
57
|
+
} else {
|
|
58
|
+
// Find child to insert into
|
|
59
|
+
while (i >= 0 && key < node.keys[i]) {
|
|
60
|
+
i--;
|
|
61
|
+
}
|
|
62
|
+
i++;
|
|
63
|
+
|
|
64
|
+
// If child is full, split it
|
|
65
|
+
if (node.children[i].keys.length >= this.order) {
|
|
66
|
+
this._splitChild(node, i);
|
|
67
|
+
if (key > node.keys[i]) {
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._insertNonFull(node.children[i], key, value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_splitChild(parent, index) {
|
|
77
|
+
const fullNode = parent.children[index];
|
|
78
|
+
const newNode = new BTreeNode(fullNode.isLeaf);
|
|
79
|
+
const mid = Math.floor(this.order / 2);
|
|
80
|
+
|
|
81
|
+
// Move half of keys to new node
|
|
82
|
+
newNode.keys = fullNode.keys.splice(mid);
|
|
83
|
+
|
|
84
|
+
if (fullNode.isLeaf) {
|
|
85
|
+
newNode.values = fullNode.values.splice(mid);
|
|
86
|
+
} else {
|
|
87
|
+
newNode.children = fullNode.children.splice(mid);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Move middle key up to parent
|
|
91
|
+
const middleKey = newNode.keys.shift();
|
|
92
|
+
if (fullNode.isLeaf) {
|
|
93
|
+
newNode.values.shift();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parent.keys.splice(index, 0, middleKey);
|
|
97
|
+
parent.children.splice(index + 1, 0, newNode);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Search for a key in the index
|
|
102
|
+
* @param {*} key - The key to search for
|
|
103
|
+
* @returns {Array} - Array of values associated with the key
|
|
104
|
+
*/
|
|
105
|
+
search(key) {
|
|
106
|
+
return this._searchNode(this.root, key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_searchNode(node, key) {
|
|
110
|
+
let i = 0;
|
|
111
|
+
|
|
112
|
+
// Find the first key greater than or equal to the search key
|
|
113
|
+
while (i < node.keys.length && key > node.keys[i]) {
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if key is found
|
|
118
|
+
if (i < node.keys.length && key === node.keys[i]) {
|
|
119
|
+
if (node.isLeaf) {
|
|
120
|
+
return Array.isArray(node.values[i]) ? node.values[i] : [node.values[i]];
|
|
121
|
+
} else {
|
|
122
|
+
// Continue search in child
|
|
123
|
+
return this._searchNode(node.children[i + 1], key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If not found and this is a leaf, return empty
|
|
128
|
+
if (node.isLeaf) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Search in appropriate child
|
|
133
|
+
return this._searchNode(node.children[i], key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Range query: find all keys between min and max
|
|
138
|
+
* @param {*} min - Minimum key (inclusive)
|
|
139
|
+
* @param {*} max - Maximum key (inclusive)
|
|
140
|
+
* @returns {Array} - Array of values in range
|
|
141
|
+
*/
|
|
142
|
+
range(min, max) {
|
|
143
|
+
const results = [];
|
|
144
|
+
this._rangeSearch(this.root, min, max, results);
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_rangeSearch(node, min, max, results) {
|
|
149
|
+
let i = 0;
|
|
150
|
+
|
|
151
|
+
while (i < node.keys.length) {
|
|
152
|
+
if (node.isLeaf) {
|
|
153
|
+
if (node.keys[i] >= min && node.keys[i] <= max) {
|
|
154
|
+
const values = Array.isArray(node.values[i]) ? node.values[i] : [node.values[i]];
|
|
155
|
+
results.push(...values);
|
|
156
|
+
}
|
|
157
|
+
i++;
|
|
158
|
+
} else {
|
|
159
|
+
if (node.keys[i] > min) {
|
|
160
|
+
this._rangeSearch(node.children[i], min, max, results);
|
|
161
|
+
}
|
|
162
|
+
if (node.keys[i] >= min && node.keys[i] <= max) {
|
|
163
|
+
const values = Array.isArray(node.values[i]) ? node.values[i] : [node.values[i]];
|
|
164
|
+
results.push(...values);
|
|
165
|
+
}
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Search rightmost child
|
|
171
|
+
if (!node.isLeaf && node.children.length > i) {
|
|
172
|
+
this._rangeSearch(node.children[i], min, max, results);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get all values (full scan)
|
|
178
|
+
* @returns {Array} - All values in the index
|
|
179
|
+
*/
|
|
180
|
+
all() {
|
|
181
|
+
const results = [];
|
|
182
|
+
this._collectAll(this.root, results);
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_collectAll(node, results) {
|
|
187
|
+
if (node.isLeaf) {
|
|
188
|
+
for (const value of node.values) {
|
|
189
|
+
const values = Array.isArray(value) ? value : [value];
|
|
190
|
+
results.push(...values);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
194
|
+
this._collectAll(node.children[i], results);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Delete a key from the index
|
|
201
|
+
* @param {*} key - The key to delete
|
|
202
|
+
*/
|
|
203
|
+
delete(key) {
|
|
204
|
+
this._deleteFromNode(this.root, key);
|
|
205
|
+
|
|
206
|
+
// If root is empty after deletion, make its only child the new root
|
|
207
|
+
if (this.root.keys.length === 0 && !this.root.isLeaf) {
|
|
208
|
+
this.root = this.root.children[0];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_deleteFromNode(node, key) {
|
|
213
|
+
let i = 0;
|
|
214
|
+
while (i < node.keys.length && key > node.keys[i]) {
|
|
215
|
+
i++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (i < node.keys.length && key === node.keys[i]) {
|
|
219
|
+
if (node.isLeaf) {
|
|
220
|
+
// Remove key and value
|
|
221
|
+
node.keys.splice(i, 1);
|
|
222
|
+
node.values.splice(i, 1);
|
|
223
|
+
return true;
|
|
224
|
+
} else {
|
|
225
|
+
// Internal node deletion (simplified - not fully balanced)
|
|
226
|
+
return this._deleteFromNode(node.children[i + 1], key);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (node.isLeaf) {
|
|
231
|
+
return false; // Key not found
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Recursively delete from child
|
|
235
|
+
return this._deleteFromNode(node.children[i], key);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get index statistics
|
|
240
|
+
*/
|
|
241
|
+
stats() {
|
|
242
|
+
let nodeCount = 0;
|
|
243
|
+
let leafCount = 0;
|
|
244
|
+
let keyCount = 0;
|
|
245
|
+
let maxDepth = 0;
|
|
246
|
+
|
|
247
|
+
const traverse = (node, depth) => {
|
|
248
|
+
nodeCount++;
|
|
249
|
+
keyCount += node.keys.length;
|
|
250
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
251
|
+
|
|
252
|
+
if (node.isLeaf) {
|
|
253
|
+
leafCount++;
|
|
254
|
+
} else {
|
|
255
|
+
for (const child of node.children) {
|
|
256
|
+
traverse(child, depth + 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
traverse(this.root, 0);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
name: this.name,
|
|
265
|
+
keyField: this.keyField,
|
|
266
|
+
nodeCount,
|
|
267
|
+
leafCount,
|
|
268
|
+
keyCount,
|
|
269
|
+
maxDepth,
|
|
270
|
+
order: this.order
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear the index
|
|
276
|
+
*/
|
|
277
|
+
clear() {
|
|
278
|
+
this.root = new BTreeNode(true);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = BTreeIndex;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const PAGE_SIZE = 4096;
|
|
4
|
+
const MAGIC = 'WOWO';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pager handles 4KB page I/O
|
|
8
|
+
*/
|
|
9
|
+
class Pager {
|
|
10
|
+
constructor(filePath) {
|
|
11
|
+
this.filePath = filePath;
|
|
12
|
+
this.fd = null;
|
|
13
|
+
this._open();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_open() {
|
|
17
|
+
if (!fs.existsSync(this.filePath)) {
|
|
18
|
+
this.fd = fs.openSync(this.filePath, 'w+');
|
|
19
|
+
this._initNewFile();
|
|
20
|
+
} else {
|
|
21
|
+
this.fd = fs.openSync(this.filePath, 'r+');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_initNewFile() {
|
|
26
|
+
const buf = Buffer.alloc(PAGE_SIZE);
|
|
27
|
+
buf.write(MAGIC, 0);
|
|
28
|
+
buf.writeUInt32LE(1, 4); // Total Pages = 1
|
|
29
|
+
buf.writeUInt32LE(0, 8); // Num Tables = 0
|
|
30
|
+
fs.writeSync(this.fd, buf, 0, PAGE_SIZE, 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
readPage(pageId) {
|
|
34
|
+
const buf = Buffer.alloc(PAGE_SIZE);
|
|
35
|
+
const offset = pageId * PAGE_SIZE;
|
|
36
|
+
fs.readSync(this.fd, buf, 0, PAGE_SIZE, offset);
|
|
37
|
+
return buf;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writePage(pageId, buf) {
|
|
41
|
+
if (buf.length !== PAGE_SIZE) throw new Error("Buffer must be 4KB");
|
|
42
|
+
const offset = pageId * PAGE_SIZE;
|
|
43
|
+
fs.writeSync(this.fd, buf, 0, PAGE_SIZE, offset);
|
|
44
|
+
// STABILITY UPGRADE: Force write to disk.
|
|
45
|
+
try { fs.fsyncSync(this.fd); } catch (e) { /* Ignore if not supported */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
allocPage() {
|
|
49
|
+
const page0 = this.readPage(0);
|
|
50
|
+
const totalPages = page0.readUInt32LE(4);
|
|
51
|
+
|
|
52
|
+
const newPageId = totalPages;
|
|
53
|
+
const newTotal = totalPages + 1;
|
|
54
|
+
|
|
55
|
+
page0.writeUInt32LE(newTotal, 4);
|
|
56
|
+
this.writePage(0, page0);
|
|
57
|
+
|
|
58
|
+
const newPage = Buffer.alloc(PAGE_SIZE);
|
|
59
|
+
newPage.writeUInt32LE(0, 0); // Next Page = 0
|
|
60
|
+
newPage.writeUInt16LE(0, 4); // Count = 0
|
|
61
|
+
newPage.writeUInt16LE(8, 6); // Free Offset = 8
|
|
62
|
+
this.writePage(newPageId, newPage);
|
|
63
|
+
|
|
64
|
+
return newPageId;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Pager.PAGE_SIZE = PAGE_SIZE;
|
|
69
|
+
|
|
70
|
+
module.exports = Pager;
|