jexidb 1.0.2

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.
@@ -0,0 +1,186 @@
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, matchAny=false) {
87
+ if (!criteria) throw new Error('No query criteria provided')
88
+ const fields = Object.keys(criteria)
89
+ if (!fields.length) throw new Error('No valid query criteria provided')
90
+ let matchingLines = matchAny ? new Set() : null
91
+ for (const field of fields) {
92
+ if (typeof(this.index.data[field]) == 'undefined') continue
93
+ const criteriaValue = criteria[field]
94
+ let lineNumbersForField = new Set()
95
+ const isNumericField = this.opts.indexes[field] === 'number'
96
+ if (typeof(criteriaValue) === 'object' && !Array.isArray(criteriaValue)) {
97
+ const fieldIndex = this.index.data[field];
98
+ for (const value in fieldIndex) {
99
+ let includeValue = true
100
+ if (isNumericField) {
101
+ const numericValue = parseFloat(value);
102
+ if (!isNaN(numericValue)) {
103
+ if (criteriaValue['>'] !== undefined && numericValue <= criteriaValue['>']) {
104
+ includeValue = false;
105
+ }
106
+ if (criteriaValue['>='] !== undefined && numericValue < criteriaValue['>=']) {
107
+ includeValue = false;
108
+ }
109
+ if (criteriaValue['<'] !== undefined && numericValue >= criteriaValue['<']) {
110
+ includeValue = false;
111
+ }
112
+ if (criteriaValue['<='] !== undefined && numericValue > criteriaValue['<=']) {
113
+ includeValue = false;
114
+ }
115
+ if (criteriaValue['!='] !== undefined) {
116
+ const excludeValues = Array.isArray(criteriaValue['!=']) ? criteriaValue['!='] : [criteriaValue['!=']];
117
+ if (excludeValues.includes(numericValue)) {
118
+ includeValue = false;
119
+ }
120
+ }
121
+ }
122
+ } else {
123
+ if (criteriaValue['contains'] !== undefined && typeof value === 'string') {
124
+ if (!value.includes(criteriaValue['contains'])) {
125
+ includeValue = false;
126
+ }
127
+ }
128
+ if (criteriaValue['regex'] !== undefined && typeof value === 'string') {
129
+ const regex = new RegExp(criteriaValue['regex']);
130
+ if (!regex.test(value)) {
131
+ includeValue = false;
132
+ }
133
+ }
134
+ if (criteriaValue['!='] !== undefined) {
135
+ const excludeValues = Array.isArray(criteriaValue['!=']) ? criteriaValue['!='] : [criteriaValue['!=']];
136
+ if (excludeValues.includes(value)) {
137
+ includeValue = false;
138
+ }
139
+ }
140
+ }
141
+
142
+ if (includeValue) {
143
+ for (const lineNumber of fieldIndex[value]) {
144
+ lineNumbersForField.add(lineNumber);
145
+ }
146
+ }
147
+ }
148
+ } else {
149
+ const values = Array.isArray(criteriaValue) ? criteriaValue : [criteriaValue];
150
+ for (const value of values) {
151
+ if (this.index.data[field][value]) {
152
+ for (const lineNumber of this.index.data[field][value]) {
153
+ lineNumbersForField.add(lineNumber);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ if (matchAny) {
159
+ matchingLines = new Set([...matchingLines, ...lineNumbersForField]);
160
+ } else {
161
+ if (matchingLines === null) {
162
+ matchingLines = lineNumbersForField
163
+ } else {
164
+ matchingLines = new Set([...matchingLines].filter(n => lineNumbersForField.has(n)));
165
+ }
166
+ if (!matchingLines.size) {
167
+ return new Set()
168
+ }
169
+ }
170
+ }
171
+ return matchingLines || new Set();
172
+ }
173
+
174
+ load(index) {
175
+ for(const field in index.data) {
176
+ for(const term in index.data[field]) {
177
+ index.data[field][term] = new Set(index.data[field][term]) // set to array
178
+ }
179
+ }
180
+ this.index = index
181
+ }
182
+
183
+ readColumnIndex(column) {
184
+ return new Set((this.index.data && this.index.data[column]) ? Object.keys(this.index.data[column]) : [])
185
+ }
186
+ }
@@ -0,0 +1,120 @@
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.linebreak = Buffer.from([0x0A])
10
+ this.delimiter = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF])
11
+ this.defaultBuffer = Buffer.alloc(4096)
12
+ this.brotliOptions = {
13
+ params: {
14
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
15
+ }
16
+ }
17
+ }
18
+
19
+ async serialize(data, opts={}) {
20
+ let line
21
+ let header = 0x00 // 1 byte de header
22
+ const useV8 = this.opts.v8 || opts.v8 === true
23
+ const compress = this.opts.compress || opts.compress === true
24
+ if (useV8) {
25
+ header |= 0x02 // set V8
26
+ line = v8.serialize(data)
27
+ } else {
28
+ if(compress) {
29
+ line = Buffer.from(JSON.stringify(data), 'utf-8')
30
+ } else {
31
+ return Buffer.from(JSON.stringify(data) + (opts.linebreak !== false ? '\n' : ''), 'utf-8')
32
+ }
33
+ }
34
+ if (compress) {
35
+ let err
36
+ const buffer = await this.compress(line).catch(e => err = e)
37
+ if(!err) {
38
+ header |= 0x01
39
+ line = buffer
40
+ }
41
+ }
42
+ const totalLength = 1 + line.length + (opts.linebreak !== false ? 1 : 0)
43
+ const result = Buffer.alloc(totalLength)
44
+ result[0] = header
45
+ line.copy(result, 1)
46
+ if (opts.linebreak !== false) {
47
+ result[totalLength - 1] = 0x0A
48
+ }
49
+ return result
50
+ }
51
+
52
+ async deserialize(data, opts={}) {
53
+ let line
54
+ const header = data.readUInt8(0)
55
+ const valid = header === 0x00 || header === 0x01 || header === 0x02 || header === 0x03
56
+ let isCompressed, isV8, decompresssed
57
+ if(valid) {
58
+ isCompressed = (header & 0x01) === 0x01
59
+ isV8 = (header & 0x02) === 0x02
60
+ line = data.subarray(1) // remove byte header
61
+ } else {
62
+ isCompressed = isV8 = false
63
+ line = data
64
+ }
65
+ if (isCompressed) {
66
+ let err
67
+ const buffer = await this.decompress(line).catch(e => err = e)
68
+ if(!err) {
69
+ decompresssed = true
70
+ line = buffer
71
+ }
72
+ }
73
+ if (isV8) {
74
+ try {
75
+ return v8.deserialize(line)
76
+ } catch (e) {
77
+ throw new Error('Failed to deserialize V8 data')
78
+ }
79
+ } else {
80
+ try {
81
+ return JSON.parse(line.toString('utf-8').trim())
82
+ } catch (e) {
83
+ console.error('Failed to deserialize', header, line.toString('utf-8').trim())
84
+ throw new Error('Failed to deserialize JSON data')
85
+ }
86
+ }
87
+ }
88
+
89
+ compress(data) {
90
+ return new Promise((resolve, reject) => {
91
+ zlib.brotliCompress(data, this.brotliOptions, (err, buffer) => {
92
+ if (err) {
93
+ reject(err)
94
+ } else {
95
+ resolve(buffer)
96
+ }
97
+ })
98
+ })
99
+ }
100
+
101
+ decompress(data) {
102
+ return new Promise((resolve, reject) => {
103
+ zlib.brotliDecompress(data, (err, buffer) => {
104
+ if (err) {
105
+ reject(err)
106
+ } else {
107
+ resolve(buffer)
108
+ }
109
+ })
110
+ })
111
+ }
112
+
113
+ async safeDeserialize(json) {
114
+ try {
115
+ return await this.deserialize(json)
116
+ } catch (e) {
117
+ return null
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,21 @@
1
+ export default class Serializer {
2
+
3
+ constructor(opts={}) {
4
+ this.opts = Object.assign({}, opts)
5
+ }
6
+
7
+ async serialize(data, opts={}) {
8
+ return Buffer.from(JSON.stringify(data) + (opts.linebreak !== false ? '\n' : ''), 'utf-8')
9
+ }
10
+
11
+ async deserialize(data, opts={}) {
12
+ const line = data.toString('utf-8')
13
+ try {
14
+ return JSON.parse(line)
15
+ } catch (e) {
16
+ console.error('Failed to deserialize', line)
17
+ throw new Error('Failed to deserialize JSON data')
18
+ }
19
+ }
20
+
21
+ }
Binary file
@@ -0,0 +1,12 @@
1
+ {"id":2,"name":"Sub-Zero","signatureMove":"Ice Ball","powerType":"Cryomancy"}
2
+ {"id":3,"name":"Raiden","signatureMove":"Electric Fly","powerType":"Lightning"}
3
+ {"id":4,"name":"Jax","signatureMove":"Ground Pound","powerType":"Strength"}
4
+ {"id":5,"name":"Sindel","signatureMove":"Sonic Scream","powerType":"Sound"}
5
+ {"id":6,"name":"Ermac","signatureMove":"Telekinesis","powerType":"Telekinesis"}
6
+ {"id":7,"name":"Mileena","signatureMove":"Sai Throw","powerType":"Teleportation"}
7
+ {"id":8,"name":"Kenshi","signatureMove":"Telekinetic Slash","powerType":"Telekinesis"}
8
+ {"id":9,"name":"D'Vorah","signatureMove":"Swarm","powerType":"Insects"}
9
+ {"id":10,"name":"Sonya Blade","signatureMove":"Energy Rings","powerType":"Special Forces Technology"}
10
+ {"id":11,"name":"Kotal Kahn","signatureMove":"Sunstone","powerType":"Osh-Tekk Strength"}
11
+ {"data":{"id":{"2":[0],"3":[1],"4":[2],"5":[3],"6":[4],"7":[5],"8":[6],"9":[7],"10":[8],"11":[9]},"name":{"Sub-Zero":[0],"Raiden":[1],"Jax":[2],"Sindel":[3],"Ermac":[4],"Mileena":[5],"Kenshi":[6],"D'Vorah":[7],"Sonya Blade":[8],"Kotal Kahn":[9]}}}
12
+ [0,78,158,234,310,390,472,559,631,733,822,1070]
Binary file
Binary file
package/test/test.mjs ADDED
@@ -0,0 +1,152 @@
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
+ // Function to clear the test file before each run
54
+ const clearTestFile = () => {
55
+ fs.writeFileSync(testFilePath, '', { encoding: null });
56
+ }
57
+
58
+ console.log('Battle #' + id + ' (' + format + ') is starting...\n');
59
+ clearTestFile(); // Clear the file before starting the tests
60
+ const start = Date.now()
61
+ const db = new Database(testFilePath, opts); // Instantiate the database
62
+
63
+ // 1. Test if the instance was created correctly
64
+ await db.init(); // Call init() right after the instance is created
65
+ console.assert(db.initialized === true, `Test failed: Database didn't initialize. Looks like Raiden needs to give it a shock!`);
66
+
67
+ // 2. Test data insertion with Mortal Kombat characters
68
+ await db.insert({ id: 1, name: character.name, signatureMove: character.signatureMove, powerType: character.powerType });
69
+ await db.insert({ id: 2, name: 'Sub-Zero', signatureMove: 'Ice Ball', powerType: 'Cryomancy' });
70
+ await db.insert({ id: 3, name: 'Raiden', signatureMove: 'Electric Fly', powerType: 'Lightning' });
71
+ await db.insert({ id: 4, name: 'Jax', signatureMove: 'Ground Pound', powerType: 'Strength' });
72
+ await db.insert({ id: 5, name: 'Sindel', signatureMove: 'Sonic Scream', powerType: 'Sound' });
73
+ await db.insert({ id: 6, name: 'Ermac', signatureMove: 'Telekinesis', powerType: 'Telekinesis' });
74
+ await db.insert({ id: 7, name: 'Mileena', signatureMove: 'Sai Throw', powerType: 'Teleportation' });
75
+ await db.insert({ id: 8, name: 'Kenshi', signatureMove: 'Telekinetic Slash', powerType: 'Telekinesis' });
76
+ await db.insert({ id: 9, name: 'D\'Vorah', signatureMove: 'Swarm', powerType: 'Insects' });
77
+ await db.insert({ id: 10, name: 'Sonya Blade', signatureMove: 'Energy Rings', powerType: 'Special Forces Technology' });
78
+ await db.insert({ id: 11, name: 'Kotal Kahn', signatureMove: 'Sunstone', powerType: 'Osh-Tekk Strength' });
79
+
80
+ // "Flawless Victory" if the insertion is successful
81
+ console.log('Round 1 - CREATE: Flawless Victory! All characters inserted successfully.');
82
+
83
+ // 3. Test if the data was inserted correctly
84
+ let results = await db.query({ id: { '<=': 5 } });
85
+ const pass1 = results.length === 5;
86
+ const pass2 = results[0].name === character.name;
87
+ console.assert(pass1, `Round 2 - READ: Test failed: Where is everyone? ${character.missingMessage}`);
88
+ console.assert(pass2, `Round 2 - READ: Test failed: ${character.name} seems to have been teleported out of the database!`);
89
+ if(pass1 && pass2) console.log(`Round 2 - READ: Flawless Victory! All characters inserted successfully, led by ${character.name}.`);
90
+
91
+ // 4. 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
+ // 5. Test data deletion
99
+ await db.delete({ name: character.name + ' Updated' });
100
+
101
+ results = await db.query({ id: { '<=': 2 } });
102
+ const pass5 = results.length === 1;
103
+ const pass6 = results[0].name === 'Sub-Zero';
104
+ console.assert(pass5, `Round 4 - DELETE: Test failed: ${character.deleteMessage}`);
105
+ console.assert(pass6, `Round 4 - DELETE: Test failed: Sub-Zero is nowhere to be seen. Did he freeze the system?`);
106
+ if(pass5 && pass6) console.log(`Round 4 - DELETE: Flawless Victory! ${character.name} has been eliminated successfully.`);
107
+
108
+ // End the battle and log the result
109
+ if(pass1 && pass2 && pass4 && pass5 && pass6) {
110
+ let err, elapsed = Date.now() - start;
111
+ elapsed = elapsed < 1000 ? elapsed + 'ms' : (elapsed / 1000).toFixed(3) + 's';
112
+ const { size } = await fs.promises.stat(testFilePath);
113
+ benchmarks[format] = { elapsed, size };
114
+ console.log(`\nBattle #${id} ended: All tests with format "${format}" ran successfully! Fatality avoided this time.\n\n`);
115
+ } else {
116
+ benchmarks[format] = { elapsed: 'Error', size: 'Error' };
117
+ throw `\nBattle #${id} ended: Some tests failed with format "${format}"! Time to train harder.\n\n`;
118
+ }
119
+ }
120
+
121
+ async function runAllTests() {
122
+ let err
123
+ await runTests(1, 'json', 'JSON', {
124
+ indexes: {id: 'number', name: 'string'},
125
+ v8: false,
126
+ compress: false,
127
+ compressIndex: false
128
+ }).catch(e => err = e)
129
+ await runTests(2, 'v8', 'V8 serialization', {
130
+ indexes: {id: 'number', name: 'string'},
131
+ v8: true,
132
+ compress: false,
133
+ compressIndex: false
134
+ }).catch(e => err = e)
135
+ await runTests(3, 'json-compressed', 'JSON with Brotli compression', {
136
+ indexes: {id: 'number', name: 'string'},
137
+ v8: false,
138
+ compress: false,
139
+ compressIndex: true
140
+ }).catch(e => err = e)
141
+ await runTests(3, 'v8-compressed', 'V8 with Brotli compression', {
142
+ indexes: {id: 'number', name: 'string'},
143
+ v8: true,
144
+ compress: false,
145
+ compressIndex: true
146
+ }).catch(e => err = e)
147
+ console.table(benchmarks)
148
+ process.exit(err ? 1 : 0)
149
+ }
150
+
151
+ // Run the tests
152
+ runAllTests().catch(error => console.error('Error during tests:', error));