jexidb 2.1.0 → 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.
@@ -101,10 +101,10 @@ describe('Index File Persistence', () => {
101
101
  expect(fieldKeys.length).toBeGreaterThan(0)
102
102
 
103
103
  if (field === 'category') {
104
- // With term mapping, category field uses term IDs instead of original strings
105
- // Just verify that we have some term IDs (numeric strings)
106
- const hasTermIds = fieldKeys.some(key => /^\d+$/.test(key))
107
- expect(hasTermIds).toBe(true)
104
+ // Fields with type 'string' use original values, not term IDs
105
+ // Just verify that we have some actual category values
106
+ const hasExpectedValues = fieldKeys.length > 0
107
+ expect(hasExpectedValues).toBe(true)
108
108
  } else if (field === 'tags') {
109
109
  // Should contain tag entries like 'admin', 'membro', 'ativo', etc.
110
110
  const hasExpectedValues = fieldKeys.some(key =>
@@ -112,10 +112,10 @@ describe('Index File Persistence', () => {
112
112
  )
113
113
  expect(hasExpectedValues).toBe(true)
114
114
  } else if (field === 'name') {
115
- // With term mapping, name field uses term IDs instead of original strings
116
- // Just verify that we have some term IDs (numeric strings)
117
- const hasTermIds = fieldKeys.some(key => /^\d+$/.test(key))
118
- expect(hasTermIds).toBe(true)
115
+ // Fields with type 'string' use original values, not term IDs
116
+ // Just verify that we have some actual name values
117
+ const hasExpectedValues = fieldKeys.length > 0
118
+ expect(hasExpectedValues).toBe(true)
119
119
  }
120
120
  }
121
121
 
@@ -148,12 +148,12 @@ describe('Index File Persistence', () => {
148
148
 
149
149
  await db.init()
150
150
 
151
- // Don't insert any data, just destroy
151
+ // Don't insert any data, just close
152
152
  await db.close()
153
153
 
154
- // .idx file SHOULD be created for databases with indexes, even if empty
155
- // This ensures the database structure is complete
156
- expect(fs.existsSync(testIdxPath)).toBe(true)
154
+ // .idx file should NOT be created for empty databases
155
+ // This prevents creating empty index files that could be mistaken for valid indexes
156
+ expect(fs.existsSync(testIdxPath)).toBe(false)
157
157
 
158
158
  // Verify we can still recreate the database and it works correctly
159
159
  const db2 = new Database(testDbPath, {
@@ -262,20 +262,20 @@ describe('Index File Persistence', () => {
262
262
  expect(indexData.priority).toBeDefined()
263
263
 
264
264
  // Verify status index contains our test values
265
- // With term mapping, status field uses term IDs instead of original strings
265
+ // Fields with type 'string' use original values, not term IDs
266
266
  const statusKeys = Object.keys(indexData.status)
267
267
  expect(statusKeys.length).toBeGreaterThan(0)
268
- // Verify we have term IDs (numeric strings)
269
- const hasStatusTermIds = statusKeys.some(key => /^\d+$/.test(key))
270
- expect(hasStatusTermIds).toBe(true)
268
+ // Verify we have actual string values
269
+ const hasStatusValues = statusKeys.length > 0
270
+ expect(hasStatusValues).toBe(true)
271
271
 
272
272
  // Verify priority index contains our test values
273
- // With term mapping, priority field uses term IDs instead of original strings
273
+ // Fields with type 'string' use original values, not term IDs
274
274
  const priorityKeys = Object.keys(indexData.priority)
275
275
  expect(priorityKeys.length).toBeGreaterThan(0)
276
- // Verify we have term IDs (numeric strings)
277
- const hasPriorityTermIds = priorityKeys.some(key => /^\d+$/.test(key))
278
- expect(hasPriorityTermIds).toBe(true)
276
+ // Verify we have actual string values
277
+ const hasPriorityValues = priorityKeys.length > 0
278
+ expect(hasPriorityValues).toBe(true)
279
279
 
280
280
  // Recreate database and verify consistency
281
281
  const db2 = new Database(testDbPath, {
@@ -303,4 +303,189 @@ describe('Index File Persistence', () => {
303
303
 
304
304
  await db2.destroy()
305
305
  })
306
+
307
+ test('should NOT overwrite valid index with empty data when closing empty database', async () => {
308
+ // Create database with indexes and data
309
+ const db1 = new Database(testDbPath, {
310
+ indexes: { name: 'string', value: 'number' },
311
+ debugMode: false
312
+ })
313
+
314
+ await db1.init()
315
+
316
+ // Insert test data
317
+ await db1.insert({ id: 1, name: 'Test', value: 100 })
318
+ await db1.insert({ id: 2, name: 'Test2', value: 200 })
319
+
320
+ // Query to build indexes
321
+ await db1.find({ name: 'Test' })
322
+
323
+ await db1.close()
324
+
325
+ // Verify index file exists and has data
326
+ expect(fs.existsSync(testIdxPath)).toBe(true)
327
+ const idxContent1 = JSON.parse(fs.readFileSync(testIdxPath, 'utf8'))
328
+ expect(idxContent1.index).toBeDefined()
329
+ expect(Object.keys(idxContent1.index.data || {}).length).toBeGreaterThan(0)
330
+ expect(idxContent1.offsets.length).toBe(2)
331
+
332
+ // Now open database WITHOUT loading index properly (simulate corrupted load)
333
+ // This should trigger a rebuild, but we'll simulate closing with empty index
334
+ const db2 = new Database(testDbPath, {
335
+ create: false,
336
+ indexes: { name: 'string', value: 'number' },
337
+ debugMode: false
338
+ })
339
+
340
+ await db2.init()
341
+
342
+ // Verify index was loaded correctly
343
+ const countBefore = await db2.count({ name: 'Test' })
344
+ expect(countBefore).toBe(1)
345
+
346
+ // Now simulate a scenario where index becomes empty (shouldn't happen, but test protection)
347
+ // Clear the index manager's data
348
+ db2.indexManager.index = { data: {} }
349
+ db2.offsets = []
350
+
351
+ // Close - this should NOT overwrite the valid index file
352
+ await db2.close()
353
+
354
+ // Verify index file still has the original data (not overwritten)
355
+ const idxContent2 = JSON.parse(fs.readFileSync(testIdxPath, 'utf8'))
356
+ expect(idxContent2.index).toBeDefined()
357
+ // The index should still have data (either from original or from rebuild)
358
+ // But importantly, it should NOT be empty
359
+ expect(Object.keys(idxContent2.index.data || {}).length).toBeGreaterThan(0)
360
+ expect(idxContent2.offsets.length).toBeGreaterThan(0)
361
+
362
+ await db2.destroy()
363
+ })
364
+
365
+ test('should throw error when index is corrupted and allowIndexRebuild is false (default)', async () => {
366
+ // Create database with indexes and data
367
+ const db1 = new Database(testDbPath, {
368
+ indexes: { name: 'string', value: 'number' },
369
+ debugMode: false
370
+ })
371
+
372
+ await db1.init()
373
+
374
+ // Insert test data
375
+ await db1.insert({ id: 1, name: 'Test', value: 100 })
376
+ await db1.insert({ id: 2, name: 'Test2', value: 200 })
377
+
378
+ // Query to build indexes
379
+ await db1.find({ name: 'Test' })
380
+
381
+ await db1.close()
382
+
383
+ // Corrupt the index file by making it empty but valid JSON
384
+ // Need to ensure it has the right structure so it parses but has no data
385
+ const corruptedIdx = JSON.stringify({
386
+ index: { data: {} },
387
+ offsets: [],
388
+ indexOffset: 0,
389
+ config: {
390
+ schema: [],
391
+ indexes: { name: 'string', value: 'number' }
392
+ }
393
+ })
394
+ fs.writeFileSync(testIdxPath, corruptedIdx)
395
+
396
+ // Try to open database - allowIndexRebuild defaults to false, will throw error
397
+ const db2 = new Database(testDbPath, {
398
+ create: false,
399
+ indexes: { name: 'string', value: 'number' },
400
+ // allowIndexRebuild defaults to false - will throw error
401
+ debugMode: false
402
+ })
403
+
404
+ // Should throw error during init() - corrupted index (empty but file exists)
405
+ await expect(db2.init()).rejects.toThrow(/Index file is corrupted.*exists but contains no index data/)
406
+ })
407
+
408
+ test('should throw error when index is missing and allowIndexRebuild is false (default)', async () => {
409
+ // Create database with indexes and data
410
+ const db1 = new Database(testDbPath, {
411
+ indexes: { name: 'string', value: 'number' },
412
+ debugMode: false
413
+ })
414
+
415
+ await db1.init()
416
+
417
+ // Insert test data
418
+ await db1.insert({ id: 1, name: 'Test', value: 100 })
419
+
420
+ await db1.save()
421
+ await db1.close()
422
+
423
+ // Delete the index file
424
+ if (fs.existsSync(testIdxPath)) {
425
+ fs.unlinkSync(testIdxPath)
426
+ }
427
+
428
+ // Try to open database - allowIndexRebuild defaults to false, will throw error
429
+ const db2 = new Database(testDbPath, {
430
+ create: false,
431
+ indexes: { name: 'string', value: 'number' },
432
+ // allowIndexRebuild defaults to false - will throw error
433
+ debugMode: false
434
+ })
435
+
436
+ // Should throw error during init() - missing index file
437
+ await expect(db2.init()).rejects.toThrow(/Index file is missing or corrupted.*does not exist or is invalid/)
438
+ })
439
+
440
+ test('should rebuild automatically when allowIndexRebuild is explicitly true', async () => {
441
+ // Create database with indexes and data
442
+ const db1 = new Database(testDbPath, {
443
+ indexes: { name: 'string', value: 'number' },
444
+ debugMode: false
445
+ })
446
+
447
+ await db1.init()
448
+
449
+ // Insert test data
450
+ await db1.insert({ id: 1, name: 'Test', value: 100 })
451
+ await db1.insert({ id: 2, name: 'Test2', value: 200 })
452
+
453
+ await db1.save()
454
+ await db1.close()
455
+
456
+ // Corrupt the index file by making it empty but valid JSON with config
457
+ // This simulates a corrupted index that still has config info
458
+ const corruptedIdx = JSON.stringify({
459
+ index: { data: {} },
460
+ offsets: [],
461
+ indexOffset: 0,
462
+ config: {
463
+ schema: ['id', 'name', 'value'],
464
+ indexes: { name: 'string', value: 'number' }
465
+ }
466
+ })
467
+ fs.writeFileSync(testIdxPath, corruptedIdx)
468
+
469
+ // Open database with allowIndexRebuild explicitly set to true
470
+ const db2 = new Database(testDbPath, {
471
+ create: false,
472
+ indexes: { name: 'string', value: 'number' },
473
+ allowIndexRebuild: true, // Explicitly enable rebuild
474
+ debugMode: false
475
+ })
476
+
477
+ // Should succeed and rebuild automatically
478
+ await db2.init()
479
+
480
+ // Rebuild happens lazily on first query - trigger it
481
+ // Query should work after rebuild
482
+ const count = await db2.count({ name: 'Test' })
483
+ expect(count).toBe(1)
484
+
485
+ const results = await db2.find({ name: 'Test' })
486
+ expect(results.length).toBe(1)
487
+ expect(results[0].id).toBe(1)
488
+
489
+ await db2.destroy()
490
+ })
306
491
  })
@@ -0,0 +1,353 @@
1
+ import { Database } from '../src/Database.mjs'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ describe('InsertSession Auto-Flush', () => {
6
+ let testDir
7
+ let db
8
+
9
+ beforeEach(() => {
10
+ testDir = path.join(process.cwd(), 'test-files', 'insert-session-auto-flush')
11
+ fs.mkdirSync(testDir, { recursive: true })
12
+ })
13
+
14
+ afterEach(async () => {
15
+ if (db) {
16
+ // Wait for all active insert sessions to complete
17
+ if (db.activeInsertSessions && db.activeInsertSessions.size > 0) {
18
+ const sessions = Array.from(db.activeInsertSessions)
19
+ await Promise.all(sessions.map(session => {
20
+ return session.waitForAutoFlushes().catch(() => {})
21
+ }))
22
+ }
23
+
24
+ // Wait for any pending operations before closing
25
+ await db.waitForOperations()
26
+ await db.close()
27
+ }
28
+ // Clean up test files
29
+ if (fs.existsSync(testDir)) {
30
+ try {
31
+ fs.rmSync(testDir, { recursive: true, force: true })
32
+ } catch (error) {
33
+ console.warn('Could not clean up test directory:', testDir)
34
+ }
35
+ }
36
+ })
37
+
38
+ describe('Auto-flush on batch size', () => {
39
+ test('should auto-flush when batch size is reached', async () => {
40
+ const dbPath = path.join(testDir, 'auto-flush-basic.jdb')
41
+ db = new Database(dbPath, { clear: true, create: true })
42
+ await db.init()
43
+
44
+ const session = db.beginInsertSession({ batchSize: 10 })
45
+
46
+ // Insert exactly 10 records (should trigger auto-flush)
47
+ for (let i = 0; i < 10; i++) {
48
+ await session.add({ name: `Record ${i}`, value: i })
49
+ }
50
+
51
+ // Wait a bit for auto-flush to complete
52
+ await new Promise(resolve => setTimeout(resolve, 100))
53
+
54
+ // Verify data was inserted (auto-flush should have processed it)
55
+ expect(db.length).toBe(10)
56
+
57
+ // Verify batches array is empty (auto-flush processed it)
58
+ expect(session.batches.length).toBe(0)
59
+
60
+ await session.commit()
61
+ })
62
+
63
+ test('should auto-flush multiple batches without accumulating in memory', async () => {
64
+ const dbPath = path.join(testDir, 'auto-flush-multiple.jdb')
65
+ db = new Database(dbPath, { clear: true, create: true })
66
+ await db.init()
67
+
68
+ const session = db.beginInsertSession({ batchSize: 100 })
69
+
70
+ // Insert 5000 records (enough to test auto-flush without timeout)
71
+ // This should create ~50 batches, but they should be auto-flushed
72
+ const totalRecords = 5000
73
+
74
+ for (let i = 0; i < totalRecords; i++) {
75
+ await session.add({
76
+ name: `Record ${i}`,
77
+ value: i,
78
+ data: `Data for record ${i}`.repeat(10) // Add some data to make it realistic
79
+ })
80
+
81
+ // Periodically check that batches don't accumulate
82
+ if (i % 1000 === 0 && i > 0) {
83
+ // Wait a bit for auto-flushes to catch up
84
+ await new Promise(resolve => setTimeout(resolve, 50))
85
+
86
+ // Verify batches array doesn't grow unbounded
87
+ // It should be small (only pending batches waiting to be flushed)
88
+ expect(session.batches.length).toBeLessThan(10)
89
+ }
90
+ }
91
+
92
+ // Wait for all auto-flushes to complete
93
+ await session.waitForAutoFlushes()
94
+
95
+ // Final commit should be fast (most data already flushed)
96
+ await session.commit()
97
+
98
+ // Verify all data was inserted
99
+ expect(db.length).toBe(totalRecords)
100
+
101
+ // Verify batches are empty after commit
102
+ expect(session.batches.length).toBe(0)
103
+ expect(session.currentBatch.length).toBe(0)
104
+ })
105
+
106
+ test('should handle concurrent inserts during flush', async () => {
107
+ const dbPath = path.join(testDir, 'auto-flush-concurrent.jdb')
108
+ db = new Database(dbPath, { clear: true, create: true })
109
+ await db.init()
110
+
111
+ const session = db.beginInsertSession({ batchSize: 50 })
112
+
113
+ // Insert records concurrently while flush is happening
114
+ const insertPromises = []
115
+ for (let i = 0; i < 500; i++) {
116
+ insertPromises.push(
117
+ session.add({ name: `Record ${i}`, value: i })
118
+ )
119
+ }
120
+
121
+ // All inserts should complete
122
+ await Promise.all(insertPromises)
123
+
124
+ // Wait for all auto-flushes
125
+ await session.waitForAutoFlushes()
126
+
127
+ // Commit should process any remaining data
128
+ await session.commit()
129
+
130
+ // Verify all data was inserted
131
+ expect(db.length).toBe(500)
132
+ })
133
+ })
134
+
135
+ describe('Commit waits for auto-flushes', () => {
136
+ test('commit() should wait for all pending auto-flushes', async () => {
137
+ const dbPath = path.join(testDir, 'commit-waits.jdb')
138
+ db = new Database(dbPath, { clear: true, create: true })
139
+ await db.init()
140
+
141
+ const session = db.beginInsertSession({ batchSize: 10 })
142
+
143
+ // Insert many records to trigger multiple auto-flushes
144
+ for (let i = 0; i < 250; i++) {
145
+ await session.add({ name: `Record ${i}`, value: i })
146
+ }
147
+
148
+ // Commit should wait for all auto-flushes to complete
149
+ const insertedCount = await session.commit()
150
+
151
+ // Verify all records were inserted
152
+ expect(insertedCount).toBe(250)
153
+ expect(db.length).toBe(250)
154
+
155
+ // Verify no pending operations
156
+ expect(session.hasPendingOperations()).toBe(false)
157
+
158
+ // Verify all auto-flushes completed (pendingAutoFlushes should be empty)
159
+ expect(session.pendingAutoFlushes.size).toBe(0)
160
+ })
161
+
162
+ test('commit() should flush remaining data after waiting for auto-flushes', async () => {
163
+ const dbPath = path.join(testDir, 'commit-flush-remaining.jdb')
164
+ db = new Database(dbPath, { clear: true, create: true })
165
+ await db.init()
166
+
167
+ const session = db.beginInsertSession({ batchSize: 100 })
168
+
169
+ // Insert 83 records (less than batchSize, so no auto-flush)
170
+ for (let i = 0; i < 83; i++) {
171
+ await session.add({ name: `Record ${i}`, value: i })
172
+ }
173
+
174
+ // Verify data is in currentBatch but not flushed
175
+ expect(session.currentBatch.length).toBe(83)
176
+ expect(db.length).toBe(0) // Not yet inserted
177
+
178
+ // Commit should flush the remaining data
179
+ await session.commit()
180
+
181
+ // Verify all data was inserted
182
+ expect(db.length).toBe(83)
183
+ expect(session.currentBatch.length).toBe(0)
184
+ })
185
+ })
186
+
187
+ describe('_doFlush handles concurrent inserts', () => {
188
+ test('_doFlush should process all data even if new data is added during flush', async () => {
189
+ const dbPath = path.join(testDir, 'doflush-concurrent.jdb')
190
+ db = new Database(dbPath, { clear: true, create: true })
191
+ await db.init()
192
+
193
+ const session = db.beginInsertSession({ batchSize: 10 })
194
+
195
+ // Add initial batch
196
+ for (let i = 0; i < 10; i++) {
197
+ await session.add({ name: `Record ${i}`, value: i })
198
+ }
199
+
200
+ // Manually trigger flush, but add more data during flush
201
+ const flushPromise = session._doFlush()
202
+
203
+ // Add more data while flush is happening
204
+ for (let i = 10; i < 25; i++) {
205
+ await session.add({ name: `Record ${i}`, value: i })
206
+ }
207
+
208
+ // Wait for flush to complete
209
+ await flushPromise
210
+
211
+ // Verify all data was processed
212
+ // The flush should have processed everything, including data added during flush
213
+ expect(session.batches.length).toBe(0)
214
+
215
+ // Final commit to ensure everything is inserted
216
+ await session.commit()
217
+ expect(db.length).toBe(25)
218
+ })
219
+
220
+ test('_doFlush should continue until queue is empty', async () => {
221
+ const dbPath = path.join(testDir, 'doflush-empty-queue.jdb')
222
+ db = new Database(dbPath, { clear: true, create: true })
223
+ await db.init()
224
+
225
+ const session = db.beginInsertSession({ batchSize: 5 })
226
+
227
+ // Add multiple batches
228
+ for (let i = 0; i < 23; i++) {
229
+ await session.add({ name: `Record ${i}`, value: i })
230
+ }
231
+
232
+ // Wait for any auto-flushes to complete
233
+ await session.waitForAutoFlushes()
234
+
235
+ // Manually trigger flush (should process everything including currentBatch)
236
+ await session._doFlush()
237
+
238
+ // Verify all batches were processed
239
+ // _doFlush processes everything, including partial currentBatch
240
+ expect(session.batches.length).toBe(0)
241
+ // After _doFlush, currentBatch should be empty (it was processed)
242
+ expect(session.currentBatch.length).toBe(0)
243
+
244
+ // Verify all data was inserted
245
+ expect(db.length).toBe(23)
246
+ })
247
+ })
248
+
249
+ describe('Memory management', () => {
250
+ test('should not accumulate batches in memory', async () => {
251
+ const dbPath = path.join(testDir, 'memory-management.jdb')
252
+ db = new Database(dbPath, { clear: true, create: true })
253
+ await db.init()
254
+
255
+ const session = db.beginInsertSession({ batchSize: 100 })
256
+
257
+ // Track memory usage
258
+ const initialBatches = session.batches.length
259
+
260
+ // Insert large amount of data
261
+ for (let i = 0; i < 10000; i++) {
262
+ await session.add({ name: `Record ${i}`, value: i })
263
+
264
+ // Check periodically that batches don't accumulate
265
+ if (i % 500 === 0 && i > 0) {
266
+ await new Promise(resolve => setTimeout(resolve, 10))
267
+
268
+ // Batches should be small (auto-flush is working)
269
+ expect(session.batches.length).toBeLessThan(5)
270
+ }
271
+ }
272
+
273
+ // Wait for all auto-flushes
274
+ await session.waitForAutoFlushes()
275
+
276
+ // Final commit
277
+ await session.commit()
278
+
279
+ // Verify all data was inserted
280
+ expect(db.length).toBe(10000)
281
+
282
+ // Verify batches are empty
283
+ expect(session.batches.length).toBe(0)
284
+ expect(session.currentBatch.length).toBe(0)
285
+ })
286
+ })
287
+
288
+ describe('Edge cases', () => {
289
+ test('should handle empty commit', async () => {
290
+ const dbPath = path.join(testDir, 'empty-commit.jdb')
291
+ db = new Database(dbPath, { clear: true, create: true })
292
+ await db.init()
293
+
294
+ const session = db.beginInsertSession()
295
+
296
+ // Commit without adding anything
297
+ const count = await session.commit()
298
+
299
+ expect(count).toBe(0)
300
+ expect(db.length).toBe(0)
301
+ })
302
+
303
+ test('should handle multiple commits on same session', async () => {
304
+ const dbPath = path.join(testDir, 'multiple-commits.jdb')
305
+ db = new Database(dbPath, { clear: true, create: true })
306
+ await db.init()
307
+
308
+ const session = db.beginInsertSession({ batchSize: 10 })
309
+
310
+ // First commit
311
+ for (let i = 0; i < 15; i++) {
312
+ await session.add({ name: `Record ${i}`, value: i })
313
+ }
314
+ await session.commit()
315
+ expect(db.length).toBe(15)
316
+
317
+ // Second commit
318
+ for (let i = 15; i < 30; i++) {
319
+ await session.add({ name: `Record ${i}`, value: i })
320
+ }
321
+ await session.commit()
322
+ expect(db.length).toBe(30)
323
+ })
324
+
325
+ test('should handle hasPendingOperations correctly', async () => {
326
+ const dbPath = path.join(testDir, 'pending-operations.jdb')
327
+ db = new Database(dbPath, { clear: true, create: true })
328
+ await db.init()
329
+
330
+ const session = db.beginInsertSession({ batchSize: 10 })
331
+
332
+ // Initially no pending operations
333
+ expect(session.hasPendingOperations()).toBe(false)
334
+
335
+ // Add records (triggers auto-flush)
336
+ for (let i = 0; i < 15; i++) {
337
+ await session.add({ name: `Record ${i}`, value: i })
338
+ }
339
+
340
+ // Should have pending operations (auto-flush in progress)
341
+ // Wait a bit and check
342
+ await new Promise(resolve => setTimeout(resolve, 50))
343
+
344
+ // After auto-flush completes, should have no pending (except currentBatch if < batchSize)
345
+ await session.waitForAutoFlushes()
346
+
347
+ // Commit to clear everything
348
+ await session.commit()
349
+ expect(session.hasPendingOperations()).toBe(false)
350
+ })
351
+ })
352
+ })
353
+