@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.
@@ -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;