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,379 @@
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;