jexidb 1.0.8 → 2.0.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,363 @@
1
+ /**
2
+ * IndexManager - In-memory index management
3
+ * Supports different data types and query operations
4
+ */
5
+ class IndexManager {
6
+ constructor(indexes = {}) {
7
+ this.indexes = {};
8
+ this.offsets = [];
9
+ this.recordCount = 0;
10
+
11
+ // Initialize indexes based on configuration
12
+ for (const [field, type] of Object.entries(indexes)) {
13
+ this.indexes[field] = {
14
+ type,
15
+ values: new Map() // Map<value, Set<offsetIndex>>
16
+ };
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Adds a record to the index
22
+ */
23
+ addRecord(record, offsetIndex) {
24
+ this.offsets[offsetIndex] = record._offset || 0;
25
+ this.recordCount = Math.max(this.recordCount, offsetIndex + 1);
26
+
27
+ // Add to indexes
28
+ for (const [field, index] of Object.entries(this.indexes)) {
29
+ const value = this.getNestedValue(record, field);
30
+ if (value !== undefined) {
31
+ if (!index.values.has(value)) {
32
+ index.values.set(value, new Set());
33
+ }
34
+ index.values.get(value).add(offsetIndex);
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Removes a record from the index
41
+ */
42
+ removeRecord(offsetIndex) {
43
+ // Remove from indexes
44
+ for (const [field, index] of Object.entries(this.indexes)) {
45
+ for (const [value, offsetSet] of index.values.entries()) {
46
+ offsetSet.delete(offsetIndex);
47
+ if (offsetSet.size === 0) {
48
+ index.values.delete(value);
49
+ }
50
+ }
51
+ }
52
+
53
+ // Mark as removed in the offsets array
54
+ this.offsets[offsetIndex] = null;
55
+ }
56
+
57
+ /**
58
+ * Updates a record in the index
59
+ */
60
+ updateRecord(record, offsetIndex, oldRecord = null) {
61
+ // Remove old values from indexes
62
+ if (oldRecord) {
63
+ for (const [field, index] of Object.entries(this.indexes)) {
64
+ const oldValue = this.getNestedValue(oldRecord, field);
65
+ if (oldValue !== undefined) {
66
+ const offsetSet = index.values.get(oldValue);
67
+ if (offsetSet) {
68
+ offsetSet.delete(offsetIndex);
69
+ if (offsetSet.size === 0) {
70
+ index.values.delete(oldValue);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ // Add new values to indexes
78
+ for (const [field, index] of Object.entries(this.indexes)) {
79
+ const value = this.getNestedValue(record, field);
80
+ if (value !== undefined) {
81
+ if (!index.values.has(value)) {
82
+ index.values.set(value, new Set());
83
+ }
84
+ index.values.get(value).add(offsetIndex);
85
+ }
86
+ }
87
+
88
+ // Update offset
89
+ this.offsets[offsetIndex] = record._offset || 0;
90
+ }
91
+
92
+ /**
93
+ * Searches records based on criteria
94
+ */
95
+ findRecords(criteria, options = {}) {
96
+ const { caseInsensitive = false, matchAny = false } = options;
97
+
98
+ if (!criteria || Object.keys(criteria).length === 0) {
99
+ // Returns all valid records
100
+ return Array.from(this.offsets.keys()).filter(i => this.offsets[i] !== null);
101
+ }
102
+
103
+ let matchingOffsets = null;
104
+
105
+ for (const [field, criteriaValue] of Object.entries(criteria)) {
106
+ const index = this.indexes[field];
107
+ if (!index) {
108
+ // If no index exists for this field, we need to scan all records
109
+ // For now, return empty result if any field doesn't have an index
110
+ return [];
111
+ }
112
+
113
+ const fieldOffsets = this.findFieldMatches(field, criteriaValue, caseInsensitive);
114
+
115
+ if (matchingOffsets === null) {
116
+ matchingOffsets = fieldOffsets;
117
+ } else if (matchAny) {
118
+ // Union (OR)
119
+ matchingOffsets = new Set([...matchingOffsets, ...fieldOffsets]);
120
+ } else {
121
+ // Intersection (AND)
122
+ matchingOffsets = new Set([...matchingOffsets].filter(x => fieldOffsets.has(x)));
123
+ }
124
+
125
+ if (!matchAny && matchingOffsets.size === 0) {
126
+ break; // No intersection, stop search
127
+ }
128
+ }
129
+
130
+ return matchingOffsets ? Array.from(matchingOffsets) : [];
131
+ }
132
+
133
+ /**
134
+ * Searches for matches in a specific field
135
+ */
136
+ findFieldMatches(field, criteriaValue, caseInsensitive) {
137
+ const index = this.indexes[field];
138
+ if (!index) return new Set();
139
+
140
+ const matches = new Set();
141
+
142
+ if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue)) {
143
+ // Comparison operators
144
+ for (const [value, offsetSet] of index.values.entries()) {
145
+ if (this.matchesOperator(value, criteriaValue, caseInsensitive)) {
146
+ for (const offset of offsetSet) {
147
+ matches.add(offset);
148
+ }
149
+ }
150
+ }
151
+ } else {
152
+ // Direct comparison
153
+ const values = Array.isArray(criteriaValue) ? criteriaValue : [criteriaValue];
154
+ for (const searchValue of values) {
155
+ const offsetSet = index.values.get(searchValue);
156
+ if (offsetSet) {
157
+ for (const offset of offsetSet) {
158
+ matches.add(offset);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return matches;
165
+ }
166
+
167
+ /**
168
+ * Checks if a value matches the operators
169
+ */
170
+ matchesOperator(value, operators, caseInsensitive) {
171
+ for (const [operator, operatorValue] of Object.entries(operators)) {
172
+ switch (operator) {
173
+ case '>':
174
+ if (value <= operatorValue) return false;
175
+ break;
176
+ case '>=':
177
+ if (value < operatorValue) return false;
178
+ break;
179
+ case '<':
180
+ if (value >= operatorValue) return false;
181
+ break;
182
+ case '<=':
183
+ if (value > operatorValue) return false;
184
+ break;
185
+ case '!=':
186
+ if (value === operatorValue) return false;
187
+ break;
188
+ case 'in':
189
+ if (!Array.isArray(operatorValue) || !operatorValue.includes(value)) return false;
190
+ break;
191
+ case 'nin':
192
+ if (Array.isArray(operatorValue) && operatorValue.includes(value)) return false;
193
+ break;
194
+ case 'regex':
195
+ const regex = new RegExp(operatorValue, caseInsensitive ? 'i' : '');
196
+ if (!regex.test(String(value))) return false;
197
+ break;
198
+ case 'contains':
199
+ const searchStr = String(operatorValue);
200
+ const valueStr = String(value);
201
+ if (caseInsensitive) {
202
+ if (!valueStr.toLowerCase().includes(searchStr.toLowerCase())) return false;
203
+ } else {
204
+ if (!valueStr.includes(searchStr)) return false;
205
+ }
206
+ break;
207
+ }
208
+ }
209
+ return true;
210
+ }
211
+
212
+ /**
213
+ * Gets nested value from an object
214
+ */
215
+ getNestedValue(obj, path) {
216
+ return path.split('.').reduce((current, key) => {
217
+ return current && current[key] !== undefined ? current[key] : undefined;
218
+ }, obj);
219
+ }
220
+
221
+ /**
222
+ * Recalculates all offsets after modifications
223
+ */
224
+ recalculateOffsets() {
225
+ let currentOffset = 0;
226
+ const newOffsets = [];
227
+
228
+ for (let i = 0; i < this.offsets.length; i++) {
229
+ if (this.offsets[i] !== null) {
230
+ newOffsets[i] = currentOffset;
231
+ currentOffset += this.offsets[i];
232
+ }
233
+ }
234
+
235
+ this.offsets = newOffsets;
236
+ }
237
+
238
+ /**
239
+ * Recalculates offsets after a file rewrite
240
+ * This method should be called after the file has been rewritten
241
+ */
242
+ async recalculateOffsetsFromFile(fileHandler) {
243
+ const newOffsets = [];
244
+ let currentOffset = 0;
245
+ let recordIndex = 0;
246
+
247
+ // Read the file line by line and recalculate offsets
248
+ for await (const line of this.walkFile(fileHandler)) {
249
+ if (line && !line._deleted) {
250
+ newOffsets[recordIndex] = currentOffset;
251
+ currentOffset += fileHandler.getByteLength(fileHandler.serialize(line));
252
+ recordIndex++;
253
+ }
254
+ }
255
+
256
+ this.offsets = newOffsets;
257
+ this.recordCount = recordIndex;
258
+ }
259
+
260
+ /**
261
+ * Walks through the file to read all records
262
+ */
263
+ async *walkFile(fileHandler) {
264
+ let offset = 0;
265
+
266
+ while (true) {
267
+ const line = await fileHandler.readLine(offset);
268
+ if (line === null) break;
269
+
270
+ try {
271
+ const record = fileHandler.deserialize(line);
272
+ yield record;
273
+ offset += fileHandler.getByteLength(line + '\n');
274
+ } catch (error) {
275
+ // Skip corrupted lines
276
+ offset += fileHandler.getByteLength(line + '\n');
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Gets index statistics
283
+ */
284
+ getStats() {
285
+ const stats = {
286
+ recordCount: this.recordCount,
287
+ indexCount: Object.keys(this.indexes).length,
288
+ indexes: {}
289
+ };
290
+
291
+ for (const [field, index] of Object.entries(this.indexes)) {
292
+ stats.indexes[field] = {
293
+ type: index.type,
294
+ uniqueValues: index.values.size,
295
+ totalReferences: Array.from(index.values.values()).reduce((sum, set) => sum + set.size, 0)
296
+ };
297
+ }
298
+
299
+ return stats;
300
+ }
301
+
302
+ /**
303
+ * Clears all indexes
304
+ */
305
+ clear() {
306
+ this.indexes = {};
307
+ this.offsets = [];
308
+ this.recordCount = 0;
309
+ }
310
+
311
+ /**
312
+ * Serializes indexes for persistence
313
+ */
314
+ serialize() {
315
+ const serialized = {
316
+ indexes: {},
317
+ offsets: this.offsets,
318
+ recordCount: this.recordCount
319
+ };
320
+
321
+ for (const [field, index] of Object.entries(this.indexes)) {
322
+ serialized.indexes[field] = {
323
+ type: index.type,
324
+ values: {}
325
+ };
326
+
327
+ for (const [value, offsetSet] of index.values.entries()) {
328
+ serialized.indexes[field].values[value] = Array.from(offsetSet);
329
+ }
330
+ }
331
+
332
+ return serialized;
333
+ }
334
+
335
+ /**
336
+ * Deserializes indexes from persistence
337
+ */
338
+ deserialize(data) {
339
+ if (!data || !data.indexes) return;
340
+ this.indexes = {};
341
+ this.offsets = data.offsets || [];
342
+ this.recordCount = data.recordCount || 0;
343
+
344
+ for (const [field, indexData] of Object.entries(data.indexes)) {
345
+ const type = indexData.type;
346
+ const values = new Map();
347
+ for (const [valueStr, offsetArr] of Object.entries(indexData.values)) {
348
+ let key;
349
+ if (type === 'number') {
350
+ key = Number(valueStr);
351
+ } else if (type === 'boolean') {
352
+ key = valueStr === 'true';
353
+ } else {
354
+ key = valueStr;
355
+ }
356
+ values.set(key, new Set(offsetArr));
357
+ }
358
+ this.indexes[field] = { type, values };
359
+ }
360
+ }
361
+ }
362
+
363
+ export default IndexManager;