jexidb 2.0.3 → 2.1.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/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +132 -101
- package/LICENSE +21 -21
- package/README.md +301 -639
- package/babel.config.json +5 -0
- package/dist/Database.cjs +3896 -0
- package/docs/API.md +1051 -390
- package/docs/EXAMPLES.md +701 -177
- package/docs/README.md +194 -184
- package/examples/iterate-usage-example.js +157 -0
- package/examples/simple-iterate-example.js +115 -0
- package/jest.config.js +24 -0
- package/package.json +63 -54
- package/scripts/README.md +47 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/src/Database.mjs +4130 -0
- package/src/FileHandler.mjs +1101 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +511 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +1403 -0
- package/src/managers/QueryManager.mjs +1273 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/test/$not-operator-with-and.test.js +282 -0
- package/test/README.md +8 -0
- package/test/close-init-cycle.test.js +256 -0
- package/test/critical-bugs-fixes.test.js +1069 -0
- package/test/index-persistence.test.js +306 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/iterate-method.test.js +272 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +238 -0
- package/test/setup.js +17 -0
- package/test/term-mapping-minimal.test.js +154 -0
- package/test/term-mapping-simple.test.js +257 -0
- package/test/term-mapping.test.js +514 -0
- package/test/writebuffer-flush-resilience.test.js +204 -0
- package/dist/FileHandler.js +0 -688
- package/dist/IndexManager.js +0 -353
- package/dist/IntegrityChecker.js +0 -364
- package/dist/JSONLDatabase.js +0 -1333
- package/dist/index.js +0 -617
- package/docs/MIGRATION.md +0 -295
- package/examples/auto-save-example.js +0 -158
- package/examples/cjs-usage.cjs +0 -82
- package/examples/close-vs-delete-example.js +0 -71
- package/examples/esm-usage.js +0 -113
- package/examples/example-columns.idx.jdb +0 -0
- package/examples/example-columns.jdb +0 -9
- package/examples/example-options.idx.jdb +0 -0
- package/examples/example-options.jdb +0 -0
- package/examples/example-users.idx.jdb +0 -0
- package/examples/example-users.jdb +0 -5
- package/examples/simple-test.js +0 -55
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1391
- package/src/index.js +0 -608
package/src/IntegrityChecker.js
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* IntegrityChecker - JSONL file integrity validation
|
|
5
|
-
* Checks consistency between data, indexes and offsets
|
|
6
|
-
*/
|
|
7
|
-
class IntegrityChecker {
|
|
8
|
-
constructor(fileHandler, indexManager) {
|
|
9
|
-
this.fileHandler = fileHandler;
|
|
10
|
-
this.indexManager = indexManager;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Validates the complete integrity of the database
|
|
15
|
-
*/
|
|
16
|
-
async validateIntegrity(options = {}) {
|
|
17
|
-
const {
|
|
18
|
-
checkData = true,
|
|
19
|
-
checkIndexes = true,
|
|
20
|
-
checkOffsets = true,
|
|
21
|
-
verbose = false
|
|
22
|
-
} = options;
|
|
23
|
-
|
|
24
|
-
const results = {
|
|
25
|
-
isValid: true,
|
|
26
|
-
errors: [],
|
|
27
|
-
warnings: [],
|
|
28
|
-
stats: {
|
|
29
|
-
totalRecords: 0,
|
|
30
|
-
validRecords: 0,
|
|
31
|
-
corruptedRecords: 0,
|
|
32
|
-
missingIndexes: 0,
|
|
33
|
-
orphanedIndexes: 0
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (verbose) {
|
|
38
|
-
console.log('🔍 Starting integrity validation...');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if file exists
|
|
42
|
-
const fileExists = await this.fileHandler.exists();
|
|
43
|
-
if (!fileExists) {
|
|
44
|
-
results.errors.push('Data file does not exist');
|
|
45
|
-
results.isValid = false;
|
|
46
|
-
return results;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Validate file data
|
|
50
|
-
if (checkData) {
|
|
51
|
-
const dataResults = await this.validateDataFile(verbose);
|
|
52
|
-
results.errors.push(...dataResults.errors);
|
|
53
|
-
results.warnings.push(...dataResults.warnings);
|
|
54
|
-
results.stats = { ...results.stats, ...dataResults.stats };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Validate indexes
|
|
58
|
-
if (checkIndexes) {
|
|
59
|
-
const indexResults = await this.validateIndexes(verbose);
|
|
60
|
-
results.errors.push(...indexResults.errors);
|
|
61
|
-
results.warnings.push(...indexResults.warnings);
|
|
62
|
-
results.stats = { ...results.stats, ...indexResults.stats };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Validate offsets
|
|
66
|
-
if (checkOffsets) {
|
|
67
|
-
const offsetResults = await this.validateOffsets(verbose);
|
|
68
|
-
results.errors.push(...offsetResults.errors);
|
|
69
|
-
results.warnings.push(...offsetResults.warnings);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Determine if valid
|
|
73
|
-
results.isValid = results.errors.length === 0;
|
|
74
|
-
|
|
75
|
-
if (verbose) {
|
|
76
|
-
console.log(`✅ Validation completed: ${results.isValid ? 'VALID' : 'INVALID'}`);
|
|
77
|
-
console.log(`📊 Statistics:`, results.stats);
|
|
78
|
-
if (results.errors.length > 0) {
|
|
79
|
-
console.log(`❌ Errors found:`, results.errors);
|
|
80
|
-
}
|
|
81
|
-
if (results.warnings.length > 0) {
|
|
82
|
-
console.log(`⚠️ Warnings:`, results.warnings);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return results;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Validates the JSONL data file
|
|
91
|
-
*/
|
|
92
|
-
async validateDataFile(verbose = false) {
|
|
93
|
-
const results = {
|
|
94
|
-
errors: [],
|
|
95
|
-
warnings: [],
|
|
96
|
-
stats: {
|
|
97
|
-
totalRecords: 0,
|
|
98
|
-
validRecords: 0,
|
|
99
|
-
corruptedRecords: 0
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const fd = await fs.open(this.fileHandler.filePath, 'r');
|
|
105
|
-
let lineNumber = 0;
|
|
106
|
-
let offset = 0;
|
|
107
|
-
const buffer = Buffer.alloc(8192);
|
|
108
|
-
let lineBuffer = '';
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
while (true) {
|
|
112
|
-
const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
|
|
113
|
-
if (bytesRead === 0) break;
|
|
114
|
-
|
|
115
|
-
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
116
|
-
lineBuffer += chunk;
|
|
117
|
-
|
|
118
|
-
// Process complete lines
|
|
119
|
-
let newlineIndex;
|
|
120
|
-
while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
|
|
121
|
-
const line = lineBuffer.substring(0, newlineIndex);
|
|
122
|
-
lineBuffer = lineBuffer.substring(newlineIndex + 1);
|
|
123
|
-
|
|
124
|
-
results.stats.totalRecords++;
|
|
125
|
-
|
|
126
|
-
if (line.trim() === '') {
|
|
127
|
-
results.warnings.push(`Line ${lineNumber + 1}: Empty line`);
|
|
128
|
-
} else {
|
|
129
|
-
try {
|
|
130
|
-
const record = JSON.parse(line);
|
|
131
|
-
|
|
132
|
-
// Check if it's a deleted record
|
|
133
|
-
if (record._deleted) {
|
|
134
|
-
if (verbose) {
|
|
135
|
-
console.log(`🗑️ Line ${lineNumber + 1}: Deleted record`);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
results.stats.validRecords++;
|
|
139
|
-
if (verbose) {
|
|
140
|
-
console.log(`✅ Line ${lineNumber + 1}: Valid record`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
results.stats.corruptedRecords++;
|
|
145
|
-
results.errors.push(`Line ${lineNumber + 1}: Invalid JSON - ${error.message}`);
|
|
146
|
-
if (verbose) {
|
|
147
|
-
console.log(`❌ Line ${lineNumber + 1}: Corrupted JSON`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
lineNumber++;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
offset += bytesRead;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Process last line if it doesn't end with \n
|
|
159
|
-
if (lineBuffer.trim() !== '') {
|
|
160
|
-
results.warnings.push(`Line ${lineNumber + 1}: File doesn't end with newline`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
} finally {
|
|
164
|
-
await fd.close();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
} catch (error) {
|
|
168
|
-
results.errors.push(`Error reading file: ${error.message}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return results;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Validates index consistency
|
|
176
|
-
*/
|
|
177
|
-
async validateIndexes(verbose = false) {
|
|
178
|
-
const results = {
|
|
179
|
-
errors: [],
|
|
180
|
-
warnings: [],
|
|
181
|
-
stats: {
|
|
182
|
-
missingIndexes: 0,
|
|
183
|
-
orphanedIndexes: 0
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const indexData = this.indexManager.serialize();
|
|
188
|
-
const validOffsets = new Set();
|
|
189
|
-
|
|
190
|
-
// Collect all valid offsets
|
|
191
|
-
for (let i = 0; i < this.indexManager.offsets.length; i++) {
|
|
192
|
-
if (this.indexManager.offsets[i] !== null) {
|
|
193
|
-
validOffsets.add(i);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Check each index
|
|
198
|
-
for (const [field, fieldIndexData] of Object.entries(indexData.indexes)) {
|
|
199
|
-
if (verbose) {
|
|
200
|
-
console.log(`🔍 Checking index: ${field}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
for (const [value, offsetArray] of Object.entries(fieldIndexData.values)) {
|
|
204
|
-
for (const offsetIndex of offsetArray) {
|
|
205
|
-
if (!validOffsets.has(offsetIndex)) {
|
|
206
|
-
results.stats.orphanedIndexes++;
|
|
207
|
-
results.errors.push(`Orphaned index: ${field}=${value} points to non-existent offset ${offsetIndex}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check if there are valid records without index
|
|
214
|
-
for (const offsetIndex of validOffsets) {
|
|
215
|
-
let hasIndex = false;
|
|
216
|
-
for (const [field, index] of Object.entries(this.indexManager.indexes)) {
|
|
217
|
-
for (const [value, offsetSet] of index.values.entries()) {
|
|
218
|
-
if (offsetSet.has(offsetIndex)) {
|
|
219
|
-
hasIndex = true;
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (hasIndex) break;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!hasIndex) {
|
|
227
|
-
results.stats.missingIndexes++;
|
|
228
|
-
results.warnings.push(`Record at offset ${offsetIndex} is not indexed`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return results;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Validates offset consistency
|
|
237
|
-
*/
|
|
238
|
-
async validateOffsets(verbose = false) {
|
|
239
|
-
const results = {
|
|
240
|
-
errors: [],
|
|
241
|
-
warnings: []
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const stats = await this.fileHandler.getStats();
|
|
245
|
-
const fileSize = stats.size;
|
|
246
|
-
|
|
247
|
-
// Check if offsets are valid
|
|
248
|
-
for (let i = 0; i < this.indexManager.offsets.length; i++) {
|
|
249
|
-
const offset = this.indexManager.offsets[i];
|
|
250
|
-
if (offset !== null) {
|
|
251
|
-
if (offset < 0) {
|
|
252
|
-
results.errors.push(`Offset ${i}: Negative value (${offset})`);
|
|
253
|
-
} else if (offset >= fileSize) {
|
|
254
|
-
results.errors.push(`Offset ${i}: Out of file bounds (${offset} >= ${fileSize})`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return results;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Rebuilds indexes from the data file
|
|
264
|
-
*/
|
|
265
|
-
async rebuildIndexes(verbose = false) {
|
|
266
|
-
if (verbose) {
|
|
267
|
-
console.log('🔧 Rebuilding indexes...');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Store the configured indexes before clearing
|
|
271
|
-
const configuredIndexes = this.indexManager.indexes;
|
|
272
|
-
|
|
273
|
-
// Clear current indexes but preserve configuration
|
|
274
|
-
this.indexManager.clear();
|
|
275
|
-
|
|
276
|
-
// Restore the configured indexes
|
|
277
|
-
for (const [field, indexConfig] of Object.entries(configuredIndexes)) {
|
|
278
|
-
this.indexManager.indexes[field] = {
|
|
279
|
-
type: indexConfig.type,
|
|
280
|
-
values: new Map()
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const fd = await fs.open(this.fileHandler.filePath, 'r');
|
|
286
|
-
let lineNumber = 0;
|
|
287
|
-
let offset = 0;
|
|
288
|
-
const buffer = Buffer.alloc(8192);
|
|
289
|
-
let lineBuffer = '';
|
|
290
|
-
|
|
291
|
-
try {
|
|
292
|
-
while (true) {
|
|
293
|
-
const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
|
|
294
|
-
if (bytesRead === 0) break;
|
|
295
|
-
|
|
296
|
-
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
297
|
-
lineBuffer += chunk;
|
|
298
|
-
|
|
299
|
-
// Process complete lines
|
|
300
|
-
let newlineIndex;
|
|
301
|
-
while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
|
|
302
|
-
const line = lineBuffer.substring(0, newlineIndex);
|
|
303
|
-
lineBuffer = lineBuffer.substring(newlineIndex + 1);
|
|
304
|
-
|
|
305
|
-
if (line.trim() !== '') {
|
|
306
|
-
try {
|
|
307
|
-
const record = JSON.parse(line);
|
|
308
|
-
|
|
309
|
-
// Only index non-deleted records
|
|
310
|
-
if (!record._deleted) {
|
|
311
|
-
record._offset = offset;
|
|
312
|
-
this.indexManager.addRecord(record, lineNumber);
|
|
313
|
-
|
|
314
|
-
if (verbose) {
|
|
315
|
-
console.log(`✅ Reindexed: line ${lineNumber + 1}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (verbose) {
|
|
320
|
-
console.log(`⚠️ Line ${lineNumber + 1}: Ignored (invalid JSON)`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
lineNumber++;
|
|
326
|
-
offset += this.fileHandler.getByteLength(line + '\n');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
} finally {
|
|
331
|
-
await fd.close();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Save rebuilt indexes
|
|
335
|
-
await this.fileHandler.writeIndex(this.indexManager.serialize());
|
|
336
|
-
|
|
337
|
-
if (verbose) {
|
|
338
|
-
console.log('✅ Indexes rebuilt successfully');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return true;
|
|
342
|
-
|
|
343
|
-
} catch (error) {
|
|
344
|
-
if (verbose) {
|
|
345
|
-
console.error('❌ Error rebuilding indexes:', error.message);
|
|
346
|
-
}
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Exports detailed statistics
|
|
353
|
-
*/
|
|
354
|
-
async exportStats() {
|
|
355
|
-
const stats = await this.fileHandler.getStats();
|
|
356
|
-
const indexStats = this.indexManager.getStats();
|
|
357
|
-
const integrityResults = await this.validateIntegrity({ verbose: false });
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
file: {
|
|
361
|
-
path: this.fileHandler.filePath,
|
|
362
|
-
size: stats.size,
|
|
363
|
-
created: stats.created,
|
|
364
|
-
modified: stats.modified
|
|
365
|
-
},
|
|
366
|
-
indexes: indexStats,
|
|
367
|
-
integrity: integrityResults,
|
|
368
|
-
summary: {
|
|
369
|
-
totalRecords: indexStats.recordCount,
|
|
370
|
-
fileSize: stats.size,
|
|
371
|
-
isValid: integrityResults.isValid,
|
|
372
|
-
errorCount: integrityResults.errors.length,
|
|
373
|
-
warningCount: integrityResults.warnings.length
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export default IntegrityChecker;
|