jexidb 2.0.3 → 2.1.1

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.
Files changed (79) hide show
  1. package/.babelrc +13 -0
  2. package/.gitattributes +2 -0
  3. package/CHANGELOG.md +132 -101
  4. package/LICENSE +21 -21
  5. package/README.md +301 -639
  6. package/babel.config.json +5 -0
  7. package/dist/Database.cjs +5204 -0
  8. package/docs/API.md +908 -241
  9. package/docs/EXAMPLES.md +701 -177
  10. package/docs/README.md +194 -184
  11. package/examples/iterate-usage-example.js +157 -0
  12. package/examples/simple-iterate-example.js +115 -0
  13. package/jest.config.js +24 -0
  14. package/package.json +63 -54
  15. package/scripts/README.md +47 -0
  16. package/scripts/benchmark-array-serialization.js +108 -0
  17. package/scripts/clean-test-files.js +75 -0
  18. package/scripts/prepare.js +31 -0
  19. package/scripts/run-tests.js +80 -0
  20. package/scripts/score-mode-demo.js +45 -0
  21. package/src/Database.mjs +5325 -0
  22. package/src/FileHandler.mjs +1140 -0
  23. package/src/OperationQueue.mjs +279 -0
  24. package/src/SchemaManager.mjs +268 -0
  25. package/src/Serializer.mjs +702 -0
  26. package/src/managers/ConcurrencyManager.mjs +257 -0
  27. package/src/managers/IndexManager.mjs +2094 -0
  28. package/src/managers/QueryManager.mjs +1490 -0
  29. package/src/managers/StatisticsManager.mjs +262 -0
  30. package/src/managers/StreamingProcessor.mjs +429 -0
  31. package/src/managers/TermManager.mjs +278 -0
  32. package/src/utils/operatorNormalizer.mjs +116 -0
  33. package/test/$not-operator-with-and.test.js +282 -0
  34. package/test/README.md +8 -0
  35. package/test/close-init-cycle.test.js +256 -0
  36. package/test/coverage-method.test.js +93 -0
  37. package/test/critical-bugs-fixes.test.js +1069 -0
  38. package/test/deserialize-corruption-fixes.test.js +296 -0
  39. package/test/exists-method.test.js +318 -0
  40. package/test/explicit-indexes-comparison.test.js +219 -0
  41. package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
  42. package/test/index-line-number-regression.test.js +100 -0
  43. package/test/index-missing-index-data.test.js +91 -0
  44. package/test/index-persistence.test.js +491 -0
  45. package/test/index-serialization.test.js +314 -0
  46. package/test/indexed-query-mode.test.js +360 -0
  47. package/test/insert-session-auto-flush.test.js +353 -0
  48. package/test/iterate-method.test.js +272 -0
  49. package/test/legacy-operator-compat.test.js +154 -0
  50. package/test/query-operators.test.js +238 -0
  51. package/test/regex-array-fields.test.js +129 -0
  52. package/test/score-method.test.js +298 -0
  53. package/test/setup.js +17 -0
  54. package/test/term-mapping-minimal.test.js +154 -0
  55. package/test/term-mapping-simple.test.js +257 -0
  56. package/test/term-mapping.test.js +514 -0
  57. package/test/writebuffer-flush-resilience.test.js +204 -0
  58. package/dist/FileHandler.js +0 -688
  59. package/dist/IndexManager.js +0 -353
  60. package/dist/IntegrityChecker.js +0 -364
  61. package/dist/JSONLDatabase.js +0 -1333
  62. package/dist/index.js +0 -617
  63. package/docs/MIGRATION.md +0 -295
  64. package/examples/auto-save-example.js +0 -158
  65. package/examples/cjs-usage.cjs +0 -82
  66. package/examples/close-vs-delete-example.js +0 -71
  67. package/examples/esm-usage.js +0 -113
  68. package/examples/example-columns.idx.jdb +0 -0
  69. package/examples/example-columns.jdb +0 -9
  70. package/examples/example-options.idx.jdb +0 -0
  71. package/examples/example-options.jdb +0 -0
  72. package/examples/example-users.idx.jdb +0 -0
  73. package/examples/example-users.jdb +0 -5
  74. package/examples/simple-test.js +0 -55
  75. package/src/FileHandler.js +0 -674
  76. package/src/IndexManager.js +0 -363
  77. package/src/IntegrityChecker.js +0 -379
  78. package/src/JSONLDatabase.js +0 -1391
  79. package/src/index.js +0 -608
