jexidb 2.0.2 → 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.
- package/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +140 -0
- package/LICENSE +21 -21
- package/README.md +301 -527
- package/babel.config.json +5 -0
- package/dist/Database.cjs +3896 -0
- package/docs/API.md +1051 -0
- package/docs/EXAMPLES.md +701 -0
- package/docs/README.md +194 -0
- package/examples/iterate-usage-example.js +157 -0
- package/examples/simple-iterate-example.js +115 -0
- package/jest.config.js +24 -0
- package/package.json +63 -51
- package/scripts/README.md +47 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/src/Database.mjs +4130 -0
- package/src/FileHandler.mjs +1101 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +511 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +1403 -0
- package/src/managers/QueryManager.mjs +1273 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/test/$not-operator-with-and.test.js +282 -0
- package/test/README.md +8 -0
- package/test/close-init-cycle.test.js +256 -0
- package/test/critical-bugs-fixes.test.js +1069 -0
- package/test/index-persistence.test.js +306 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/iterate-method.test.js +272 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +238 -0
- package/test/setup.js +17 -0
- package/test/term-mapping-minimal.test.js +154 -0
- package/test/term-mapping-simple.test.js +257 -0
- package/test/term-mapping.test.js +514 -0
- package/test/writebuffer-flush-resilience.test.js +204 -0
- package/dist/FileHandler.js +0 -688
- package/dist/IndexManager.js +0 -353
- package/dist/IntegrityChecker.js +0 -364
- package/dist/JSONLDatabase.js +0 -1194
- package/dist/index.js +0 -617
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1248
- 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
|
+
})
|