sehawq.db 3.0.0 → 4.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.
- package/.github/workflows/npm-publish.yml +1 -1
- package/index.js +2 -0
- package/package.json +25 -9
- package/readme.md +342 -170
- package/src/core/Database.js +295 -0
- package/src/core/Events.js +286 -0
- package/src/core/IndexManager.js +814 -0
- package/src/core/Persistence.js +376 -0
- package/src/core/QueryEngine.js +448 -0
- package/src/core/Storage.js +322 -0
- package/src/core/Validator.js +325 -0
- package/src/index.js +106 -750
- package/src/performance/Cache.js +339 -0
- package/src/performance/LazyLoader.js +355 -0
- package/src/performance/MemoryManager.js +496 -0
- package/src/server/api.js +688 -0
- package/src/server/websocket.js +528 -0
- package/src/utils/benchmark.js +52 -0
- package/src/utils/dot-notation.js +248 -0
- package/src/utils/helpers.js +276 -0
- package/src/utils/profiler.js +71 -0
- package/src/version.js +38 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Engine - Makes data searching fast and intuitive
|
|
3
|
+
*
|
|
4
|
+
* Went from simple filters to a mini-query language
|
|
5
|
+
* Because scanning everything is for beginners 😎
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { performance } = require('perf_hooks');
|
|
9
|
+
|
|
10
|
+
class QueryEngine {
|
|
11
|
+
constructor(database) {
|
|
12
|
+
this.db = database;
|
|
13
|
+
this.stats = {
|
|
14
|
+
queries: 0,
|
|
15
|
+
fullScans: 0,
|
|
16
|
+
indexScans: 0,
|
|
17
|
+
avgQueryTime: 0,
|
|
18
|
+
queryTimes: []
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Query operators - because who remembers syntax?
|
|
22
|
+
this.operators = {
|
|
23
|
+
'=': (a, b) => a === b,
|
|
24
|
+
'!=': (a, b) => a !== b,
|
|
25
|
+
'>': (a, b) => a > b,
|
|
26
|
+
'<': (a, b) => a < b,
|
|
27
|
+
'>=': (a, b) => a >= b,
|
|
28
|
+
'<=': (a, b) => a <= b,
|
|
29
|
+
'in': (a, b) => Array.isArray(b) && b.includes(a),
|
|
30
|
+
'contains': (a, b) => String(a).includes(String(b)),
|
|
31
|
+
'startsWith': (a, b) => String(a).startsWith(String(b)),
|
|
32
|
+
'endsWith': (a, b) => String(a).endsWith(String(b)),
|
|
33
|
+
'matches': (a, b) => new RegExp(b).test(String(a))
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Cache for compiled filter functions
|
|
37
|
+
this.filterCache = new Map();
|
|
38
|
+
this.cacheHits = 0;
|
|
39
|
+
this.cacheMisses = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find records using a filter function
|
|
44
|
+
* The OG method - simple but powerful
|
|
45
|
+
*/
|
|
46
|
+
find(filterFn, options = {}) {
|
|
47
|
+
const startTime = performance.now();
|
|
48
|
+
this.stats.queries++;
|
|
49
|
+
|
|
50
|
+
const results = [];
|
|
51
|
+
const data = this.db.data;
|
|
52
|
+
|
|
53
|
+
// Use index if available and applicable
|
|
54
|
+
if (options.useIndex !== false && this._canUseIndex(filterFn)) {
|
|
55
|
+
results.push(...this._findWithIndex(filterFn));
|
|
56
|
+
this.stats.indexScans++;
|
|
57
|
+
} else {
|
|
58
|
+
// Full table scan - reliable but slower
|
|
59
|
+
for (const [key, value] of data) {
|
|
60
|
+
if (filterFn(value, key)) {
|
|
61
|
+
results.push({ key, value });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.stats.fullScans++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const queryTime = performance.now() - startTime;
|
|
68
|
+
this._recordQueryTime(queryTime);
|
|
69
|
+
|
|
70
|
+
return new QueryResult(results, this, {
|
|
71
|
+
queryTime,
|
|
72
|
+
usedIndex: this.stats.indexScans > 0
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* MongoDB-style where queries
|
|
78
|
+
* Because sometimes you want to feel professional
|
|
79
|
+
*/
|
|
80
|
+
where(field, operator, value) {
|
|
81
|
+
const filterFn = this._compileWhereClause(field, operator, value);
|
|
82
|
+
return this.find(filterFn, { useIndex: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compile where clause into efficient filter function
|
|
87
|
+
* With caching because compiling is expensive
|
|
88
|
+
*/
|
|
89
|
+
_compileWhereClause(field, operator, value) {
|
|
90
|
+
const cacheKey = `${field}|${operator}|${JSON.stringify(value)}`;
|
|
91
|
+
|
|
92
|
+
// Check cache first
|
|
93
|
+
if (this.filterCache.has(cacheKey)) {
|
|
94
|
+
this.cacheHits++;
|
|
95
|
+
return this.filterCache.get(cacheKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.cacheMisses++;
|
|
99
|
+
|
|
100
|
+
// Get the operator function
|
|
101
|
+
const opFunc = this.operators[operator];
|
|
102
|
+
if (!opFunc) {
|
|
103
|
+
throw new Error(`Unknown operator: ${operator}. Available: ${Object.keys(this.operators).join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Compile the filter function
|
|
107
|
+
let filterFn;
|
|
108
|
+
|
|
109
|
+
if (field.includes('.')) {
|
|
110
|
+
// Dot notation - user.profile.name
|
|
111
|
+
const fieldParts = field.split('.');
|
|
112
|
+
filterFn = (item) => {
|
|
113
|
+
let fieldValue = item;
|
|
114
|
+
for (const part of fieldParts) {
|
|
115
|
+
fieldValue = fieldValue?.[part];
|
|
116
|
+
if (fieldValue === undefined) break;
|
|
117
|
+
}
|
|
118
|
+
return opFunc(fieldValue, value);
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
// Simple field access
|
|
122
|
+
filterFn = (item) => opFunc(item[field], value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Cache the compiled function
|
|
126
|
+
if (this.filterCache.size < 1000) { // Prevent memory leaks
|
|
127
|
+
this.filterCache.set(cacheKey, filterFn);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return filterFn;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if we can use indexes for this query
|
|
135
|
+
*/
|
|
136
|
+
_canUseIndex(filterFn) {
|
|
137
|
+
// TODO: Implement index detection logic
|
|
138
|
+
// For now, we'll use full scans until IndexManager is ready
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Use indexes for faster queries
|
|
144
|
+
*/
|
|
145
|
+
_findWithIndex(filterFn) {
|
|
146
|
+
// TODO: Implement index-based search
|
|
147
|
+
// This will make large datasets queryable in milliseconds
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Aggregation functions - for when you need answers
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
count(filterFn = null) {
|
|
156
|
+
if (filterFn) {
|
|
157
|
+
return this.find(filterFn).count();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fast path for total count
|
|
161
|
+
return this.db.data.size;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// sum() function optimized to avoid multiple iterations
|
|
165
|
+
sum(field, filterFn = null) {
|
|
166
|
+
const results = filterFn ? this.find(filterFn) : this.findAll();
|
|
167
|
+
const resultsArray = results.toArray();
|
|
168
|
+
let total = 0;
|
|
169
|
+
|
|
170
|
+
for (const item of resultsArray) {
|
|
171
|
+
const value = this._getFieldValue(item.value, field);
|
|
172
|
+
if (typeof value === 'number') {
|
|
173
|
+
total += value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return total;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
avg(field, filterFn = null) {
|
|
180
|
+
const results = filterFn ? this.find(filterFn) : this.findAll();
|
|
181
|
+
let total = 0;
|
|
182
|
+
let count = 0;
|
|
183
|
+
|
|
184
|
+
// results.toArray() used to avoid multiple iterations
|
|
185
|
+
const resultsArray = results.toArray();
|
|
186
|
+
|
|
187
|
+
for (const item of resultsArray) {
|
|
188
|
+
const value = this._getFieldValue(item.value, field);
|
|
189
|
+
if (typeof value === 'number') {
|
|
190
|
+
total += value;
|
|
191
|
+
count++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return count > 0 ? total / count : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
min(field, filterFn = null) {
|
|
199
|
+
const results = filterFn ? this.find(filterFn) : this.findAll();
|
|
200
|
+
const resultsArray = results.toArray();
|
|
201
|
+
let min = Infinity;
|
|
202
|
+
|
|
203
|
+
for (const item of resultsArray) {
|
|
204
|
+
const value = this._getFieldValue(item.value, field);
|
|
205
|
+
if (typeof value === 'number' && value < min) {
|
|
206
|
+
min = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return min !== Infinity ? min : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
max(field, filterFn = null) {
|
|
213
|
+
const results = filterFn ? this.find(filterFn) : this.findAll();
|
|
214
|
+
const resultsArray = results.toArray();
|
|
215
|
+
let max = -Infinity;
|
|
216
|
+
|
|
217
|
+
for (const item of resultsArray) {
|
|
218
|
+
const value = this._getFieldValue(item.value, field);
|
|
219
|
+
if (typeof value === 'number' && value > max) {
|
|
220
|
+
max = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return max !== -Infinity ? max : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all records as QueryResult
|
|
228
|
+
*/
|
|
229
|
+
findAll() {
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const [key, value] of this.db.data) {
|
|
232
|
+
results.push({ key, value });
|
|
233
|
+
}
|
|
234
|
+
return new QueryResult(results, this);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get nested field value using dot notation
|
|
239
|
+
*/
|
|
240
|
+
_getFieldValue(obj, fieldPath) {
|
|
241
|
+
if (!fieldPath.includes('.')) {
|
|
242
|
+
return obj[fieldPath];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parts = fieldPath.split('.');
|
|
246
|
+
let value = obj;
|
|
247
|
+
|
|
248
|
+
for (const part of parts) {
|
|
249
|
+
value = value?.[part];
|
|
250
|
+
if (value === undefined) break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Group by field - SQL-like power
|
|
258
|
+
*/
|
|
259
|
+
groupBy(field, aggregateFn = null) {
|
|
260
|
+
const groups = new Map();
|
|
261
|
+
|
|
262
|
+
for (const [key, value] of this.db.data) {
|
|
263
|
+
const groupKey = this._getFieldValue(value, field);
|
|
264
|
+
|
|
265
|
+
if (!groups.has(groupKey)) {
|
|
266
|
+
groups.set(groupKey, []);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
groups.get(groupKey).push({ key, value });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Apply aggregation if provided
|
|
273
|
+
if (aggregateFn) {
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const [groupKey, items] of groups) {
|
|
276
|
+
result[groupKey] = aggregateFn(items);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return Object.fromEntries(groups);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Performance tracking
|
|
286
|
+
*/
|
|
287
|
+
_recordQueryTime(queryTime) {
|
|
288
|
+
this.stats.queryTimes.push(queryTime);
|
|
289
|
+
|
|
290
|
+
// Keep only last 100 times
|
|
291
|
+
if (this.stats.queryTimes.length > 100) {
|
|
292
|
+
this.stats.queryTimes.shift();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.stats.avgQueryTime = this.stats.queryTimes.reduce((a, b) => a + b, 0) / this.stats.queryTimes.length;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get query statistics
|
|
300
|
+
*/
|
|
301
|
+
getStats() {
|
|
302
|
+
return {
|
|
303
|
+
...this.stats,
|
|
304
|
+
cacheHitRate: this.cacheHits + this.cacheMisses > 0
|
|
305
|
+
? ((this.cacheHits / (this.cacheHits + this.cacheMisses)) * 100).toFixed(2) + '%'
|
|
306
|
+
: '0%',
|
|
307
|
+
filterCacheSize: this.filterCache.size
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Clear filter cache
|
|
313
|
+
*/
|
|
314
|
+
clearCache() {
|
|
315
|
+
this.filterCache.clear();
|
|
316
|
+
this.cacheHits = 0;
|
|
317
|
+
this.cacheMisses = 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* QueryResult - Enables method chaining
|
|
323
|
+
* Because .find().where().sort().limit() looks cool 😎
|
|
324
|
+
*/
|
|
325
|
+
class QueryResult {
|
|
326
|
+
constructor(results, queryEngine, meta = {}) {
|
|
327
|
+
this.results = results;
|
|
328
|
+
this.queryEngine = queryEngine;
|
|
329
|
+
this.meta = meta;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Sort results by field
|
|
334
|
+
*/
|
|
335
|
+
sort(field, direction = 'asc') {
|
|
336
|
+
const sorted = [...this.results].sort((a, b) => {
|
|
337
|
+
const aVal = this.queryEngine._getFieldValue(a.value, field);
|
|
338
|
+
const bVal = this.queryEngine._getFieldValue(b.value, field);
|
|
339
|
+
|
|
340
|
+
if (aVal === bVal) return 0;
|
|
341
|
+
|
|
342
|
+
if (direction === 'asc') {
|
|
343
|
+
return aVal < bVal ? -1 : 1;
|
|
344
|
+
} else {
|
|
345
|
+
return aVal > bVal ? -1 : 1;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return new QueryResult(sorted, this.queryEngine, this.meta);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Limit number of results
|
|
354
|
+
*/
|
|
355
|
+
limit(count) {
|
|
356
|
+
const limited = this.results.slice(0, count);
|
|
357
|
+
return new QueryResult(limited, this.queryEngine, this.meta);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Skip number of results
|
|
362
|
+
*/
|
|
363
|
+
skip(count) {
|
|
364
|
+
const skipped = this.results.slice(count);
|
|
365
|
+
return new QueryResult(skipped, this.queryEngine, this.meta);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get first result
|
|
370
|
+
*/
|
|
371
|
+
first() {
|
|
372
|
+
return this.results[0] || null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get last result
|
|
377
|
+
*/
|
|
378
|
+
last() {
|
|
379
|
+
return this.results[this.results.length - 1] || null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get result count
|
|
384
|
+
*/
|
|
385
|
+
count() {
|
|
386
|
+
return this.results.length;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get only values
|
|
391
|
+
*/
|
|
392
|
+
values() {
|
|
393
|
+
return this.results.map(item => item.value);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get only keys
|
|
398
|
+
*/
|
|
399
|
+
keys() {
|
|
400
|
+
return this.results.map(item => item.key);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Convert to array
|
|
405
|
+
*/
|
|
406
|
+
toArray() {
|
|
407
|
+
return this.results;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get query performance info
|
|
412
|
+
*/
|
|
413
|
+
getMeta() {
|
|
414
|
+
return this.meta;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Execute another query on these results
|
|
419
|
+
*/
|
|
420
|
+
find(filterFn) {
|
|
421
|
+
const filtered = this.results.filter(item => filterFn(item.value, item.key));
|
|
422
|
+
return new QueryResult(filtered, this.queryEngine, this.meta);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Map over results
|
|
427
|
+
*/
|
|
428
|
+
map(fn) {
|
|
429
|
+
return this.results.map((item, index) => fn(item.value, item.key, index));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Filter results
|
|
434
|
+
*/
|
|
435
|
+
filter(fn) {
|
|
436
|
+
const filtered = this.results.filter((item, index) => fn(item.value, item.key, index));
|
|
437
|
+
return new QueryResult(filtered, this.queryEngine, this.meta);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* ForEach loop
|
|
442
|
+
*/
|
|
443
|
+
forEach(fn) {
|
|
444
|
+
this.results.forEach((item, index) => fn(item.value, item.key, index));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = QueryEngine;
|