jexidb 1.1.0 → 2.0.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.
@@ -1,230 +0,0 @@
1
- export default class IndexManager {
2
- constructor(opts) {
3
- this.opts = Object.assign({}, opts)
4
- this.index = Object.assign({data: {}}, this.opts.index)
5
- Object.keys(this.opts.indexes).forEach(field => {
6
- this.index.data[field] = {}
7
- })
8
- }
9
-
10
- add(row, lineNumber) {
11
- if (typeof row !== 'object' || !row) {
12
- throw new Error('Invalid \'row\' parameter, it must be an object')
13
- }
14
- if (typeof lineNumber !== 'number') {
15
- throw new Error('Invalid line number')
16
- }
17
- for (const field in this.index.data) {
18
- if (row[field]) {
19
- const values = Array.isArray(row[field]) ? row[field] : [row[field]]
20
- for (const value of values) {
21
- if (!this.index.data[field][value]) {
22
- this.index.data[field][value] = new Set()
23
- }
24
- if (!this.index.data[field][value].has(lineNumber)) {
25
- this.index.data[field][value].add(lineNumber)
26
- }
27
- }
28
- }
29
- }
30
- }
31
-
32
- dryRemove(ln) { // remove line numbers from index without adjusting the rest
33
- for (const field in this.index.data) {
34
- for (const value in this.index.data[field]) {
35
- if (this.index.data[field][value].has(ln)) {
36
- this.index.data[field][value].delete(ln)
37
- }
38
- if (this.index.data[field][value].size === 0) {
39
- delete this.index.data[field][value]
40
- }
41
- }
42
- }
43
- }
44
-
45
- remove(lineNumbers) { // remove line numbers from index and adjust the rest
46
- lineNumbers.sort((a, b) => a - b) // Sort ascending to make calculations easier
47
- for (const field in this.index.data) {
48
- for (const value in this.index.data[field]) {
49
- const newSet = new Set()
50
- for (const ln of this.index.data[field][value]) {
51
- let offset = 0
52
- for (const lineNumber of lineNumbers) {
53
- if (lineNumber < ln) {
54
- offset++
55
- } else if (lineNumber === ln) {
56
- offset = -1 // Marca para remoção
57
- break
58
- }
59
- }
60
- if (offset >= 0) {
61
- newSet.add(ln - offset) // Atualiza o valor
62
- }
63
- }
64
- if (newSet.size > 0) {
65
- this.index.data[field][value] = newSet
66
- } else {
67
- delete this.index.data[field][value]
68
- }
69
- }
70
- }
71
- }
72
-
73
- replace(map) {
74
- for (const field in this.index.data) {
75
- for (const value in this.index.data[field]) {
76
- for(const lineNumber of this.index.data[field][value]) {
77
- if (map.has(lineNumber)) {
78
- this.index.data[field][value].delete(lineNumber)
79
- this.index.data[field][value].add(map.get(lineNumber))
80
- }
81
- }
82
- }
83
- }
84
- }
85
-
86
- query(criteria, options = {}) {
87
- if (typeof options === 'boolean') {
88
- options = { matchAny: options };
89
- }
90
- const { matchAny = false, caseInsensitive = false } = options;
91
- if (!criteria) throw new Error('No query criteria provided');
92
- const fields = Object.keys(criteria);
93
- if (!fields.length) throw new Error('No valid query criteria provided');
94
- let matchingLines = matchAny ? new Set() : null;
95
-
96
- for (const field of fields) {
97
- if (typeof this.index.data[field] === 'undefined') continue;
98
- const criteriaValue = criteria[field];
99
- let lineNumbersForField = new Set();
100
- const isNumericField = this.opts.indexes[field] === 'number';
101
-
102
- if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue)) {
103
- const fieldIndex = this.index.data[field];
104
- for (const value in fieldIndex) {
105
- let includeValue = true;
106
- if (isNumericField) {
107
- const numericValue = parseFloat(value);
108
- if (!isNaN(numericValue)) {
109
- if (criteriaValue['>'] !== undefined && numericValue <= criteriaValue['>']) {
110
- includeValue = false;
111
- }
112
- if (criteriaValue['>='] !== undefined && numericValue < criteriaValue['>=']) {
113
- includeValue = false;
114
- }
115
- if (criteriaValue['<'] !== undefined && numericValue >= criteriaValue['<']) {
116
- includeValue = false;
117
- }
118
- if (criteriaValue['<='] !== undefined && numericValue > criteriaValue['<=']) {
119
- includeValue = false;
120
- }
121
- if (criteriaValue['!='] !== undefined) {
122
- const excludeValues = Array.isArray(criteriaValue['!='])
123
- ? criteriaValue['!=']
124
- : [criteriaValue['!=']];
125
- if (excludeValues.includes(numericValue)) {
126
- includeValue = false;
127
- }
128
- }
129
- }
130
- } else {
131
- if (criteriaValue['contains'] !== undefined && typeof value === 'string') {
132
- const term = String(criteriaValue['contains']);
133
- if (caseInsensitive) {
134
- if (!value.toLowerCase().includes(term.toLowerCase())) {
135
- includeValue = false;
136
- }
137
- } else {
138
- if (!value.includes(term)) {
139
- includeValue = false;
140
- }
141
- }
142
- }
143
- if (criteriaValue['regex'] !== undefined) {
144
- let regex;
145
- if (typeof criteriaValue['regex'] === 'string') {
146
- regex = new RegExp(criteriaValue['regex'], caseInsensitive ? 'i' : '');
147
- } else if (criteriaValue['regex'] instanceof RegExp) {
148
- if (caseInsensitive && !criteriaValue['regex'].ignoreCase) {
149
- const flags = criteriaValue['regex'].flags.includes('i')
150
- ? criteriaValue['regex'].flags
151
- : criteriaValue['regex'].flags + 'i';
152
- regex = new RegExp(criteriaValue['regex'].source, flags);
153
- } else {
154
- regex = criteriaValue['regex'];
155
- }
156
- }
157
- if (regex && !regex.test(value)) {
158
- includeValue = false;
159
- }
160
- }
161
- if (criteriaValue['!='] !== undefined) {
162
- const excludeValues = Array.isArray(criteriaValue['!='])
163
- ? criteriaValue['!=']
164
- : [criteriaValue['!=']];
165
- if (excludeValues.includes(value)) {
166
- includeValue = false;
167
- }
168
- }
169
- }
170
-
171
- if (includeValue) {
172
- for (const lineNumber of fieldIndex[value]) {
173
- lineNumbersForField.add(lineNumber);
174
- }
175
- }
176
- }
177
- } else {
178
- // Comparação simples de igualdade
179
- const values = Array.isArray(criteriaValue) ? criteriaValue : [criteriaValue];
180
- const fieldData = this.index.data[field];
181
- for (const searchValue of values) {
182
- for (const key in fieldData) {
183
- let match = false;
184
- if (isNumericField) {
185
- // Converter ambas as partes para número
186
- match = Number(key) === Number(searchValue);
187
- } else {
188
- match = caseInsensitive
189
- ? key.toLowerCase() === String(searchValue).toLowerCase()
190
- : key === searchValue;
191
- }
192
- if (match) {
193
- for (const lineNumber of fieldData[key]) {
194
- lineNumbersForField.add(lineNumber);
195
- }
196
- }
197
- }
198
- }
199
- }
200
-
201
- // Consolida os resultados de cada campo
202
- if (matchAny) {
203
- matchingLines = new Set([...matchingLines, ...lineNumbersForField]);
204
- } else {
205
- if (matchingLines === null) {
206
- matchingLines = lineNumbersForField;
207
- } else {
208
- matchingLines = new Set([...matchingLines].filter(n => lineNumbersForField.has(n)));
209
- }
210
- if (!matchingLines.size) {
211
- return new Set();
212
- }
213
- }
214
- }
215
- return matchingLines || new Set();
216
- }
217
-
218
- load(index) {
219
- for(const field in index.data) {
220
- for(const term in index.data[field]) {
221
- index.data[field][term] = new Set(index.data[field][term]) // set to array
222
- }
223
- }
224
- this.index = index
225
- }
226
-
227
- readColumnIndex(column) {
228
- return new Set((this.index.data && this.index.data[column]) ? Object.keys(this.index.data[column]) : [])
229
- }
230
- }
@@ -1,120 +0,0 @@
1
- import { EventEmitter } from 'events'
2
- import zlib from 'zlib'
3
- import v8 from 'v8'
4
-
5
- export default class Serializer extends EventEmitter {
6
- constructor(opts = {}) {
7
- super()
8
- this.opts = Object.assign({}, opts)
9
- this.brotliOptions = {
10
- params: {
11
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4
12
- }
13
- }
14
- }
15
-
16
- async serialize(data, opts={}) {
17
- let line
18
- let header = 0x00 // 1 byte de header
19
- const useV8 = (this.opts.v8 && opts.json !== true) || opts.v8 === true
20
- const compress = (this.opts.compress && opts.compress !== false) || opts.compress === true
21
- const addLinebreak = opts.linebreak !== false || (!useV8 && !compress && opts.linebreak !== false)
22
- if (useV8) {
23
- header |= 0x02 // set V8
24
- line = v8.serialize(data)
25
- } else {
26
- const json = JSON.stringify(data)
27
- line = Buffer.from(json, 'utf-8')
28
- }
29
- if (compress) {
30
- let err
31
- const compressionType = useV8 ? 'deflate' : 'brotli'
32
- const buffer = await this.compress(line, compressionType).catch(e => err = e)
33
- if(!err && buffer.length && buffer.length < line.length) {
34
- header |= 0x01
35
- line = buffer
36
- }
37
- }
38
- const totalLength = 1 + line.length + (addLinebreak ? 1 : 0)
39
- const result = Buffer.alloc(totalLength)
40
- result[0] = header
41
- line.copy(result, 1, 0, line.length)
42
- if(addLinebreak) {
43
- result[result.length - 1] = 0x0A
44
- }
45
- return result
46
- }
47
-
48
- async deserialize(data) {
49
- if(data.length === 0) {
50
- return null
51
- }
52
- let line, isCompressed, isV8
53
- const header = data.readUInt8(0)
54
- const valid = header === 0x00 || header === 0x01 || header === 0x02 || header === 0x03
55
- if(valid) {
56
- isCompressed = (header & 0x01) === 0x01
57
- isV8 = (header & 0x02) === 0x02
58
- line = data.subarray(1) // remove byte header
59
- } else {
60
- isCompressed = isV8 = false
61
- try {
62
- return JSON.parse(data.toString('utf-8').trim())
63
- } catch (e) {
64
- throw new Error('Failed to deserialize JSON data')
65
- }
66
- }
67
- if (isCompressed) {
68
- const compressionType = isV8 ? 'deflate' : 'brotli'
69
- line = await this.decompress(line, compressionType).catch(e => err = e)
70
- }
71
- if (isV8) {
72
- try {
73
- return v8.deserialize(line)
74
- } catch (e) {
75
- throw new Error('Failed to deserialize V8 data')
76
- }
77
- } else {
78
- try {
79
- return JSON.parse(line.toString('utf-8').trim())
80
- } catch (e) {
81
- throw new Error('Failed to deserialize JSON data')
82
- }
83
- }
84
- }
85
-
86
- compress(data, type) {
87
- return new Promise((resolve, reject) => {
88
- const callback = (err, buffer) => {
89
- if (err) {
90
- reject(err)
91
- } else {
92
- resolve(buffer)
93
- }
94
- }
95
- if(type === 'brotli') {
96
- zlib.brotliCompress(data, this.brotliOptions, callback)
97
- } else {
98
- zlib.deflate(data, callback)
99
- }
100
- })
101
- }
102
-
103
- decompress(data, type) {
104
- return new Promise((resolve, reject) => {
105
- const callback = (err, buffer) => {
106
- if (err) {
107
- reject(err)
108
- } else {
109
- resolve(buffer)
110
- }
111
- }
112
- if(type === 'brotli') {
113
- zlib.brotliDecompress(data, callback)
114
- } else {
115
- zlib.inflate(data, callback)
116
- }
117
- })
118
- }
119
-
120
- }
package/test/README.md DELETED
@@ -1,13 +0,0 @@
1
- ## Test Results
2
- The following are the results of the automated tests conducted on my PC for different serialization formats and compression methods.
3
-
4
- | Format | Size (bytes) | Time elapsed (ms) |
5
- |-------------------------------|--------------|--------------------|
6
- | JSON | 1117 | 21 |
7
- | V8 Serialization | 1124 | 12 &#127942; |
8
- | JSON with Brotli Compression | 1038 &#127942; | 20 |
9
- | V8 with Brotli Compression | 1052 | 15 |
10
-
11
- When using V8 for serialization, be aware that serialized data may not deserialize correctly across different versions of V8 (which powers Chromium, Node.js, and Electron), leading to potential data loss or corruption. This issue is specific to V8 serialization; however, when using JSON, the data remains universal and compatible across environments.
12
-
13
- To avoid this issue, always use your V8 database with the same version of Node.js.
Binary file
Binary file
Binary file
package/test/test-v8.jdb DELETED
Binary file
package/test/test.mjs DELETED
@@ -1,173 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { Database } from '../src/Database.mjs';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- const benchmarks = {}
9
-
10
- // Array of objects containing the name and specific messages for each character
11
- const characters = [
12
- {
13
- name: 'Scorpion',
14
- missingMessage: 'Did Scorpion pull a "GET OVER HERE" on the missing entries?',
15
- updateMessage: 'Scorpion refuses to update. Maybe he’s stuck in the Netherrealm?',
16
- deleteMessage: 'I thought Scorpion was gone, but he’s still here! Must be that "Hellfire Resurrection."',
17
- signatureMove: 'Spear', powerType: 'Hellfire'
18
- },
19
- {
20
- name: 'Scarlet',
21
- missingMessage: 'Did Scarlet drain the life from the missing entries with her blood magic?',
22
- updateMessage: 'Scarlet refuses to update. Maybe her blood magic is causing interference?',
23
- deleteMessage: 'I thought Scarlet was gone, but she’s still here! Must be that blood regeneration ability.',
24
- signatureMove: 'Blood Tentacle', powerType: 'Blood Manipulation'
25
- },
26
- {
27
- name: 'Frost',
28
- missingMessage: 'Did Frost freeze the missing entries?',
29
- updateMessage: 'Frost refuses to update. Maybe she’s stuck in an ice block?',
30
- deleteMessage: 'I thought Frost was gone, but she’s still here! Must be that "Ice Shield."',
31
- signatureMove: 'Ice Daggers',
32
- powerType: 'Cryomancy'
33
- },
34
- {
35
- name: 'Frost',
36
- missingMessage: 'Did Frost freeze the missing entries?',
37
- updateMessage: 'Frost refuses to update. Maybe she’s stuck in an ice block?',
38
- deleteMessage: 'I thought Frost was gone, but she’s still here! Must be that "Ice Shield."',
39
- signatureMove: 'Ice Daggers',
40
- powerType: 'Cryomancy'
41
- }
42
- ];
43
-
44
- // Function to run the tests
45
- const runTests = async (id, name, format, opts) => {
46
-
47
- // Define the character for this battle based on the battle ID
48
- const character = characters[(id - 1) % characters.length];
49
-
50
- // Path to the test file
51
- const testFilePath = path.join(__dirname, 'test-' + name + '.jdb');
52
-
53
- console.log('Battle #' + id + ' (' + format + ') is starting...\n');
54
- fs.writeFileSync(testFilePath, '', { encoding: null }); // Clear the file before starting the tests
55
- const start = Date.now()
56
- const db = new Database(testFilePath, opts); // Instantiate the database
57
-
58
- // 1. Test if the instance was created correctly
59
- await db.init(); // Call init() right after the instance is created
60
- console.assert(db.initialized === true, `Test failed: Database didn't initialize. Looks like Raiden needs to give it a shock!`);
61
-
62
- // 2. Test data insertion with Mortal Kombat characters
63
- await db.insert({ id: 1, name: character.name, signatureMove: character.signatureMove, powerType: character.powerType });
64
- await db.insert({ id: 2, name: 'Sub-Zero', signatureMove: 'Ice Ball', powerType: 'Cryomancy' });
65
- await db.insert({ id: 3, name: 'Raiden', signatureMove: 'Electric Fly', powerType: 'Lightning' });
66
- await db.insert({ id: 4, name: 'Jax', signatureMove: 'Ground Pound', powerType: 'Strength' });
67
- await db.insert({ id: 5, name: 'Sindel', signatureMove: 'Sonic Scream', powerType: 'Sound' });
68
- await db.insert({ id: 6, name: 'Ermac', signatureMove: 'Telekinesis', powerType: 'Telekinesis' });
69
- await db.insert({ id: 7, name: 'Mileena', signatureMove: 'Sai Throw', powerType: 'Teleportation' });
70
- await db.insert({ id: 8, name: 'Kenshi', signatureMove: 'Telekinetic Slash', powerType: 'Telekinesis' });
71
- await db.insert({ id: 9, name: 'D\'Vorah', signatureMove: 'Swarm', powerType: 'Insects' });
72
- await db.insert({ id: 10, name: 'Sonya Blade', signatureMove: 'Energy Rings', powerType: 'Special Forces Technology' });
73
- await db.insert({ id: 11, name: 'Kotal Kahn', signatureMove: 'Sunstone', powerType: 'Osh-Tekk Strength' });
74
-
75
- // "Flawless Victory" if the insertion is successful
76
- console.log('Round 1 - CREATE: Flawless Victory! All characters inserted successfully.');
77
-
78
- // 3. Test if the data was inserted correctly
79
- let results = await db.query({ id: { '<=': 5 } });
80
- const pass1 = results.length === 5;
81
- const pass2 = results[0].name === character.name;
82
- console.assert(pass1, `Round 2 - READ: Test failed: Where is everyone? ${character.missingMessage}`);
83
- console.assert(pass2, `Round 2 - READ: Test failed: ${character.name} seems to have been teleported out of the database!`);
84
- if(pass1 && pass2) console.log(`Round 2 - READ: Flawless Victory! All characters inserted successfully, led by ${character.name}.`);
85
-
86
- // 4. Test indexes
87
- const pass3 = await db.indexManager.readColumnIndex('name').has(character.name)
88
- console.assert(pass3, `Round 3 - INDEX: Test failed: ${character.name} is not in the index.`);
89
- if(pass3) console.log(`Round 3 - INDEX: Flawless Victory! ${character.name} is in the index.`);
90
-
91
- // 5. Test data update
92
- await db.update({ id: 1 }, { name: character.name + ' Updated' });
93
- results = await db.query({ id: 1 });
94
- const pass4 = results.length === 1 && results[0].name === character.name + ' Updated';
95
- console.assert(pass4, `Round 3 - UPDATE: Test failed: ${character.updateMessage}`);
96
- if(pass4) console.log(`Round 3 - UPDATE: Flawless Victory! ${character.name} has been updated successfully.`);
97
-
98
- // 6. Test data deletion
99
- await db.delete({ name: character.name + ' Updated' });
100
- results = await db.query({ id: { '<=': 2 } });
101
- const pass5 = results.length === 1;
102
- const pass6 = results[0].name === 'Sub-Zero';
103
- console.assert(pass5, `Round 4 - DELETE: Test failed: ${character.deleteMessage}`);
104
- console.assert(pass6, `Round 4 - DELETE: Test failed: Sub-Zero is nowhere to be seen. Did he freeze the system?`);
105
- if(pass5 && pass6) console.log(`Round 4 - DELETE: Flawless Victory! ${character.name} has been eliminated successfully.`);
106
-
107
- // End the battle and log the result
108
- if(pass1 && pass2 && pass4 && pass5 && pass6) {
109
- let err, elapsed = Date.now() - start;
110
- const { size } = await fs.promises.stat(testFilePath);
111
- if(!benchmarks[format]) {
112
- benchmarks[format] = { elapsed, size }
113
- } else {
114
- benchmarks[format].elapsed = (elapsed + benchmarks[format].elapsed)
115
- benchmarks[format].size = (size + benchmarks[format].size)
116
- }
117
- console.log(`\nBattle #${id} ended: All tests with format "${format}" ran successfully! Fatality avoided this time.\n\n`);
118
- global.gc()
119
- } else {
120
- benchmarks[format] = { elapsed: 'Error', size: 'Error' };
121
- global.gc();
122
- throw `\nBattle #${id} ended: Some tests failed with format "${format}"! Time to train harder.\n\n`;
123
- }
124
- }
125
-
126
- async function runAllTests() {
127
- const depth = 10
128
- let err, i = 1
129
- let tests = [
130
- ['json', 'JSON', { indexes: { id: 'number', name: 'string' }, v8: false, compress: false, compressIndex: false }],
131
- ['v8', 'V8 serialization', { indexes: { id: 'number', name: 'string' }, v8: true, compress: false, compressIndex: false }],
132
- ['json-compressed', 'JSON with Brotli compression', { indexes: { id: 'number', name: 'string' }, v8: false, compress: false, compressIndex: true }],
133
- ['v8-compressed', 'V8 with Deflate compression', { indexes: { id: 'number', name: 'string' }, v8: true, compress: false, compressIndex: true }]
134
- ]
135
- tests = Array(depth).fill(tests).flat()
136
- tests = tests.map(value => ({ value, sort: Math.random() })).sort((a, b) => a.sort - b.sort).map(({ value }) => value)
137
- for(const test of tests) {
138
- await runTests(i++, test[0], test[1], test[2]).catch(e => {
139
- benchmarks[test[1]] = { elapsed: 'Error', size: 'Error' };
140
- console.error(e)
141
- err = e
142
- })
143
- }
144
- const winners = {}
145
- for (const [format, result] of Object.entries(benchmarks)) {
146
- if (result.elapsed !== 'Error' && result.size !== 'Error') {
147
- if (typeof(winners.elapsed) === 'undefined' || result.elapsed < winners.elapsed) {
148
- winners.elapsed = result.elapsed
149
- winners.format = format
150
- }
151
- if (typeof(winners.size) === 'undefined' || result.size < winners.size) {
152
- winners.size = result.size
153
- winners.format = format
154
- }
155
- }
156
- }
157
- for(const format in benchmarks) {
158
- for(const prop of ['elapsed', 'size']) {
159
- if(benchmarks[format][prop] === winners[prop]) {
160
- benchmarks[format][prop] += ' \uD83C\uDFC6'
161
- }
162
- }
163
- }
164
- console.log('Benchmarks results after '+ tests.length +' battles:')
165
- console.table(benchmarks)
166
- // setInterval(() => {}, 1000)
167
- // global.Database = Database
168
- // global.__dirname = __dirname
169
- process.exit(err ? 1 : 0)
170
- }
171
-
172
- // Run the tests
173
- runAllTests().catch(error => console.error('Error during tests:', error));