jexidb 2.1.0 → 2.1.2

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 (41) hide show
  1. package/dist/Database.cjs +9253 -437
  2. package/package.json +9 -2
  3. package/src/Database.mjs +1572 -212
  4. package/src/FileHandler.mjs +83 -44
  5. package/src/OperationQueue.mjs +23 -23
  6. package/src/SchemaManager.mjs +325 -268
  7. package/src/Serializer.mjs +234 -24
  8. package/src/managers/IndexManager.mjs +778 -87
  9. package/src/managers/QueryManager.mjs +340 -67
  10. package/src/managers/TermManager.mjs +7 -7
  11. package/src/utils/operatorNormalizer.mjs +116 -0
  12. package/.babelrc +0 -13
  13. package/.gitattributes +0 -2
  14. package/CHANGELOG.md +0 -140
  15. package/babel.config.json +0 -5
  16. package/docs/API.md +0 -1051
  17. package/docs/EXAMPLES.md +0 -701
  18. package/docs/README.md +0 -194
  19. package/examples/iterate-usage-example.js +0 -157
  20. package/examples/simple-iterate-example.js +0 -115
  21. package/jest.config.js +0 -24
  22. package/scripts/README.md +0 -47
  23. package/scripts/clean-test-files.js +0 -75
  24. package/scripts/prepare.js +0 -31
  25. package/scripts/run-tests.js +0 -80
  26. package/test/$not-operator-with-and.test.js +0 -282
  27. package/test/README.md +0 -8
  28. package/test/close-init-cycle.test.js +0 -256
  29. package/test/critical-bugs-fixes.test.js +0 -1069
  30. package/test/index-persistence.test.js +0 -306
  31. package/test/index-serialization.test.js +0 -314
  32. package/test/indexed-query-mode.test.js +0 -360
  33. package/test/iterate-method.test.js +0 -272
  34. package/test/query-operators.test.js +0 -238
  35. package/test/regex-array-fields.test.js +0 -129
  36. package/test/score-method.test.js +0 -238
  37. package/test/setup.js +0 -17
  38. package/test/term-mapping-minimal.test.js +0 -154
  39. package/test/term-mapping-simple.test.js +0 -257
  40. package/test/term-mapping.test.js +0 -514
  41. package/test/writebuffer-flush-resilience.test.js +0 -204
