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.
- package/dist/Database.cjs +1642 -334
- package/docs/API.md +1057 -1051
- package/package.json +1 -1
- package/scripts/benchmark-array-serialization.js +108 -0
- package/scripts/score-mode-demo.js +45 -0
- package/src/Database.mjs +1362 -167
- package/src/FileHandler.mjs +83 -44
- package/src/OperationQueue.mjs +23 -23
- package/src/Serializer.mjs +214 -23
- package/src/managers/IndexManager.mjs +778 -87
- package/src/managers/QueryManager.mjs +266 -49
- package/src/managers/TermManager.mjs +7 -7
- package/src/utils/operatorNormalizer.mjs +116 -0
- package/test/coverage-method.test.js +93 -0
- package/test/deserialize-corruption-fixes.test.js +296 -0
- package/test/exists-method.test.js +318 -0
- package/test/explicit-indexes-comparison.test.js +219 -0
- package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
- package/test/index-line-number-regression.test.js +100 -0
- package/test/index-missing-index-data.test.js +91 -0
- package/test/index-persistence.test.js +205 -20
- package/test/insert-session-auto-flush.test.js +353 -0
- package/test/legacy-operator-compat.test.js +154 -0
- package/test/score-method.test.js +60 -0
|
@@ -101,10 +101,10 @@ describe('Index File Persistence', () => {
|
|
|
101
101
|
expect(fieldKeys.length).toBeGreaterThan(0)
|
|
102
102
|
|
|
103
103
|
if (field === 'category') {
|
|
104
|
-
//
|
|
105
|
-
// Just verify that we have some
|
|
106
|
-
const
|
|
107
|
-
expect(
|
|
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
|
-
//
|
|
116
|
-
// Just verify that we have some
|
|
117
|
-
const
|
|
118
|
-
expect(
|
|
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
|
|
151
|
+
// Don't insert any data, just close
|
|
152
152
|
await db.close()
|
|
153
153
|
|
|
154
|
-
// .idx file
|
|
155
|
-
// This
|
|
156
|
-
expect(fs.existsSync(testIdxPath)).toBe(
|
|
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
|
-
//
|
|
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
|
|
269
|
-
const
|
|
270
|
-
expect(
|
|
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
|
-
//
|
|
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
|
|
277
|
-
const
|
|
278
|
-
expect(
|
|
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
|
+
|