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.
- package/LICENSE +2 -2
- package/README.md +556 -127
- package/dist/FileHandler.js +688 -0
- package/dist/IndexManager.js +353 -0
- package/dist/IntegrityChecker.js +364 -0
- package/dist/JSONLDatabase.js +1132 -0
- package/dist/index.js +598 -0
- package/package.json +65 -59
- package/src/FileHandler.js +674 -0
- package/src/IndexManager.js +363 -0
- package/src/IntegrityChecker.js +379 -0
- package/src/JSONLDatabase.js +1189 -0
- package/src/index.js +594 -0
- package/.gitattributes +0 -2
- package/babel.config.json +0 -5
- package/dist/Database.cjs +0 -1085
- package/src/Database.mjs +0 -376
- package/src/FileHandler.mjs +0 -202
- package/src/IndexManager.mjs +0 -230
- package/src/Serializer.mjs +0 -120
- package/test/README.md +0 -13
- package/test/test-json-compressed.jdb +0 -0
- package/test/test-json.jdb +0 -0
- package/test/test-v8-compressed.jdb +0 -0
- package/test/test-v8.jdb +0 -0
- package/test/test.mjs +0 -168
|
@@ -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;
|