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.
- package/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +132 -101
- package/LICENSE +21 -21
- package/README.md +301 -639
- package/babel.config.json +5 -0
- package/dist/Database.cjs +5204 -0
- package/docs/API.md +908 -241
- package/docs/EXAMPLES.md +701 -177
- package/docs/README.md +194 -184
- 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 -54
- package/scripts/README.md +47 -0
- package/scripts/benchmark-array-serialization.js +108 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/scripts/score-mode-demo.js +45 -0
- package/src/Database.mjs +5325 -0
- package/src/FileHandler.mjs +1140 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +702 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +2094 -0
- package/src/managers/QueryManager.mjs +1490 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/src/utils/operatorNormalizer.mjs +116 -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/coverage-method.test.js +93 -0
- package/test/critical-bugs-fixes.test.js +1069 -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 +491 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/insert-session-auto-flush.test.js +353 -0
- package/test/iterate-method.test.js +272 -0
- package/test/legacy-operator-compat.test.js +154 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +298 -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 -1333
- package/dist/index.js +0 -617
- package/docs/MIGRATION.md +0 -295
- package/examples/auto-save-example.js +0 -158
- package/examples/cjs-usage.cjs +0 -82
- package/examples/close-vs-delete-example.js +0 -71
- package/examples/esm-usage.js +0 -113
- package/examples/example-columns.idx.jdb +0 -0
- package/examples/example-columns.jdb +0 -9
- package/examples/example-options.idx.jdb +0 -0
- package/examples/example-options.jdb +0 -0
- package/examples/example-users.idx.jdb +0 -0
- package/examples/example-users.jdb +0 -5
- package/examples/simple-test.js +0 -55
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1391
- package/src/index.js +0 -608
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Database } from '../src/Database.mjs'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
|
|
4
|
+
describe('Legacy operator compatibility', () => {
|
|
5
|
+
let testDbPath
|
|
6
|
+
let testIdxPath
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
10
|
+
testDbPath = `test-legacy-operators-${uniqueSuffix}.jdb`
|
|
11
|
+
testIdxPath = testDbPath.replace('.jdb', '.idx.jdb')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const filePath of [testDbPath, testIdxPath]) {
|
|
16
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
17
|
+
try {
|
|
18
|
+
fs.unlinkSync(filePath)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.warn(`⚠️ Failed to delete ${filePath}: ${error.message}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('should support string comparison operators for find/count', async () => {
|
|
27
|
+
const db = new Database(testDbPath, {
|
|
28
|
+
indexes: {
|
|
29
|
+
start: 'number',
|
|
30
|
+
end: 'number'
|
|
31
|
+
},
|
|
32
|
+
termMapping: true,
|
|
33
|
+
debugMode: false
|
|
34
|
+
})
|
|
35
|
+
await db.init()
|
|
36
|
+
|
|
37
|
+
const now = Math.floor(Date.now() / 1000)
|
|
38
|
+
|
|
39
|
+
await db.insert({
|
|
40
|
+
id: '1',
|
|
41
|
+
start: now - 60,
|
|
42
|
+
end: now + 60
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
await db.insert({
|
|
46
|
+
id: '2',
|
|
47
|
+
start: now - 3600,
|
|
48
|
+
end: now - 300
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const legacyCount = await db.count({ end: { '>': now } })
|
|
52
|
+
const mongoCount = await db.count({ end: { $gt: now } })
|
|
53
|
+
expect(legacyCount).toBe(1)
|
|
54
|
+
expect(legacyCount).toBe(mongoCount)
|
|
55
|
+
|
|
56
|
+
const legacyResults = await db.find({
|
|
57
|
+
start: { '<=': now },
|
|
58
|
+
end: { '>': now }
|
|
59
|
+
})
|
|
60
|
+
const canonicalResults = await db.find({
|
|
61
|
+
start: { $lte: now },
|
|
62
|
+
end: { $gt: now }
|
|
63
|
+
})
|
|
64
|
+
const mongoResults = await db.find({
|
|
65
|
+
start: { $lte: now },
|
|
66
|
+
end: { $gt: now }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(legacyResults.map(record => record.id)).toEqual(['1'])
|
|
70
|
+
expect(canonicalResults.map(record => record.id)).toEqual(['1'])
|
|
71
|
+
expect(mongoResults.map(record => record.id)).toEqual(['1'])
|
|
72
|
+
|
|
73
|
+
await db.save()
|
|
74
|
+
await db.destroy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('should support string inequality operator', async () => {
|
|
78
|
+
const db = new Database(testDbPath, {
|
|
79
|
+
indexes: {
|
|
80
|
+
end: 'number'
|
|
81
|
+
},
|
|
82
|
+
termMapping: true,
|
|
83
|
+
debugMode: false
|
|
84
|
+
})
|
|
85
|
+
await db.init()
|
|
86
|
+
|
|
87
|
+
const now = Math.floor(Date.now() / 1000)
|
|
88
|
+
|
|
89
|
+
await db.insert({
|
|
90
|
+
id: 'A',
|
|
91
|
+
end: now + 120
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await db.insert({
|
|
95
|
+
id: 'B',
|
|
96
|
+
end: now + 240
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const legacyResults = await db.find({
|
|
100
|
+
end: { '!=': now + 120 }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(legacyResults.map(record => record.id)).toEqual(['B'])
|
|
104
|
+
|
|
105
|
+
await db.save()
|
|
106
|
+
await db.destroy()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('should support mongo-style comparison operators when using indexes', async () => {
|
|
110
|
+
const db = new Database(testDbPath, {
|
|
111
|
+
indexes: {
|
|
112
|
+
numericField: 'number'
|
|
113
|
+
},
|
|
114
|
+
debugMode: false
|
|
115
|
+
})
|
|
116
|
+
await db.init()
|
|
117
|
+
|
|
118
|
+
await db.insert({ id: '10', numericField: 100 })
|
|
119
|
+
await db.insert({ id: '20', numericField: 200 })
|
|
120
|
+
|
|
121
|
+
await db.save()
|
|
122
|
+
await db.close()
|
|
123
|
+
|
|
124
|
+
const reopenedDb = new Database(testDbPath, {
|
|
125
|
+
create: false,
|
|
126
|
+
indexes: {
|
|
127
|
+
numericField: 'number'
|
|
128
|
+
},
|
|
129
|
+
debugMode: false
|
|
130
|
+
})
|
|
131
|
+
await reopenedDb.init()
|
|
132
|
+
|
|
133
|
+
const greaterThanResults = await reopenedDb.find({ numericField: { $gt: 150 } })
|
|
134
|
+
expect(greaterThanResults.map(record => record.id)).toEqual(['20'])
|
|
135
|
+
|
|
136
|
+
const greaterOrEqualResults = await reopenedDb.find({ numericField: { $gte: 200 } })
|
|
137
|
+
expect(greaterOrEqualResults.map(record => record.id)).toEqual(['20'])
|
|
138
|
+
|
|
139
|
+
const lessThanResults = await reopenedDb.find({ numericField: { $lt: 150 } })
|
|
140
|
+
expect(lessThanResults.map(record => record.id)).toEqual(['10'])
|
|
141
|
+
|
|
142
|
+
const lessOrEqualResults = await reopenedDb.find({ numericField: { $lte: 100 } })
|
|
143
|
+
expect(lessOrEqualResults.map(record => record.id)).toEqual(['10'])
|
|
144
|
+
|
|
145
|
+
const notEqualResults = await reopenedDb.find({ numericField: { $ne: 100 } })
|
|
146
|
+
expect(notEqualResults.map(record => record.id)).toEqual(['20'])
|
|
147
|
+
|
|
148
|
+
const countResults = await reopenedDb.count({ numericField: { $gt: 50, $lt: 250 } })
|
|
149
|
+
expect(countResults).toBe(2)
|
|
150
|
+
|
|
151
|
+
await reopenedDb.destroy()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for query operators
|
|
3
|
+
* Tests the fixes for the $not operator and default operator behavior bugs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
7
|
+
import Database from '../src/Database.mjs';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
describe('Query Operators', () => {
|
|
12
|
+
let db;
|
|
13
|
+
const testDbPath = 'test-query-operators-comprehensive';
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Clean up any existing test database
|
|
17
|
+
if (fs.existsSync(testDbPath + '.jdb')) {
|
|
18
|
+
fs.unlinkSync(testDbPath + '.jdb');
|
|
19
|
+
}
|
|
20
|
+
if (fs.existsSync(testDbPath + '.terms.jdb')) {
|
|
21
|
+
fs.unlinkSync(testDbPath + '.terms.jdb');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
db = new Database(testDbPath, {
|
|
25
|
+
debugMode: false,
|
|
26
|
+
termMapping: true,
|
|
27
|
+
termMappingFields: ['nameTerms', 'tags']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await db.init();
|
|
31
|
+
|
|
32
|
+
// Insert comprehensive test data
|
|
33
|
+
const testData = [
|
|
34
|
+
{ id: 1, name: 'TV Câmara', nameTerms: ['tv', 'câmara'], tags: ['news', 'politics'], group: 'Brazil', rating: 4.5 },
|
|
35
|
+
{ id: 2, name: 'TV Cultura', nameTerms: ['tv', 'cultura'], tags: ['culture', 'education'], group: 'Brazil', rating: 4.2 },
|
|
36
|
+
{ id: 3, name: 'SBT', nameTerms: ['sbt'], tags: ['entertainment'], group: 'Brazil', rating: 3.8 },
|
|
37
|
+
{ id: 4, name: 'Record News', nameTerms: ['record', 'news'], tags: ['news'], group: 'Brazil', rating: 4.0 },
|
|
38
|
+
{ id: 5, name: 'CNN', nameTerms: ['cnn'], tags: ['news', 'international'], group: 'International', rating: 4.7 },
|
|
39
|
+
{ id: 6, name: 'BBC', nameTerms: ['bbc'], tags: ['news', 'international'], group: 'International', rating: 4.6 },
|
|
40
|
+
{ id: 7, name: 'TV Globo', nameTerms: ['tv', 'globo'], tags: ['entertainment', 'news'], group: 'Brazil', rating: 4.3 },
|
|
41
|
+
{ id: 8, name: 'TV Record', nameTerms: ['tv', 'record'], tags: ['entertainment'], group: 'Brazil', rating: 3.9 },
|
|
42
|
+
{ id: 9, name: 'Discovery', nameTerms: ['discovery'], tags: ['documentary', 'education'], group: 'International', rating: 4.4 },
|
|
43
|
+
{ id: 10, name: 'National Geographic', nameTerms: ['national', 'geographic'], tags: ['documentary', 'nature'], group: 'International', rating: 4.8 }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const record of testData) {
|
|
47
|
+
await db.insert(record);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
if (db) {
|
|
53
|
+
await db.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Clean up test files
|
|
57
|
+
const files = [testDbPath + '.jdb', testDbPath + '.terms.jdb'];
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
if (fs.existsSync(file)) {
|
|
60
|
+
fs.unlinkSync(file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('$not Operator Fixes', () => {
|
|
66
|
+
it('should handle $not operator consistently with or without explicit $and', async () => {
|
|
67
|
+
// Test 1: $not without explicit $and (the bug case)
|
|
68
|
+
const result1 = await db.find({nameTerms: 'tv', $not: {nameTerms: { $in: ['cultura'] }}});
|
|
69
|
+
expect(result1).toHaveLength(3); // TV Câmara, TV Globo, TV Record
|
|
70
|
+
expect(result1.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Globo', 'TV Record']));
|
|
71
|
+
expect(result1.map(r => r.name)).not.toContain('TV Cultura');
|
|
72
|
+
|
|
73
|
+
// Test 2: $not with explicit $and (should give same result)
|
|
74
|
+
const result2 = await db.find({"$and": [{nameTerms: 'tv'}, {$not: {nameTerms: { $in: ['cultura'] }}}]});
|
|
75
|
+
expect(result2).toHaveLength(3);
|
|
76
|
+
expect(result2.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Globo', 'TV Record']));
|
|
77
|
+
expect(result2.map(r => r.name)).not.toContain('TV Cultura');
|
|
78
|
+
|
|
79
|
+
// Both results should be identical
|
|
80
|
+
expect(result1.map(r => r.id).sort()).toEqual(result2.map(r => r.id).sort());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle $not with different fields', async () => {
|
|
84
|
+
const result = await db.find({group: 'Brazil', $not: {nameTerms: { $in: ['globo'] }}});
|
|
85
|
+
expect(result).toHaveLength(5); // All Brazil channels except TV Globo
|
|
86
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Cultura', 'SBT', 'Record News', 'TV Record']));
|
|
87
|
+
expect(result.map(r => r.name)).not.toContain('TV Globo');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle complex $not queries', async () => {
|
|
91
|
+
const result = await db.find({nameTerms: 'tv', $not: {nameTerms: { $in: ['cultura', 'globo'] }}});
|
|
92
|
+
expect(result).toHaveLength(2); // TV Câmara, TV Record
|
|
93
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Record']));
|
|
94
|
+
expect(result.map(r => r.name)).not.toContain('TV Cultura');
|
|
95
|
+
expect(result.map(r => r.name)).not.toContain('TV Globo');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle $not with numeric comparisons', async () => {
|
|
99
|
+
const result = await db.find({group: 'Brazil', $not: {rating: { $gte: 4.0 }}});
|
|
100
|
+
expect(result).toHaveLength(2); // SBT (3.8), TV Record (3.9)
|
|
101
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['SBT', 'TV Record']));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle nested $not operators', async () => {
|
|
105
|
+
const result = await db.find({$not: {nameTerms: { $in: ['tv'] }}});
|
|
106
|
+
expect(result).toHaveLength(6); // All channels without 'tv' in nameTerms
|
|
107
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['SBT', 'Record News', 'CNN', 'BBC', 'Discovery', 'National Geographic']));
|
|
108
|
+
expect(result.map(r => r.name)).not.toContain('TV Câmara');
|
|
109
|
+
expect(result.map(r => r.name)).not.toContain('TV Cultura');
|
|
110
|
+
expect(result.map(r => r.name)).not.toContain('TV Globo');
|
|
111
|
+
expect(result.map(r => r.name)).not.toContain('TV Record');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Default Operator Behavior (AND Logic)', () => {
|
|
116
|
+
it('should use AND logic for multiple conditions at root level', async () => {
|
|
117
|
+
const result = await db.find({nameTerms: 'tv', group: 'Brazil'});
|
|
118
|
+
expect(result).toHaveLength(4); // All TV channels in Brazil
|
|
119
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Cultura', 'TV Globo', 'TV Record']));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use AND logic for multiple field conditions', async () => {
|
|
123
|
+
const result = await db.find({group: 'International', rating: { $gte: 4.5 }});
|
|
124
|
+
expect(result).toHaveLength(3); // CNN, BBC, National Geographic
|
|
125
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['CNN', 'BBC', 'National Geographic']));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should use AND logic with array field conditions', async () => {
|
|
129
|
+
const result = await db.find({tags: { $in: ['news'] }, group: 'Brazil'});
|
|
130
|
+
expect(result).toHaveLength(3); // TV Câmara, Record News, TV Globo
|
|
131
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'Record News', 'TV Globo']));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should use AND logic with mixed operators', async () => {
|
|
135
|
+
const result = await db.find({
|
|
136
|
+
nameTerms: 'tv',
|
|
137
|
+
group: 'Brazil',
|
|
138
|
+
rating: { $gte: 4.0 },
|
|
139
|
+
tags: { $in: ['news'] }
|
|
140
|
+
});
|
|
141
|
+
expect(result).toHaveLength(2); // TV Câmara, TV Globo
|
|
142
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Globo']));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Complex Query Combinations', () => {
|
|
147
|
+
it('should handle $and with $not', async () => {
|
|
148
|
+
const result = await db.find({
|
|
149
|
+
"$and": [
|
|
150
|
+
{group: 'Brazil'},
|
|
151
|
+
{$not: {nameTerms: { $in: ['cultura', 'globo'] }}}
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
expect(result).toHaveLength(4); // All Brazil channels except TV Cultura and TV Globo
|
|
155
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'SBT', 'Record News', 'TV Record']));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle $or with $not', async () => {
|
|
159
|
+
const result = await db.find({
|
|
160
|
+
"$or": [
|
|
161
|
+
{nameTerms: 'tv'},
|
|
162
|
+
{group: 'International'}
|
|
163
|
+
],
|
|
164
|
+
$not: {rating: { $lt: 4.0 }}
|
|
165
|
+
});
|
|
166
|
+
// Should include TV channels with rating >= 4.0 OR International channels with rating >= 4.0
|
|
167
|
+
expect(result.length).toBeGreaterThan(0);
|
|
168
|
+
result.forEach(record => {
|
|
169
|
+
expect(record.rating).toBeGreaterThanOrEqual(4.0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle multiple $not conditions', async () => {
|
|
174
|
+
// Note: Multiple $not conditions with the same key will overwrite each other
|
|
175
|
+
// This is expected JavaScript behavior. For multiple conditions, use $and
|
|
176
|
+
const result = await db.find({
|
|
177
|
+
"$and": [
|
|
178
|
+
{group: 'Brazil'},
|
|
179
|
+
{$not: {nameTerms: { $in: ['cultura'] }}},
|
|
180
|
+
{$not: {rating: { $lt: 4.0 }}}
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
// This should be equivalent to: Brazil AND NOT cultura AND NOT rating < 4.0
|
|
184
|
+
expect(result.length).toBeGreaterThan(0);
|
|
185
|
+
result.forEach(record => {
|
|
186
|
+
expect(record.group).toBe('Brazil');
|
|
187
|
+
expect(record.nameTerms).not.toContain('cultura');
|
|
188
|
+
expect(record.rating).toBeGreaterThanOrEqual(4.0);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Edge Cases', () => {
|
|
194
|
+
it('should handle empty $not condition', async () => {
|
|
195
|
+
const result = await db.find({nameTerms: 'tv', $not: {}});
|
|
196
|
+
expect(result).toHaveLength(4); // All TV channels (empty $not matches nothing, so excludes nothing)
|
|
197
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Cultura', 'TV Globo', 'TV Record']));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle $not with non-existent field', async () => {
|
|
201
|
+
const result = await db.find({nameTerms: 'tv', $not: {nonExistentField: 'value'}});
|
|
202
|
+
expect(result).toHaveLength(4); // All TV channels (non-existent field never matches)
|
|
203
|
+
expect(result.map(r => r.name)).toEqual(expect.arrayContaining(['TV Câmara', 'TV Cultura', 'TV Globo', 'TV Record']));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle $not with null values', async () => {
|
|
207
|
+
// Insert a record with null value
|
|
208
|
+
await db.insert({ id: 99, name: 'Test Channel', nameTerms: null, group: 'Test' });
|
|
209
|
+
|
|
210
|
+
const result = await db.find({group: 'Test', $not: {nameTerms: null}});
|
|
211
|
+
expect(result).toHaveLength(0); // No records match (the only Test record has null nameTerms)
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Performance and Consistency', () => {
|
|
216
|
+
it('should produce consistent results across multiple queries', async () => {
|
|
217
|
+
const query = {nameTerms: 'tv', $not: {nameTerms: { $in: ['cultura'] }}};
|
|
218
|
+
|
|
219
|
+
// Run the same query multiple times
|
|
220
|
+
const results = [];
|
|
221
|
+
for (let i = 0; i < 5; i++) {
|
|
222
|
+
const result = await db.find(query);
|
|
223
|
+
results.push(result.map(r => r.id).sort());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// All results should be identical
|
|
227
|
+
for (let i = 1; i < results.length; i++) {
|
|
228
|
+
expect(results[i]).toEqual(results[0]);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle large result sets with $not', async () => {
|
|
233
|
+
// This test ensures the fix works with larger datasets
|
|
234
|
+
const result = await db.find({$not: {group: 'NonExistent'}});
|
|
235
|
+
expect(result).toHaveLength(10); // All records (since no records have group 'NonExistent')
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'
|
|
2
|
+
import { Database } from '../src/Database.mjs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
describe('RegExp Queries on Array Fields', () => {
|
|
6
|
+
let testDir
|
|
7
|
+
let db
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
testDir = path.join(process.cwd(), 'test-dbs')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
if (db) {
|
|
15
|
+
await db.save()
|
|
16
|
+
await db.destroy()
|
|
17
|
+
db = null
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('Bug Fix: RegExp on Array Fields', () => {
|
|
22
|
+
test('should correctly filter array fields with RegExp queries', async () => {
|
|
23
|
+
const dbPath = path.join(testDir, 'regex-array-fields.jdb')
|
|
24
|
+
|
|
25
|
+
db = new Database(dbPath, {
|
|
26
|
+
clear: true,
|
|
27
|
+
create: true,
|
|
28
|
+
debugMode: false
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await db.init()
|
|
32
|
+
|
|
33
|
+
// Insert test data
|
|
34
|
+
await db.insert({ name: 'Globo', nameTerms: ['globo'] })
|
|
35
|
+
await db.insert({ name: 'FlixHD', nameTerms: ['flixhd'] })
|
|
36
|
+
await db.insert({ name: 'Netflix', nameTerms: ['netflix'] })
|
|
37
|
+
await db.insert({ name: 'Global News', nameTerms: ['global', 'news'] })
|
|
38
|
+
|
|
39
|
+
// Test 1: RegExp query that should match multiple records
|
|
40
|
+
const results1 = await db.find({ nameTerms: new RegExp('glob', 'i') })
|
|
41
|
+
expect(results1.length).toBe(2)
|
|
42
|
+
expect(results1.some(r => r.name === 'Globo')).toBe(true)
|
|
43
|
+
expect(results1.some(r => r.name === 'Global News')).toBe(true)
|
|
44
|
+
|
|
45
|
+
// Test 2: RegExp query with impossible pattern
|
|
46
|
+
const results2 = await db.find({ nameTerms: new RegExp('IMPOSSIBLE_PATTERN_12345', 'i') })
|
|
47
|
+
expect(results2.length).toBe(0)
|
|
48
|
+
|
|
49
|
+
// Test 3: RegExp query that matches multiple records with "flix"
|
|
50
|
+
const results3 = await db.find({ nameTerms: new RegExp('flix', 'i') })
|
|
51
|
+
expect(results3.length).toBe(2)
|
|
52
|
+
expect(results3.some(r => r.name === 'FlixHD')).toBe(true)
|
|
53
|
+
expect(results3.some(r => r.name === 'Netflix')).toBe(true)
|
|
54
|
+
|
|
55
|
+
// Test 4: RegExp query with anchor (start of string)
|
|
56
|
+
const results4 = await db.find({ nameTerms: new RegExp('^flix', 'i') })
|
|
57
|
+
expect(results4.length).toBe(1)
|
|
58
|
+
expect(results4[0].name).toBe('FlixHD')
|
|
59
|
+
|
|
60
|
+
// Test 5: RegExp query with case sensitivity
|
|
61
|
+
const results5 = await db.find({ nameTerms: new RegExp('GLOBO') })
|
|
62
|
+
expect(results5.length).toBe(0) // Should not match because it's case-sensitive
|
|
63
|
+
|
|
64
|
+
const results6 = await db.find({ nameTerms: new RegExp('GLOBO', 'i') })
|
|
65
|
+
expect(results6.length).toBe(1) // Should match because it's case-insensitive
|
|
66
|
+
expect(results6[0].name).toBe('Globo')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('should correctly handle RegExp queries with multi-element arrays', async () => {
|
|
70
|
+
const dbPath = path.join(testDir, 'regex-multi-array.jdb')
|
|
71
|
+
|
|
72
|
+
db = new Database(dbPath, {
|
|
73
|
+
clear: true,
|
|
74
|
+
create: true,
|
|
75
|
+
debugMode: false
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await db.init()
|
|
79
|
+
|
|
80
|
+
// Insert test data with multi-element arrays
|
|
81
|
+
await db.insert({ name: 'Test 1', tags: ['javascript', 'nodejs', 'backend'] })
|
|
82
|
+
await db.insert({ name: 'Test 2', tags: ['python', 'django', 'backend'] })
|
|
83
|
+
await db.insert({ name: 'Test 3', tags: ['javascript', 'react', 'frontend'] })
|
|
84
|
+
await db.insert({ name: 'Test 4', tags: ['ruby', 'rails', 'backend'] })
|
|
85
|
+
|
|
86
|
+
// Test: RegExp query that matches first element
|
|
87
|
+
const results1 = await db.find({ tags: new RegExp('java', 'i') })
|
|
88
|
+
expect(results1.length).toBe(2)
|
|
89
|
+
expect(results1.some(r => r.name === 'Test 1')).toBe(true)
|
|
90
|
+
expect(results1.some(r => r.name === 'Test 3')).toBe(true)
|
|
91
|
+
|
|
92
|
+
// Test: RegExp query that matches middle element
|
|
93
|
+
const results2 = await db.find({ tags: new RegExp('react', 'i') })
|
|
94
|
+
expect(results2.length).toBe(1)
|
|
95
|
+
expect(results2[0].name).toBe('Test 3')
|
|
96
|
+
|
|
97
|
+
// Test: RegExp query that matches last element
|
|
98
|
+
const results3 = await db.find({ tags: new RegExp('backend', 'i') })
|
|
99
|
+
expect(results3.length).toBe(3)
|
|
100
|
+
expect(results3.some(r => r.name === 'Test 1')).toBe(true)
|
|
101
|
+
expect(results3.some(r => r.name === 'Test 2')).toBe(true)
|
|
102
|
+
expect(results3.some(r => r.name === 'Test 4')).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('should correctly handle $regex operator on array fields', async () => {
|
|
106
|
+
const dbPath = path.join(testDir, 'regex-operator-array.jdb')
|
|
107
|
+
|
|
108
|
+
db = new Database(dbPath, {
|
|
109
|
+
clear: true,
|
|
110
|
+
create: true,
|
|
111
|
+
debugMode: false
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
await db.init()
|
|
115
|
+
|
|
116
|
+
// Insert test data
|
|
117
|
+
await db.insert({ name: 'Globo', nameTerms: ['globo'] })
|
|
118
|
+
await db.insert({ name: 'FlixHD', nameTerms: ['flixhd'] })
|
|
119
|
+
await db.insert({ name: 'Netflix', nameTerms: ['netflix'] })
|
|
120
|
+
await db.insert({ name: 'Global News', nameTerms: ['global', 'news'] })
|
|
121
|
+
|
|
122
|
+
// Test: $regex operator query
|
|
123
|
+
const results = await db.find({ nameTerms: { $regex: 'glob' } })
|
|
124
|
+
expect(results.length).toBe(2)
|
|
125
|
+
expect(results.some(r => r.name === 'Globo')).toBe(true)
|
|
126
|
+
expect(results.some(r => r.name === 'Global News')).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|