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.
Files changed (55) hide show
  1. package/.babelrc +13 -0
  2. package/.gitattributes +2 -0
  3. package/CHANGELOG.md +140 -0
  4. package/LICENSE +21 -21
  5. package/README.md +301 -527
  6. package/babel.config.json +5 -0
  7. package/dist/Database.cjs +3896 -0
  8. package/docs/API.md +1051 -0
  9. package/docs/EXAMPLES.md +701 -0
  10. package/docs/README.md +194 -0
  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 -51
  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 -1194
  50. package/dist/index.js +0 -617
  51. package/src/FileHandler.js +0 -674
  52. package/src/IndexManager.js +0 -363
  53. package/src/IntegrityChecker.js +0 -379
  54. package/src/JSONLDatabase.js +0 -1248
  55. package/src/index.js +0 -608
@@ -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
+ })
@@ -0,0 +1,257 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { Database } from '../src/Database.mjs'
4
+ import TermManager from '../src/managers/TermManager.mjs'
5
+
6
+ describe('Term Mapping Tests', () => {
7
+ let db
8
+ let testDbPath
9
+ let testIdxPath
10
+
11
+ beforeEach(async () => {
12
+ testDbPath = path.join(process.cwd(), 'test-term-mapping.jdb')
13
+ testIdxPath = path.join(process.cwd(), 'test-term-mapping.idx.jdb')
14
+
15
+ // Clean up any existing test files
16
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
17
+ if (fs.existsSync(testIdxPath)) fs.unlinkSync(testIdxPath)
18
+ })
19
+
20
+ afterEach(async () => {
21
+ if (db) {
22
+ await db.close()
23
+ }
24
+
25
+ // Clean up test files
26
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
27
+ if (fs.existsSync(testIdxPath)) fs.unlinkSync(testIdxPath)
28
+ })
29
+
30
+ describe('TermManager', () => {
31
+ let termManager
32
+
33
+ beforeEach(() => {
34
+ termManager = new TermManager()
35
+ })
36
+
37
+ it('should create and retrieve term IDs', () => {
38
+ const term1 = termManager.getTermId('bra')
39
+ const term2 = termManager.getTermId('globo')
40
+ const term3 = termManager.getTermId('bra') // Duplicate
41
+
42
+ expect(term1).toBe(1)
43
+ expect(term2).toBe(2)
44
+ expect(term3).toBe(1) // Should return same ID
45
+
46
+ expect(termManager.getTerm(1)).toBe('bra')
47
+ expect(termManager.getTerm(2)).toBe('globo')
48
+ expect(termManager.getTerm(999)).toBeNull()
49
+ })
50
+
51
+ it('should track term usage counts', () => {
52
+ const termId = termManager.getTermId('test')
53
+ expect(termManager.termCounts.get(termId)).toBe(1)
54
+
55
+ termManager.incrementTermCount(termId)
56
+ expect(termManager.termCounts.get(termId)).toBe(2)
57
+
58
+ termManager.decrementTermCount(termId)
59
+ expect(termManager.termCounts.get(termId)).toBe(1)
60
+
61
+ termManager.decrementTermCount(termId)
62
+ expect(termManager.termCounts.get(termId)).toBe(0)
63
+ })
64
+
65
+ it('should clean up orphaned terms', async () => {
66
+ const term1 = termManager.getTermId('orphan1')
67
+ const term2 = termManager.getTermId('orphan2')
68
+ const term3 = termManager.getTermId('active')
69
+
70
+ // Make term1 and term2 orphaned
71
+ termManager.decrementTermCount(term1)
72
+ termManager.decrementTermCount(term2)
73
+
74
+ const orphanedCount = await termManager.cleanupOrphanedTerms(true)
75
+ expect(orphanedCount).toBe(2)
76
+
77
+ expect(termManager.hasTerm('orphan1')).toBe(false)
78
+ expect(termManager.hasTerm('orphan2')).toBe(false)
79
+ expect(termManager.hasTerm('active')).toBe(true)
80
+ })
81
+
82
+ it('should load and save terms', async () => {
83
+ // Add some terms
84
+ termManager.getTermId('bra')
85
+ termManager.getTermId('globo')
86
+ termManager.getTermId('brasil')
87
+
88
+ const termsData = await termManager.saveTerms()
89
+ expect(termsData).toEqual({
90
+ '1': 'bra',
91
+ '2': 'globo',
92
+ '3': 'brasil'
93
+ })
94
+
95
+ // Create new manager and load terms
96
+ const newManager = new TermManager()
97
+ await newManager.loadTerms(termsData)
98
+
99
+ expect(newManager.getTerm(1)).toBe('bra')
100
+ expect(newManager.getTerm(2)).toBe('globo')
101
+ expect(newManager.getTerm(3)).toBe('brasil')
102
+ expect(newManager.nextId).toBe(4)
103
+ })
104
+
105
+ it('should provide statistics', () => {
106
+ termManager.getTermId('term1')
107
+ termManager.getTermId('term2')
108
+
109
+ const stats = termManager.getStats()
110
+ expect(stats.totalTerms).toBe(2)
111
+ expect(stats.nextId).toBe(3)
112
+ expect(stats.orphanedTerms).toBe(0)
113
+ })
114
+ })
115
+
116
+ describe('Database with Term Mapping', () => {
117
+ beforeEach(async () => {
118
+ db = new Database(testDbPath, {
119
+ indexes: { nameTerms: 'array:string', groupTerms: 'array:string' },
120
+ termMappingCleanup: true,
121
+ debugMode: false
122
+ })
123
+ await db.initialize()
124
+ })
125
+
126
+ it('should insert records with term mapping', async () => {
127
+ const record = {
128
+ name: 'Test Record',
129
+ nameTerms: ['bra', 'globo', 'brasil'],
130
+ groupTerms: ['channel', 'discovery']
131
+ }
132
+
133
+ const inserted = await db.insert(record)
134
+ expect(inserted.nameTerms).toEqual(['bra', 'globo', 'brasil'])
135
+ expect(inserted.groupTerms).toEqual(['channel', 'discovery'])
136
+
137
+ // Check that terms are mapped (order may vary)
138
+ const allTerms = ['bra', 'globo', 'brasil', 'channel', 'discovery']
139
+ const mappedTerms = []
140
+ for (let i = 1; i <= 5; i++) {
141
+ const term = db.termManager.getTerm(i)
142
+ if (term) mappedTerms.push(term)
143
+ }
144
+ expect(mappedTerms.sort()).toEqual(allTerms.sort())
145
+ })
146
+
147
+ it('should query with term mapping', async () => {
148
+ // Insert test data
149
+ await db.insert({
150
+ name: 'Record 1',
151
+ nameTerms: ['bra', 'globo'],
152
+ groupTerms: ['channel']
153
+ })
154
+
155
+ await db.insert({
156
+ name: 'Record 2',
157
+ nameTerms: ['brasil', 'discovery'],
158
+ groupTerms: ['channel', 'discovery']
159
+ })
160
+
161
+ // Query by term
162
+ const results1 = await db.find({ nameTerms: 'bra' })
163
+ expect(results1).toHaveLength(1)
164
+ expect(results1[0].name).toBe('Record 1')
165
+
166
+ const results2 = await db.find({ groupTerms: 'channel' })
167
+ expect(results2).toHaveLength(2)
168
+
169
+ const results3 = await db.find({ nameTerms: 'nonexistent' })
170
+ expect(results3).toHaveLength(0)
171
+ })
172
+
173
+ it('should update records with term mapping', async () => {
174
+ const record = await db.insert({
175
+ name: 'Test Record',
176
+ nameTerms: ['bra', 'globo'],
177
+ groupTerms: ['channel']
178
+ })
179
+
180
+ // Update with new terms
181
+ await db.update({ id: record.id }, {
182
+ nameTerms: ['brasil', 'discovery'],
183
+ groupTerms: ['discovery']
184
+ })
185
+
186
+ // Check that old terms are decremented and new terms are added
187
+ expect(db.termManager.termCounts.get(1)).toBe(0) // 'bra' should be orphaned
188
+ expect(db.termManager.termCounts.get(2)).toBe(0) // 'globo' should be orphaned
189
+ expect(db.termManager.termCounts.get(3)).toBe(0) // 'channel' should be orphaned
190
+ expect(db.termManager.termCounts.get(4)).toBe(1) // 'brasil' should be active
191
+ expect(db.termManager.termCounts.get(5)).toBe(2) // 'discovery' should be active (used in both nameTerms and groupTerms)
192
+
193
+ // Query to verify update
194
+ const results = await db.find({ nameTerms: 'brasil' })
195
+ expect(results).toHaveLength(1)
196
+ expect(results[0].nameTerms).toEqual(['brasil', 'discovery'])
197
+ })
198
+
199
+ it('should delete records with term mapping', async () => {
200
+ const record = await db.insert({
201
+ name: 'Test Record',
202
+ nameTerms: ['bra', 'globo'],
203
+ groupTerms: ['channel']
204
+ })
205
+
206
+ await db.delete({ id: record.id })
207
+
208
+ // Check that terms are decremented
209
+ expect(db.termManager.termCounts.get(1)).toBe(0) // 'bra' should be orphaned
210
+ expect(db.termManager.termCounts.get(2)).toBe(0) // 'globo' should be orphaned
211
+ expect(db.termManager.termCounts.get(3)).toBe(0) // 'channel' should be orphaned
212
+
213
+ // Verify record is deleted
214
+ const results = await db.find({ id: record.id })
215
+ expect(results).toHaveLength(0)
216
+ })
217
+
218
+ it('should maintain compatibility with non-term-mapping fields', async () => {
219
+ const record = await db.insert({
220
+ name: 'Test Record',
221
+ nameTerms: ['bra', 'globo'], // Term mapping field
222
+ groupTerms: ['channel'], // Term mapping field
223
+ category: 'news', // Non-term mapping field
224
+ tags: ['urgent', 'breaking'] // Non-term mapping field
225
+ })
226
+
227
+ // Query by non-term-mapping field
228
+ const results = await db.find({ category: 'news' })
229
+ expect(results).toHaveLength(1)
230
+ expect(results[0].name).toBe('Test Record')
231
+
232
+ // Query by tags (non-term-mapping array field)
233
+ const results2 = await db.find({ tags: 'urgent' })
234
+ expect(results2).toHaveLength(1)
235
+ })
236
+
237
+ it('should provide correct statistics', async () => {
238
+ await db.insert({
239
+ name: 'Record 1',
240
+ nameTerms: ['bra', 'globo'],
241
+ groupTerms: ['channel']
242
+ })
243
+
244
+ await db.insert({
245
+ name: 'Record 2',
246
+ nameTerms: ['brasil', 'discovery'],
247
+ groupTerms: ['channel', 'discovery']
248
+ })
249
+
250
+ const stats = db.getStats()
251
+ expect(stats.records).toBe(2)
252
+
253
+ const termStats = db.termManager.getStats()
254
+ expect(termStats.totalTerms).toBe(5) // bra, globo, brasil, discovery, channel
255
+ })
256
+ })
257
+ })