jexidb 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) 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 +3896 -0
  8. package/docs/API.md +1051 -390
  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/clean-test-files.js +75 -0
  17. package/scripts/prepare.js +31 -0
  18. package/scripts/run-tests.js +80 -0
  19. package/src/Database.mjs +4130 -0
  20. package/src/FileHandler.mjs +1101 -0
  21. package/src/OperationQueue.mjs +279 -0
  22. package/src/SchemaManager.mjs +268 -0
  23. package/src/Serializer.mjs +511 -0
  24. package/src/managers/ConcurrencyManager.mjs +257 -0
  25. package/src/managers/IndexManager.mjs +1403 -0
  26. package/src/managers/QueryManager.mjs +1273 -0
  27. package/src/managers/StatisticsManager.mjs +262 -0
  28. package/src/managers/StreamingProcessor.mjs +429 -0
  29. package/src/managers/TermManager.mjs +278 -0
  30. package/test/$not-operator-with-and.test.js +282 -0
  31. package/test/README.md +8 -0
  32. package/test/close-init-cycle.test.js +256 -0
  33. package/test/critical-bugs-fixes.test.js +1069 -0
  34. package/test/index-persistence.test.js +306 -0
  35. package/test/index-serialization.test.js +314 -0
  36. package/test/indexed-query-mode.test.js +360 -0
  37. package/test/iterate-method.test.js +272 -0
  38. package/test/query-operators.test.js +238 -0
  39. package/test/regex-array-fields.test.js +129 -0
  40. package/test/score-method.test.js +238 -0
  41. package/test/setup.js +17 -0
  42. package/test/term-mapping-minimal.test.js +154 -0
  43. package/test/term-mapping-simple.test.js +257 -0
  44. package/test/term-mapping.test.js +514 -0
  45. package/test/writebuffer-flush-resilience.test.js +204 -0
  46. package/dist/FileHandler.js +0 -688
  47. package/dist/IndexManager.js +0 -353
  48. package/dist/IntegrityChecker.js +0 -364
  49. package/dist/JSONLDatabase.js +0 -1333
  50. package/dist/index.js +0 -617
  51. package/docs/MIGRATION.md +0 -295
  52. package/examples/auto-save-example.js +0 -158
  53. package/examples/cjs-usage.cjs +0 -82
  54. package/examples/close-vs-delete-example.js +0 -71
  55. package/examples/esm-usage.js +0 -113
  56. package/examples/example-columns.idx.jdb +0 -0
  57. package/examples/example-columns.jdb +0 -9
  58. package/examples/example-options.idx.jdb +0 -0
  59. package/examples/example-options.jdb +0 -0
  60. package/examples/example-users.idx.jdb +0 -0
  61. package/examples/example-users.jdb +0 -5
  62. package/examples/simple-test.js +0 -55
  63. package/src/FileHandler.js +0 -674
  64. package/src/IndexManager.js +0 -363
  65. package/src/IntegrityChecker.js +0 -379
  66. package/src/JSONLDatabase.js +0 -1391
  67. package/src/index.js +0 -608