package/src/index.js DELETED
@@ -1,608 +0,0 @@
1
- import JSONLDatabase from './JSONLDatabase.js';
2
- import FileHandler from './FileHandler.js';
3
- import IndexManager from './IndexManager.js';
4
- import IntegrityChecker from './IntegrityChecker.js';
5
-
6
- /**
7
- * JexiDB Compatibility Wrapper
8
- * Extends JSONLDatabase to provide backward compatibility with JexiDB 1.x test expectations
9
- */
10
- class JexiDBCompatibility extends JSONLDatabase {
11
- constructor(filePath, options = {}) {
12
- // Support both .jdb and .jsonl extensions
13
- // .jdb is the preferred extension for JexiDB databases
14
- let normalizedPath = filePath;
15
-
16
- // If no extension is provided, default to .jdb
17
- if (!filePath.includes('.')) {
18
- normalizedPath = filePath + '.jdb';
19
- }
20
-
21
- // If .jdb extension is used, it's internally stored as JSONL format
22
- // but the user sees .jdb for better branding
23
- if (normalizedPath.endsWith('.jdb')) {
24
- // Store internally as .jsonl but present as .jdb to user
25
- const jsonlPath = normalizedPath.replace('.jdb', '.jsonl');
26
- super(jsonlPath, options);
27
- this.userPath = normalizedPath; // Keep track of user's preferred path
28
- } else {
29
- super(normalizedPath, options);
30
- this.userPath = normalizedPath;
31
- }
32
-
33
- this.isDestroyed = false;
34
- }
35
-
36
- /**
37
- * Get the user's preferred file path (with .jdb extension if used)
38
- */
39
- get userFilePath() {
40
- return this.userPath;
41
- }
42
-
43
- /**
44
- * Compatibility method: destroy() -> close()
45
- */
46
- async destroy() {
47
- this.isDestroyed = true;
48
- return this.close();
49
- }
50
-
51
- /**
52
- * Compatibility method: findOne() -> find() with limit 1
53
- */
54
- async findOne(criteria = {}) {
55
- const results = await this.find(criteria, { limit: 1 });
56
- return results.length > 0 ? results[0] : null;
57
- }
58
-
59
- /**
60
- * Enhanced find method with options support
61
- */
62
- async find(criteria = {}, options = {}) {
63
- let results = await super.find(criteria);
64
-
65
- // Apply sorting
66
- if (options.sort) {
67
- results = this.sortResults(results, options.sort);
68
- }
69
-
70
- // Apply skip
71
- if (options.skip) {
72
- results = results.slice(options.skip);
73
- }
74
-
75
- // Apply limit
76
- if (options.limit) {
77
- results = results.slice(0, options.limit);
78
- }
79
-
80
- return results;
81
- }
82
-
83
- /**
84
- * Retrocompatibility method: query() -> find()
85
- * Supports the same API as JexiDB 1.x
86
- */
87
- async query(criteria = {}, options = {}) {
88
- // Handle caseInsensitive option from JexiDB 1.x
89
- if (options.caseInsensitive) {
90
- // For case insensitive queries, we need to modify the criteria
91
- const caseInsensitiveCriteria = {};
92
- for (const [key, value] of Object.entries(criteria)) {
93
- if (typeof value === 'string') {
94
- // Convert string values to regex for case insensitive matching
95
- caseInsensitiveCriteria[key] = { $regex: value, $options: 'i' };
96
- } else {
97
- caseInsensitiveCriteria[key] = value;
98
- }
99
- }
100
- criteria = caseInsensitiveCriteria;
101
- }
102
-
103
- return await this.find(criteria, options);
104
- }
105
-
106
- /**
107
- * Sort results based on criteria
108
- */
109
- sortResults(results, sortCriteria) {
110
- return results.sort((a, b) => {
111
- for (const [field, direction] of Object.entries(sortCriteria)) {
112
- const aValue = this.getNestedValue(a, field);
113
- const bValue = this.getNestedValue(b, field);
114
-
115
- if (aValue < bValue) return direction === 1 ? -1 : 1;
116
- if (aValue > bValue) return direction === 1 ? 1 : -1;
117
- }
118
- return 0;
119
- });
120
- }
121
-
122
- /**
123
- * Get nested value from record (copied from parent class)
124
- */
125
- getNestedValue(record, field) {
126
- const parts = field.split('.');
127
- let value = record;
128
-
129
- for (const part of parts) {
130
- if (value && typeof value === 'object' && part in value) {
131
- value = value[part];
132
- } else {
133
- return undefined;
134
- }
135
- }
136
-
137
- return value;
138
- }
139
-
140
- /**
141
- * Compatibility method: insertMany() -> multiple insert() calls
142
- */
143
- async insertMany(records) {
144
- const results = [];
145
- for (const record of records) {
146
- const result = await this.insert(record);
147
- results.push(result);
148
- }
149
- return results;
150
- }
151
-
152
- /**
153
- * Override insert to add _updated field for compatibility
154
- */
155
- async insert(data) {
156
- const record = await super.insert(data);
157
- record._updated = record._created; // Set _updated to same as _created for new records
158
- return record;
159
- }
160
-
161
- /**
162
- * Compatibility method: count() -> find() with length
163
- */
164
- async count(criteria = {}) {
165
- const results = await this.find(criteria);
166
- return results.length;
167
- }
168
-
169
- /**
170
- * Compatibility method: getStats() -> stats getter
171
- */
172
- async getStats() {
173
- // Call the parent class getStats method to get actual file size
174
- return await super.getStats();
175
- }
176
-
177
- /**
178
- * Compatibility method: validateIntegrity() -> basic validation
179
- */
180
- async validateIntegrity() {
181
- // Call the parent class validateIntegrity method to get actual file integrity check
182
- return await super.validateIntegrity();
183
- }
184
-
185
- /**
186
- * Streaming iterator over records without loading all into memory
187
- * Supports a limited subset of options: { limit }
188
- */
189
- async *walk(options = {}) {
190
- // Delegate to core engine streaming implementation
191
- // Ensure pending inserts are flushed by calling super.walk
192
- for await (const record of super.walk({ limit: options.limit })) {
193
- yield record;
194
- }
195
- }
196
-
197
- /**
198
- * Compatibility property: indexStats
199
- */
200
- get indexStats() {
201
- const stats = this.stats;
202
- return {
203
- recordCount: stats.recordCount,
204
- indexCount: stats.indexedFields.length
205
- };
206
- }
207
-
208
- /**
209
- * Override update to return array format for compatibility
210
- */
211
- async update(criteria, updates) {
212
- const result = await super.update(criteria, updates);
213
- // Convert { updatedCount: n } to array format for tests
214
- if (typeof result === 'object' && result.updatedCount !== undefined) {
215
- const updatedRecords = await this.find(criteria);
216
- return updatedRecords;
217
- }
218
- return result;
219
- }
220
-
221
- /**
222
- * Override delete to return number format for compatibility
223
- */
224
- async delete(criteria) {
225
- const result = await super.delete(criteria);
226
- // Convert { deletedCount: n } to number format for tests
227
- if (typeof result === 'object' && result.deletedCount !== undefined) {
228
- return result.deletedCount;
229
- }
230
- return result;
231
- }
232
-
233
- /**
234
- * Compatibility method: readColumnIndex - gets unique values from indexed columns only
235
- * Maintains compatibility with JexiDB v1 code
236
- * @param {string} column - The column name to get unique values from
237
- * @returns {Set} Set of unique values in the column (indexed columns only)
238
- */
239
- readColumnIndex(column) {
240
- return super.readColumnIndex(column);
241
- }
242
- }
243
-
244
- /**
245
- * JexiDB - Robust JSONL database
246
- * Complete rewrite of JexiDB with JSONL architecture, fixing all critical bugs from version 1.x
247
- *
248
- * Features:
249
- * - One file per table (pure JSONL)
250
- * - Punctual reading (doesn't load everything in memory)
251
- * - In-memory indexes for performance
252
- * - Safe truncation after operations
253
- * - Integrity validation
254
- * - Event-driven architecture
255
- *
256
- * API similar to JexiDB 1.x:
257
- * - new JexiDB('file.jsonl', { indexes: { id: 'number' } })
258
- * - await db.init()
259
- * - await db.insert({ id: 1, name: 'John' })
260
- * - await db.find({ id: 1 })
261
- * - await db.update({ id: 1 }, { name: 'John Smith' })
262
- * - await db.delete({ id: 1 })
263
- * - await db.save()
264
- * - await db.destroy()
265
- * - await db.walk() - Iterator for large volumes
266
- * - await db.validateIntegrity() - Manual verification
267
- */
268
-
269
- // Export the compatibility wrapper as default
270
- export default JexiDBCompatibility;
271
- // Retrocompat: named export 'Database' (JexiDB v1 used { Database })
272
- export const Database = JexiDBCompatibility;
273
-
274
- // Export auxiliary classes for advanced use
275
- export { FileHandler, IndexManager, IntegrityChecker };
276
-
277
- // Export useful constants
278
- export const OPERATORS = {
279
- GT: '>',
280
- GTE: '>=',
281
- LT: '<',
282
- LTE: '<=',
283
- NE: '!=',
284
- IN: 'in',
285
- NIN: 'nin',
286
- REGEX: 'regex',
287
- CONTAINS: 'contains'
288
- };
289
-
290
- // Export utility functions
291
- export const utils = {
292
- /**
293
- * Creates a database with default settings
294
- */
295
- createDatabase(filePath, indexes = {}) {
296
- return new JSONLDatabase(filePath, { indexes });
297
- },
298
-
299
- /**
300
- * Validates if a JSONL file is valid
301
- * @param {string} filePath - Path to the JSONL file
302
- * @returns {Promise<Object>} Validation result with errors and line count
303
- */
304
- async validateJSONLFile(filePath) {
305
- const { promises: fs } = await import('fs');
306
- const readline = await import('readline');
307
- const { createReadStream } = await import('fs');
308
-
309
- try {
310
- const fileStream = createReadStream(filePath);
311
- const rl = readline.createInterface({
312
- input: fileStream,
313
- crlfDelay: Infinity
314
- });
315
-
316
- let lineNumber = 0;
317
- const errors = [];
318
-
319
- for await (const line of rl) {
320
- lineNumber++;
321
- if (line.trim() !== '') {
322
- try {
323
- JSON.parse(line);
324
- } catch (error) {
325
- errors.push(`Line ${lineNumber}: ${error.message}`);
326
- }
327
- }
328
- }
329
-
330
- return {
331
- isValid: errors.length === 0,
332
- errors,
333
- lineCount: lineNumber
334
- };
335
- } catch (error) {
336
- return {
337
- isValid: false,
338
- errors: [`Error reading file: ${error.message}`],
339
- lineCount: 0
340
- };
341
- }
342
- },
343
-
344
- /**
345
- * Converts a JSON file to JSONL (basic conversion)
346
- * @param {string} jsonFilePath - Path to the JSON file
347
- * @param {string} jsonlFilePath - Path to the output JSONL file
348
- * @returns {Promise<Object>} Conversion result
349
- */
350
- async convertJSONToJSONL(jsonFilePath, jsonlFilePath) {
351
- const { promises: fs } = await import('fs');
352
-
353
- try {
354
- const jsonData = await fs.readFile(jsonFilePath, 'utf8');
355
- const data = JSON.parse(jsonData);
356
-
357
- const records = Array.isArray(data) ? data : [data];
358
- const jsonlContent = records.map(record => JSON.stringify(record)).join('\n') + '\n';
359
-
360
- await fs.writeFile(jsonlFilePath, jsonlContent, 'utf8');
361
-
362
- return {
363
- success: true,
364
- recordCount: records.length
365
- };
366
- } catch (error) {
367
- return {
368
- success: false,
369
- error: error.message
370
- };
371
- }
372
- },
373
-
374
- /**
375
- * Converts a JSONL file to JSON
376
- * @param {string} jsonlFilePath - Path to the JSONL file
377
- * @param {string} jsonFilePath - Path to the output JSON file
378
- * @returns {Promise<Object>} Conversion result
379
- */
380
- async convertJSONLToJSON(jsonlFilePath, jsonFilePath) {
381
- const { promises: fs } = await import('fs');
382
- const readline = await import('readline');
383
- const { createReadStream } = await import('fs');
384
-
385
- try {
386
- const fileStream = createReadStream(jsonlFilePath);
387
- const rl = readline.createInterface({
388
- input: fileStream,
389
- crlfDelay: Infinity
390
- });
391
-
392
- const records = [];
393
-
394
- for await (const line of rl) {
395
- if (line.trim() !== '') {
396
- try {
397
- const record = JSON.parse(line);
398
- records.push(record);
399
- } catch (error) {
400
- console.warn(`Line ignored: ${error.message}`);
401
- }
402
- }
403
- }
404
-
405
- const jsonContent = JSON.stringify(records, null, 2);
406
- await fs.writeFile(jsonFilePath, jsonContent, 'utf8');
407
-
408
- return {
409
- success: true,
410
- recordCount: records.length
411
- };
412
- } catch (error) {
413
- return {
414
- success: false,
415
- error: error.message
416
- };
417
- }
418
- },
419
-
420
- /**
421
- * Creates a JexiDB database from a JSON file with automatic index detection
422
- * @param {string} jsonFilePath - Path to the JSON file
423
- * @param {string} dbFilePath - Path to the output JexiDB file
424
- * @param {Object} options - Options for database creation
425
- * @param {Object} options.indexes - Manual index configuration
426
- * @param {boolean} options.autoDetectIndexes - Auto-detect common index fields (default: true)
427
- * @param {Array<string>} options.autoIndexFields - Fields to auto-index (default: ['id', '_id', 'email', 'name'])
428
- * @returns {Promise<Object>} Database creation result
429
- */
430
- async createDatabaseFromJSON(jsonFilePath, dbFilePath, options = {}) {
431
- const { promises: fs } = await import('fs');
432
-
433
- try {
434
- // Read JSON data
435
- const jsonData = await fs.readFile(jsonFilePath, 'utf8');
436
- const data = JSON.parse(jsonData);
437
- const records = Array.isArray(data) ? data : [data];
438
-
439
- if (records.length === 0) {
440
- return {
441
- success: false,
442
- error: 'No records found in JSON file'
443
- };
444
- }
445
-
446
- // Auto-detect indexes if enabled
447
- let indexes = options.indexes || {};
448
-
449
- if (options.autoDetectIndexes !== false) {
450
- const autoIndexFields = options.autoIndexFields || ['id', '_id', 'email', 'name', 'username'];
451
- const sampleRecord = records[0];
452
-
453
- for (const field of autoIndexFields) {
454
- if (sampleRecord.hasOwnProperty(field)) {
455
- const value = sampleRecord[field];
456
- if (typeof value === 'number') {
457
- indexes[field] = 'number';
458
- } else if (typeof value === 'string') {
459
- indexes[field] = 'string';
460
- }
461
- }
462
- }
463
- }
464
-
465
- // Create database
466
- const db = new JSONLDatabase(dbFilePath, {
467
- indexes,
468
- autoSave: false,
469
- validateOnInit: false
470
- });
471
-
472
- await db.init();
473
-
474
- // Insert all records
475
- for (const record of records) {
476
- await db.insert(record);
477
- }
478
-
479
- // Save and close the database
480
- await db.save();
481
- await db.close();
482
-
483
- return {
484
- success: true,
485
- recordCount: records.length,
486
- indexes: Object.keys(indexes),
487
- dbPath: dbFilePath
488
- };
489
- } catch (error) {
490
- return {
491
- success: false,
492
- error: error.message
493
- };
494
- }
495
- },
496
-
497
- /**
498
- * Analyzes a JSON file and suggests optimal indexes
499
- * @param {string} jsonFilePath - Path to the JSON file
500
- * @param {number} sampleSize - Number of records to analyze (default: 100)
501
- * @returns {Promise<Object>} Index suggestions
502
- */
503
- async analyzeJSONForIndexes(jsonFilePath, sampleSize = 100) {
504
- const { promises: fs } = await import('fs');
505
-
506
- try {
507
- const jsonData = await fs.readFile(jsonFilePath, 'utf8');
508
- const data = JSON.parse(jsonData);
509
- const records = Array.isArray(data) ? data : [data];
510
-
511
- if (records.length === 0) {
512
- return {
513
- success: false,
514
- error: 'No records found in JSON file'
515
- };
516
- }
517
-
518
- // Analyze sample records
519
- const sample = records.slice(0, Math.min(sampleSize, records.length));
520
- const fieldAnalysis = {};
521
-
522
- // First, collect all possible fields from all records
523
- const allFields = new Set();
524
- for (const record of sample) {
525
- for (const field of Object.keys(record)) {
526
- allFields.add(field);
527
- }
528
- }
529
-
530
- // Initialize analysis for all fields
531
- for (const field of allFields) {
532
- fieldAnalysis[field] = {
533
- type: 'unknown',
534
- uniqueValues: new Set(),
535
- nullCount: 0,
536
- totalCount: 0
537
- };
538
- }
539
-
540
- // Analyze each field across all records
541
- for (const record of sample) {
542
- for (const field of allFields) {
543
- const value = record[field];
544
- fieldAnalysis[field].totalCount++;
545
-
546
- if (value === null || value === undefined) {
547
- fieldAnalysis[field].nullCount++;
548
- } else {
549
- if (fieldAnalysis[field].type === 'unknown') {
550
- fieldAnalysis[field].type = typeof value;
551
- }
552
- fieldAnalysis[field].uniqueValues.add(value);
553
- }
554
- }
555
- }
556
-
557
- // Generate suggestions
558
- const suggestions = {
559
- recommended: [],
560
- optional: [],
561
- notRecommended: []
562
- };
563
-
564
- for (const [field, analysis] of Object.entries(fieldAnalysis)) {
565
- const coverage = (analysis.totalCount - analysis.nullCount) / analysis.totalCount;
566
- const uniqueness = analysis.uniqueValues.size / analysis.totalCount;
567
-
568
- const suggestion = {
569
- field,
570
- type: analysis.type,
571
- coverage: Math.round(coverage * 100),
572
- uniqueness: Math.round(uniqueness * 100),
573
- uniqueValues: analysis.uniqueValues.size
574
- };
575
-
576
- // Recommendation logic
577
- if (coverage > 0.9 && uniqueness > 0.8) {
578
- suggestions.recommended.push(suggestion);
579
- } else if (coverage > 0.7 && uniqueness > 0.5) {
580
- suggestions.optional.push(suggestion);
581
- } else {
582
- suggestions.notRecommended.push(suggestion);
583
- }
584
- }
585
-
586
- // Convert suggestions to the expected format for tests
587
- const suggestedIndexes = {};
588
- for (const suggestion of suggestions.recommended) {
589
- suggestedIndexes[suggestion.field] = suggestion.type;
590
- }
591
-
592
- return {
593
- success: true,
594
- totalRecords: records.length,
595
- analyzedRecords: sample.length,
596
- suggestedIndexes,
597
- suggestions
598
- };
599
- } catch (error) {
600
- return {
601
- success: false,
602
- error: error.message
603
- };
604
- }
605
- },
606
-
607
-
608
- };