jexidb 2.0.3 → 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.
Files changed (79) 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 +5204 -0
  8. package/docs/API.md +908 -241
  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/benchmark-array-serialization.js +108 -0
  17. package/scripts/clean-test-files.js +75 -0
  18. package/scripts/prepare.js +31 -0
  19. package/scripts/run-tests.js +80 -0
  20. package/scripts/score-mode-demo.js +45 -0
  21. package/src/Database.mjs +5325 -0
  22. package/src/FileHandler.mjs +1140 -0
  23. package/src/OperationQueue.mjs +279 -0
  24. package/src/SchemaManager.mjs +268 -0
  25. package/src/Serializer.mjs +702 -0
  26. package/src/managers/ConcurrencyManager.mjs +257 -0
  27. package/src/managers/IndexManager.mjs +2094 -0
  28. package/src/managers/QueryManager.mjs +1490 -0
  29. package/src/managers/StatisticsManager.mjs +262 -0
  30. package/src/managers/StreamingProcessor.mjs +429 -0
  31. package/src/managers/TermManager.mjs +278 -0
  32. package/src/utils/operatorNormalizer.mjs +116 -0
  33. package/test/$not-operator-with-and.test.js +282 -0
  34. package/test/README.md +8 -0
  35. package/test/close-init-cycle.test.js +256 -0
  36. package/test/coverage-method.test.js +93 -0
  37. package/test/critical-bugs-fixes.test.js +1069 -0
  38. package/test/deserialize-corruption-fixes.test.js +296 -0
  39. package/test/exists-method.test.js +318 -0
  40. package/test/explicit-indexes-comparison.test.js +219 -0
  41. package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
  42. package/test/index-line-number-regression.test.js +100 -0
  43. package/test/index-missing-index-data.test.js +91 -0
  44. package/test/index-persistence.test.js +491 -0
  45. package/test/index-serialization.test.js +314 -0
  46. package/test/indexed-query-mode.test.js +360 -0
  47. package/test/insert-session-auto-flush.test.js +353 -0
  48. package/test/iterate-method.test.js +272 -0
  49. package/test/legacy-operator-compat.test.js +154 -0
  50. package/test/query-operators.test.js +238 -0
  51. package/test/regex-array-fields.test.js +129 -0
  52. package/test/score-method.test.js +298 -0
  53. package/test/setup.js +17 -0
  54. package/test/term-mapping-minimal.test.js +154 -0
  55. package/test/term-mapping-simple.test.js +257 -0
  56. package/test/term-mapping.test.js +514 -0
  57. package/test/writebuffer-flush-resilience.test.js +204 -0
  58. package/dist/FileHandler.js +0 -688
  59. package/dist/IndexManager.js +0 -353
  60. package/dist/IntegrityChecker.js +0 -364
  61. package/dist/JSONLDatabase.js +0 -1333
  62. package/dist/index.js +0 -617
  63. package/docs/MIGRATION.md +0 -295
  64. package/examples/auto-save-example.js +0 -158
  65. package/examples/cjs-usage.cjs +0 -82
  66. package/examples/close-vs-delete-example.js +0 -71
  67. package/examples/esm-usage.js +0 -113
  68. package/examples/example-columns.idx.jdb +0 -0
  69. package/examples/example-columns.jdb +0 -9
  70. package/examples/example-options.idx.jdb +0 -0
  71. package/examples/example-options.jdb +0 -0
  72. package/examples/example-users.idx.jdb +0 -0
  73. package/examples/example-users.jdb +0 -5
  74. package/examples/simple-test.js +0 -55
  75. package/src/FileHandler.js +0 -674
  76. package/src/IndexManager.js +0 -363
  77. package/src/IntegrityChecker.js +0 -379
  78. package/src/JSONLDatabase.js +0 -1391
  79. package/src/index.js +0 -608
