jexidb 2.0.0 → 2.0.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/README.md +524 -556
- package/dist/FileHandler.js +1 -1
- package/dist/JSONLDatabase.js +65 -3
- package/dist/index.js +10 -0
- package/package.json +1 -1
- package/src/FileHandler.js +1 -1
- package/src/JSONLDatabase.js +62 -3
- package/src/index.js +10 -0
package/README.md
CHANGED
|
@@ -1,556 +1,524 @@
|
|
|
1
|
-
# JexiDB - Pure JavaScript JSONL Database
|
|
2
|
-
|
|
3
|
-
**JexiDB** is a lightweight, high-performance JSONL (JSON Lines) database for Node.js built in pure JavaScript that provides fast data storage and retrieval with persistent indexing.
|
|
4
|
-
|
|
5
|
-
## 🚀 Features
|
|
6
|
-
|
|
7
|
-
- **JSONL Architecture**: Each database is a single JSONL file for simplicity and portability
|
|
8
|
-
- **Persistent Indexes**: Fast searches with disk-persisted indexes that don't need rebuilding
|
|
9
|
-
- **Point Reading**: Efficient memory usage - only reads necessary data
|
|
10
|
-
- **Rich Query API**: Support for complex queries with operators, sorting, and pagination
|
|
11
|
-
- **Automatic Integrity Validation**: Built-in data integrity checking and repair
|
|
12
|
-
- **Event System**: Real-time notifications for database operations
|
|
13
|
-
- **Legacy Compatibility**: Automatic migration from JexiDB 1.x databases
|
|
14
|
-
- **Pure JavaScript**: No native dependencies, works everywhere, easy to deploy
|
|
15
|
-
|
|
16
|
-
## 📦 Installation
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install jexidb
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## 🚀 Quick Start
|
|
23
|
-
|
|
24
|
-
```javascript
|
|
25
|
-
// import { Database } from 'jexidb'
|
|
26
|
-
const { Database } = require('jexidb');
|
|
27
|
-
|
|
28
|
-
// Create database with indexes (supports both .jdb and .jsonl)
|
|
29
|
-
const db = new Database('./users.jdb', {
|
|
30
|
-
indexes: {
|
|
31
|
-
id: 'number',
|
|
32
|
-
email: 'string',
|
|
33
|
-
age: 'number'
|
|
34
|
-
},
|
|
35
|
-
autoSave: true,
|
|
36
|
-
validateOnInit: true
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Initialize
|
|
40
|
-
await db.init();
|
|
41
|
-
|
|
42
|
-
// Event listeners
|
|
43
|
-
db.on('insert', (record, index) => console.log(`Record inserted at index ${index}`));
|
|
44
|
-
db.on('update', (record, index) => console.log(`Record updated at index ${index}`));
|
|
45
|
-
db.on('save', () => console.log('Changes saved'));
|
|
46
|
-
|
|
47
|
-
// Insert data
|
|
48
|
-
const user = await db.insert({
|
|
49
|
-
id: 1,
|
|
50
|
-
name: 'John Doe',
|
|
51
|
-
email: 'john@example.com',
|
|
52
|
-
age: 30
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Search data (both methods work)
|
|
56
|
-
const john = await db.findOne({ id: 1 });
|
|
57
|
-
const youngUsers = await db.find({ age: { '<': 30 } });
|
|
58
|
-
|
|
59
|
-
// JexiDB 1.x compatible query
|
|
60
|
-
const results = await db.query({ name: 'john doe' }, { caseInsensitive: true });
|
|
61
|
-
|
|
62
|
-
// Update data
|
|
63
|
-
await db.update({ id: 1 }, { age: 31 });
|
|
64
|
-
|
|
65
|
-
// Remove data
|
|
66
|
-
await db.delete({ id: 1 });
|
|
67
|
-
|
|
68
|
-
// Save changes
|
|
69
|
-
await db.save();
|
|
70
|
-
|
|
71
|
-
// Destroy database
|
|
72
|
-
await db.destroy();
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## 📚 API Reference
|
|
76
|
-
|
|
77
|
-
### Constructor
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
const db = new Database(filePath, options);
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**Parameters:**
|
|
84
|
-
- `filePath` (string): Path to the main file (.jdb)
|
|
85
|
-
- `options` (object): Configuration options
|
|
86
|
-
|
|
87
|
-
**Options:**
|
|
88
|
-
```javascript
|
|
89
|
-
{
|
|
90
|
-
indexes: {}, // Indexes for fields
|
|
91
|
-
markDeleted: true, // Mark as deleted instead of physically removing
|
|
92
|
-
autoSave: true, // Automatically save after operations
|
|
93
|
-
validateOnInit: false // Validate integrity on initialization
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Main Methods
|
|
98
|
-
|
|
99
|
-
#### `init()`
|
|
100
|
-
Initializes the database.
|
|
101
|
-
|
|
102
|
-
#### `insert(data)`
|
|
103
|
-
Inserts a record.
|
|
104
|
-
|
|
105
|
-
#### `insertMany(dataArray)`
|
|
106
|
-
Inserts multiple records.
|
|
107
|
-
|
|
108
|
-
#### `find(criteria, options)` / `query(criteria, options)`
|
|
109
|
-
Searches records with optional criteria. Both methods work identically.
|
|
110
|
-
|
|
111
|
-
**Supported operators:**
|
|
112
|
-
```javascript
|
|
113
|
-
// Comparison
|
|
114
|
-
{ age: { '>': 25 } }
|
|
115
|
-
{ age: { '>=': 25 } }
|
|
116
|
-
{ age: { '<': 30 } }
|
|
117
|
-
{ age: { '<=': 30 } }
|
|
118
|
-
{ age: { '!=': 25 } }
|
|
119
|
-
|
|
120
|
-
// Arrays
|
|
121
|
-
{ tags: { in: ['developer', 'admin'] } }
|
|
122
|
-
{ tags: { nin: ['designer'] } }
|
|
123
|
-
|
|
124
|
-
// Strings
|
|
125
|
-
{ name: { regex: 'john' } }
|
|
126
|
-
{ name: { contains: 'john' } }
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Options:**
|
|
130
|
-
```javascript
|
|
131
|
-
{
|
|
132
|
-
limit: 10, // Limit results
|
|
133
|
-
skip: 5, // Skip records
|
|
134
|
-
sort: { age: 1 }, // Sorting (1 = ascending, -1 = descending)
|
|
135
|
-
caseInsensitive: false, // Case insensitive matching (query() only)
|
|
136
|
-
matchAny: false // OR instead of AND
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**JexiDB 1.x Compatibility:**
|
|
141
|
-
```javascript
|
|
142
|
-
// Both work identically
|
|
143
|
-
const results1 = await db.find({ name: 'John' });
|
|
144
|
-
const results2 = await db.query({ name: 'John' });
|
|
145
|
-
|
|
146
|
-
// Case insensitive query (JexiDB 1.x style)
|
|
147
|
-
const results = await db.query({ name: 'john' }, { caseInsensitive: true });
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
#### `findOne(criteria, options)`
|
|
151
|
-
Searches for one record.
|
|
152
|
-
|
|
153
|
-
#### `update(criteria, updateData, options)`
|
|
154
|
-
Updates records.
|
|
155
|
-
|
|
156
|
-
#### `delete(criteria, options)`
|
|
157
|
-
Removes records.
|
|
158
|
-
|
|
159
|
-
**Delete options:**
|
|
160
|
-
```javascript
|
|
161
|
-
{
|
|
162
|
-
physical: false, // Physically remove instead of marking as deleted
|
|
163
|
-
limit: 1 // Limit number of records to delete
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
#### `count(criteria, options)`
|
|
168
|
-
Counts records.
|
|
169
|
-
|
|
170
|
-
#### `save()`
|
|
171
|
-
Saves pending changes.
|
|
172
|
-
|
|
173
|
-
#### `destroy()`
|
|
174
|
-
Destroys the database.
|
|
175
|
-
|
|
176
|
-
#### `validateIntegrity(options)`
|
|
177
|
-
Validates database integrity.
|
|
178
|
-
|
|
179
|
-
#### `rebuildIndexes(options)`
|
|
180
|
-
Rebuilds indexes.
|
|
181
|
-
|
|
182
|
-
#### `getStats()`
|
|
183
|
-
Gets detailed statistics.
|
|
184
|
-
|
|
185
|
-
### `walk()` Iterator
|
|
186
|
-
|
|
187
|
-
For traversing large volumes of data:
|
|
188
|
-
|
|
189
|
-
```javascript
|
|
190
|
-
// Traverse all records
|
|
191
|
-
for await (const record of db.walk()) {
|
|
192
|
-
console.log(record.name);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// With options
|
|
196
|
-
for await (const record of db.walk({
|
|
197
|
-
limit: 100,
|
|
198
|
-
skip: 50,
|
|
199
|
-
includeDeleted: false
|
|
200
|
-
})) {
|
|
201
|
-
console.log(record.name);
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Properties
|
|
206
|
-
|
|
207
|
-
#### `length`
|
|
208
|
-
Total number of records.
|
|
209
|
-
|
|
210
|
-
#### `indexStats`
|
|
211
|
-
Index statistics.
|
|
212
|
-
|
|
213
|
-
### Events
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
db.on('init', () => console.log('Database initialized'));
|
|
217
|
-
db.on('insert', (record, index) => console.log('Record inserted'));
|
|
218
|
-
db.on('update', (record, index) => console.log('Record updated'));
|
|
219
|
-
db.on('delete', (record, index) => console.log('Record deleted'));
|
|
220
|
-
db.on('before-save', () => console.log('Before save'));
|
|
221
|
-
db.on('save', () => console.log('Save completed'));
|
|
222
|
-
db.on('destroy', () => console.log('Database destroyed'));
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## 📁 File Structure
|
|
226
|
-
|
|
227
|
-
For each database, 2 files are created:
|
|
228
|
-
|
|
229
|
-
```
|
|
230
|
-
users.jdb # Data (JSON Lines format)
|
|
231
|
-
users.idx.jdb # Compressed persistent indexes
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### 🔄 Legacy Compatibility
|
|
235
|
-
|
|
236
|
-
JexiDB automatically detects and migrates JexiDB 1.x files:
|
|
237
|
-
|
|
238
|
-
**Legacy Format (JexiDB 1.x):**
|
|
239
|
-
```
|
|
240
|
-
users.jsonl # Data + indexes + offsets in single file
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**New Format (JexiDB):**
|
|
244
|
-
```
|
|
245
|
-
users.jdb # Data + offsets
|
|
246
|
-
users.idx.jdb # Compressed indexes
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
### 🚀 Persistent Indexes
|
|
252
|
-
|
|
253
|
-
JexiDB implements **persistent indexes** that are saved to disk:
|
|
254
|
-
|
|
255
|
-
**Benefits:**
|
|
256
|
-
- **Fast startup**: No need to read all data to rebuild indexes
|
|
257
|
-
- **Scalable**: Works well with large databases (100k+ records)
|
|
258
|
-
- **Consistent**: Indexes synchronized with data
|
|
259
|
-
- **Portable**: Only 2 files to manage
|
|
260
|
-
- **Compressed**: Indexes compressed using gzip
|
|
261
|
-
|
|
262
|
-
**🔧 How it works:**
|
|
263
|
-
1. **First open**: Indexes are built by reading data
|
|
264
|
-
2. **Save**: Indexes are persisted and compressed to `users.idx.jdb`
|
|
265
|
-
3. **Reopen**: Indexes are loaded instantly from disk
|
|
266
|
-
4. **Fallback**: If index file is corrupted, rebuilds automatically
|
|
267
|
-
|
|
268
|
-
### JSONL Format
|
|
269
|
-
|
|
270
|
-
Each line is a valid JSON record:
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{"id":1,"name":"John","email":"john@example.com","_created":"2024-12-19T10:00:00.000Z","_updated":"2024-12-19T10:00:00.000Z"}
|
|
274
|
-
{"id":2,"name":"Jane","email":"jane@example.com","_created":"2024-12-19T10:01:00.000Z","_updated":"2024-12-19T10:01:00.000Z"}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
## 🔍 Advanced Examples
|
|
278
|
-
|
|
279
|
-
### Complex Search
|
|
280
|
-
|
|
281
|
-
```javascript
|
|
282
|
-
// Young users from New York who are developers
|
|
283
|
-
const users = await db.find({
|
|
284
|
-
age: { '<': 30 },
|
|
285
|
-
'profile.city': 'New York',
|
|
286
|
-
tags: { in: ['developer'] }
|
|
287
|
-
}, {
|
|
288
|
-
sort: { age: 1 },
|
|
289
|
-
limit: 10
|
|
290
|
-
});
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Batch Update
|
|
294
|
-
|
|
295
|
-
```javascript
|
|
296
|
-
// Update age of all users from a city
|
|
297
|
-
const updated = await db.update(
|
|
298
|
-
{ 'profile.city': 'New York' },
|
|
299
|
-
{ 'profile.country': 'USA' }
|
|
300
|
-
);
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Integrity Validation
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
// Validate integrity with details
|
|
307
|
-
const integrity = await db.validateIntegrity({
|
|
308
|
-
checkData: true,
|
|
309
|
-
checkIndexes: true,
|
|
310
|
-
checkOffsets: true,
|
|
311
|
-
verbose: true
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
if (!integrity.isValid) {
|
|
315
|
-
console.log('Errors:', integrity.errors);
|
|
316
|
-
console.log('Warnings:', integrity.warnings);
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Detailed Statistics
|
|
321
|
-
|
|
322
|
-
```javascript
|
|
323
|
-
const stats = await db.getStats();
|
|
324
|
-
console.log('File size:', stats.file.size);
|
|
325
|
-
console.log('Total records:', stats.summary.totalRecords);
|
|
326
|
-
console.log('Indexes:', stats.indexes.indexCount);
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
## 🧪 Tests
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
npm test
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
**Automatic Cleanup**: The test script automatically removes all test files after execution to keep the project directory clean.
|
|
336
|
-
|
|
337
|
-
**Manual Cleanup**: If you need to clean up test files manually:
|
|
338
|
-
```bash
|
|
339
|
-
npm run test:clean
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Available Test Scripts**:
|
|
343
|
-
- `npm test` - Run all tests with automatic cleanup
|
|
344
|
-
- `npm run test:watch` - Run tests in watch mode
|
|
345
|
-
- `npm run test:clean` - Clean up test files manually
|
|
346
|
-
- `npm run test:optimized` - Run optimized performance tests
|
|
347
|
-
- `npm run test:parallel` - Run tests in parallel
|
|
348
|
-
- `npm run test:fast` - Run fast tests without isolation
|
|
349
|
-
|
|
350
|
-
## 📈 Performance
|
|
351
|
-
|
|
352
|
-
### JSONL Features
|
|
353
|
-
|
|
354
|
-
- **Point reading**: Only reads necessary lines
|
|
355
|
-
- **In-memory indexes**: Fast search by indexed fields
|
|
356
|
-
- **No complete parsing**: Doesn't load entire file into memory
|
|
357
|
-
- **Large volume support**: Scales with millions of records
|
|
358
|
-
|
|
359
|
-
### Comparison: JexiDB vs 1.x
|
|
360
|
-
|
|
361
|
-
| Feature | JexiDB | JexiDB 1.x |
|
|
362
|
-
|---------|---------------|------------|
|
|
363
|
-
| Safe truncation | ✅ | ❌ |
|
|
364
|
-
| Consistent offsets | ✅ | ❌ |
|
|
365
|
-
| Integrity validation | ✅ | ❌ |
|
|
366
|
-
| Isolated tests | ✅ | ❌ |
|
|
367
|
-
| No V8 dependency | ✅ | ❌ |
|
|
368
|
-
| Similar API | ✅ | ✅ |
|
|
369
|
-
|
|
370
|
-
## 🔧 Utilities
|
|
371
|
-
|
|
372
|
-
```javascript
|
|
373
|
-
const { utils } = require('jexidb');
|
|
374
|
-
|
|
375
|
-
// Validate JSONL file
|
|
376
|
-
const validation = await utils.validateJSONLFile('./data.jsonl');
|
|
377
|
-
|
|
378
|
-
// Convert JSON to JSONL (basic)
|
|
379
|
-
await utils.convertJSONToJSONL('./data.json', './data.jsonl');
|
|
380
|
-
|
|
381
|
-
// Convert JSONL to JSON
|
|
382
|
-
await utils.convertJSONLToJSON('./data.jsonl', './data.json');
|
|
383
|
-
|
|
384
|
-
// Create JexiDB database with automatic indexes
|
|
385
|
-
const result = await utils.createDatabaseFromJSON('./users.json', './users.jsonl', {
|
|
386
|
-
autoDetectIndexes: true,
|
|
387
|
-
autoIndexFields: ['id', 'email', 'name', 'username']
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// Analyze JSON and suggest optimal indexes
|
|
391
|
-
const analysis = await utils.analyzeJSONForIndexes('./users.json', 100);
|
|
392
|
-
console.log('Recommended indexes:', analysis.suggestions.recommended);
|
|
393
|
-
|
|
394
|
-
// Migrate from JexiDB 1.x to JexiDB
|
|
395
|
-
await utils.migrateFromJexiDB('./jexidb-v1-database', './users.jsonl');
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### 🔍 **How Utilities Work**
|
|
399
|
-
|
|
400
|
-
#### **1. Basic Conversion (No Indexes)**
|
|
401
|
-
```javascript
|
|
402
|
-
// Only converts format - DOES NOT add indexes
|
|
403
|
-
await utils.convertJSONToJSONL('./data.json', './data.jsonl');
|
|
404
|
-
```
|
|
405
|
-
- ✅ Converts JSON to JSONL
|
|
406
|
-
- ❌ **DOES NOT create indexes**
|
|
407
|
-
- ❌ **DOES NOT create JexiDB database**
|
|
408
|
-
- ✅ Pure JSONL file
|
|
409
|
-
|
|
410
|
-
#### **2. Database Creation with Automatic Indexes**
|
|
411
|
-
```javascript
|
|
412
|
-
// Create complete JexiDB database with indexes
|
|
413
|
-
const result = await utils.createDatabaseFromJSON('./users.json', './users.jsonl', {
|
|
414
|
-
autoDetectIndexes: true,
|
|
415
|
-
autoIndexFields: ['id', 'email', 'name']
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
console.log(result);
|
|
419
|
-
// {
|
|
420
|
-
// success: true,
|
|
421
|
-
// recordCount: 1000,
|
|
422
|
-
// indexes: ['id', 'email', 'name'],
|
|
423
|
-
// dbPath: './users.jsonl'
|
|
424
|
-
// }
|
|
425
|
-
```
|
|
426
|
-
- ✅ Converts JSON to JSONL
|
|
427
|
-
- ✅ **Creates indexes automatically**
|
|
428
|
-
- ✅ **Creates complete JexiDB database**
|
|
429
|
-
- ✅ File ready for use
|
|
430
|
-
|
|
431
|
-
#### **3. Intelligent Index Analysis**
|
|
432
|
-
```javascript
|
|
433
|
-
// Analyze data and suggest optimal indexes
|
|
434
|
-
const analysis = await utils.analyzeJSONForIndexes('./users.json');
|
|
435
|
-
|
|
436
|
-
console.log('Recommended:', analysis.suggestions.recommended);
|
|
437
|
-
// [
|
|
438
|
-
// { field: 'id', type: 'number', coverage: 100, uniqueness: 100 },
|
|
439
|
-
// { field: 'email', type: 'string', coverage: 95, uniqueness: 98 }
|
|
440
|
-
// ]
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
## 🔄 Migration from JexiDB 1.x
|
|
446
|
-
|
|
447
|
-
### Seamless Migration
|
|
448
|
-
|
|
449
|
-
JexiDB is **fully backward compatible** with JexiDB 1.x! You can use the same API:
|
|
450
|
-
|
|
451
|
-
```javascript
|
|
452
|
-
// JexiDB 1.x code works unchanged in JexiDB
|
|
453
|
-
import { Database } from 'jexidb';
|
|
454
|
-
|
|
455
|
-
const db = new Database('./database.jdb', {
|
|
456
|
-
indexes: { id: 'number', name: 'string' }
|
|
457
|
-
});
|
|
458
|
-
await db.init();
|
|
459
|
-
|
|
460
|
-
// All JexiDB 1.x methods work:
|
|
461
|
-
await db.insert({ id: 1, name: 'John Doe' });
|
|
462
|
-
const results = await db.query({ name: 'John Doe' }, { caseInsensitive: true });
|
|
463
|
-
await db.update({ id: 1 }, { name: 'John Smith' });
|
|
464
|
-
await db.delete({ id: 1 });
|
|
465
|
-
await db.save();
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### File Format Support
|
|
469
|
-
|
|
470
|
-
JexiDB supports both file formats:
|
|
471
|
-
- **`.jdb`** (preferred) - JexiDB's branded extension
|
|
472
|
-
- **`.jsonl`** (standard) - JSON Lines format
|
|
473
|
-
|
|
474
|
-
```javascript
|
|
475
|
-
// Both work identically:
|
|
476
|
-
const db1 = new Database('./users.jdb', { indexes: { id: 'number' } });
|
|
477
|
-
const db2 = new Database('./users.jsonl', { indexes: { id: 'number' } });
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
### Key Improvements
|
|
481
|
-
|
|
482
|
-
| Feature | JexiDB 1.x | JexiDB |
|
|
483
|
-
|---------|------------|--------------|
|
|
484
|
-
| **API Compatibility** | Original | ✅ **100% Backward Compatible** |
|
|
485
|
-
| **Query Methods** | `db.query()` | ✅ `db.query()` + `db.find()` |
|
|
486
|
-
| **File Format** | `.jdb` (proprietary) | ✅ `.jdb` + `.jsonl` support |
|
|
487
|
-
| **Performance** | Basic | ✅ **10-100x faster** |
|
|
488
|
-
| **Memory Usage** | Higher | ✅ **25% less memory** |
|
|
489
|
-
| **Data Integrity** | Basic | ✅ **Advanced validation** |
|
|
490
|
-
|
|
491
|
-
## 📝 Changelog
|
|
492
|
-
|
|
493
|
-
See [CHANGELOG.md](CHANGELOG.md) for complete change history.
|
|
494
|
-
|
|
495
|
-
## 🤝 Contributing
|
|
496
|
-
|
|
497
|
-
1. Fork the project
|
|
498
|
-
2. Create a branch for your feature
|
|
499
|
-
3. Commit your changes
|
|
500
|
-
4. Push to the branch
|
|
501
|
-
5. Open a Pull Request
|
|
502
|
-
|
|
503
|
-
## 📄 License
|
|
504
|
-
|
|
505
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
|
|
509
|
-
## 🎯 About JexiDB
|
|
510
|
-
|
|
511
|
-
JexiDB maintains the original JexiDB philosophy while fixing bugs and implementing a more robust architecture.
|
|
512
|
-
|
|
513
|
-
###
|
|
514
|
-
|
|
515
|
-
**JexiDB**
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
### 🚀 Performance
|
|
527
|
-
|
|
528
|
-
**JexiDB** performance compared to version 1.x:
|
|
529
|
-
|
|
530
|
-
- **Find operations**: 103x faster
|
|
531
|
-
- **Update operations**: 26x faster
|
|
532
|
-
- **Insert operations**: 6-11x faster
|
|
533
|
-
- **Memory usage**: 25% less memory
|
|
534
|
-
|
|
535
|
-
### 🔧 Simple Migration
|
|
536
|
-
|
|
537
|
-
Migration from JexiDB 1.x to JexiDB is simple:
|
|
538
|
-
|
|
539
|
-
```javascript
|
|
540
|
-
// Before (JexiDB 1.x)
|
|
541
|
-
const { Database } = require('jexidb');
|
|
542
|
-
const db = new Database('./database.jdb', {
|
|
543
|
-
indexes: { id: 'number', name: 'string' }
|
|
544
|
-
});
|
|
545
|
-
await db.init();
|
|
546
|
-
await db.insert({ id: 1, name: 'John Doe' });
|
|
547
|
-
await db.save();
|
|
548
|
-
|
|
549
|
-
// Now (JexiDB)
|
|
550
|
-
import { Database } from 'jexidb';
|
|
551
|
-
const db = new Database('./users.jdb', { indexes: { id: 'number' } });
|
|
552
|
-
await db.init();
|
|
553
|
-
await db.insert({ id: 1, name: 'John Doe' });
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
**We maintain compatibility with the JexiDB 1.x API with improved performance and reliability.**
|
|
1
|
+
# JexiDB - Pure JavaScript JSONL Database
|
|
2
|
+
|
|
3
|
+
**JexiDB** is a lightweight, high-performance JSONL (JSON Lines) database for Node.js built in pure JavaScript that provides fast data storage and retrieval with persistent indexing.
|
|
4
|
+
|
|
5
|
+
## 🚀 Features
|
|
6
|
+
|
|
7
|
+
- **JSONL Architecture**: Each database is a single JSONL file for simplicity and portability
|
|
8
|
+
- **Persistent Indexes**: Fast searches with disk-persisted indexes that don't need rebuilding
|
|
9
|
+
- **Point Reading**: Efficient memory usage - only reads necessary data
|
|
10
|
+
- **Rich Query API**: Support for complex queries with operators, sorting, and pagination
|
|
11
|
+
- **Automatic Integrity Validation**: Built-in data integrity checking and repair
|
|
12
|
+
- **Event System**: Real-time notifications for database operations
|
|
13
|
+
- **Legacy Compatibility**: Automatic migration from JexiDB 1.x databases
|
|
14
|
+
- **Pure JavaScript**: No native dependencies, works everywhere, easy to deploy
|
|
15
|
+
|
|
16
|
+
## 📦 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install jexidb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 🚀 Quick Start
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
// import { Database } from 'jexidb'
|
|
26
|
+
const { Database } = require('jexidb');
|
|
27
|
+
|
|
28
|
+
// Create database with indexes (supports both .jdb and .jsonl)
|
|
29
|
+
const db = new Database('./users.jdb', {
|
|
30
|
+
indexes: {
|
|
31
|
+
id: 'number',
|
|
32
|
+
email: 'string',
|
|
33
|
+
age: 'number'
|
|
34
|
+
},
|
|
35
|
+
autoSave: true,
|
|
36
|
+
validateOnInit: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Initialize
|
|
40
|
+
await db.init();
|
|
41
|
+
|
|
42
|
+
// Event listeners
|
|
43
|
+
db.on('insert', (record, index) => console.log(`Record inserted at index ${index}`));
|
|
44
|
+
db.on('update', (record, index) => console.log(`Record updated at index ${index}`));
|
|
45
|
+
db.on('save', () => console.log('Changes saved'));
|
|
46
|
+
|
|
47
|
+
// Insert data
|
|
48
|
+
const user = await db.insert({
|
|
49
|
+
id: 1,
|
|
50
|
+
name: 'John Doe',
|
|
51
|
+
email: 'john@example.com',
|
|
52
|
+
age: 30
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Search data (both methods work)
|
|
56
|
+
const john = await db.findOne({ id: 1 });
|
|
57
|
+
const youngUsers = await db.find({ age: { '<': 30 } });
|
|
58
|
+
|
|
59
|
+
// JexiDB 1.x compatible query
|
|
60
|
+
const results = await db.query({ name: 'john doe' }, { caseInsensitive: true });
|
|
61
|
+
|
|
62
|
+
// Update data
|
|
63
|
+
await db.update({ id: 1 }, { age: 31 });
|
|
64
|
+
|
|
65
|
+
// Remove data
|
|
66
|
+
await db.delete({ id: 1 });
|
|
67
|
+
|
|
68
|
+
// Save changes
|
|
69
|
+
await db.save();
|
|
70
|
+
|
|
71
|
+
// Destroy database
|
|
72
|
+
await db.destroy();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 📚 API Reference
|
|
76
|
+
|
|
77
|
+
### Constructor
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const db = new Database(filePath, options);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Parameters:**
|
|
84
|
+
- `filePath` (string): Path to the main file (.jdb)
|
|
85
|
+
- `options` (object): Configuration options
|
|
86
|
+
|
|
87
|
+
**Options:**
|
|
88
|
+
```javascript
|
|
89
|
+
{
|
|
90
|
+
indexes: {}, // Indexes for fields
|
|
91
|
+
markDeleted: true, // Mark as deleted instead of physically removing
|
|
92
|
+
autoSave: true, // Automatically save after operations
|
|
93
|
+
validateOnInit: false // Validate integrity on initialization
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Main Methods
|
|
98
|
+
|
|
99
|
+
#### `init()`
|
|
100
|
+
Initializes the database.
|
|
101
|
+
|
|
102
|
+
#### `insert(data)`
|
|
103
|
+
Inserts a record.
|
|
104
|
+
|
|
105
|
+
#### `insertMany(dataArray)`
|
|
106
|
+
Inserts multiple records.
|
|
107
|
+
|
|
108
|
+
#### `find(criteria, options)` / `query(criteria, options)`
|
|
109
|
+
Searches records with optional criteria. Both methods work identically.
|
|
110
|
+
|
|
111
|
+
**Supported operators:**
|
|
112
|
+
```javascript
|
|
113
|
+
// Comparison
|
|
114
|
+
{ age: { '>': 25 } }
|
|
115
|
+
{ age: { '>=': 25 } }
|
|
116
|
+
{ age: { '<': 30 } }
|
|
117
|
+
{ age: { '<=': 30 } }
|
|
118
|
+
{ age: { '!=': 25 } }
|
|
119
|
+
|
|
120
|
+
// Arrays
|
|
121
|
+
{ tags: { in: ['developer', 'admin'] } }
|
|
122
|
+
{ tags: { nin: ['designer'] } }
|
|
123
|
+
|
|
124
|
+
// Strings
|
|
125
|
+
{ name: { regex: 'john' } }
|
|
126
|
+
{ name: { contains: 'john' } }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Options:**
|
|
130
|
+
```javascript
|
|
131
|
+
{
|
|
132
|
+
limit: 10, // Limit results
|
|
133
|
+
skip: 5, // Skip records
|
|
134
|
+
sort: { age: 1 }, // Sorting (1 = ascending, -1 = descending)
|
|
135
|
+
caseInsensitive: false, // Case insensitive matching (query() only)
|
|
136
|
+
matchAny: false // OR instead of AND
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**JexiDB 1.x Compatibility:**
|
|
141
|
+
```javascript
|
|
142
|
+
// Both work identically
|
|
143
|
+
const results1 = await db.find({ name: 'John' });
|
|
144
|
+
const results2 = await db.query({ name: 'John' });
|
|
145
|
+
|
|
146
|
+
// Case insensitive query (JexiDB 1.x style)
|
|
147
|
+
const results = await db.query({ name: 'john' }, { caseInsensitive: true });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `findOne(criteria, options)`
|
|
151
|
+
Searches for one record.
|
|
152
|
+
|
|
153
|
+
#### `update(criteria, updateData, options)`
|
|
154
|
+
Updates records.
|
|
155
|
+
|
|
156
|
+
#### `delete(criteria, options)`
|
|
157
|
+
Removes records.
|
|
158
|
+
|
|
159
|
+
**Delete options:**
|
|
160
|
+
```javascript
|
|
161
|
+
{
|
|
162
|
+
physical: false, // Physically remove instead of marking as deleted
|
|
163
|
+
limit: 1 // Limit number of records to delete
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### `count(criteria, options)`
|
|
168
|
+
Counts records.
|
|
169
|
+
|
|
170
|
+
#### `save()`
|
|
171
|
+
Saves pending changes.
|
|
172
|
+
|
|
173
|
+
#### `destroy()`
|
|
174
|
+
Destroys the database.
|
|
175
|
+
|
|
176
|
+
#### `validateIntegrity(options)`
|
|
177
|
+
Validates database integrity.
|
|
178
|
+
|
|
179
|
+
#### `rebuildIndexes(options)`
|
|
180
|
+
Rebuilds indexes.
|
|
181
|
+
|
|
182
|
+
#### `getStats()`
|
|
183
|
+
Gets detailed statistics.
|
|
184
|
+
|
|
185
|
+
### `walk()` Iterator
|
|
186
|
+
|
|
187
|
+
For traversing large volumes of data:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Traverse all records
|
|
191
|
+
for await (const record of db.walk()) {
|
|
192
|
+
console.log(record.name);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// With options
|
|
196
|
+
for await (const record of db.walk({
|
|
197
|
+
limit: 100,
|
|
198
|
+
skip: 50,
|
|
199
|
+
includeDeleted: false
|
|
200
|
+
})) {
|
|
201
|
+
console.log(record.name);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Properties
|
|
206
|
+
|
|
207
|
+
#### `length`
|
|
208
|
+
Total number of records.
|
|
209
|
+
|
|
210
|
+
#### `indexStats`
|
|
211
|
+
Index statistics.
|
|
212
|
+
|
|
213
|
+
### Events
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
db.on('init', () => console.log('Database initialized'));
|
|
217
|
+
db.on('insert', (record, index) => console.log('Record inserted'));
|
|
218
|
+
db.on('update', (record, index) => console.log('Record updated'));
|
|
219
|
+
db.on('delete', (record, index) => console.log('Record deleted'));
|
|
220
|
+
db.on('before-save', () => console.log('Before save'));
|
|
221
|
+
db.on('save', () => console.log('Save completed'));
|
|
222
|
+
db.on('destroy', () => console.log('Database destroyed'));
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 📁 File Structure
|
|
226
|
+
|
|
227
|
+
For each database, 2 files are created:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
users.jdb # Data (JSON Lines format)
|
|
231
|
+
users.idx.jdb # Compressed persistent indexes
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 🔄 Legacy Compatibility
|
|
235
|
+
|
|
236
|
+
JexiDB automatically detects and migrates JexiDB 1.x files:
|
|
237
|
+
|
|
238
|
+
**Legacy Format (JexiDB 1.x):**
|
|
239
|
+
```
|
|
240
|
+
users.jsonl # Data + indexes + offsets in single file
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**New Format (JexiDB):**
|
|
244
|
+
```
|
|
245
|
+
users.jdb # Data + offsets
|
|
246
|
+
users.idx.jdb # Compressed indexes
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
### 🚀 Persistent Indexes
|
|
252
|
+
|
|
253
|
+
JexiDB implements **persistent indexes** that are saved to disk:
|
|
254
|
+
|
|
255
|
+
**Benefits:**
|
|
256
|
+
- **Fast startup**: No need to read all data to rebuild indexes
|
|
257
|
+
- **Scalable**: Works well with large databases (100k+ records)
|
|
258
|
+
- **Consistent**: Indexes synchronized with data
|
|
259
|
+
- **Portable**: Only 2 files to manage
|
|
260
|
+
- **Compressed**: Indexes compressed using gzip
|
|
261
|
+
|
|
262
|
+
**🔧 How it works:**
|
|
263
|
+
1. **First open**: Indexes are built by reading data
|
|
264
|
+
2. **Save**: Indexes are persisted and compressed to `users.idx.jdb`
|
|
265
|
+
3. **Reopen**: Indexes are loaded instantly from disk
|
|
266
|
+
4. **Fallback**: If index file is corrupted, rebuilds automatically
|
|
267
|
+
|
|
268
|
+
### JSONL Format
|
|
269
|
+
|
|
270
|
+
Each line is a valid JSON record:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{"id":1,"name":"John","email":"john@example.com","_created":"2024-12-19T10:00:00.000Z","_updated":"2024-12-19T10:00:00.000Z"}
|
|
274
|
+
{"id":2,"name":"Jane","email":"jane@example.com","_created":"2024-12-19T10:01:00.000Z","_updated":"2024-12-19T10:01:00.000Z"}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 🔍 Advanced Examples
|
|
278
|
+
|
|
279
|
+
### Complex Search
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// Young users from New York who are developers
|
|
283
|
+
const users = await db.find({
|
|
284
|
+
age: { '<': 30 },
|
|
285
|
+
'profile.city': 'New York',
|
|
286
|
+
tags: { in: ['developer'] }
|
|
287
|
+
}, {
|
|
288
|
+
sort: { age: 1 },
|
|
289
|
+
limit: 10
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Batch Update
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
// Update age of all users from a city
|
|
297
|
+
const updated = await db.update(
|
|
298
|
+
{ 'profile.city': 'New York' },
|
|
299
|
+
{ 'profile.country': 'USA' }
|
|
300
|
+
);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Integrity Validation
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
// Validate integrity with details
|
|
307
|
+
const integrity = await db.validateIntegrity({
|
|
308
|
+
checkData: true,
|
|
309
|
+
checkIndexes: true,
|
|
310
|
+
checkOffsets: true,
|
|
311
|
+
verbose: true
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!integrity.isValid) {
|
|
315
|
+
console.log('Errors:', integrity.errors);
|
|
316
|
+
console.log('Warnings:', integrity.warnings);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Detailed Statistics
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
const stats = await db.getStats();
|
|
324
|
+
console.log('File size:', stats.file.size);
|
|
325
|
+
console.log('Total records:', stats.summary.totalRecords);
|
|
326
|
+
console.log('Indexes:', stats.indexes.indexCount);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## 🧪 Tests
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npm test
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Automatic Cleanup**: The test script automatically removes all test files after execution to keep the project directory clean.
|
|
336
|
+
|
|
337
|
+
**Manual Cleanup**: If you need to clean up test files manually:
|
|
338
|
+
```bash
|
|
339
|
+
npm run test:clean
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Available Test Scripts**:
|
|
343
|
+
- `npm test` - Run all tests with automatic cleanup
|
|
344
|
+
- `npm run test:watch` - Run tests in watch mode
|
|
345
|
+
- `npm run test:clean` - Clean up test files manually
|
|
346
|
+
- `npm run test:optimized` - Run optimized performance tests
|
|
347
|
+
- `npm run test:parallel` - Run tests in parallel
|
|
348
|
+
- `npm run test:fast` - Run fast tests without isolation
|
|
349
|
+
|
|
350
|
+
## 📈 Performance
|
|
351
|
+
|
|
352
|
+
### JSONL Features
|
|
353
|
+
|
|
354
|
+
- **Point reading**: Only reads necessary lines
|
|
355
|
+
- **In-memory indexes**: Fast search by indexed fields
|
|
356
|
+
- **No complete parsing**: Doesn't load entire file into memory
|
|
357
|
+
- **Large volume support**: Scales with millions of records
|
|
358
|
+
|
|
359
|
+
### Comparison: JexiDB vs 1.x
|
|
360
|
+
|
|
361
|
+
| Feature | JexiDB | JexiDB 1.x |
|
|
362
|
+
|---------|---------------|------------|
|
|
363
|
+
| Safe truncation | ✅ | ❌ |
|
|
364
|
+
| Consistent offsets | ✅ | ❌ |
|
|
365
|
+
| Integrity validation | ✅ | ❌ |
|
|
366
|
+
| Isolated tests | ✅ | ❌ |
|
|
367
|
+
| No V8 dependency | ✅ | ❌ |
|
|
368
|
+
| Similar API | ✅ | ✅ |
|
|
369
|
+
|
|
370
|
+
## 🔧 Utilities
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
const { utils } = require('jexidb');
|
|
374
|
+
|
|
375
|
+
// Validate JSONL file
|
|
376
|
+
const validation = await utils.validateJSONLFile('./data.jsonl');
|
|
377
|
+
|
|
378
|
+
// Convert JSON to JSONL (basic)
|
|
379
|
+
await utils.convertJSONToJSONL('./data.json', './data.jsonl');
|
|
380
|
+
|
|
381
|
+
// Convert JSONL to JSON
|
|
382
|
+
await utils.convertJSONLToJSON('./data.jsonl', './data.json');
|
|
383
|
+
|
|
384
|
+
// Create JexiDB database with automatic indexes
|
|
385
|
+
const result = await utils.createDatabaseFromJSON('./users.json', './users.jsonl', {
|
|
386
|
+
autoDetectIndexes: true,
|
|
387
|
+
autoIndexFields: ['id', 'email', 'name', 'username']
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Analyze JSON and suggest optimal indexes
|
|
391
|
+
const analysis = await utils.analyzeJSONForIndexes('./users.json', 100);
|
|
392
|
+
console.log('Recommended indexes:', analysis.suggestions.recommended);
|
|
393
|
+
|
|
394
|
+
// Migrate from JexiDB 1.x to JexiDB
|
|
395
|
+
await utils.migrateFromJexiDB('./jexidb-v1-database', './users.jsonl');
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 🔍 **How Utilities Work**
|
|
399
|
+
|
|
400
|
+
#### **1. Basic Conversion (No Indexes)**
|
|
401
|
+
```javascript
|
|
402
|
+
// Only converts format - DOES NOT add indexes
|
|
403
|
+
await utils.convertJSONToJSONL('./data.json', './data.jsonl');
|
|
404
|
+
```
|
|
405
|
+
- ✅ Converts JSON to JSONL
|
|
406
|
+
- ❌ **DOES NOT create indexes**
|
|
407
|
+
- ❌ **DOES NOT create JexiDB database**
|
|
408
|
+
- ✅ Pure JSONL file
|
|
409
|
+
|
|
410
|
+
#### **2. Database Creation with Automatic Indexes**
|
|
411
|
+
```javascript
|
|
412
|
+
// Create complete JexiDB database with indexes
|
|
413
|
+
const result = await utils.createDatabaseFromJSON('./users.json', './users.jsonl', {
|
|
414
|
+
autoDetectIndexes: true,
|
|
415
|
+
autoIndexFields: ['id', 'email', 'name']
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
console.log(result);
|
|
419
|
+
// {
|
|
420
|
+
// success: true,
|
|
421
|
+
// recordCount: 1000,
|
|
422
|
+
// indexes: ['id', 'email', 'name'],
|
|
423
|
+
// dbPath: './users.jsonl'
|
|
424
|
+
// }
|
|
425
|
+
```
|
|
426
|
+
- ✅ Converts JSON to JSONL
|
|
427
|
+
- ✅ **Creates indexes automatically**
|
|
428
|
+
- ✅ **Creates complete JexiDB database**
|
|
429
|
+
- ✅ File ready for use
|
|
430
|
+
|
|
431
|
+
#### **3. Intelligent Index Analysis**
|
|
432
|
+
```javascript
|
|
433
|
+
// Analyze data and suggest optimal indexes
|
|
434
|
+
const analysis = await utils.analyzeJSONForIndexes('./users.json');
|
|
435
|
+
|
|
436
|
+
console.log('Recommended:', analysis.suggestions.recommended);
|
|
437
|
+
// [
|
|
438
|
+
// { field: 'id', type: 'number', coverage: 100, uniqueness: 100 },
|
|
439
|
+
// { field: 'email', type: 'string', coverage: 95, uniqueness: 98 }
|
|
440
|
+
// ]
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
## 🔄 Migration from JexiDB 1.x
|
|
446
|
+
|
|
447
|
+
### Seamless Migration
|
|
448
|
+
|
|
449
|
+
JexiDB is **fully backward compatible** with JexiDB 1.x! You can use the same API:
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
// JexiDB 1.x code works unchanged in JexiDB
|
|
453
|
+
import { Database } from 'jexidb';
|
|
454
|
+
|
|
455
|
+
const db = new Database('./database.jdb', {
|
|
456
|
+
indexes: { id: 'number', name: 'string' }
|
|
457
|
+
});
|
|
458
|
+
await db.init();
|
|
459
|
+
|
|
460
|
+
// All JexiDB 1.x methods work:
|
|
461
|
+
await db.insert({ id: 1, name: 'John Doe' });
|
|
462
|
+
const results = await db.query({ name: 'John Doe' }, { caseInsensitive: true });
|
|
463
|
+
await db.update({ id: 1 }, { name: 'John Smith' });
|
|
464
|
+
await db.delete({ id: 1 });
|
|
465
|
+
await db.save();
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### File Format Support
|
|
469
|
+
|
|
470
|
+
JexiDB supports both file formats:
|
|
471
|
+
- **`.jdb`** (preferred) - JexiDB's branded extension
|
|
472
|
+
- **`.jsonl`** (standard) - JSON Lines format
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
// Both work identically:
|
|
476
|
+
const db1 = new Database('./users.jdb', { indexes: { id: 'number' } });
|
|
477
|
+
const db2 = new Database('./users.jsonl', { indexes: { id: 'number' } });
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Key Improvements
|
|
481
|
+
|
|
482
|
+
| Feature | JexiDB 1.x | JexiDB |
|
|
483
|
+
|---------|------------|--------------|
|
|
484
|
+
| **API Compatibility** | Original | ✅ **100% Backward Compatible** |
|
|
485
|
+
| **Query Methods** | `db.query()` | ✅ `db.query()` + `db.find()` |
|
|
486
|
+
| **File Format** | `.jdb` (proprietary) | ✅ `.jdb` + `.jsonl` support |
|
|
487
|
+
| **Performance** | Basic | ✅ **10-100x faster** |
|
|
488
|
+
| **Memory Usage** | Higher | ✅ **25% less memory** |
|
|
489
|
+
| **Data Integrity** | Basic | ✅ **Advanced validation** |
|
|
490
|
+
|
|
491
|
+
## 📝 Changelog
|
|
492
|
+
|
|
493
|
+
See [CHANGELOG.md](CHANGELOG.md) for complete change history.
|
|
494
|
+
|
|
495
|
+
## 🤝 Contributing
|
|
496
|
+
|
|
497
|
+
1. Fork the project
|
|
498
|
+
2. Create a branch for your feature
|
|
499
|
+
3. Commit your changes
|
|
500
|
+
4. Push to the branch
|
|
501
|
+
5. Open a Pull Request
|
|
502
|
+
|
|
503
|
+
## 📄 License
|
|
504
|
+
|
|
505
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 🎯 About JexiDB
|
|
510
|
+
|
|
511
|
+
JexiDB maintains the original JexiDB philosophy while fixing bugs and implementing a more robust architecture.
|
|
512
|
+
|
|
513
|
+
### 🚀 Performance
|
|
514
|
+
|
|
515
|
+
**JexiDB** performance compared to version 1.x:
|
|
516
|
+
|
|
517
|
+
- **Find operations**: 103x faster
|
|
518
|
+
- **Update operations**: 26x faster
|
|
519
|
+
- **Insert operations**: 6-11x faster
|
|
520
|
+
- **Memory usage**: 25% less memory
|
|
521
|
+
|
|
522
|
+
<p align="center">
|
|
523
|
+
<img width="420" src="https://edenware.app/jexidb/images/jexi-mascot.webp" alt="JexiDB mascot" title="JexiDB mascot" />
|
|
524
|
+
</p>
|
package/dist/FileHandler.js
CHANGED
|
@@ -606,7 +606,7 @@ class FileHandler {
|
|
|
606
606
|
} catch (error) {
|
|
607
607
|
if (error.code === 'ENOENT') {
|
|
608
608
|
return {
|
|
609
|
-
version: '2.0.
|
|
609
|
+
version: '2.0.1',
|
|
610
610
|
// Keep version number for internal tracking
|
|
611
611
|
created: new Date().toISOString(),
|
|
612
612
|
lastModified: new Date().toISOString(),
|
package/dist/JSONLDatabase.js
CHANGED
|
@@ -36,8 +36,17 @@ class JSONLDatabase extends _events.EventEmitter {
|
|
|
36
36
|
this.options = {
|
|
37
37
|
batchSize: 100,
|
|
38
38
|
// Batch size for inserts
|
|
39
|
+
create: true,
|
|
40
|
+
// Create database if it doesn't exist (default: true)
|
|
41
|
+
clear: false,
|
|
42
|
+
// Clear database on load if not empty (default: false)
|
|
39
43
|
...options
|
|
40
44
|
};
|
|
45
|
+
|
|
46
|
+
// If clear is true, create should also be true
|
|
47
|
+
if (this.options.clear === true) {
|
|
48
|
+
this.options.create = true;
|
|
49
|
+
}
|
|
41
50
|
this.isInitialized = false;
|
|
42
51
|
this.offsets = [];
|
|
43
52
|
this.indexOffset = 0;
|
|
@@ -80,10 +89,49 @@ class JSONLDatabase extends _events.EventEmitter {
|
|
|
80
89
|
await _fs.promises.mkdir(dir, {
|
|
81
90
|
recursive: true
|
|
82
91
|
});
|
|
92
|
+
|
|
93
|
+
// Check if file exists before loading
|
|
94
|
+
const fileExists = await _fs.promises.access(this.filePath).then(() => true).catch(() => false);
|
|
95
|
+
|
|
96
|
+
// Handle clear option
|
|
97
|
+
if (this.options.clear && fileExists) {
|
|
98
|
+
await _fs.promises.writeFile(this.filePath, '');
|
|
99
|
+
this.offsets = [];
|
|
100
|
+
this.indexOffset = 0;
|
|
101
|
+
this.recordCount = 0;
|
|
102
|
+
console.log(`Database cleared: ${this.filePath}`);
|
|
103
|
+
this.isInitialized = true;
|
|
104
|
+
this.emit('init');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle create option
|
|
109
|
+
if (!fileExists) {
|
|
110
|
+
if (this.options.create) {
|
|
111
|
+
await _fs.promises.writeFile(this.filePath, '');
|
|
112
|
+
this.offsets = [];
|
|
113
|
+
this.indexOffset = 0;
|
|
114
|
+
this.recordCount = 0;
|
|
115
|
+
console.log(`Database created: ${this.filePath}`);
|
|
116
|
+
this.isInitialized = true;
|
|
117
|
+
this.emit('init');
|
|
118
|
+
return;
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(`Database file does not exist: ${this.filePath}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Load existing database
|
|
83
125
|
await this.loadDataWithOffsets();
|
|
84
126
|
this.isInitialized = true;
|
|
85
127
|
this.emit('init');
|
|
86
128
|
} catch (error) {
|
|
129
|
+
// If create is false and file doesn't exist or is corrupted, throw error
|
|
130
|
+
if (!this.options.create) {
|
|
131
|
+
throw new Error(`Failed to load database: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If create is true, initialize empty database
|
|
87
135
|
this.recordCount = 0;
|
|
88
136
|
this.offsets = [];
|
|
89
137
|
this.indexOffset = 0;
|
|
@@ -171,9 +219,7 @@ class JSONLDatabase extends _events.EventEmitter {
|
|
|
171
219
|
}
|
|
172
220
|
this.recordCount = this.offsets.length;
|
|
173
221
|
} catch (error) {
|
|
174
|
-
|
|
175
|
-
this.offsets = [];
|
|
176
|
-
this.indexOffset = 0;
|
|
222
|
+
throw error; // Re-throw to be handled by init()
|
|
177
223
|
}
|
|
178
224
|
}
|
|
179
225
|
async loadLegacyFormat(lines) {
|
|
@@ -821,6 +867,22 @@ class JSONLDatabase extends _events.EventEmitter {
|
|
|
821
867
|
};
|
|
822
868
|
}
|
|
823
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Compatibility method: readColumnIndex - gets unique values from indexed columns only
|
|
872
|
+
* Maintains compatibility with JexiDB v1 code
|
|
873
|
+
* @param {string} column - The column name to get unique values from
|
|
874
|
+
* @returns {Set} Set of unique values in the column (indexed columns only)
|
|
875
|
+
*/
|
|
876
|
+
readColumnIndex(column) {
|
|
877
|
+
// Only works with indexed columns
|
|
878
|
+
if (this.indexes[column]) {
|
|
879
|
+
return new Set(this.indexes[column].keys());
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// For non-indexed columns, throw error
|
|
883
|
+
throw new Error(`Column '${column}' is not indexed. Only indexed columns are supported.`);
|
|
884
|
+
}
|
|
885
|
+
|
|
824
886
|
// Intelligent criteria matching for non-indexed fields
|
|
825
887
|
matchesCriteria(record, criteria, options = {}) {
|
|
826
888
|
const {
|
package/dist/index.js
CHANGED
|
@@ -251,6 +251,16 @@ class JexiDBCompatibility extends _JSONLDatabase.default {
|
|
|
251
251
|
}
|
|
252
252
|
return result;
|
|
253
253
|
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compatibility method: readColumnIndex - gets unique values from indexed columns only
|
|
257
|
+
* Maintains compatibility with JexiDB v1 code
|
|
258
|
+
* @param {string} column - The column name to get unique values from
|
|
259
|
+
* @returns {Set} Set of unique values in the column (indexed columns only)
|
|
260
|
+
*/
|
|
261
|
+
readColumnIndex(column) {
|
|
262
|
+
return super.readColumnIndex(column);
|
|
263
|
+
}
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
/**
|
package/package.json
CHANGED
package/src/FileHandler.js
CHANGED
|
@@ -600,7 +600,7 @@ class FileHandler {
|
|
|
600
600
|
} catch (error) {
|
|
601
601
|
if (error.code === 'ENOENT') {
|
|
602
602
|
return {
|
|
603
|
-
version: '2.0.
|
|
603
|
+
version: '2.0.1', // Keep version number for internal tracking
|
|
604
604
|
created: new Date().toISOString(),
|
|
605
605
|
lastModified: new Date().toISOString(),
|
|
606
606
|
recordCount: 0,
|
package/src/JSONLDatabase.js
CHANGED
|
@@ -30,9 +30,16 @@ class JSONLDatabase extends EventEmitter {
|
|
|
30
30
|
|
|
31
31
|
this.options = {
|
|
32
32
|
batchSize: 100, // Batch size for inserts
|
|
33
|
+
create: true, // Create database if it doesn't exist (default: true)
|
|
34
|
+
clear: false, // Clear database on load if not empty (default: false)
|
|
33
35
|
...options
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
// If clear is true, create should also be true
|
|
39
|
+
if (this.options.clear === true) {
|
|
40
|
+
this.options.create = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
this.isInitialized = false;
|
|
37
44
|
this.offsets = [];
|
|
38
45
|
this.indexOffset = 0;
|
|
@@ -77,12 +84,50 @@ class JSONLDatabase extends EventEmitter {
|
|
|
77
84
|
const dir = path.dirname(this.filePath);
|
|
78
85
|
await fs.mkdir(dir, { recursive: true });
|
|
79
86
|
|
|
87
|
+
// Check if file exists before loading
|
|
88
|
+
const fileExists = await fs.access(this.filePath).then(() => true).catch(() => false);
|
|
89
|
+
|
|
90
|
+
// Handle clear option
|
|
91
|
+
if (this.options.clear && fileExists) {
|
|
92
|
+
await fs.writeFile(this.filePath, '');
|
|
93
|
+
this.offsets = [];
|
|
94
|
+
this.indexOffset = 0;
|
|
95
|
+
this.recordCount = 0;
|
|
96
|
+
console.log(`Database cleared: ${this.filePath}`);
|
|
97
|
+
this.isInitialized = true;
|
|
98
|
+
this.emit('init');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle create option
|
|
103
|
+
if (!fileExists) {
|
|
104
|
+
if (this.options.create) {
|
|
105
|
+
await fs.writeFile(this.filePath, '');
|
|
106
|
+
this.offsets = [];
|
|
107
|
+
this.indexOffset = 0;
|
|
108
|
+
this.recordCount = 0;
|
|
109
|
+
console.log(`Database created: ${this.filePath}`);
|
|
110
|
+
this.isInitialized = true;
|
|
111
|
+
this.emit('init');
|
|
112
|
+
return;
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error(`Database file does not exist: ${this.filePath}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Load existing database
|
|
80
119
|
await this.loadDataWithOffsets();
|
|
81
120
|
|
|
82
121
|
this.isInitialized = true;
|
|
83
122
|
this.emit('init');
|
|
84
123
|
|
|
85
124
|
} catch (error) {
|
|
125
|
+
// If create is false and file doesn't exist or is corrupted, throw error
|
|
126
|
+
if (!this.options.create) {
|
|
127
|
+
throw new Error(`Failed to load database: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If create is true, initialize empty database
|
|
86
131
|
this.recordCount = 0;
|
|
87
132
|
this.offsets = [];
|
|
88
133
|
this.indexOffset = 0;
|
|
@@ -180,9 +225,7 @@ class JSONLDatabase extends EventEmitter {
|
|
|
180
225
|
this.recordCount = this.offsets.length;
|
|
181
226
|
|
|
182
227
|
} catch (error) {
|
|
183
|
-
|
|
184
|
-
this.offsets = [];
|
|
185
|
-
this.indexOffset = 0;
|
|
228
|
+
throw error; // Re-throw to be handled by init()
|
|
186
229
|
}
|
|
187
230
|
}
|
|
188
231
|
|
|
@@ -867,6 +910,22 @@ class JSONLDatabase extends EventEmitter {
|
|
|
867
910
|
};
|
|
868
911
|
}
|
|
869
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Compatibility method: readColumnIndex - gets unique values from indexed columns only
|
|
915
|
+
* Maintains compatibility with JexiDB v1 code
|
|
916
|
+
* @param {string} column - The column name to get unique values from
|
|
917
|
+
* @returns {Set} Set of unique values in the column (indexed columns only)
|
|
918
|
+
*/
|
|
919
|
+
readColumnIndex(column) {
|
|
920
|
+
// Only works with indexed columns
|
|
921
|
+
if (this.indexes[column]) {
|
|
922
|
+
return new Set(this.indexes[column].keys());
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// For non-indexed columns, throw error
|
|
926
|
+
throw new Error(`Column '${column}' is not indexed. Only indexed columns are supported.`);
|
|
927
|
+
}
|
|
928
|
+
|
|
870
929
|
// Intelligent criteria matching for non-indexed fields
|
|
871
930
|
matchesCriteria(record, criteria, options = {}) {
|
|
872
931
|
const { caseInsensitive = false } = options;
|
package/src/index.js
CHANGED
|
@@ -227,6 +227,16 @@ class JexiDBCompatibility extends JSONLDatabase {
|
|
|
227
227
|
}
|
|
228
228
|
return result;
|
|
229
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compatibility method: readColumnIndex - gets unique values from indexed columns only
|
|
233
|
+
* Maintains compatibility with JexiDB v1 code
|
|
234
|
+
* @param {string} column - The column name to get unique values from
|
|
235
|
+
* @returns {Set} Set of unique values in the column (indexed columns only)
|
|
236
|
+
*/
|
|
237
|
+
readColumnIndex(column) {
|
|
238
|
+
return super.readColumnIndex(column);
|
|
239
|
+
}
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
/**
|