@@ -0,0 +1,360 @@
1
+ import { Database } from '../src/Database.mjs'
2
+ import fs from 'fs'
3
+
4
+ describe('Indexed Query Mode Control', () => {
5
+ let db
6
+ let testDbPath
7
+
8
+ beforeEach(async () => {
9
+ testDbPath = `test-indexed-mode-${Date.now()}-${Math.random()}.jdb`
10
+ db = new Database(testDbPath, {
11
+ indexes: { name: 'string', age: 'number' },
12
+ indexedQueryMode: 'permissive',
13
+ debugMode: false
14
+ })
15
+ await db.init()
16
+
17
+ // Insert test data
18
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
19
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
20
+ })
21
+
22
+ afterEach(async () => {
23
+ if (db && !db.destroyed) {
24
+ try {
25
+ await db.close()
26
+ } catch (error) {
27
+ // Ignore destroy errors in tests
28
+ console.warn('Destroy error in test cleanup:', error.message)
29
+ }
30
+ }
31
+ // Clean up test files with error handling
32
+ try {
33
+ if (fs.existsSync(testDbPath)) {
34
+ fs.unlinkSync(testDbPath)
35
+ }
36
+ if (fs.existsSync(testDbPath.replace('.jdb', '.idx.jdb'))) {
37
+ fs.unlinkSync(testDbPath.replace('.jdb', '.idx.jdb'))
38
+ }
39
+ } catch (error) {
40
+ // Ignore file cleanup errors in tests
41
+ console.warn('File cleanup error in test:', error.message)
42
+ }
43
+ })
44
+
45
+ describe('Permissive Mode (Default)', () => {
46
+ test('should allow queries on indexed fields', async () => {
47
+ const results = await db.find({ name: 'John' })
48
+ expect(results).toHaveLength(1)
49
+ expect(results[0].name).toBe('John')
50
+ })
51
+
52
+ test('should allow queries on non-indexed fields (streaming)', async () => {
53
+ const results = await db.find({ title: 'Developer' })
54
+ expect(results).toHaveLength(1)
55
+ expect(results[0].title).toBe('Developer')
56
+ })
57
+
58
+ test('should allow mixed queries with indexed and non-indexed fields', async () => {
59
+ const results = await db.find({ name: 'John', title: 'Developer' })
60
+ expect(results).toHaveLength(1)
61
+ expect(results[0].name).toBe('John')
62
+ expect(results[0].title).toBe('Developer')
63
+ })
64
+
65
+ test('should allow empty criteria queries', async () => {
66
+ const results = await db.find({})
67
+ expect(results).toHaveLength(2)
68
+ })
69
+
70
+ test('should work with findOne on non-indexed fields', async () => {
71
+ const result = await db.findOne({ title: 'Manager' })
72
+ expect(result).toBeTruthy()
73
+ expect(result.title).toBe('Manager')
74
+ })
75
+
76
+ test('should work with count on non-indexed fields', async () => {
77
+ const count = await db.count({ title: 'Developer' })
78
+ expect(count).toBe(1)
79
+ })
80
+ })
81
+
82
+ describe('Strict Mode', () => {
83
+ beforeEach(async () => {
84
+ if (db && !db.destroyed) {
85
+ await db.close()
86
+ }
87
+ testDbPath = `test-indexed-mode-strict-${Date.now()}-${Math.random()}.jdb`
88
+ db = new Database(testDbPath, {
89
+ indexes: { name: 'string', age: 'number' },
90
+ indexedQueryMode: 'strict',
91
+ debugMode: false
92
+ })
93
+ await db.init()
94
+
95
+ // Insert test data
96
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
97
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
98
+ })
99
+
100
+ test('should allow queries on indexed fields', async () => {
101
+ const results = await db.find({ name: 'John' })
102
+ expect(results).toHaveLength(1)
103
+ expect(results[0].name).toBe('John')
104
+ })
105
+
106
+ test('should allow queries on multiple indexed fields', async () => {
107
+ const results = await db.find({ name: 'John', age: 25 })
108
+ expect(results).toHaveLength(1)
109
+ expect(results[0].name).toBe('John')
110
+ expect(results[0].age).toBe(25)
111
+ })
112
+
113
+ test('should throw error for queries on non-indexed fields', async () => {
114
+ await expect(db.find({ title: 'Developer' })).rejects.toThrow(
115
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
116
+ )
117
+ })
118
+
119
+ test('should throw error for mixed queries with non-indexed fields', async () => {
120
+ await expect(db.find({ name: 'John', title: 'Developer' })).rejects.toThrow(
121
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
122
+ )
123
+ })
124
+
125
+ test('should allow empty criteria queries', async () => {
126
+ const results = await db.find({})
127
+ expect(results).toHaveLength(2)
128
+ })
129
+
130
+ test('should throw error for findOne on non-indexed fields', async () => {
131
+ await expect(db.findOne({ title: 'Manager' })).rejects.toThrow(
132
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
133
+ )
134
+ })
135
+
136
+ test('should throw error for count on non-indexed fields', async () => {
137
+ await expect(db.count({ title: 'Developer' })).rejects.toThrow(
138
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
139
+ )
140
+ })
141
+
142
+ test('should work with findOne on indexed fields', async () => {
143
+ const result = await db.findOne({ name: 'John' })
144
+ expect(result).toBeTruthy()
145
+ expect(result.name).toBe('John')
146
+ })
147
+
148
+ test('should work with count on indexed fields', async () => {
149
+ const count = await db.count({ age: 30 })
150
+ expect(count).toBe(1)
151
+ })
152
+ })
153
+
154
+ describe('Logical Operators', () => {
155
+ beforeEach(async () => {
156
+ if (db && !db.destroyed) {
157
+ await db.close()
158
+ }
159
+ testDbPath = `test-indexed-mode-logical-${Date.now()}-${Math.random()}.jdb`
160
+ db = new Database(testDbPath, {
161
+ indexes: { name: 'string', age: 'number' },
162
+ indexedQueryMode: 'strict',
163
+ debugMode: false
164
+ })
165
+ await db.init()
166
+
167
+ // Insert test data
168
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
169
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
170
+ })
171
+
172
+ test('should allow $or with indexed fields', async () => {
173
+ const results = await db.find({
174
+ $or: [
175
+ { name: 'John' },
176
+ { age: 30 }
177
+ ]
178
+ })
179
+ expect(results).toHaveLength(2)
180
+ })
181
+
182
+ test('should throw error for $or with non-indexed fields', async () => {
183
+ await expect(db.find({
184
+ $or: [
185
+ { title: 'Developer' },
186
+ { title: 'Manager' }
187
+ ]
188
+ })).rejects.toThrow(
189
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
190
+ )
191
+ })
192
+
193
+ test('should allow $and with indexed fields', async () => {
194
+ const results = await db.find({
195
+ $and: [
196
+ { name: 'John' },
197
+ { age: 25 }
198
+ ]
199
+ })
200
+ expect(results).toHaveLength(1)
201
+ expect(results[0].name).toBe('John')
202
+ expect(results[0].age).toBe(25)
203
+ })
204
+
205
+ test('should throw error for $and with non-indexed fields', async () => {
206
+ await expect(db.find({
207
+ $and: [
208
+ { name: 'John' },
209
+ { title: 'Developer' }
210
+ ]
211
+ })).rejects.toThrow(
212
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
213
+ )
214
+ })
215
+
216
+ test('should allow $not with indexed fields', async () => {
217
+ const results = await db.find({
218
+ $not: { name: 'John' }
219
+ })
220
+ expect(results).toHaveLength(1)
221
+ expect(results[0].name).toBe('Jane')
222
+ })
223
+
224
+ test('should throw error for $not with non-indexed fields', async () => {
225
+ await expect(db.find({
226
+ $not: { title: 'Developer' }
227
+ })).rejects.toThrow(
228
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
229
+ )
230
+ })
231
+ })
232
+
233
+ describe('Default Behavior (No Mode Specified)', () => {
234
+ beforeEach(async () => {
235
+ if (db && !db.destroyed) {
236
+ await db.close()
237
+ }
238
+ testDbPath = `test-indexed-mode-default-${Date.now()}-${Math.random()}.jdb`
239
+ db = new Database(testDbPath, {
240
+ indexes: { name: 'string', age: 'number' }
241
+ })
242
+ await db.init()
243
+
244
+ // Insert test data
245
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
246
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
247
+ })
248
+
249
+ test('should default to permissive mode', async () => {
250
+ // Should allow non-indexed field queries
251
+ const results = await db.find({ title: 'Developer' })
252
+ expect(results).toHaveLength(1)
253
+ expect(results[0].title).toBe('Developer')
254
+ })
255
+
256
+ test('should allow indexed field queries', async () => {
257
+ const results = await db.find({ name: 'John' })
258
+ expect(results).toHaveLength(1)
259
+ expect(results[0].name).toBe('John')
260
+ })
261
+ })
262
+
263
+ describe('Error Messages', () => {
264
+ beforeEach(async () => {
265
+ if (db && !db.destroyed) {
266
+ await db.close()
267
+ }
268
+ testDbPath = `test-indexed-mode-errors-${Date.now()}-${Math.random()}.jdb`
269
+ db = new Database(testDbPath, {
270
+ indexes: { name: 'string', age: 'number' },
271
+ indexedQueryMode: 'strict',
272
+ debugMode: false
273
+ })
274
+ await db.init()
275
+
276
+ // Insert test data
277
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
278
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
279
+ })
280
+
281
+ test('should provide clear error message for single non-indexed field', async () => {
282
+ await expect(db.find({ title: 'Developer' })).rejects.toThrow(
283
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age"
284
+ )
285
+ })
286
+
287
+ test('should provide clear error message for multiple non-indexed fields', async () => {
288
+ await expect(db.find({ title: 'Developer', department: 'Engineering' })).rejects.toThrow(
289
+ "Strict indexed mode: Fields 'title', 'department' are not indexed. Available indexed fields: name, age"
290
+ )
291
+ })
292
+
293
+ test('should list all available indexed fields in error message', async () => {
294
+ const moreIndexesPath = `test-more-indexes-${Date.now()}-${Math.random()}.jdb`
295
+ const dbWithMoreIndexes = new Database(moreIndexesPath, {
296
+ indexes: { name: 'string', age: 'number', email: 'string', salary: 'number' },
297
+ indexedQueryMode: 'strict',
298
+ debugMode: false
299
+ })
300
+ await dbWithMoreIndexes.init()
301
+
302
+ await expect(dbWithMoreIndexes.find({ title: 'Developer' })).rejects.toThrow(
303
+ "Strict indexed mode: Field 'title' is not indexed. Available indexed fields: name, age, email, salary"
304
+ )
305
+
306
+ await dbWithMoreIndexes.destroy()
307
+ // Clean up test files
308
+ if (fs.existsSync(moreIndexesPath)) {
309
+ fs.unlinkSync(moreIndexesPath)
310
+ }
311
+ if (fs.existsSync(moreIndexesPath.replace('.jdb', '.idx.jdb'))) {
312
+ fs.unlinkSync(moreIndexesPath.replace('.jdb', '.idx.jdb'))
313
+ }
314
+ })
315
+ })
316
+
317
+ describe('Edge Cases', () => {
318
+ beforeEach(async () => {
319
+ if (db && !db.destroyed) {
320
+ await db.close()
321
+ }
322
+ testDbPath = `test-indexed-mode-edges-${Date.now()}-${Math.random()}.jdb`
323
+ db = new Database(testDbPath, {
324
+ indexes: { name: 'string', age: 'number' },
325
+ indexedQueryMode: 'strict',
326
+ debugMode: false
327
+ })
328
+ await db.init()
329
+
330
+ // Insert test data
331
+ await db.insert({ name: 'John', age: 25, title: 'Developer' })
332
+ await db.insert({ name: 'Jane', age: 30, title: 'Manager' })
333
+ })
334
+
335
+ test('should handle null criteria gracefully', async () => {
336
+ const results = await db.find(null)
337
+ expect(Array.isArray(results)).toBe(true)
338
+ })
339
+
340
+ test('should handle undefined criteria gracefully', async () => {
341
+ const results = await db.find(undefined)
342
+ expect(Array.isArray(results)).toBe(true)
343
+ })
344
+
345
+ test('should handle empty object criteria', async () => {
346
+ const results = await db.find({})
347
+ expect(Array.isArray(results)).toBe(true)
348
+ })
349
+
350
+ test('should handle criteria with only logical operators', async () => {
351
+ const results = await db.find({
352
+ $or: [
353
+ { name: 'John' },
354
+ { age: 30 }
355
+ ]
356
+ })
357
+ expect(results).toHaveLength(2)
358
+ })
359
+ })
360
+ })
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Test suite for the new iterate() method
3
+ * Tests bulk update capabilities with streaming performance
4
+ */
5
+
6
+ import { Database } from '../src/Database.mjs'
7
+ import fs from 'fs'
8
+ import path from 'path'
9
+
10
+ describe('Database.iterate() Method', () => {
11
+ let db
12
+ const testFile = 'test-iterate.jdb'
13
+ const testIdxFile = 'test-iterate.idx.jdb'
14
+
15
+ beforeEach(async () => {
16
+ // Clean up any existing test files
17
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile)
18
+ if (fs.existsSync(testIdxFile)) fs.unlinkSync(testIdxFile)
19
+
20
+ db = new Database(testFile, {
21
+ debugMode: false,
22
+ termMapping: true,
23
+ indexedFields: ['category', 'name', 'price']
24
+ })
25
+ await db.init()
26
+ })
27
+
28
+ afterEach(async () => {
29
+ if (db && !db.destroyed) {
30
+ await db.close()
31
+ }
32
+ // Clean up test files
33
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile)
34
+ if (fs.existsSync(testIdxFile)) fs.unlinkSync(testIdxFile)
35
+ })
36
+
37
+ describe('Basic Functionality', () => {
38
+ test('should iterate through records without modifications', async () => {
39
+ // Insert test data
40
+ await db.insert({ id: 1, name: 'Apple', category: 'fruits', price: 1.50 })
41
+ await db.insert({ id: 2, name: 'Banana', category: 'fruits', price: 0.80 })
42
+ await db.insert({ id: 3, name: 'Carrot', category: 'vegetables', price: 0.60 })
43
+
44
+ const results = []
45
+ for await (const entry of db.iterate({ category: 'fruits' })) {
46
+ results.push(entry)
47
+ }
48
+
49
+ expect(results).toHaveLength(2)
50
+ expect(results.map(r => r.name)).toEqual(['Apple', 'Banana'])
51
+ })
52
+
53
+ test('should detect and process modifications', async () => {
54
+ // Insert test data
55
+ await db.insert({ id: 1, name: 'Apple', category: 'fruits', price: 1.50 })
56
+ await db.insert({ id: 2, name: 'Banana', category: 'fruits', price: 0.80 })
57
+
58
+ // Iterate and modify records
59
+ for await (const entry of db.iterate({ category: 'fruits' })) {
60
+ if (entry.name === 'Apple') {
61
+ entry.price = 2.00 // Modify price
62
+ }
63
+ }
64
+
65
+ // Verify changes were applied
66
+ const apple = await db.findOne({ name: 'Apple' })
67
+ expect(apple.price).toBe(2.00)
68
+
69
+ const banana = await db.findOne({ name: 'Banana' })
70
+ expect(banana.price).toBe(0.80) // Unchanged
71
+ })
72
+
73
+ test('should handle deletions by setting entry to null', async () => {
74
+ // Insert test data
75
+ await db.insert({ id: 1, name: 'Apple', category: 'fruits', price: 1.50 })
76
+ await db.insert({ id: 2, name: 'Banana', category: 'fruits', price: 0.80 })
77
+ await db.insert({ id: 3, name: 'Carrot', category: 'vegetables', price: 0.60 })
78
+
79
+ // Iterate and delete some records
80
+ for await (const entry of db.iterate({ category: 'fruits' })) {
81
+ if (entry.name === 'Apple') {
82
+ // Delete the record using the delete method
83
+ entry.delete()
84
+ }
85
+ }
86
+
87
+ // Verify deletion
88
+ const fruits = await db.find({ category: 'fruits' })
89
+ expect(fruits).toHaveLength(1)
90
+ expect(fruits[0].name).toBe('Banana')
91
+
92
+ // Verify other categories unaffected
93
+ const vegetables = await db.find({ category: 'vegetables' })
94
+ expect(vegetables).toHaveLength(1)
95
+ })
96
+ })
97
+
98
+ describe('Performance Features', () => {
99
+ test('should process records in batches', async () => {
100
+ // Insert many records
101
+ const records = []
102
+ for (let i = 1; i <= 2500; i++) {
103
+ records.push({
104
+ id: i,
105
+ name: `Item${i}`,
106
+ category: i % 2 === 0 ? 'even' : 'odd',
107
+ price: i * 0.1
108
+ })
109
+ }
110
+
111
+ // Insert all records
112
+ for (const record of records) {
113
+ await db.insert(record)
114
+ }
115
+
116
+ let processedCount = 0
117
+ let modifiedCount = 0
118
+
119
+ // Iterate with progress callback
120
+ for await (const entry of db.iterate(
121
+ { category: 'even' },
122
+ {
123
+ chunkSize: 500,
124
+ progressCallback: (progress) => {
125
+ processedCount = progress.processed
126
+ modifiedCount = progress.modified
127
+ }
128
+ }
129
+ )) {
130
+ // Modify every 20th record (to get exactly 125 from 1250)
131
+ if (entry.id % 20 === 0) {
132
+ entry.price = entry.price * 2
133
+ }
134
+ }
135
+
136
+ expect(processedCount).toBe(1250) // Half of 2500
137
+ expect(modifiedCount).toBe(125) // Every 20th of 1250 (20, 40, 60, ..., 2500)
138
+ })
139
+
140
+ test('should handle large datasets efficiently', async () => {
141
+ // Insert test data
142
+ const records = []
143
+ for (let i = 1; i <= 1000; i++) {
144
+ records.push({
145
+ id: i,
146
+ name: `Product${i}`,
147
+ category: 'electronics',
148
+ price: Math.random() * 100
149
+ })
150
+ }
151
+
152
+ // Insert all records
153
+ for (const record of records) {
154
+ await db.insert(record)
155
+ }
156
+
157
+ const startTime = Date.now()
158
+ let count = 0
159
+
160
+ // Iterate through all records
161
+ for await (const entry of db.iterate({ category: 'electronics' })) {
162
+ count++
163
+ // Simple modification
164
+ entry.lastProcessed = Date.now()
165
+ }
166
+
167
+ const elapsed = Date.now() - startTime
168
+
169
+ expect(count).toBe(1000)
170
+ expect(elapsed).toBeLessThan(5000) // Should complete in under 5 seconds
171
+
172
+ // Verify modifications were applied
173
+ const sample = await db.findOne({ id: 1 })
174
+ expect(sample.lastProcessed).toBeDefined()
175
+ })
176
+ })
177
+
178
+ describe('Options and Configuration', () => {
179
+ test('should respect chunkSize option', async () => {
180
+ // Insert test data
181
+ for (let i = 1; i <= 100; i++) {
182
+ await db.insert({ id: i, name: `Item${i}`, category: 'test' })
183
+ }
184
+
185
+ let batchCount = 0
186
+ const chunkSize = 25
187
+
188
+ for await (const entry of db.iterate(
189
+ { category: 'test' },
190
+ {
191
+ chunkSize,
192
+ progressCallback: (progress) => {
193
+ if (progress.processed > 0 && progress.processed % chunkSize === 0) {
194
+ batchCount++
195
+ }
196
+ }
197
+ }
198
+ )) {
199
+ entry.processed = true
200
+ }
201
+
202
+ // Should have processed in 4-5 batches (100 / 25)
203
+ expect(batchCount).toBeGreaterThanOrEqual(4)
204
+ expect(batchCount).toBeLessThanOrEqual(5)
205
+ })
206
+
207
+ test('should work with manual change detection', async () => {
208
+ await db.insert({ id: 1, name: 'Test', category: 'test', value: 1 })
209
+
210
+ for await (const entry of db.iterate(
211
+ { category: 'test' },
212
+ { detectChanges: false }
213
+ )) {
214
+ entry.value = 2
215
+ entry._modified = true // Manual flag
216
+ }
217
+
218
+ const result = await db.findOne({ id: 1 })
219
+ expect(result.value).toBe(2)
220
+ })
221
+
222
+ test('should handle autoSave option', async () => {
223
+ await db.insert({ id: 1, name: 'Test', category: 'test' })
224
+
225
+ for await (const entry of db.iterate(
226
+ { category: 'test' },
227
+ { autoSave: true, chunkSize: 1 }
228
+ )) {
229
+ entry.name = 'Modified'
230
+ }
231
+
232
+ // Verify changes were saved
233
+ const result = await db.findOne({ id: 1 })
234
+ expect(result.name).toBe('Modified')
235
+ })
236
+ })
237
+
238
+ describe('Error Handling', () => {
239
+ test('should handle errors gracefully', async () => {
240
+ await db.insert({ id: 1, name: 'Test', category: 'test' })
241
+
242
+ let errorCaught = false
243
+ try {
244
+ for await (const entry of db.iterate({ category: 'test' })) {
245
+ // Simulate an error
246
+ throw new Error('Test error')
247
+ }
248
+ } catch (error) {
249
+ errorCaught = true
250
+ expect(error.message).toBe('Test error')
251
+ }
252
+
253
+ expect(errorCaught).toBe(true)
254
+ })
255
+
256
+ test('should validate state before iteration', async () => {
257
+ await db.destroy()
258
+
259
+ let errorCaught = false
260
+ try {
261
+ for await (const entry of db.iterate({})) {
262
+ // This should not execute
263
+ }
264
+ } catch (error) {
265
+ errorCaught = true
266
+ expect(error.message).toContain('destroyed')
267
+ }
268
+
269
+ expect(errorCaught).toBe(true)
270
+ })
271
+ })
272
+ })