@@ -0,0 +1,298 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { Database } from '../src/Database.mjs'
4
+
5
+ // Clean up test files
6
+ const cleanUp = (filePath) => {
7
+ try {
8
+ if (fs.existsSync(filePath)) {
9
+ fs.unlinkSync(filePath)
10
+ }
11
+ } catch (error) {
12
+ // Ignore cleanup errors
13
+ }
14
+ }
15
+
16
+ describe('Score Method Tests', () => {
17
+ let db
18
+ let testDbPath
19
+ let testIdxPath
20
+
21
+ beforeEach(async () => {
22
+ const testId = Math.random().toString(36).substring(7)
23
+ testDbPath = path.join(process.cwd(), `test-score-${testId}.jdb`)
24
+ testIdxPath = path.join(process.cwd(), `test-score-${testId}.idx.jdb`)
25
+
26
+ // Clean up any existing files
27
+ cleanUp(testDbPath)
28
+ cleanUp(testIdxPath)
29
+
30
+ // Create and initialize database
31
+ db = new Database(testDbPath, {
32
+ indexes: {
33
+ 'terms': 'array:string'
34
+ }
35
+ })
36
+ await db.init()
37
+ })
38
+
39
+ afterEach(async () => {
40
+ if (db) {
41
+ await db.close()
42
+ db = null
43
+ }
44
+
45
+ // Clean up test files
46
+ cleanUp(testDbPath)
47
+ cleanUp(testIdxPath)
48
+ })
49
+
50
+ describe('Basic Score Functionality', () => {
51
+ test('should score records based on terms in array field', async () => {
52
+ // Insert test data
53
+ await db.insert({ id: 1, title: 'Action Movie', terms: ['action', 'movie'] })
54
+ await db.insert({ id: 2, title: 'Comedy Show', terms: ['comedy', 'show'] })
55
+ await db.insert({ id: 3, title: 'Action Comedy', terms: ['action', 'comedy'] })
56
+ await db.insert({ id: 4, title: 'Documentary', terms: ['documentary'] })
57
+ await db.save()
58
+
59
+ // Score records
60
+ const results = await db.score('terms', {
61
+ 'action': 1.0,
62
+ 'comedy': 0.8
63
+ })
64
+
65
+ expect(results).toHaveLength(3)
66
+
67
+ // Check first result (Action Comedy - highest score: 1.8)
68
+ expect(results[0].title).toBe('Action Comedy')
69
+ expect(results[0].score).toBe(1.8)
70
+ expect(results[0]._).toBeDefined()
71
+
72
+ // Check second result (Action Movie - score: 1.0)
73
+ expect(results[1].title).toBe('Action Movie')
74
+ expect(results[1].score).toBe(1.0)
75
+
76
+ // Check third result (Comedy Show - score: 0.8)
77
+ expect(results[2].title).toBe('Comedy Show')
78
+ expect(results[2].score).toBe(0.8)
79
+ })
80
+
81
+ test('should exclude records with zero scores', async () => {
82
+ await db.insert({ id: 1, title: 'Item 1', terms: ['tech'] })
83
+ await db.insert({ id: 2, title: 'Item 2', terms: ['science'] })
84
+ await db.insert({ id: 3, title: 'Item 3', terms: ['news'] })
85
+ await db.save()
86
+
87
+ const results = await db.score('terms', {
88
+ 'tech': 1.0
89
+ })
90
+
91
+ expect(results).toHaveLength(1)
92
+ expect(results[0].title).toBe('Item 1')
93
+ })
94
+
95
+ test('should handle decimal weights', async () => {
96
+ await db.insert({ id: 1, title: 'High Priority', terms: ['urgent', 'important'] })
97
+ await db.insert({ id: 2, title: 'Normal Priority', terms: ['normal'] })
98
+ await db.save()
99
+
100
+ const results = await db.score('terms', {
101
+ 'urgent': 0.9,
102
+ 'important': 0.7,
103
+ 'normal': 0.3
104
+ })
105
+
106
+ expect(results).toHaveLength(2)
107
+ expect(results[0].title).toBe('High Priority')
108
+ expect(results[0].score).toBe(1.6)
109
+ expect(results[1].title).toBe('Normal Priority')
110
+ expect(results[1].score).toBe(0.3)
111
+ })
112
+ })
113
+
114
+ describe('Options Tests', () => {
115
+ test('should respect limit option', async () => {
116
+ await db.insert({ id: 1, title: 'Item 1', terms: ['a'] })
117
+ await db.insert({ id: 2, title: 'Item 2', terms: ['a', 'b'] })
118
+ await db.insert({ id: 3, title: 'Item 3', terms: ['a', 'b', 'c'] })
119
+ await db.insert({ id: 4, title: 'Item 4', terms: ['a'] })
120
+ await db.save()
121
+
122
+ const results = await db.score('terms', {
123
+ 'a': 1.0,
124
+ 'b': 2.0,
125
+ 'c': 3.0
126
+ }, { limit: 2 })
127
+
128
+ expect(results).toHaveLength(2)
129
+ })
130
+
131
+ test('should respect sort ascending option', async () => {
132
+ await db.insert({ id: 1, title: 'High', terms: ['high'] })
133
+ await db.insert({ id: 2, title: 'Medium', terms: ['medium'] })
134
+ await db.insert({ id: 3, title: 'Low', terms: ['low'] })
135
+ await db.save()
136
+
137
+ const results = await db.score('terms', {
138
+ 'high': 3.0,
139
+ 'medium': 2.0,
140
+ 'low': 1.0
141
+ }, { sort: 'asc' })
142
+
143
+ expect(results).toHaveLength(3)
144
+ expect(results[0].title).toBe('Low')
145
+ expect(results[1].title).toBe('Medium')
146
+ expect(results[2].title).toBe('High')
147
+ })
148
+
149
+ test('should not include score when includeScore is false', async () => {
150
+ await db.insert({ id: 1, title: 'Test', terms: ['a'] })
151
+ await db.save()
152
+
153
+ const results = await db.score('terms', {
154
+ 'a': 1.0
155
+ }, { includeScore: false })
156
+
157
+ expect(results).toHaveLength(1)
158
+ expect(results[0].score).toBeUndefined()
159
+ expect(results[0]._).toBeDefined()
160
+ })
161
+
162
+ test('should default to including score', async () => {
163
+ await db.insert({ id: 1, title: 'Test', terms: ['a'] })
164
+ await db.save()
165
+
166
+ const results = await db.score('terms', {
167
+ 'a': 1.0
168
+ })
169
+
170
+ expect(results).toHaveLength(1)
171
+ expect(results[0].score).toBe(1.0)
172
+ })
173
+ })
174
+
175
+ describe('Mode Options', () => {
176
+ test('should support max mode', async () => {
177
+ await db.insert({ id: 1, title: 'Action Only', terms: ['action'] })
178
+ await db.insert({ id: 2, title: 'Action Comedy', terms: ['action', 'comedy'] })
179
+ await db.insert({ id: 3, title: 'Comedy Only', terms: ['comedy'] })
180
+ await db.save()
181
+
182
+ const results = await db.score('terms', {
183
+ 'action': 2.0,
184
+ 'comedy': 1.0
185
+ }, { mode: 'max' })
186
+
187
+ expect(results).toHaveLength(3)
188
+ expect(results[0].title).toBe('Action Only')
189
+ expect(results[0].score).toBe(2.0)
190
+ expect(results[1].title).toBe('Action Comedy')
191
+ expect(results[1].score).toBe(2.0)
192
+ expect(results[2].title).toBe('Comedy Only')
193
+ expect(results[2].score).toBe(1.0)
194
+ })
195
+
196
+ test('should support avg mode', async () => {
197
+ await db.insert({ id: 1, title: 'Mixed', terms: ['action', 'comedy'] })
198
+ await db.insert({ id: 2, title: 'Action Only', terms: ['action'] })
199
+ await db.insert({ id: 3, title: 'Comedy Only', terms: ['comedy'] })
200
+ await db.save()
201
+
202
+ const results = await db.score('terms', {
203
+ 'action': 1.5,
204
+ 'comedy': 0.9
205
+ }, { mode: 'avg' })
206
+
207
+ expect(results).toHaveLength(3)
208
+ expect(results[0].title).toBe('Action Only')
209
+ expect(results[0].score).toBeCloseTo(1.5)
210
+ expect(results[1].title).toBe('Mixed')
211
+ expect(results[1].score).toBeCloseTo((1.5 + 0.9) / 2)
212
+ expect(results[2].title).toBe('Comedy Only')
213
+ expect(results[2].score).toBeCloseTo(0.9)
214
+ })
215
+
216
+ test('should support first mode with term priority', async () => {
217
+ await db.insert({ id: 1, title: 'High Priority', terms: ['primary', 'secondary'] })
218
+ await db.insert({ id: 2, title: 'Secondary Only', terms: ['secondary'] })
219
+ await db.insert({ id: 3, title: 'Unmatched', terms: ['other'] })
220
+ await db.save()
221
+
222
+ const results = await db.score('terms', {
223
+ 'primary': 5,
224
+ 'secondary': 2
225
+ }, { mode: 'first' })
226
+
227
+ expect(results).toHaveLength(2)
228
+ expect(results[0].title).toBe('High Priority')
229
+ expect(results[0].score).toBe(5)
230
+ expect(results[1].title).toBe('Secondary Only')
231
+ expect(results[1].score).toBe(2)
232
+ })
233
+ })
234
+
235
+ describe('Edge Cases', () => {
236
+ test('should return empty array for empty scores', async () => {
237
+ await db.insert({ id: 1, title: 'Test', terms: ['a'] })
238
+ await db.save()
239
+
240
+ const results = await db.score('terms', {})
241
+ expect(results).toHaveLength(0)
242
+ })
243
+
244
+ test('should return empty array when no terms match', async () => {
245
+ await db.insert({ id: 1, title: 'Test', terms: ['a'] })
246
+ await db.save()
247
+
248
+ const results = await db.score('terms', {
249
+ 'nonexistent': 1.0
250
+ })
251
+
252
+ expect(results).toHaveLength(0)
253
+ })
254
+
255
+ test('should handle empty database', async () => {
256
+ const results = await db.score('terms', {
257
+ 'a': 1.0
258
+ })
259
+
260
+ expect(results).toHaveLength(0)
261
+ })
262
+
263
+ test('should handle multiple occurrences of same term', async () => {
264
+ await db.insert({ id: 1, title: 'Test 1', terms: ['important', 'important'] })
265
+ await db.insert({ id: 2, title: 'Test 2', terms: ['important'] })
266
+ await db.save()
267
+
268
+ const results = await db.score('terms', {
269
+ 'important': 1.0
270
+ })
271
+
272
+ // Both should have score 1.0 (duplicates in array don't multiply score)
273
+ expect(results).toHaveLength(2)
274
+ })
275
+ })
276
+
277
+ describe('Error Handling', () => {
278
+ test('should throw error for invalid fieldName', async () => {
279
+ await expect(db.score('', { 'a': 1.0 })).rejects.toThrow('non-empty string')
280
+ await expect(db.score(null, { 'a': 1.0 })).rejects.toThrow('non-empty string')
281
+ })
282
+
283
+ test('should throw error for non-indexed field', async () => {
284
+ await expect(db.score('nonexistent', { 'a': 1.0 }))
285
+ .rejects.toThrow('not indexed')
286
+ })
287
+
288
+ test('should throw error for invalid scores object', async () => {
289
+ await expect(db.score('terms', null)).rejects.toThrow('must be an object')
290
+ await expect(db.score('terms', [])).rejects.toThrow('must be an object')
291
+ })
292
+
293
+ test('should throw error for non-numeric scores', async () => {
294
+ await expect(db.score('terms', { 'a': 'invalid' }))
295
+ .rejects.toThrow('must be a number')
296
+ })
297
+ })
298
+ })
package/test/setup.js ADDED
@@ -0,0 +1,17 @@
1
+ // Setup file to add Mocha-style matchers to Jest
2
+ import { expect } from '@jest/globals'
3
+
4
+ // Add the chai-style API to expect
5
+ expect.to = {
6
+ deep: {
7
+ equal: (received, expected) => {
8
+ return expect(received).toEqual(expected)
9
+ }
10
+ },
11
+ equal: (received, expected) => {
12
+ return expect(received).toBe(expected)
13
+ }
14
+ }
15
+
16
+ // Also add to global expect
17
+ global.expect = expect
@@ -0,0 +1,154 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import TermManager from '../src/managers/TermManager.mjs'
4
+
5
+ describe('Term Mapping - Minimal Tests', () => {
6
+ let termManager
7
+
8
+ beforeEach(() => {
9
+ termManager = new TermManager()
10
+ })
11
+
12
+ describe('TermManager', () => {
13
+ it('should create and retrieve term IDs', () => {
14
+ const id1 = termManager.getTermId('bra')
15
+ const id2 = termManager.getTermId('globo')
16
+ const id3 = termManager.getTermId('bra') // Same term
17
+
18
+ expect(id1).toBe(1)
19
+ expect(id2).toBe(2)
20
+ expect(id3).toBe(1) // Should return same ID
21
+
22
+ expect(termManager.getTerm(1)).toBe('bra')
23
+ expect(termManager.getTerm(2)).toBe('globo')
24
+ })
25
+
26
+ it('should track term usage counts', () => {
27
+ const id1 = termManager.getTermId('bra')
28
+ const id2 = termManager.getTermId('bra')
29
+ const id3 = termManager.getTermId('bra')
30
+
31
+ expect(termManager.termCounts.get(id1)).toBe(3)
32
+ })
33
+
34
+ it('should clean up orphaned terms', () => {
35
+ const id1 = termManager.getTermId('bra')
36
+ const id2 = termManager.getTermId('globo')
37
+
38
+ // Decrement counts to make them orphaned
39
+ termManager.decrementTermCount(id1)
40
+ termManager.decrementTermCount(id1)
41
+ termManager.decrementTermCount(id2)
42
+
43
+ const orphanedCount = termManager.cleanupOrphanedTerms(true)
44
+ expect(orphanedCount).toBe(2)
45
+
46
+ // Terms should be removed
47
+ expect(termManager.getTerm(id1)).toBeNull()
48
+ expect(termManager.getTerm(id2)).toBeNull()
49
+ })
50
+
51
+ it('should load and save terms', () => {
52
+ // Create some terms
53
+ termManager.getTermId('bra')
54
+ termManager.getTermId('globo')
55
+ termManager.getTermId('brasil')
56
+
57
+ // Save terms
58
+ const savedTerms = termManager.saveTerms()
59
+ expect(savedTerms).toEqual({
60
+ '1': 'bra',
61
+ '2': 'globo',
62
+ '3': 'brasil'
63
+ })
64
+
65
+ // Create new manager and load terms
66
+ const newManager = new TermManager()
67
+ newManager.loadTerms(savedTerms)
68
+
69
+ expect(newManager.getTerm(1)).toBe('bra')
70
+ expect(newManager.getTerm(2)).toBe('globo')
71
+ expect(newManager.getTerm(3)).toBe('brasil')
72
+ })
73
+
74
+ it('should provide statistics', () => {
75
+ termManager.getTermId('bra')
76
+ termManager.getTermId('globo')
77
+
78
+ const stats = termManager.getStats()
79
+ expect(stats.totalTerms).toBe(2)
80
+ expect(stats.nextId).toBe(3)
81
+ })
82
+ })
83
+
84
+ describe('Term Mapping Concept', () => {
85
+ it('should demonstrate term mapping benefits', () => {
86
+ // Simulate a large dataset with repeated terms
87
+ const terms = ['bra', 'globo', 'brasil', 'discovery', 'channel']
88
+ const repeatedTerms = []
89
+
90
+ // Create 1000 records with repeated terms
91
+ for (let i = 0; i < 1000; i++) {
92
+ const randomTerms = []
93
+ for (let j = 0; j < 5; j++) {
94
+ randomTerms.push(terms[Math.floor(Math.random() * terms.length)])
95
+ }
96
+ repeatedTerms.push(randomTerms)
97
+ }
98
+
99
+ // Map all terms to IDs
100
+ const startTime = Date.now()
101
+ const mappedData = repeatedTerms.map(record =>
102
+ record.map(term => termManager.getTermId(term))
103
+ )
104
+ const mappingTime = Date.now() - startTime
105
+
106
+ // Verify mapping worked
107
+ expect(mappedData.length).toBe(1000)
108
+ expect(mappedData[0].length).toBe(5)
109
+ expect(termManager.getStats().totalTerms).toBe(5) // Only 5 unique terms
110
+
111
+ console.log(`✅ Mapped 5000 terms to 5 unique IDs in ${mappingTime}ms`)
112
+ console.log(`📊 Term mapping stats:`, termManager.getStats())
113
+ })
114
+
115
+ it('should show size reduction potential', () => {
116
+ // Original data with repeated strings
117
+ const originalData = [
118
+ { id: 1, nameTerms: ['bra', 'globo', 'brasil'] },
119
+ { id: 2, nameTerms: ['bra', 'discovery', 'channel'] },
120
+ { id: 3, nameTerms: ['globo', 'brasil', 'discovery'] },
121
+ { id: 4, nameTerms: ['bra', 'globo', 'channel'] },
122
+ { id: 5, nameTerms: ['brasil', 'discovery', 'channel'] }
123
+ ]
124
+
125
+ // Calculate original size
126
+ const originalSize = JSON.stringify(originalData).length
127
+
128
+ // Map terms to IDs
129
+ const mappedData = originalData.map(record => ({
130
+ id: record.id,
131
+ nameTerms: record.nameTerms.map(term => termManager.getTermId(term))
132
+ }))
133
+
134
+ // Create term mapping
135
+ const termMapping = termManager.saveTerms()
136
+
137
+ // Calculate new size
138
+ const mappedSize = JSON.stringify(mappedData).length
139
+ const termMappingSize = JSON.stringify(termMapping).length
140
+ const totalNewSize = mappedSize + termMappingSize
141
+
142
+ const reduction = ((originalSize - totalNewSize) / originalSize * 100).toFixed(1)
143
+
144
+ console.log(`📊 Size comparison:`)
145
+ console.log(` Original: ${originalSize} bytes`)
146
+ console.log(` Mapped: ${mappedSize} bytes`)
147
+ console.log(` Terms: ${termMappingSize} bytes`)
148
+ console.log(` Total: ${totalNewSize} bytes`)
149
+ console.log(` Reduction: ${reduction}%`)
150
+
151
+ expect(totalNewSize).toBeLessThan(originalSize)
152
+ })
153
+ })
154
+ })