@@ -1,1069 +0,0 @@
1
- import { Database } from '../src/Database.mjs'
2
- import fs from 'fs'
3
- import path from 'path'
4
-
5
- describe('Critical Bugs Fixes', () => {
6
- let testDir
7
- let db
8
-
9
- beforeEach(() => {
10
- testDir = path.join(process.cwd(), 'test-files', 'critical-bugs')
11
- fs.mkdirSync(testDir, { recursive: true })
12
- })
13
-
14
- afterEach(async () => {
15
- if (db) {
16
- await db.close()
17
- }
18
- // Clean up test files with retry mechanism
19
- if (fs.existsSync(testDir)) {
20
- try {
21
- fs.rmSync(testDir, { recursive: true, force: true })
22
- } catch (error) {
23
- // If removal fails, try again after a short delay
24
- await new Promise(resolve => setTimeout(resolve, 100))
25
- try {
26
- fs.rmSync(testDir, { recursive: true, force: true })
27
- } catch (retryError) {
28
- console.warn('Could not clean up test directory:', testDir)
29
- }
30
- }
31
- }
32
- })
33
-
34
- describe('Bug #1: File Descriptor Exhaustion (EMFILE)', () => {
35
- test('should handle concurrent operations without EMFILE errors', async () => {
36
- const dbPath = path.join(testDir, 'concurrent-test.jdb')
37
-
38
- // Create database with connection pooling enabled
39
- db = new Database(dbPath, {
40
- clear: true,
41
- create: true,
42
- indexes: {
43
- nameTerms: 'array:string',
44
- groupTerms: 'array:string'
45
- },
46
- debugMode: false
47
- })
48
-
49
- await db.init()
50
-
51
- // Test concurrent inserts
52
- const insertPromises = []
53
- for (let i = 0; i < 100; i++) {
54
- insertPromises.push(
55
- db.insert({
56
- name: `Channel ${i}`,
57
- nameTerms: [`channel`, `${i}`],
58
- url: `http://example.com/${i}`
59
- })
60
- )
61
- }
62
-
63
- // This should complete successfully
64
- await expect(Promise.all(insertPromises)).resolves.not.toThrow()
65
-
66
- // Verify data was inserted
67
- expect(db.length).toBe(100)
68
- })
69
-
70
- test('should handle concurrent queries without EMFILE errors', async () => {
71
- const dbPath = path.join(testDir, 'concurrent-queries.jdb')
72
-
73
- db = new Database(dbPath, {
74
- clear: true,
75
- create: true,
76
- indexes: {
77
- nameTerms: 'array:string'
78
- },
79
- })
80
-
81
- await db.init()
82
-
83
- // Insert test data
84
- for (let i = 0; i < 50; i++) {
85
- await db.insert({
86
- name: `Channel ${i}`,
87
- nameTerms: [`channel`, `${i}`],
88
- url: `http://example.com/${i}`
89
- })
90
- }
91
-
92
- // Test concurrent queries
93
- const queryPromises = []
94
- for (let i = 0; i < 100; i++) {
95
- queryPromises.push(
96
- db.findOne({ nameTerms: { $in: ['channel'] } })
97
- )
98
- }
99
-
100
- // This should complete successfully
101
- await expect(Promise.all(queryPromises)).resolves.not.toThrow()
102
- })
103
- })
104
-
105
- describe('Bug #2: Object Reference Corruption', () => {
106
- test('should not corrupt object references during insert', async () => {
107
- const dbPath = path.join(testDir, 'reference-corruption.jdb')
108
-
109
- db = new Database(dbPath, {
110
- clear: true,
111
- create: true,
112
- indexes: {
113
- nameTerms: 'array:string'
114
- }
115
- })
116
-
117
- await db.init()
118
-
119
- // Create test entry
120
- const entry = {
121
- name: 'Test Channel',
122
- nameTerms: ['test', 'channel'],
123
- url: 'http://example.com'
124
- }
125
-
126
- // Store original values
127
- const originalNameTerms = [...entry.nameTerms]
128
- const originalName = entry.name
129
-
130
- // Insert entry
131
- await db.insert(entry)
132
-
133
- // Verify original object was not corrupted
134
- expect(entry.nameTerms).toEqual(originalNameTerms)
135
- expect(entry.name).toBe(originalName)
136
- expect(entry.nameTerms).not.toBeUndefined()
137
- expect(entry.nameTerms.length).toBe(2)
138
- })
139
-
140
- test('should handle concurrent inserts without reference corruption', async () => {
141
- const dbPath = path.join(testDir, 'concurrent-reference.jdb')
142
-
143
- db = new Database(dbPath, {
144
- clear: true,
145
- create: true,
146
- indexes: {
147
- nameTerms: 'array:string'
148
- },
149
- })
150
-
151
- await db.init()
152
-
153
- // Create multiple entries with same reference structure
154
- const entries = []
155
- for (let i = 0; i < 20; i++) {
156
- entries.push({
157
- name: `Channel ${i}`,
158
- nameTerms: ['test', 'channel', `${i}`],
159
- url: `http://example.com/${i}`
160
- })
161
- }
162
-
163
- // Insert all entries concurrently
164
- const insertPromises = entries.map(entry => db.insert(entry))
165
- await Promise.all(insertPromises)
166
-
167
- // Verify all original entries are intact
168
- entries.forEach((entry, index) => {
169
- expect(entry.nameTerms).toEqual(['test', 'channel', `${index}`])
170
- expect(entry.name).toBe(`Channel ${index}`)
171
- expect(entry.nameTerms.length).toBe(3)
172
- })
173
- })
174
- })
175
-
176
- describe('Bug #3: Array Query Syntax Issues', () => {
177
- test('should handle direct array field queries automatically', async () => {
178
- const dbPath = path.join(testDir, 'array-query.jdb')
179
-
180
- db = new Database(dbPath, {
181
- clear: true,
182
- create: true,
183
- debugMode: false,
184
- indexes: {
185
- nameTerms: 'array:string',
186
- tags: 'array:string'
187
- }
188
- })
189
-
190
- await db.init()
191
-
192
- // Insert test data
193
- await db.insert({
194
- name: 'CNN International',
195
- nameTerms: ['cnn', 'international', 'news'],
196
- tags: ['news', 'international']
197
- })
198
-
199
- // Test direct value queries (should work automatically)
200
- const result1 = await db.findOne({ nameTerms: 'cnn' })
201
- expect(result1).toBeTruthy()
202
- expect(result1.name).toBe('CNN International')
203
-
204
- // Test array value queries (should work automatically)
205
- const result2 = await db.findOne({ nameTerms: ['cnn'] })
206
- expect(result2).toBeTruthy()
207
- expect(result2.name).toBe('CNN International')
208
-
209
- // Test $in queries (should still work)
210
- const result3 = await db.findOne({ nameTerms: { $in: ['cnn'] } })
211
- expect(result3).toBeTruthy()
212
- expect(result3.name).toBe('CNN International')
213
-
214
- // Test multiple values
215
- const result4 = await db.findOne({ tags: 'news' })
216
- expect(result4).toBeTruthy()
217
- expect(result4.name).toBe('CNN International')
218
- })
219
-
220
- test('should provide clear error messages for invalid array queries', async () => {
221
- const dbPath = path.join(testDir, 'array-query-error.jdb')
222
-
223
- db = new Database(dbPath, {
224
- clear: true,
225
- create: true,
226
- indexes: {
227
- nameTerms: 'array:string'
228
- }
229
- })
230
-
231
- await db.init()
232
-
233
- // Insert test data
234
- await db.insert({
235
- name: 'Test Channel',
236
- nameTerms: ['test', 'channel']
237
- })
238
-
239
- // Test invalid query (should throw clear error message)
240
- await expect(db.findOne({ nameTerms: null })).rejects.toThrow(
241
- "Invalid query for array field 'nameTerms'. Use { $in: [value] } syntax or direct value."
242
- )
243
- })
244
- })
245
-
246
- describe('Bug #4: Memory Leaks and Performance Issues', () => {
247
- test('should properly clean up resources on destroy', async () => {
248
- const dbPath = path.join(testDir, 'memory-cleanup.jdb')
249
-
250
- db = new Database(dbPath, {
251
- clear: true,
252
- create: true,
253
- indexes: {
254
- nameTerms: 'array:string'
255
- },
256
- debugMode: false
257
- })
258
-
259
- await db.init()
260
-
261
- // Insert some data
262
- for (let i = 0; i < 100; i++) {
263
- await db.insert({
264
- name: `Channel ${i}`,
265
- nameTerms: [`channel`, `${i}`]
266
- })
267
- }
268
-
269
- // Get memory usage before save
270
- const memBefore = db.getMemoryUsage()
271
- expect(memBefore.writeBufferSize).toBe(100) // Records should be in writeBuffer
272
- expect(memBefore.total).toBe(100) // Total should be 100 records
273
-
274
- // Save data before destroy
275
- await db.save()
276
-
277
- // Verify writeBuffer is cleared after save
278
- const memAfter = db.getMemoryUsage()
279
- expect(memAfter.writeBufferSize).toBe(0) // writeBuffer should be empty after save
280
-
281
- // Destroy database
282
- await db.destroy()
283
-
284
- // Verify database is destroyed
285
- expect(db.destroyed).toBe(true)
286
- expect(db.operationQueue).toBeNull()
287
- })
288
-
289
- test('should handle auto-save functionality', async () => {
290
- const dbPath = path.join(testDir, 'auto-save.jdb')
291
-
292
- db = new Database(dbPath, {
293
- clear: true,
294
- create: true,
295
- indexes: {
296
- nameTerms: 'array:string'
297
- },
298
- debugMode: false
299
- })
300
-
301
- await db.init()
302
-
303
- // Insert data to trigger auto-save
304
- for (let i = 0; i < 10; i++) {
305
- await db.insert({
306
- name: `Channel ${i}`,
307
- nameTerms: [`channel`, `${i}`]
308
- })
309
- }
310
-
311
- // Wait for auto-save to complete (it's async via setImmediate)
312
- await new Promise(resolve => setTimeout(resolve, 2000))
313
-
314
- // Verify data was saved
315
- expect(db.length).toBe(10)
316
- })
317
- })
318
-
319
- describe('Bug #5: Incomplete Database Structure', () => {
320
- test('should create complete database structure even when empty', async () => {
321
- const dbPath = path.join(testDir, 'empty-structure.jdb')
322
-
323
- db = new Database(dbPath, {
324
- clear: true,
325
- create: true,
326
- indexes: {
327
- nameTerms: 'array:string'
328
- }
329
- })
330
-
331
- await db.init()
332
-
333
- // Don't insert any data
334
- expect(db.length).toBe(0)
335
-
336
- // Save empty database
337
- await db.save()
338
-
339
- // Check if all required files were created
340
- const mainFile = dbPath
341
- const indexFile = dbPath.replace(/\.jdb$/, '.idx.jdb')
342
-
343
- expect(fs.existsSync(mainFile)).toBe(true)
344
- expect(fs.existsSync(indexFile)).toBe(true)
345
- // Note: offsets file might be combined with index file in newer versions
346
- })
347
-
348
- test('should maintain structure consistency after operations', async () => {
349
- const dbPath = path.join(testDir, 'structure-consistency.jdb')
350
-
351
- db = new Database(dbPath, {
352
- clear: true,
353
- create: true,
354
- indexes: {
355
- nameTerms: 'array:string'
356
- }
357
- })
358
-
359
- await db.init()
360
-
361
- // Insert some data
362
- await db.insert({
363
- name: 'Test Channel',
364
- nameTerms: ['test', 'channel']
365
- })
366
-
367
- await db.save()
368
-
369
- // Verify structure
370
- expect(db.length).toBe(1)
371
- expect(fs.existsSync(dbPath)).toBe(true)
372
- expect(fs.existsSync(dbPath.replace(/\.jdb$/, '.idx.jdb'))).toBe(true)
373
- })
374
- })
375
-
376
- describe('Integration Tests', () => {
377
- test('should handle real-world scenario similar to update-list-index.js', async () => {
378
- const dbPath = path.join(testDir, 'real-world.jdb')
379
-
380
- // Configuration similar to the workaround
381
- db = new Database(dbPath, {
382
- clear: true,
383
- create: true,
384
- indexes: {
385
- nameTerms: 'array:string',
386
- groupTerms: 'array:string'
387
- },
388
- maxWriteBufferSize: 256 * 1024, // 256KB
389
- integrityCheck: 'none',
390
- streamingThreshold: 0.8,
391
- indexedQueryMode: 'strict',
392
- debugMode: false,
393
- })
394
-
395
- await db.init()
396
-
397
- // Simulate the insert pattern from the workaround
398
- const entries = []
399
- for (let i = 0; i < 1000; i++) {
400
- entries.push({
401
- name: `Channel ${i}`,
402
- nameTerms: ['channel', `${i}`, 'test'],
403
- groupTerms: ['group1', 'group2'],
404
- url: `http://example.com/${i}`
405
- })
406
- }
407
-
408
- // Insert with controlled concurrency (similar to the workaround)
409
- const batchSize = 50
410
- for (let i = 0; i < entries.length; i += batchSize) {
411
- const batch = entries.slice(i, i + batchSize)
412
- const promises = batch.map(entry => db.insert(entry))
413
- await Promise.all(promises)
414
- }
415
-
416
- // Verify all data was inserted correctly
417
- expect(db.length).toBe(1000)
418
-
419
- // Test queries work correctly
420
- const result = await db.findOne({ nameTerms: 'channel' })
421
- expect(result).toBeTruthy()
422
- expect(result.name).toMatch(/^Channel \d+$/)
423
-
424
- // Test array queries work
425
- const results = await db.find({ nameTerms: 'test' })
426
- expect(results.length).toBe(1000)
427
-
428
- // Verify no object corruption occurred
429
- entries.forEach((entry, index) => {
430
- expect(entry.nameTerms).toEqual(['channel', `${index}`, 'test'])
431
- expect(entry.name).toBe(`Channel ${index}`)
432
- })
433
- })
434
- })
435
-
436
- describe('🚨 CRITICAL: Save Buffer Regression Tests', () => {
437
- test('save() should automatically flush writeBuffer and ensure it is empty', async () => {
438
- const dbPath = path.join(testDir, 'test-flush.jdb')
439
- db = new Database(dbPath, { clear: true, create: true })
440
- await db.init()
441
-
442
- // Insert data to populate writeBuffer
443
- await db.insert({ name: "Test", url: "http://test.com" })
444
- await db.insert({ name: "Test2", url: "http://test2.com" })
445
-
446
- // Verify writeBuffer has data before save
447
- expect(db.writeBuffer.length).toBeGreaterThan(0)
448
-
449
- // Save should flush the buffer
450
- await db.save()
451
-
452
- // CRITICAL: writeBuffer must be empty after save
453
- expect(db.writeBuffer.length).toBe(0)
454
- })
455
-
456
- test('save() should persist data correctly after flushing buffer', async () => {
457
- const dbPath = path.join(testDir, 'test-persist.jdb')
458
- db = new Database(dbPath, { clear: true, create: true })
459
-
460
- await db.init()
461
-
462
- // Insert test data
463
- const testData = { name: "RTP1", url: "http://example.com" }
464
- await db.insert(testData)
465
-
466
- // Save should flush and persist data
467
- await db.save()
468
-
469
- // Query should return the actual data, not empty entries
470
- const results = await db.find()
471
- expect(results).toHaveLength(1)
472
- expect(results[0].name).toBe("RTP1")
473
- expect(results[0].url).toBe("http://example.com")
474
- })
475
-
476
- test('save() should work with immediate destroy without data loss', async () => {
477
- const dbPath = path.join(testDir, 'test-immediate-destroy.jdb')
478
- db = new Database(dbPath, { clear: true, create: true })
479
- await db.init()
480
-
481
- // Insert data
482
- await db.insert({ name: "DestroyTest", url: "http://destroy.com" })
483
-
484
- // Save and immediately destroy
485
- await db.close()
486
-
487
- // Create new database instance and verify data persisted
488
- const db2 = new Database(dbPath, { create: true })
489
- await db2.init()
490
-
491
- const results = await db2.find()
492
- expect(results).toHaveLength(1)
493
- expect(results[0].name).toBe("DestroyTest")
494
- expect(results[0].url).toBe("http://destroy.com")
495
-
496
- await db2.destroy()
497
- })
498
-
499
- test('save() should handle multiple inserts correctly', async () => {
500
- const dbPath = path.join(testDir, 'test-multiple.jdb')
501
- db = new Database(dbPath, { clear: true, create: true })
502
-
503
- await db.init()
504
-
505
- // Insert multiple records
506
- const testData = [
507
- { name: "Channel1", url: "http://channel1.com" },
508
- { name: "Channel2", url: "http://channel2.com" },
509
- { name: "Channel3", url: "http://channel3.com" }
510
- ]
511
-
512
- for (const data of testData) {
513
- await db.insert(data)
514
- }
515
-
516
- // Save should flush all data
517
- await db.save()
518
-
519
- // All data should be persisted
520
- const results = await db.find()
521
- expect(results).toHaveLength(3)
522
-
523
- // Verify no empty entries
524
- results.forEach(result => {
525
- expect(result.name).not.toBe("")
526
- expect(result.url).not.toBe("")
527
- })
528
- })
529
-
530
- test('save() should not return before file operations are complete', async () => {
531
- const dbPath = path.join(testDir, 'test-io-completion.jdb')
532
- db = new Database(dbPath, { clear: true, create: true })
533
-
534
- await db.init()
535
-
536
- // Insert data
537
- await db.insert({ name: "IOTest", url: "http://iotest.com" })
538
-
539
- // Ensure data is in writeBuffer
540
- expect(db.writeBuffer.length).toBeGreaterThan(0)
541
-
542
- // Save should wait for I/O completion
543
- const saveStart = Date.now()
544
- await db.save()
545
- const saveEnd = Date.now()
546
-
547
- // Verify save actually took time (indicating I/O operations)
548
- expect(saveEnd - saveStart).toBeGreaterThan(0)
549
-
550
- // Verify data is actually persisted by checking file system
551
- const dataFile = dbPath
552
-
553
- // Data file should exist and have content
554
- const dataStats = fs.statSync(dataFile)
555
- expect(dataStats.size).toBeGreaterThan(0)
556
-
557
- // Verify data can be queried (proving persistence)
558
- const results = await db.find()
559
- expect(results).toHaveLength(1)
560
- expect(results[0].name).toBe("IOTest")
561
- })
562
-
563
- test('flushInsertionBuffer() should work for backward compatibility', async () => {
564
- const dbPath = path.join(testDir, 'test-backward-compat.jdb')
565
- db = new Database(dbPath, { clear: true, create: true })
566
-
567
- await db.init()
568
-
569
- // Insert data
570
- await db.insert({ name: "BackwardTest", url: "http://backward.com" })
571
-
572
- // Use deprecated method (should show warning but work)
573
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
574
- await db.flushInsertionBuffer()
575
- consoleSpy.mockRestore()
576
-
577
- // Data should be persisted
578
- const results = await db.find()
579
- expect(results).toHaveLength(1)
580
- expect(results[0].name).toBe("BackwardTest")
581
- })
582
-
583
- test('should handle rapid insert-save cycles', async () => {
584
- const dbPath = path.join(testDir, 'test-rapid-cycles.jdb')
585
- db = new Database(dbPath, { clear: true, create: true })
586
-
587
- await db.init()
588
-
589
- // Rapid insert-save cycles
590
- for (let i = 0; i < 10; i++) {
591
- await db.insert({ name: `Cycle${i}`, url: `http://cycle${i}.com` })
592
- await db.save()
593
- }
594
-
595
- // Wait for all operations to complete
596
- await db.waitForOperations()
597
-
598
- // Force a final save to ensure all data is persisted
599
- await db.save()
600
-
601
- // All data should be persisted
602
- const results = await db.find()
603
- expect(results).toHaveLength(10)
604
-
605
- // Verify data integrity
606
- results.forEach((result, index) => {
607
- expect(result.name).toBe(`Cycle${index}`)
608
- expect(result.url).toBe(`http://cycle${index}.com`)
609
- })
610
- })
611
- })
612
-
613
- describe('Bug #6: db.walk() returns arrays instead of objects', () => {
614
- test('should return objects consistently in db.walk() method', async () => {
615
- const dbPath = path.join(testDir, 'walk-bug-test.jdb')
616
-
617
- // Create database with EPG-like schema (similar to the bug report)
618
- db = new Database(dbPath, {
619
- clear: true,
620
- create: true,
621
- fields: {
622
- ch: 'string', // Channel name
623
- start: 'number', // Start timestamp
624
- e: 'number', // End timestamp
625
- t: 'string', // Programme title
626
- i: 'string', // Icon URL
627
- desc: 'string', // Description
628
- c: 'array:string', // Categories
629
- terms: 'array:string', // Search terms
630
- age: 'number', // Age rating
631
- lang: 'string', // Language
632
- country: 'string', // Country
633
- rating: 'string', // Rating
634
- parental: 'string', // Parental control
635
- genre: 'string', // Genre
636
- contentType: 'string', // Content type
637
- parentalLock: 'string', // Parental lock
638
- geo: 'string', // Geographic restriction
639
- ageRestriction: 'string' // Age restriction
640
- },
641
- indexes: ['ch', 'start', 'e', 'c', 'terms']
642
- })
643
-
644
- await db.init()
645
-
646
- // Insert test data that mimics EPG programme data
647
- const testProgrammes = [
648
- {
649
- ch: 'Canal Sony FHD H.265',
650
- start: 1759628040,
651
- e: 1759639740,
652
- t: 'À Espera de um Milagre',
653
- i: '',
654
- desc: '',
655
- c: ['Filme', 'Drama'],
656
- terms: ['à', 'espera', 'de', 'um', 'milagre', 'canal', 'sony', 'h', '265', 'filme', 'drama'],
657
- age: 0,
658
- lang: '',
659
- country: '',
660
- rating: '',
661
- parental: 'no',
662
- genre: 'Filme, Drama',
663
- contentType: '',
664
- parentalLock: 'false',
665
- geo: '',
666
- ageRestriction: ''
667
- },
668
- {
669
- ch: 'GLOBO',
670
- start: 1759628040,
671
- e: 1759639740,
672
- t: 'Jornal da Globo',
673
- i: '',
674
- desc: '',
675
- c: ['Jornalismo'],
676
- terms: ['jornal', 'globo', 'noticias', 'jornalismo'],
677
- age: 0,
678
- lang: '',
679
- country: '',
680
- rating: '',
681
- parental: 'no',
682
- genre: 'Jornalismo',
683
- contentType: '',
684
- parentalLock: 'false',
685
- geo: '',
686
- ageRestriction: ''
687
- },
688
- {
689
- ch: 'ESPN',
690
- start: 1759630000,
691
- e: 1759640000,
692
- t: 'ESPN SportsCenter',
693
- i: '',
694
- desc: '',
695
- c: ['Esporte'],
696
- terms: ['espn', 'sportscenter', 'esporte', 'futebol'],
697
- age: 0,
698
- lang: '',
699
- country: '',
700
- rating: '',
701
- parental: 'no',
702
- genre: 'Esporte',
703
- contentType: '',
704
- parentalLock: 'false',
705
- geo: '',
706
- ageRestriction: ''
707
- }
708
- ]
709
-
710
- // Insert data using InsertSession (as used in EPG applications)
711
- const insertSession = db.beginInsertSession({
712
- batchSize: 500,
713
- enableAutoSave: true
714
- })
715
-
716
- for (const programme of testProgrammes) {
717
- await insertSession.add(programme)
718
- }
719
-
720
- await insertSession.commit()
721
- await db.save()
722
-
723
- // Test queries that previously caused the bug
724
- const testQueries = [
725
- { terms: ['à', 'espera', 'de', 'um', 'milagre'] },
726
- { terms: ['globo', 'jornal'] },
727
- { terms: ['espn'] }
728
- ]
729
-
730
- for (const query of testQueries) {
731
- // Test 1: db.find() - should work correctly
732
- const findResults = await db.find(query)
733
- expect(findResults.length).toBeGreaterThan(0)
734
-
735
- // Verify all results are objects
736
- findResults.forEach(result => {
737
- expect(Array.isArray(result)).toBe(false)
738
- expect(typeof result).toBe('object')
739
- expect(result.ch).toBeDefined()
740
- expect(result.t).toBeDefined()
741
- })
742
-
743
- // Test 2: db.walk() without options - should return objects
744
- const walkResults = []
745
- for await (const programme of db.walk(query)) {
746
- walkResults.push(programme)
747
- }
748
-
749
- expect(walkResults.length).toBeGreaterThan(0)
750
-
751
- // CRITICAL: Verify all results are objects, not arrays
752
- walkResults.forEach(result => {
753
- expect(Array.isArray(result)).toBe(false)
754
- expect(typeof result).toBe('object')
755
- expect(result.ch).toBeDefined()
756
- expect(result.t).toBeDefined()
757
- })
758
-
759
- // Test 3: db.walk() with caseInsensitive option - should return objects
760
- const walkCaseInsensitiveResults = []
761
- for await (const programme of db.walk(query, { caseInsensitive: true })) {
762
- walkCaseInsensitiveResults.push(programme)
763
- }
764
-
765
- walkCaseInsensitiveResults.forEach(result => {
766
- expect(Array.isArray(result)).toBe(false)
767
- expect(typeof result).toBe('object')
768
- expect(result.ch).toBeDefined()
769
- expect(result.t).toBeDefined()
770
- })
771
-
772
- // Test 4: db.walk() with matchAny option - should return objects
773
- const walkMatchAnyResults = []
774
- for await (const programme of db.walk(query, { matchAny: true })) {
775
- walkMatchAnyResults.push(programme)
776
- }
777
-
778
- walkMatchAnyResults.forEach(result => {
779
- expect(Array.isArray(result)).toBe(false)
780
- expect(typeof result).toBe('object')
781
- expect(result.ch).toBeDefined()
782
- expect(result.t).toBeDefined()
783
- })
784
-
785
- // Test 5: db.walk() with both options - should return objects
786
- const walkBothOptionsResults = []
787
- for await (const programme of db.walk(query, { caseInsensitive: true, matchAny: true })) {
788
- walkBothOptionsResults.push(programme)
789
- }
790
-
791
- walkBothOptionsResults.forEach(result => {
792
- expect(Array.isArray(result)).toBe(false)
793
- expect(typeof result).toBe('object')
794
- expect(result.ch).toBeDefined()
795
- expect(result.t).toBeDefined()
796
- })
797
- }
798
-
799
- // Test 6: db.walk() with empty query - should return objects
800
- const emptyQueryResults = []
801
- for await (const programme of db.walk({})) {
802
- emptyQueryResults.push(programme)
803
- }
804
-
805
- expect(emptyQueryResults.length).toBe(3) // Should return all 3 programmes
806
-
807
- emptyQueryResults.forEach(result => {
808
- expect(Array.isArray(result)).toBe(false)
809
- expect(typeof result).toBe('object')
810
- expect(result.ch).toBeDefined()
811
- expect(result.t).toBeDefined()
812
- })
813
- })
814
-
815
- test('should maintain consistency between db.find() and db.walk() results', async () => {
816
- const dbPath = path.join(testDir, 'walk-consistency-test.jdb')
817
-
818
- db = new Database(dbPath, {
819
- clear: true,
820
- create: true,
821
- fields: {
822
- name: 'string',
823
- category: 'string',
824
- tags: 'array:string'
825
- },
826
- indexes: ['name', 'category', 'tags']
827
- })
828
-
829
- await db.init()
830
-
831
- // Insert test data
832
- const testData = [
833
- { name: 'Movie A', category: 'Entertainment', tags: ['action', 'thriller'] },
834
- { name: 'Movie B', category: 'Entertainment', tags: ['comedy', 'romance'] },
835
- { name: 'News Show', category: 'News', tags: ['current', 'events'] }
836
- ]
837
-
838
- for (const item of testData) {
839
- await db.insert(item)
840
- }
841
-
842
- await db.save()
843
-
844
- // Test various queries
845
- const queries = [
846
- { category: 'Entertainment' },
847
- { tags: ['action'] },
848
- { name: 'News Show' },
849
- { category: 'News', tags: ['current'] }
850
- ]
851
-
852
- for (const query of queries) {
853
- // Get results from both methods
854
- const findResults = await db.find(query)
855
- const walkResults = []
856
-
857
- for await (const item of db.walk(query)) {
858
- walkResults.push(item)
859
- }
860
-
861
- // Both should return the same number of results
862
- expect(walkResults.length).toBe(findResults.length)
863
-
864
- // Both should return objects with the same structure
865
- findResults.forEach((findResult, index) => {
866
- const walkResult = walkResults[index]
867
-
868
- // Both should be objects
869
- expect(Array.isArray(findResult)).toBe(false)
870
- expect(Array.isArray(walkResult)).toBe(false)
871
-
872
- // Both should have the same properties
873
- expect(walkResult.name).toBe(findResult.name)
874
- expect(walkResult.category).toBe(findResult.category)
875
- expect(walkResult.tags).toEqual(findResult.tags)
876
- })
877
- }
878
- })
879
-
880
- test('length should be correct after close() + init() cycle', async () => {
881
- const dbPath = path.join(testDir, 'test-close-init-length.jdb')
882
- db = new Database(dbPath, { clear: true, create: true })
883
-
884
- await db.init()
885
-
886
- // Insert test data
887
- const testData = [
888
- { name: "Channel1", url: "http://channel1.com" },
889
- { name: "Channel2", url: "http://channel2.com" },
890
- { name: "Channel3", url: "http://channel3.com" }
891
- ]
892
-
893
- for (const data of testData) {
894
- await db.insert(data)
895
- }
896
-
897
- // Verify data is inserted
898
- expect(db.length).toBe(3)
899
-
900
- // Save data to disk
901
- await db.save()
902
-
903
- // Verify data is still there after save
904
- expect(db.length).toBe(3)
905
-
906
- // Close database
907
- await db.close()
908
-
909
- // Create new database instance
910
- const db2 = new Database(dbPath, { create: true })
911
- await db2.init()
912
-
913
- // CRITICAL: length should be correct after init()
914
- expect(db2.length).toBe(3)
915
-
916
- // Verify data is accessible
917
- const results = await db2.find()
918
- expect(results).toHaveLength(3)
919
- expect(results[0].name).toBe("Channel1")
920
- expect(results[1].name).toBe("Channel2")
921
- expect(results[2].name).toBe("Channel3")
922
-
923
- await db2.destroy()
924
- })
925
-
926
- test('EPGManager-like scenario: database state after close + init', async () => {
927
- const dbPath = path.join(testDir, 'test-epg-manager-scenario.jdb')
928
- db = new Database(dbPath, { clear: true, create: true })
929
-
930
- await db.init()
931
-
932
- // Insert EPG-like data
933
- const epgData = [
934
- { ch: "Channel1", t: "Program 1", start: 1640995200, e: 1640998800, terms: ["news", "sports"] },
935
- { ch: "Channel2", t: "Program 2", start: 1640995200, e: 1640998800, terms: ["movie", "action"] },
936
- { ch: "Channel3", t: "Program 3", start: 1640995200, e: 1640998800, terms: ["comedy", "show"] }
937
- ]
938
-
939
- for (const data of epgData) {
940
- await db.insert(data)
941
- }
942
-
943
- // Verify data is inserted
944
- expect(db.length).toBe(3)
945
-
946
- // Save data to disk
947
- await db.save()
948
-
949
- // Close database
950
- await db.close()
951
-
952
- // Create new database instance (like EPGManager does)
953
- const db2 = new Database(dbPath, { create: true })
954
-
955
- // Simulate EPGManager initialization sequence
956
- await db2.init()
957
-
958
- // Add delay like EPGManager does
959
- await new Promise(resolve => setTimeout(resolve, 500))
960
-
961
- // Test database accessibility like EPGManager does
962
- const testCount = await db2.count()
963
- console.log(`Database test successful, total records: ${testCount}`)
964
-
965
- // CRITICAL: length should be correct after init()
966
- expect(db2.length).toBe(3)
967
- expect(testCount).toBe(3)
968
-
969
- // Verify data is accessible with EPG-like queries
970
- const results = await db2.find({ terms: { $in: ["news"] } })
971
- expect(results).toHaveLength(1)
972
- expect(results[0].t).toBe("Program 1")
973
-
974
- await db2.destroy()
975
- })
976
-
977
- test('IMMEDIATE ACCESS: No delay should be needed after init()', async () => {
978
- const dbPath = path.join(testDir, 'test-immediate-access.jdb')
979
- db = new Database(dbPath, { clear: true, create: true })
980
-
981
- await db.init()
982
-
983
- // Insert test data
984
- const testData = [
985
- { name: "Test1", value: 1 },
986
- { name: "Test2", value: 2 },
987
- { name: "Test3", value: 3 }
988
- ]
989
-
990
- for (const data of testData) {
991
- await db.insert(data)
992
- }
993
-
994
- // Save data
995
- await db.save()
996
-
997
- // Close database
998
- await db.close()
999
-
1000
- // Create new database instance
1001
- const db2 = new Database(dbPath, { create: true })
1002
-
1003
- // CRITICAL: init() should return only when everything is ready
1004
- await db2.init()
1005
-
1006
- // NO DELAY - immediate access should work
1007
- console.log(`IMMEDIATE ACCESS: length=${db2.length}, count=${await db2.count()}`)
1008
-
1009
- // These should work immediately without any delay
1010
- expect(db2.length).toBe(3)
1011
- expect(await db2.count()).toBe(3)
1012
-
1013
- // Verify data is immediately accessible
1014
- const results = await db2.find({ name: "Test1" })
1015
- expect(results).toHaveLength(1)
1016
- expect(results[0].value).toBe(1)
1017
-
1018
- await db2.destroy()
1019
- })
1020
-
1021
- test('PERFORMANCE: length vs count() performance comparison', async () => {
1022
- const dbPath = path.join(testDir, 'test-performance-comparison.jdb')
1023
- db = new Database(dbPath, { clear: true, create: true })
1024
-
1025
- await db.init()
1026
-
1027
- // Insert more data for performance test
1028
- const testData = []
1029
- for (let i = 0; i < 1000; i++) {
1030
- testData.push({ name: `Test${i}`, value: i })
1031
- }
1032
-
1033
- for (const data of testData) {
1034
- await db.insert(data)
1035
- }
1036
-
1037
- // Save data
1038
- await db.save()
1039
-
1040
- // Close database
1041
- await db.close()
1042
-
1043
- // Create new database instance
1044
- const db2 = new Database(dbPath, { create: true })
1045
- await db2.init()
1046
-
1047
- // Test performance of length vs count()
1048
- const lengthStart = Date.now()
1049
- const lengthResult = db2.length
1050
- const lengthTime = Date.now() - lengthStart
1051
-
1052
- const countStart = Date.now()
1053
- const countResult = await db2.count()
1054
- const countTime = Date.now() - countStart
1055
-
1056
- console.log(`PERFORMANCE: length=${lengthResult} (${lengthTime}ms) vs count=${countResult} (${countTime}ms)`)
1057
-
1058
- // Both should return the same result
1059
- expect(lengthResult).toBe(1000)
1060
- expect(countResult).toBe(1000)
1061
-
1062
- // length should be much faster (uses index)
1063
- // count should be slower (executes full query)
1064
- expect(lengthTime).toBeLessThan(countTime)
1065
-
1066
- await db2.destroy()
1067
- })
1068
- })
1069
- })