jexidb 2.0.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +132 -101
- package/LICENSE +21 -21
- package/README.md +301 -639
- package/babel.config.json +5 -0
- package/dist/Database.cjs +5204 -0
- package/docs/API.md +908 -241
- package/docs/EXAMPLES.md +701 -177
- package/docs/README.md +194 -184
- package/examples/iterate-usage-example.js +157 -0
- package/examples/simple-iterate-example.js +115 -0
- package/jest.config.js +24 -0
- package/package.json +63 -54
- package/scripts/README.md +47 -0
- package/scripts/benchmark-array-serialization.js +108 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/scripts/score-mode-demo.js +45 -0
- package/src/Database.mjs +5325 -0
- package/src/FileHandler.mjs +1140 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +702 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +2094 -0
- package/src/managers/QueryManager.mjs +1490 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/src/utils/operatorNormalizer.mjs +116 -0
- package/test/$not-operator-with-and.test.js +282 -0
- package/test/README.md +8 -0
- package/test/close-init-cycle.test.js +256 -0
- package/test/coverage-method.test.js +93 -0
- package/test/critical-bugs-fixes.test.js +1069 -0
- package/test/deserialize-corruption-fixes.test.js +296 -0
- package/test/exists-method.test.js +318 -0
- package/test/explicit-indexes-comparison.test.js +219 -0
- package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
- package/test/index-line-number-regression.test.js +100 -0
- package/test/index-missing-index-data.test.js +91 -0
- package/test/index-persistence.test.js +491 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/insert-session-auto-flush.test.js +353 -0
- package/test/iterate-method.test.js +272 -0
- package/test/legacy-operator-compat.test.js +154 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +298 -0
- package/test/setup.js +17 -0
- package/test/term-mapping-minimal.test.js +154 -0
- package/test/term-mapping-simple.test.js +257 -0
- package/test/term-mapping.test.js +514 -0
- package/test/writebuffer-flush-resilience.test.js +204 -0
- package/dist/FileHandler.js +0 -688
- package/dist/IndexManager.js +0 -353
- package/dist/IntegrityChecker.js +0 -364
- package/dist/JSONLDatabase.js +0 -1333
- package/dist/index.js +0 -617
- package/docs/MIGRATION.md +0 -295
- package/examples/auto-save-example.js +0 -158
- package/examples/cjs-usage.cjs +0 -82
- package/examples/close-vs-delete-example.js +0 -71
- package/examples/esm-usage.js +0 -113
- package/examples/example-columns.idx.jdb +0 -0
- package/examples/example-columns.jdb +0 -9
- package/examples/example-options.idx.jdb +0 -0
- package/examples/example-options.jdb +0 -0
- package/examples/example-users.idx.jdb +0 -0
- package/examples/example-users.jdb +0 -5
- package/examples/simple-test.js +0 -55
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1391
- package/src/index.js +0 -608
package/dist/JSONLDatabase.js
DELETED
|
@@ -1,1333 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = void 0;
|
|
7
|
-
var _fs = require("fs");
|
|
8
|
-
var _path = _interopRequireDefault(require("path"));
|
|
9
|
-
var _events = require("events");
|
|
10
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
-
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
|
|
12
|
-
* JSONLDatabase - JexiDB Core Database Engine
|
|
13
|
-
* High Performance JSONL Database optimized for JexiDB
|
|
14
|
-
* Optimized hybrid architecture combining the best strategies:
|
|
15
|
-
* - Insert: Buffer + batch write for maximum speed
|
|
16
|
-
* - Find: Intelligent hybrid (indexed + non-indexed fields)
|
|
17
|
-
* - Update/Delete: On-demand reading/writing for scalability
|
|
18
|
-
*/
|
|
19
|
-
class JSONLDatabase extends _events.EventEmitter {
|
|
20
|
-
constructor(filePath, options = {}) {
|
|
21
|
-
super();
|
|
22
|
-
|
|
23
|
-
// Expect the main data file path (with .jdb extension)
|
|
24
|
-
if (!filePath.endsWith('.jdb')) {
|
|
25
|
-
if (filePath.endsWith('.jsonl')) {
|
|
26
|
-
this.filePath = filePath.replace('.jsonl', '.jdb');
|
|
27
|
-
} else if (filePath.endsWith('.json')) {
|
|
28
|
-
this.filePath = filePath.replace('.json', '.jdb');
|
|
29
|
-
} else {
|
|
30
|
-
// If no extension provided, assume it's a base name and add .jdb
|
|
31
|
-
this.filePath = filePath + '.jdb';
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
this.filePath = filePath;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Enhanced configuration with intelligent defaults
|
|
38
|
-
this.options = {
|
|
39
|
-
// Original options
|
|
40
|
-
batchSize: 50,
|
|
41
|
-
// Reduced from 100 for faster response
|
|
42
|
-
create: true,
|
|
43
|
-
// Create database if it doesn't exist (default: true)
|
|
44
|
-
clear: false,
|
|
45
|
-
// Clear database on load if not empty (default: false)
|
|
46
|
-
|
|
47
|
-
// Auto-save intelligent configuration
|
|
48
|
-
autoSave: true,
|
|
49
|
-
// Enable auto-save by default
|
|
50
|
-
autoSaveThreshold: 50,
|
|
51
|
-
// Flush when buffer reaches 50 records
|
|
52
|
-
autoSaveInterval: 5000,
|
|
53
|
-
// Flush every 5 seconds
|
|
54
|
-
forceSaveOnClose: true,
|
|
55
|
-
// Always save when closing
|
|
56
|
-
|
|
57
|
-
// Performance configuration
|
|
58
|
-
adaptiveBatchSize: true,
|
|
59
|
-
// Adjust batch size based on usage
|
|
60
|
-
minBatchSize: 10,
|
|
61
|
-
// Minimum batch size for flush
|
|
62
|
-
maxBatchSize: 200,
|
|
63
|
-
// Maximum batch size for performance
|
|
64
|
-
|
|
65
|
-
// Memory management
|
|
66
|
-
maxMemoryUsage: 'auto',
|
|
67
|
-
// Calculate automatically or use fixed value
|
|
68
|
-
maxFlushChunkBytes: 8 * 1024 * 1024,
|
|
69
|
-
// 8MB default
|
|
70
|
-
|
|
71
|
-
...options
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// If clear is true, create should also be true
|
|
75
|
-
if (this.options.clear === true) {
|
|
76
|
-
this.options.create = true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Auto-save timer and state
|
|
80
|
-
this.autoSaveTimer = null;
|
|
81
|
-
this.lastFlushTime = null;
|
|
82
|
-
this.lastAutoSaveTime = Date.now();
|
|
83
|
-
this.isInitialized = false;
|
|
84
|
-
this.offsets = [];
|
|
85
|
-
this.indexOffset = 0;
|
|
86
|
-
this.shouldSave = false;
|
|
87
|
-
|
|
88
|
-
// Ultra-optimized index structure (kept in memory)
|
|
89
|
-
this.indexes = {};
|
|
90
|
-
|
|
91
|
-
// Initialize indexes from options or use defaults
|
|
92
|
-
if (options.indexes) {
|
|
93
|
-
for (const [field, type] of Object.entries(options.indexes)) {
|
|
94
|
-
this.indexes[field] = new Map();
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
// Default indexes
|
|
98
|
-
this.indexes = {
|
|
99
|
-
id: new Map(),
|
|
100
|
-
age: new Map(),
|
|
101
|
-
email: new Map()
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
this.recordCount = 0;
|
|
105
|
-
this.fileHandle = null; // File handle for on-demand reading
|
|
106
|
-
|
|
107
|
-
// Insert buffer (Original strategy)
|
|
108
|
-
this.insertionBuffer = [];
|
|
109
|
-
this.insertionStats = {
|
|
110
|
-
count: 0,
|
|
111
|
-
lastInsertion: Date.now(),
|
|
112
|
-
batchSize: this.options.batchSize
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
async init() {
|
|
116
|
-
if (this.isInitialized) {
|
|
117
|
-
// If already initialized, close first to reset state
|
|
118
|
-
await this.close();
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const dir = _path.default.dirname(this.filePath);
|
|
122
|
-
await _fs.promises.mkdir(dir, {
|
|
123
|
-
recursive: true
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Check if file exists before loading
|
|
127
|
-
const fileExists = await _fs.promises.access(this.filePath).then(() => true).catch(() => false);
|
|
128
|
-
|
|
129
|
-
// Handle clear option
|
|
130
|
-
if (this.options.clear && fileExists) {
|
|
131
|
-
await _fs.promises.writeFile(this.filePath, '');
|
|
132
|
-
this.offsets = [];
|
|
133
|
-
this.indexOffset = 0;
|
|
134
|
-
this.recordCount = 0;
|
|
135
|
-
console.log(`Database cleared: ${this.filePath}`);
|
|
136
|
-
this.isInitialized = true;
|
|
137
|
-
this.emit('init');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Handle create option
|
|
142
|
-
if (!fileExists) {
|
|
143
|
-
if (this.options.create) {
|
|
144
|
-
await _fs.promises.writeFile(this.filePath, '');
|
|
145
|
-
this.offsets = [];
|
|
146
|
-
this.indexOffset = 0;
|
|
147
|
-
this.recordCount = 0;
|
|
148
|
-
console.log(`Database created: ${this.filePath}`);
|
|
149
|
-
this.isInitialized = true;
|
|
150
|
-
this.emit('init');
|
|
151
|
-
return;
|
|
152
|
-
} else {
|
|
153
|
-
throw new Error(`Database file does not exist: ${this.filePath}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Load existing database
|
|
158
|
-
await this.loadDataWithOffsets();
|
|
159
|
-
this.isInitialized = true;
|
|
160
|
-
this.emit('init');
|
|
161
|
-
} catch (error) {
|
|
162
|
-
// If create is false and file doesn't exist or is corrupted, throw error
|
|
163
|
-
if (!this.options.create) {
|
|
164
|
-
throw new Error(`Failed to load database: ${error.message}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// If create is true, initialize empty database
|
|
168
|
-
this.recordCount = 0;
|
|
169
|
-
this.offsets = [];
|
|
170
|
-
this.indexOffset = 0;
|
|
171
|
-
this.isInitialized = true;
|
|
172
|
-
this.emit('init');
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
async loadDataWithOffsets() {
|
|
176
|
-
try {
|
|
177
|
-
// Open file handle for on-demand reading
|
|
178
|
-
this.fileHandle = await _fs.promises.open(this.filePath, 'r');
|
|
179
|
-
const data = await _fs.promises.readFile(this.filePath, 'utf8');
|
|
180
|
-
const lines = data.split('\n').filter(line => line.trim());
|
|
181
|
-
if (lines.length === 0) {
|
|
182
|
-
this.recordCount = 0;
|
|
183
|
-
this.offsets = [];
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if this is a legacy JexiDB file (has index and lineOffsets at the end)
|
|
188
|
-
if (lines.length >= 3) {
|
|
189
|
-
const lastLine = lines[lines.length - 1];
|
|
190
|
-
const secondLastLine = lines[lines.length - 2];
|
|
191
|
-
try {
|
|
192
|
-
const lastData = JSON.parse(lastLine);
|
|
193
|
-
const secondLastData = JSON.parse(secondLastLine);
|
|
194
|
-
|
|
195
|
-
// Legacy format: data lines + index line (object) + lineOffsets line (array)
|
|
196
|
-
// Check if secondLastLine contains index structure (has nested objects with arrays)
|
|
197
|
-
if (Array.isArray(lastData) && typeof secondLastData === 'object' && !Array.isArray(secondLastData) && Object.values(secondLastData).some(val => typeof val === 'object' && !Array.isArray(val))) {
|
|
198
|
-
console.log('🔄 Detected legacy JexiDB format, migrating...');
|
|
199
|
-
return await this.loadLegacyFormat(lines);
|
|
200
|
-
}
|
|
201
|
-
} catch (e) {
|
|
202
|
-
// Not legacy format
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Check for new format offset line
|
|
207
|
-
const lastLine = lines[lines.length - 1];
|
|
208
|
-
try {
|
|
209
|
-
const lastData = JSON.parse(lastLine);
|
|
210
|
-
if (Array.isArray(lastData) && lastData.length > 0 && typeof lastData[0] === 'number') {
|
|
211
|
-
this.offsets = lastData;
|
|
212
|
-
this.indexOffset = lastData[lastData.length - 2] || 0;
|
|
213
|
-
this.recordCount = this.offsets.length; // Number of offsets = number of records
|
|
214
|
-
|
|
215
|
-
// Try to load persistent indexes first
|
|
216
|
-
if (await this.loadPersistentIndexes()) {
|
|
217
|
-
console.log('✅ Loaded persistent indexes');
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Fallback: Load records into indexes (on-demand)
|
|
222
|
-
console.log('🔄 Rebuilding indexes from data...');
|
|
223
|
-
for (let i = 0; i < this.recordCount; i++) {
|
|
224
|
-
try {
|
|
225
|
-
const record = JSON.parse(lines[i]);
|
|
226
|
-
if (record && !record._deleted) {
|
|
227
|
-
this.addToIndex(record, i);
|
|
228
|
-
}
|
|
229
|
-
} catch (error) {
|
|
230
|
-
// Skip invalid lines
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
} catch (e) {
|
|
236
|
-
// Not an offset line
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Regular loading - no offset information
|
|
240
|
-
this.offsets = [];
|
|
241
|
-
this.indexOffset = 0;
|
|
242
|
-
for (let i = 0; i < lines.length; i++) {
|
|
243
|
-
try {
|
|
244
|
-
const record = JSON.parse(lines[i]);
|
|
245
|
-
if (record && !record._deleted) {
|
|
246
|
-
this.addToIndex(record, i);
|
|
247
|
-
this.offsets.push(i * 100); // Estimate offset
|
|
248
|
-
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
// Skip invalid lines
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
this.recordCount = this.offsets.length;
|
|
254
|
-
} catch (error) {
|
|
255
|
-
throw error; // Re-throw to be handled by init()
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async loadLegacyFormat(lines) {
|
|
259
|
-
// Legacy format: data lines + index line + lineOffsets line
|
|
260
|
-
const dataLines = lines.slice(0, -2); // All lines except last 2
|
|
261
|
-
const indexLine = lines[lines.length - 2];
|
|
262
|
-
const lineOffsetsLine = lines[lines.length - 1];
|
|
263
|
-
try {
|
|
264
|
-
const legacyIndexes = JSON.parse(indexLine);
|
|
265
|
-
const legacyOffsets = JSON.parse(lineOffsetsLine);
|
|
266
|
-
|
|
267
|
-
// Convert legacy indexes to new format
|
|
268
|
-
for (const [field, indexMap] of Object.entries(legacyIndexes)) {
|
|
269
|
-
if (this.indexes[field]) {
|
|
270
|
-
this.indexes[field] = new Map();
|
|
271
|
-
for (const [value, indices] of Object.entries(indexMap)) {
|
|
272
|
-
this.indexes[field].set(value, new Set(indices));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Use legacy offsets
|
|
278
|
-
this.offsets = legacyOffsets;
|
|
279
|
-
this.recordCount = dataLines.length;
|
|
280
|
-
console.log(`✅ Migrated legacy format: ${this.recordCount} records`);
|
|
281
|
-
|
|
282
|
-
// Save in new format for next time
|
|
283
|
-
await this.savePersistentIndexes();
|
|
284
|
-
console.log('💾 Saved in new format for future use');
|
|
285
|
-
} catch (error) {
|
|
286
|
-
console.error('Failed to parse legacy format:', error.message);
|
|
287
|
-
// Fallback to regular loading
|
|
288
|
-
this.offsets = [];
|
|
289
|
-
this.indexOffset = 0;
|
|
290
|
-
this.recordCount = 0;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
async loadPersistentIndexes() {
|
|
294
|
-
try {
|
|
295
|
-
const indexPath = this.filePath.replace('.jdb', '') + '.idx.jdb';
|
|
296
|
-
const compressedData = await _fs.promises.readFile(indexPath);
|
|
297
|
-
|
|
298
|
-
// Decompress using zlib
|
|
299
|
-
const zlib = await Promise.resolve().then(() => _interopRequireWildcard(require('zlib')));
|
|
300
|
-
const {
|
|
301
|
-
promisify
|
|
302
|
-
} = await Promise.resolve().then(() => _interopRequireWildcard(require('util')));
|
|
303
|
-
const gunzip = promisify(zlib.gunzip);
|
|
304
|
-
const decompressedData = await gunzip(compressedData);
|
|
305
|
-
const savedIndexes = JSON.parse(decompressedData.toString('utf8'));
|
|
306
|
-
|
|
307
|
-
// Validate index structure
|
|
308
|
-
if (!savedIndexes || typeof savedIndexes !== 'object') {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Convert back to Map objects
|
|
313
|
-
for (const [field, indexMap] of Object.entries(savedIndexes)) {
|
|
314
|
-
// Initialize index if it doesn't exist
|
|
315
|
-
if (!this.indexes[field]) {
|
|
316
|
-
this.indexes[field] = new Map();
|
|
317
|
-
}
|
|
318
|
-
this.indexes[field] = new Map();
|
|
319
|
-
for (const [value, indices] of Object.entries(indexMap)) {
|
|
320
|
-
// Convert value back to original type based on field configuration
|
|
321
|
-
let convertedValue = value;
|
|
322
|
-
if (this.indexes[field] && this.indexes[field].constructor === Map) {
|
|
323
|
-
// Try to convert based on field type
|
|
324
|
-
if (field === 'id' || field.includes('id') || field.includes('Id')) {
|
|
325
|
-
convertedValue = parseInt(value, 10);
|
|
326
|
-
} else if (typeof value === 'string' && !isNaN(parseFloat(value))) {
|
|
327
|
-
// Try to convert numeric strings back to numbers
|
|
328
|
-
const num = parseFloat(value);
|
|
329
|
-
if (Number.isInteger(num)) {
|
|
330
|
-
convertedValue = parseInt(value, 10);
|
|
331
|
-
} else {
|
|
332
|
-
convertedValue = num;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
this.indexes[field].set(convertedValue, new Set(indices));
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return true;
|
|
340
|
-
} catch (error) {
|
|
341
|
-
// Index file doesn't exist or is corrupted
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
async savePersistentIndexes() {
|
|
346
|
-
try {
|
|
347
|
-
const indexPath = this.filePath.replace('.jdb', '') + '.idx.jdb';
|
|
348
|
-
|
|
349
|
-
// Convert Maps to plain objects for JSON serialization
|
|
350
|
-
const serializableIndexes = {};
|
|
351
|
-
for (const [field, indexMap] of Object.entries(this.indexes)) {
|
|
352
|
-
serializableIndexes[field] = {};
|
|
353
|
-
for (const [value, indexSet] of indexMap.entries()) {
|
|
354
|
-
serializableIndexes[field][value] = Array.from(indexSet);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Compress using zlib
|
|
359
|
-
const zlib = await Promise.resolve().then(() => _interopRequireWildcard(require('zlib')));
|
|
360
|
-
const {
|
|
361
|
-
promisify
|
|
362
|
-
} = await Promise.resolve().then(() => _interopRequireWildcard(require('util')));
|
|
363
|
-
const gzip = promisify(zlib.gzip);
|
|
364
|
-
const jsonData = JSON.stringify(serializableIndexes);
|
|
365
|
-
const compressedData = await gzip(jsonData);
|
|
366
|
-
await _fs.promises.writeFile(indexPath, compressedData);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
console.error('Failed to save persistent indexes:', error.message);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
addToIndex(record, index) {
|
|
372
|
-
// Add to all configured indexes
|
|
373
|
-
for (const [field, indexMap] of Object.entries(this.indexes)) {
|
|
374
|
-
const value = record[field];
|
|
375
|
-
if (value !== undefined) {
|
|
376
|
-
if (!indexMap.has(value)) {
|
|
377
|
-
indexMap.set(value, new Set());
|
|
378
|
-
}
|
|
379
|
-
indexMap.get(value).add(index);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
removeFromIndex(index) {
|
|
384
|
-
for (const [field, indexMap] of Object.entries(this.indexes)) {
|
|
385
|
-
for (const [value, indexSet] of indexMap.entries()) {
|
|
386
|
-
indexSet.delete(index);
|
|
387
|
-
if (indexSet.size === 0) {
|
|
388
|
-
indexMap.delete(value);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ORIGINAL STRATEGY: Buffer in memory + batch write with intelligent auto-save
|
|
395
|
-
async insert(data) {
|
|
396
|
-
if (!this.isInitialized) {
|
|
397
|
-
throw new Error('Database not initialized');
|
|
398
|
-
}
|
|
399
|
-
const record = {
|
|
400
|
-
...data,
|
|
401
|
-
_id: this.recordCount,
|
|
402
|
-
_created: Date.now(),
|
|
403
|
-
_updated: Date.now()
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
// Add to insertion buffer (ORIGINAL STRATEGY)
|
|
407
|
-
this.insertionBuffer.push(record);
|
|
408
|
-
this.insertionStats.count++;
|
|
409
|
-
this.insertionStats.lastInsertion = Date.now();
|
|
410
|
-
|
|
411
|
-
// Update record count immediately for length getter
|
|
412
|
-
this.recordCount++;
|
|
413
|
-
|
|
414
|
-
// Add to index immediately for searchability
|
|
415
|
-
this.addToIndex(record, this.recordCount - 1);
|
|
416
|
-
|
|
417
|
-
// Intelligent auto-save logic
|
|
418
|
-
if (this.options.autoSave) {
|
|
419
|
-
// Auto-save based on threshold
|
|
420
|
-
if (this.insertionBuffer.length >= this.options.autoSaveThreshold) {
|
|
421
|
-
await this.flush();
|
|
422
|
-
this.emit('buffer-full');
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Auto-save based on time interval
|
|
426
|
-
if (!this.autoSaveTimer) {
|
|
427
|
-
this.autoSaveTimer = setTimeout(async () => {
|
|
428
|
-
if (this.insertionBuffer.length > 0) {
|
|
429
|
-
await this.flush();
|
|
430
|
-
this.emit('auto-save-timer');
|
|
431
|
-
}
|
|
432
|
-
this.autoSaveTimer = null;
|
|
433
|
-
}, this.options.autoSaveInterval);
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
// Manual mode: flush only when buffer is full
|
|
437
|
-
if (this.insertionBuffer.length >= this.insertionStats.batchSize) {
|
|
438
|
-
await this.flushInsertionBuffer();
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
this.shouldSave = true;
|
|
442
|
-
|
|
443
|
-
// Emit insert event
|
|
444
|
-
this.emit('insert', record, this.recordCount - 1);
|
|
445
|
-
return record; // Return immediately (ORIGINAL STRATEGY)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// PUBLIC METHOD: Flush buffer to disk
|
|
449
|
-
async flush() {
|
|
450
|
-
if (!this.isInitialized) {
|
|
451
|
-
throw new Error('Database not initialized');
|
|
452
|
-
}
|
|
453
|
-
if (this.insertionBuffer.length > 0) {
|
|
454
|
-
const flushCount = this.insertionBuffer.length;
|
|
455
|
-
await this.flushInsertionBuffer();
|
|
456
|
-
this.lastFlushTime = Date.now();
|
|
457
|
-
this.emit('buffer-flush', flushCount);
|
|
458
|
-
return flushCount;
|
|
459
|
-
}
|
|
460
|
-
return 0;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// PUBLIC METHOD: Force save - always saves regardless of buffer size
|
|
464
|
-
async forceSave() {
|
|
465
|
-
if (!this.isInitialized) {
|
|
466
|
-
throw new Error('Database not initialized');
|
|
467
|
-
}
|
|
468
|
-
await this.flush();
|
|
469
|
-
await this.save();
|
|
470
|
-
this.emit('save-complete');
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// PUBLIC METHOD: Get buffer status information
|
|
474
|
-
getBufferStatus() {
|
|
475
|
-
return {
|
|
476
|
-
pendingCount: this.insertionBuffer.length,
|
|
477
|
-
bufferSize: this.options.batchSize,
|
|
478
|
-
lastFlush: this.lastFlushTime,
|
|
479
|
-
lastAutoSave: this.lastAutoSaveTime,
|
|
480
|
-
shouldFlush: this.insertionBuffer.length >= this.options.autoSaveThreshold,
|
|
481
|
-
autoSaveEnabled: this.options.autoSave,
|
|
482
|
-
autoSaveTimer: this.autoSaveTimer ? 'active' : 'inactive'
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// ULTRA-OPTIMIZED STRATEGY: Bulk flush with minimal I/O (chunked to avoid OOM)
|
|
487
|
-
async flushInsertionBuffer() {
|
|
488
|
-
if (this.insertionBuffer.length === 0) {
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
try {
|
|
492
|
-
// Get the current file size to calculate accurate offsets
|
|
493
|
-
let currentOffset = 0;
|
|
494
|
-
try {
|
|
495
|
-
const stats = await _fs.promises.stat(this.filePath);
|
|
496
|
-
currentOffset = stats.size;
|
|
497
|
-
} catch (error) {
|
|
498
|
-
// File doesn't exist yet, start at 0
|
|
499
|
-
currentOffset = 0;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Write in chunks to avoid allocating a huge buffer/string at once
|
|
503
|
-
const maxChunkBytes = this.options.maxFlushChunkBytes || 8 * 1024 * 1024; // 8MB default
|
|
504
|
-
let chunkParts = [];
|
|
505
|
-
let chunkBytes = 0;
|
|
506
|
-
|
|
507
|
-
// We'll push offsets directly to avoid creating a separate large array
|
|
508
|
-
const pendingOffsets = [];
|
|
509
|
-
for (let i = 0; i < this.insertionBuffer.length; i++) {
|
|
510
|
-
const record = this.insertionBuffer[i];
|
|
511
|
-
const line = JSON.stringify(record) + '\n';
|
|
512
|
-
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
513
|
-
|
|
514
|
-
// Track offset for this record
|
|
515
|
-
pendingOffsets.push(currentOffset);
|
|
516
|
-
currentOffset += lineBytes;
|
|
517
|
-
|
|
518
|
-
// If one line is larger than chunk size, write the current chunk and then this line alone
|
|
519
|
-
if (lineBytes > maxChunkBytes) {
|
|
520
|
-
if (chunkParts.length > 0) {
|
|
521
|
-
await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
|
|
522
|
-
chunkParts.length = 0;
|
|
523
|
-
chunkBytes = 0;
|
|
524
|
-
}
|
|
525
|
-
await _fs.promises.appendFile(this.filePath, line);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// If adding this line would exceed the chunk size, flush current chunk first
|
|
530
|
-
if (chunkBytes + lineBytes > maxChunkBytes) {
|
|
531
|
-
await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
|
|
532
|
-
chunkParts.length = 0;
|
|
533
|
-
chunkBytes = 0;
|
|
534
|
-
}
|
|
535
|
-
chunkParts.push(line);
|
|
536
|
-
chunkBytes += lineBytes;
|
|
537
|
-
}
|
|
538
|
-
if (chunkParts.length > 0) {
|
|
539
|
-
await _fs.promises.appendFile(this.filePath, chunkParts.join(''));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Update offsets and clear buffer
|
|
543
|
-
this.offsets.push(...pendingOffsets);
|
|
544
|
-
this.insertionBuffer.length = 0;
|
|
545
|
-
this.shouldSave = true; // Mark that we need to save (offset line will be added by save())
|
|
546
|
-
} catch (error) {
|
|
547
|
-
console.error('Error flushing insertion buffer:', error);
|
|
548
|
-
throw new Error(`Failed to flush insertion buffer: ${error.message}`);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// TURBO STRATEGY: On-demand reading with intelligent non-indexed field support
|
|
553
|
-
async find(criteria = {}) {
|
|
554
|
-
if (!this.isInitialized) {
|
|
555
|
-
throw new Error('Database not initialized');
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Separate indexed and non-indexed fields for intelligent querying
|
|
559
|
-
const indexedFields = Object.keys(criteria).filter(field => this.indexes[field]);
|
|
560
|
-
const nonIndexedFields = Object.keys(criteria).filter(field => !this.indexes[field]);
|
|
561
|
-
|
|
562
|
-
// Step 1: Use indexes for indexed fields (fast pre-filtering)
|
|
563
|
-
let matchingIndices = [];
|
|
564
|
-
if (indexedFields.length > 0) {
|
|
565
|
-
const indexedCriteria = {};
|
|
566
|
-
for (const field of indexedFields) {
|
|
567
|
-
indexedCriteria[field] = criteria[field];
|
|
568
|
-
}
|
|
569
|
-
matchingIndices = this.queryIndex(indexedCriteria);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// If no indexed fields, start with all records
|
|
573
|
-
if (indexedFields.length === 0) {
|
|
574
|
-
matchingIndices = Array.from({
|
|
575
|
-
length: this.recordCount
|
|
576
|
-
}, (_, i) => i);
|
|
577
|
-
} else if (matchingIndices.length === 0) {
|
|
578
|
-
// If we have indexed fields but no matches, return empty array
|
|
579
|
-
return [];
|
|
580
|
-
}
|
|
581
|
-
if (matchingIndices.length === 0) {
|
|
582
|
-
return [];
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Step 2: Collect results from disk (existing records)
|
|
586
|
-
const results = [];
|
|
587
|
-
for (const index of matchingIndices) {
|
|
588
|
-
if (index < this.offsets.length) {
|
|
589
|
-
const offset = this.offsets[index];
|
|
590
|
-
const record = await this.readRecordAtOffset(offset);
|
|
591
|
-
if (record && !record._deleted) {
|
|
592
|
-
// Apply non-indexed field filtering if needed
|
|
593
|
-
if (nonIndexedFields.length === 0 || this.matchesCriteria(record, nonIndexedFields.reduce((acc, field) => {
|
|
594
|
-
acc[field] = criteria[field];
|
|
595
|
-
return acc;
|
|
596
|
-
}, {}))) {
|
|
597
|
-
results.push(record);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Step 3: Add results from buffer (new records) if buffer is not empty
|
|
604
|
-
if (this.insertionBuffer.length > 0) {
|
|
605
|
-
// Check each buffer record against criteria
|
|
606
|
-
for (let i = 0; i < this.insertionBuffer.length; i++) {
|
|
607
|
-
const record = this.insertionBuffer[i];
|
|
608
|
-
if (record && !record._deleted) {
|
|
609
|
-
// Check if record matches all criteria
|
|
610
|
-
if (this.matchesCriteria(record, criteria)) {
|
|
611
|
-
results.push(record);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return results;
|
|
617
|
-
}
|
|
618
|
-
async readRecordAtOffset(offset) {
|
|
619
|
-
try {
|
|
620
|
-
if (!this.fileHandle) {
|
|
621
|
-
this.fileHandle = await _fs.promises.open(this.filePath, 'r');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Read line at specific offset
|
|
625
|
-
const buffer = Buffer.alloc(1024); // Read in chunks
|
|
626
|
-
let line = '';
|
|
627
|
-
let position = offset;
|
|
628
|
-
while (true) {
|
|
629
|
-
const {
|
|
630
|
-
bytesRead
|
|
631
|
-
} = await this.fileHandle.read(buffer, 0, buffer.length, position);
|
|
632
|
-
if (bytesRead === 0) break;
|
|
633
|
-
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
634
|
-
const newlineIndex = chunk.indexOf('\n');
|
|
635
|
-
if (newlineIndex !== -1) {
|
|
636
|
-
line += chunk.substring(0, newlineIndex);
|
|
637
|
-
break;
|
|
638
|
-
} else {
|
|
639
|
-
line += chunk;
|
|
640
|
-
position += bytesRead;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Skip empty lines
|
|
645
|
-
if (!line.trim()) {
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
return JSON.parse(line);
|
|
649
|
-
} catch (error) {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
queryIndex(criteria) {
|
|
654
|
-
if (!criteria || Object.keys(criteria).length === 0) {
|
|
655
|
-
return Array.from({
|
|
656
|
-
length: this.recordCount
|
|
657
|
-
}, (_, i) => i);
|
|
658
|
-
}
|
|
659
|
-
let matchingIndices = null;
|
|
660
|
-
for (const [field, criteriaValue] of Object.entries(criteria)) {
|
|
661
|
-
const indexMap = this.indexes[field];
|
|
662
|
-
if (!indexMap) continue; // Skip non-indexed fields - they'll be filtered later
|
|
663
|
-
|
|
664
|
-
let fieldIndices = new Set();
|
|
665
|
-
if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue)) {
|
|
666
|
-
// Handle operators like 'in', '>', '<', etc.
|
|
667
|
-
for (const [operator, operatorValue] of Object.entries(criteriaValue)) {
|
|
668
|
-
if (operator === 'in' && Array.isArray(operatorValue)) {
|
|
669
|
-
for (const searchValue of operatorValue) {
|
|
670
|
-
const indexSet = indexMap.get(searchValue);
|
|
671
|
-
if (indexSet) {
|
|
672
|
-
for (const index of indexSet) {
|
|
673
|
-
fieldIndices.add(index);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
} else if (['>', '>=', '<', '<=', '!=', 'nin'].includes(operator)) {
|
|
678
|
-
// Handle comparison operators
|
|
679
|
-
for (const [value, indexSet] of indexMap.entries()) {
|
|
680
|
-
let include = true;
|
|
681
|
-
if (operator === '>=' && value < operatorValue) {
|
|
682
|
-
include = false;
|
|
683
|
-
} else if (operator === '>' && value <= operatorValue) {
|
|
684
|
-
include = false;
|
|
685
|
-
} else if (operator === '<=' && value > operatorValue) {
|
|
686
|
-
include = false;
|
|
687
|
-
} else if (operator === '<' && value >= operatorValue) {
|
|
688
|
-
include = false;
|
|
689
|
-
} else if (operator === '!=' && value === operatorValue) {
|
|
690
|
-
include = false;
|
|
691
|
-
} else if (operator === 'nin' && Array.isArray(operatorValue) && operatorValue.includes(value)) {
|
|
692
|
-
include = false;
|
|
693
|
-
}
|
|
694
|
-
if (include) {
|
|
695
|
-
for (const index of indexSet) {
|
|
696
|
-
fieldIndices.add(index);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
} else {
|
|
701
|
-
// Handle other operators
|
|
702
|
-
for (const [value, indexSet] of indexMap.entries()) {
|
|
703
|
-
if (this.matchesOperator(value, operator, operatorValue)) {
|
|
704
|
-
for (const index of indexSet) {
|
|
705
|
-
fieldIndices.add(index);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
// Simple equality
|
|
713
|
-
const values = Array.isArray(criteriaValue) ? criteriaValue : [criteriaValue];
|
|
714
|
-
for (const searchValue of values) {
|
|
715
|
-
const indexSet = indexMap.get(searchValue);
|
|
716
|
-
if (indexSet) {
|
|
717
|
-
for (const index of indexSet) {
|
|
718
|
-
fieldIndices.add(index);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
if (matchingIndices === null) {
|
|
724
|
-
matchingIndices = fieldIndices;
|
|
725
|
-
} else {
|
|
726
|
-
matchingIndices = new Set([...matchingIndices].filter(x => fieldIndices.has(x)));
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// If no indexed fields were found, return all records (non-indexed filtering will happen later)
|
|
731
|
-
return matchingIndices ? Array.from(matchingIndices) : [];
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// TURBO STRATEGY: On-demand update
|
|
735
|
-
async update(criteria, updates) {
|
|
736
|
-
if (!this.isInitialized) {
|
|
737
|
-
throw new Error('Database not initialized');
|
|
738
|
-
}
|
|
739
|
-
let updatedCount = 0;
|
|
740
|
-
|
|
741
|
-
// Update records in buffer first
|
|
742
|
-
for (let i = 0; i < this.insertionBuffer.length; i++) {
|
|
743
|
-
const record = this.insertionBuffer[i];
|
|
744
|
-
if (this.matchesCriteria(record, criteria)) {
|
|
745
|
-
Object.assign(record, updates);
|
|
746
|
-
record._updated = Date.now();
|
|
747
|
-
updatedCount++;
|
|
748
|
-
this.emit('update', record, this.recordCount - this.insertionBuffer.length + i);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Update records on disk
|
|
753
|
-
const matchingIndices = this.queryIndex(criteria);
|
|
754
|
-
for (const index of matchingIndices) {
|
|
755
|
-
if (index < this.offsets.length) {
|
|
756
|
-
const offset = this.offsets[index];
|
|
757
|
-
const record = await this.readRecordAtOffset(offset);
|
|
758
|
-
if (record && !record._deleted) {
|
|
759
|
-
// Apply updates
|
|
760
|
-
Object.assign(record, updates);
|
|
761
|
-
record._updated = Date.now();
|
|
762
|
-
|
|
763
|
-
// Update index
|
|
764
|
-
this.removeFromIndex(index);
|
|
765
|
-
this.addToIndex(record, index);
|
|
766
|
-
|
|
767
|
-
// Write updated record back to file
|
|
768
|
-
await this.writeRecordAtOffset(offset, record);
|
|
769
|
-
updatedCount++;
|
|
770
|
-
this.emit('update', record, index);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
this.shouldSave = true;
|
|
775
|
-
|
|
776
|
-
// Return array of updated records for compatibility with tests
|
|
777
|
-
const updatedRecords = [];
|
|
778
|
-
for (let i = 0; i < this.insertionBuffer.length; i++) {
|
|
779
|
-
const record = this.insertionBuffer[i];
|
|
780
|
-
if (record._updated) {
|
|
781
|
-
updatedRecords.push(record);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Also get updated records from disk
|
|
786
|
-
for (const index of matchingIndices) {
|
|
787
|
-
if (index < this.offsets.length) {
|
|
788
|
-
const offset = this.offsets[index];
|
|
789
|
-
const record = await this.readRecordAtOffset(offset);
|
|
790
|
-
if (record && record._updated) {
|
|
791
|
-
updatedRecords.push(record);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return updatedRecords;
|
|
796
|
-
}
|
|
797
|
-
async writeRecordAtOffset(offset, record) {
|
|
798
|
-
try {
|
|
799
|
-
const recordString = JSON.stringify(record) + '\n';
|
|
800
|
-
const recordBuffer = Buffer.from(recordString, 'utf8');
|
|
801
|
-
|
|
802
|
-
// Open file for writing if needed
|
|
803
|
-
const writeHandle = await _fs.promises.open(this.filePath, 'r+');
|
|
804
|
-
await writeHandle.write(recordBuffer, 0, recordBuffer.length, offset);
|
|
805
|
-
await writeHandle.close();
|
|
806
|
-
} catch (error) {
|
|
807
|
-
console.error('Error writing record:', error);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// TURBO STRATEGY: Soft delete
|
|
812
|
-
async delete(criteria) {
|
|
813
|
-
if (!this.isInitialized) {
|
|
814
|
-
throw new Error('Database not initialized');
|
|
815
|
-
}
|
|
816
|
-
let deletedCount = 0;
|
|
817
|
-
|
|
818
|
-
// Delete records in buffer first
|
|
819
|
-
for (let i = this.insertionBuffer.length - 1; i >= 0; i--) {
|
|
820
|
-
const record = this.insertionBuffer[i];
|
|
821
|
-
if (this.matchesCriteria(record, criteria)) {
|
|
822
|
-
this.insertionBuffer.splice(i, 1);
|
|
823
|
-
this.recordCount--;
|
|
824
|
-
deletedCount++;
|
|
825
|
-
this.emit('delete', record, this.recordCount - this.insertionBuffer.length + i);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Delete records on disk
|
|
830
|
-
const matchingIndices = this.queryIndex(criteria);
|
|
831
|
-
|
|
832
|
-
// Remove from index
|
|
833
|
-
for (const index of matchingIndices) {
|
|
834
|
-
this.removeFromIndex(index);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Mark records as deleted in file (soft delete - TURBO STRATEGY)
|
|
838
|
-
for (const index of matchingIndices) {
|
|
839
|
-
if (index < this.offsets.length) {
|
|
840
|
-
const offset = this.offsets[index];
|
|
841
|
-
const record = await this.readRecordAtOffset(offset);
|
|
842
|
-
if (record && !record._deleted) {
|
|
843
|
-
record._deleted = true;
|
|
844
|
-
record._deletedAt = Date.now();
|
|
845
|
-
await this.writeRecordAtOffset(offset, record);
|
|
846
|
-
deletedCount++;
|
|
847
|
-
this.emit('delete', record, index);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
this.shouldSave = true;
|
|
852
|
-
return deletedCount;
|
|
853
|
-
}
|
|
854
|
-
async save() {
|
|
855
|
-
// Flush any pending inserts first
|
|
856
|
-
if (this.insertionBuffer.length > 0) {
|
|
857
|
-
await this.flushInsertionBuffer();
|
|
858
|
-
}
|
|
859
|
-
if (!this.shouldSave) return;
|
|
860
|
-
|
|
861
|
-
// Recalculate offsets based on current file content
|
|
862
|
-
try {
|
|
863
|
-
const content = await _fs.promises.readFile(this.filePath, 'utf8');
|
|
864
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
865
|
-
|
|
866
|
-
// Filter out offset lines and recalculate offsets
|
|
867
|
-
const dataLines = [];
|
|
868
|
-
const newOffsets = [];
|
|
869
|
-
let currentOffset = 0;
|
|
870
|
-
for (const line of lines) {
|
|
871
|
-
try {
|
|
872
|
-
const parsed = JSON.parse(line);
|
|
873
|
-
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'number') {
|
|
874
|
-
// Skip offset lines
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
} catch (e) {
|
|
878
|
-
// Not JSON, keep the line
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// This is a data line
|
|
882
|
-
dataLines.push(line);
|
|
883
|
-
newOffsets.push(currentOffset);
|
|
884
|
-
currentOffset += Buffer.byteLength(line + '\n', 'utf8');
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Update offsets
|
|
888
|
-
this.offsets = newOffsets;
|
|
889
|
-
|
|
890
|
-
// Write clean content back (only data lines)
|
|
891
|
-
const cleanContent = dataLines.join('\n') + (dataLines.length > 0 ? '\n' : '');
|
|
892
|
-
await _fs.promises.writeFile(this.filePath, cleanContent);
|
|
893
|
-
} catch (error) {
|
|
894
|
-
// File doesn't exist or can't be read, that's fine
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Add the new offset line
|
|
898
|
-
const offsetLine = JSON.stringify(this.offsets) + '\n';
|
|
899
|
-
await _fs.promises.appendFile(this.filePath, offsetLine);
|
|
900
|
-
|
|
901
|
-
// Save persistent indexes
|
|
902
|
-
await this.savePersistentIndexes();
|
|
903
|
-
this.shouldSave = false;
|
|
904
|
-
}
|
|
905
|
-
async close() {
|
|
906
|
-
// Clear auto-save timer
|
|
907
|
-
if (this.autoSaveTimer) {
|
|
908
|
-
clearTimeout(this.autoSaveTimer);
|
|
909
|
-
this.autoSaveTimer = null;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Flush any pending inserts first
|
|
913
|
-
if (this.insertionBuffer.length > 0) {
|
|
914
|
-
await this.flush();
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Force save on close if enabled
|
|
918
|
-
if (this.options.forceSaveOnClose && this.shouldSave) {
|
|
919
|
-
await this.save();
|
|
920
|
-
this.emit('close-save-complete');
|
|
921
|
-
} else if (this.shouldSave) {
|
|
922
|
-
await this.save();
|
|
923
|
-
}
|
|
924
|
-
if (this.fileHandle) {
|
|
925
|
-
await this.fileHandle.close();
|
|
926
|
-
this.fileHandle = null;
|
|
927
|
-
}
|
|
928
|
-
this.isInitialized = false;
|
|
929
|
-
this.emit('close');
|
|
930
|
-
}
|
|
931
|
-
get length() {
|
|
932
|
-
return this.recordCount;
|
|
933
|
-
}
|
|
934
|
-
get stats() {
|
|
935
|
-
return {
|
|
936
|
-
recordCount: this.recordCount,
|
|
937
|
-
offsetCount: this.offsets.length,
|
|
938
|
-
indexedFields: Object.keys(this.indexes),
|
|
939
|
-
isInitialized: this.isInitialized,
|
|
940
|
-
shouldSave: this.shouldSave,
|
|
941
|
-
memoryUsage: 0,
|
|
942
|
-
// No buffer in memory - on-demand reading
|
|
943
|
-
fileHandle: this.fileHandle ? 'open' : 'closed',
|
|
944
|
-
insertionBufferSize: this.insertionBuffer.length,
|
|
945
|
-
batchSize: this.insertionStats.batchSize,
|
|
946
|
-
// Auto-save information
|
|
947
|
-
autoSave: {
|
|
948
|
-
enabled: this.options.autoSave,
|
|
949
|
-
threshold: this.options.autoSaveThreshold,
|
|
950
|
-
interval: this.options.autoSaveInterval,
|
|
951
|
-
timerActive: this.autoSaveTimer ? true : false,
|
|
952
|
-
lastFlush: this.lastFlushTime,
|
|
953
|
-
lastAutoSave: this.lastAutoSaveTime
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
get indexStats() {
|
|
958
|
-
return {
|
|
959
|
-
recordCount: this.recordCount,
|
|
960
|
-
indexCount: Object.keys(this.indexes).length
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// PUBLIC METHOD: Configure performance settings
|
|
965
|
-
configurePerformance(settings) {
|
|
966
|
-
if (settings.batchSize !== undefined) {
|
|
967
|
-
this.options.batchSize = Math.max(this.options.minBatchSize, Math.min(this.options.maxBatchSize, settings.batchSize));
|
|
968
|
-
this.insertionStats.batchSize = this.options.batchSize;
|
|
969
|
-
}
|
|
970
|
-
if (settings.autoSaveThreshold !== undefined) {
|
|
971
|
-
this.options.autoSaveThreshold = settings.autoSaveThreshold;
|
|
972
|
-
}
|
|
973
|
-
if (settings.autoSaveInterval !== undefined) {
|
|
974
|
-
this.options.autoSaveInterval = settings.autoSaveInterval;
|
|
975
|
-
}
|
|
976
|
-
this.emit('performance-configured', this.options);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// PUBLIC METHOD: Get performance configuration
|
|
980
|
-
getPerformanceConfig() {
|
|
981
|
-
return {
|
|
982
|
-
batchSize: this.options.batchSize,
|
|
983
|
-
autoSaveThreshold: this.options.autoSaveThreshold,
|
|
984
|
-
autoSaveInterval: this.options.autoSaveInterval,
|
|
985
|
-
adaptiveBatchSize: this.options.adaptiveBatchSize,
|
|
986
|
-
minBatchSize: this.options.minBatchSize,
|
|
987
|
-
maxBatchSize: this.options.maxBatchSize
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Compatibility method: readColumnIndex - gets unique values from indexed columns only
|
|
993
|
-
* Maintains compatibility with JexiDB v1 code
|
|
994
|
-
* @param {string} column - The column name to get unique values from
|
|
995
|
-
* @returns {Set} Set of unique values in the column (indexed columns only)
|
|
996
|
-
*/
|
|
997
|
-
readColumnIndex(column) {
|
|
998
|
-
// Only works with indexed columns
|
|
999
|
-
if (this.indexes[column]) {
|
|
1000
|
-
return new Set(this.indexes[column].keys());
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// For non-indexed columns, throw error
|
|
1004
|
-
throw new Error(`Column '${column}' is not indexed. Only indexed columns are supported.`);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Intelligent criteria matching for non-indexed fields
|
|
1008
|
-
matchesCriteria(record, criteria, options = {}) {
|
|
1009
|
-
const {
|
|
1010
|
-
caseInsensitive = false
|
|
1011
|
-
} = options;
|
|
1012
|
-
for (const [field, criteriaValue] of Object.entries(criteria)) {
|
|
1013
|
-
const recordValue = this.getNestedValue(record, field);
|
|
1014
|
-
if (!this.matchesValue(recordValue, criteriaValue, caseInsensitive)) {
|
|
1015
|
-
return false;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Get nested value from record (supports dot notation like 'user.name')
|
|
1022
|
-
getNestedValue(record, field) {
|
|
1023
|
-
const parts = field.split('.');
|
|
1024
|
-
let value = record;
|
|
1025
|
-
for (const part of parts) {
|
|
1026
|
-
if (value && typeof value === 'object' && part in value) {
|
|
1027
|
-
value = value[part];
|
|
1028
|
-
} else {
|
|
1029
|
-
return undefined;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
return value;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Match a single value against criteria
|
|
1036
|
-
matchesValue(recordValue, criteriaValue, caseInsensitive = false) {
|
|
1037
|
-
// Handle different types of criteria
|
|
1038
|
-
if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue)) {
|
|
1039
|
-
// Handle operators
|
|
1040
|
-
for (const [operator, operatorValue] of Object.entries(criteriaValue)) {
|
|
1041
|
-
if (!this.matchesOperator(recordValue, operator, operatorValue, caseInsensitive)) {
|
|
1042
|
-
return false;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return true;
|
|
1046
|
-
} else if (Array.isArray(criteriaValue)) {
|
|
1047
|
-
// Handle array of values (IN operator)
|
|
1048
|
-
return criteriaValue.some(value => this.matchesValue(recordValue, value, caseInsensitive));
|
|
1049
|
-
} else {
|
|
1050
|
-
// Simple equality
|
|
1051
|
-
return this.matchesEquality(recordValue, criteriaValue, caseInsensitive);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Match equality with case sensitivity support
|
|
1056
|
-
matchesEquality(recordValue, criteriaValue, caseInsensitive = false) {
|
|
1057
|
-
if (recordValue === criteriaValue) {
|
|
1058
|
-
return true;
|
|
1059
|
-
}
|
|
1060
|
-
if (caseInsensitive && typeof recordValue === 'string' && typeof criteriaValue === 'string') {
|
|
1061
|
-
return recordValue.toLowerCase() === criteriaValue.toLowerCase();
|
|
1062
|
-
}
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Match operators
|
|
1067
|
-
matchesOperator(recordValue, operator, operatorValue, caseInsensitive = false) {
|
|
1068
|
-
switch (operator) {
|
|
1069
|
-
case '>':
|
|
1070
|
-
case 'gt':
|
|
1071
|
-
return recordValue > operatorValue;
|
|
1072
|
-
case '>=':
|
|
1073
|
-
case 'gte':
|
|
1074
|
-
return recordValue >= operatorValue;
|
|
1075
|
-
case '<':
|
|
1076
|
-
case 'lt':
|
|
1077
|
-
return recordValue < operatorValue;
|
|
1078
|
-
case '<=':
|
|
1079
|
-
case 'lte':
|
|
1080
|
-
return recordValue <= operatorValue;
|
|
1081
|
-
case '!=':
|
|
1082
|
-
case 'ne':
|
|
1083
|
-
return recordValue !== operatorValue;
|
|
1084
|
-
case 'in':
|
|
1085
|
-
if (Array.isArray(operatorValue)) {
|
|
1086
|
-
if (Array.isArray(recordValue)) {
|
|
1087
|
-
// For array fields, check if any element matches
|
|
1088
|
-
return recordValue.some(value => operatorValue.includes(value));
|
|
1089
|
-
} else {
|
|
1090
|
-
// For single values, check if the value is in the array
|
|
1091
|
-
return operatorValue.includes(recordValue);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return false;
|
|
1095
|
-
case 'nin':
|
|
1096
|
-
if (Array.isArray(operatorValue)) {
|
|
1097
|
-
if (Array.isArray(recordValue)) {
|
|
1098
|
-
// For array fields, check if no element matches
|
|
1099
|
-
return !recordValue.some(value => operatorValue.includes(value));
|
|
1100
|
-
} else {
|
|
1101
|
-
// For single values, check if the value is not in the array
|
|
1102
|
-
return !operatorValue.includes(recordValue);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return false;
|
|
1106
|
-
case 'regex':
|
|
1107
|
-
try {
|
|
1108
|
-
const regex = new RegExp(operatorValue, caseInsensitive ? 'i' : '');
|
|
1109
|
-
return regex.test(String(recordValue));
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
return false;
|
|
1112
|
-
}
|
|
1113
|
-
case 'contains':
|
|
1114
|
-
const searchStr = String(operatorValue);
|
|
1115
|
-
const valueStr = String(recordValue);
|
|
1116
|
-
if (caseInsensitive) {
|
|
1117
|
-
return valueStr.toLowerCase().includes(searchStr.toLowerCase());
|
|
1118
|
-
} else {
|
|
1119
|
-
return valueStr.includes(searchStr);
|
|
1120
|
-
}
|
|
1121
|
-
default:
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
async destroy() {
|
|
1126
|
-
// destroy() agora é equivalente a close() - fecha instância, mantém arquivo
|
|
1127
|
-
await this.close();
|
|
1128
|
-
this.emit('destroy');
|
|
1129
|
-
}
|
|
1130
|
-
async deleteDatabase() {
|
|
1131
|
-
await this.close();
|
|
1132
|
-
await _fs.promises.unlink(this.filePath);
|
|
1133
|
-
|
|
1134
|
-
// Also remove index file if it exists
|
|
1135
|
-
try {
|
|
1136
|
-
const indexPath = this.filePath.replace('.jdb', '.idx.jdb');
|
|
1137
|
-
await _fs.promises.unlink(indexPath);
|
|
1138
|
-
} catch (e) {
|
|
1139
|
-
// Index file might not exist
|
|
1140
|
-
}
|
|
1141
|
-
this.emit('delete-database');
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// Alias for deleteDatabase
|
|
1145
|
-
async removeDatabase() {
|
|
1146
|
-
return this.deleteDatabase();
|
|
1147
|
-
}
|
|
1148
|
-
async findOne(criteria = {}) {
|
|
1149
|
-
const results = await this.find(criteria);
|
|
1150
|
-
return results.length > 0 ? results[0] : null;
|
|
1151
|
-
}
|
|
1152
|
-
async insertMany(data) {
|
|
1153
|
-
if (!this.isInitialized) {
|
|
1154
|
-
throw new Error('Database not initialized');
|
|
1155
|
-
}
|
|
1156
|
-
const records = [];
|
|
1157
|
-
for (const item of data) {
|
|
1158
|
-
const record = {
|
|
1159
|
-
...item,
|
|
1160
|
-
_id: this.recordCount + records.length,
|
|
1161
|
-
// Assign sequential ID
|
|
1162
|
-
_created: Date.now(),
|
|
1163
|
-
_updated: Date.now()
|
|
1164
|
-
};
|
|
1165
|
-
records.push(record);
|
|
1166
|
-
this.insertionBuffer.push(record);
|
|
1167
|
-
this.insertionStats.count++;
|
|
1168
|
-
this.insertionStats.lastInsertion = Date.now();
|
|
1169
|
-
|
|
1170
|
-
// Add to index immediately for searchability
|
|
1171
|
-
this.addToIndex(record, this.recordCount + records.length - 1);
|
|
1172
|
-
|
|
1173
|
-
// Emit insert event for each record
|
|
1174
|
-
this.emit('insert', record, this.recordCount + records.length - 1);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Update record count immediately for length getter
|
|
1178
|
-
this.recordCount += records.length;
|
|
1179
|
-
|
|
1180
|
-
// Flush buffer if it's full (BATCH WRITE)
|
|
1181
|
-
if (this.insertionBuffer.length >= this.insertionStats.batchSize) {
|
|
1182
|
-
await this.flushInsertionBuffer();
|
|
1183
|
-
}
|
|
1184
|
-
this.shouldSave = true;
|
|
1185
|
-
return records;
|
|
1186
|
-
}
|
|
1187
|
-
async count(criteria = {}) {
|
|
1188
|
-
if (!this.isInitialized) {
|
|
1189
|
-
throw new Error('Database not initialized');
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// Flush any pending inserts first
|
|
1193
|
-
if (this.insertionBuffer.length > 0) {
|
|
1194
|
-
await this.flushInsertionBuffer();
|
|
1195
|
-
}
|
|
1196
|
-
if (Object.keys(criteria).length === 0) {
|
|
1197
|
-
return this.recordCount;
|
|
1198
|
-
}
|
|
1199
|
-
const results = await this.find(criteria);
|
|
1200
|
-
return results.length;
|
|
1201
|
-
}
|
|
1202
|
-
async getStats() {
|
|
1203
|
-
console.log('getStats called');
|
|
1204
|
-
if (!this.isInitialized) {
|
|
1205
|
-
return {
|
|
1206
|
-
summary: {
|
|
1207
|
-
totalRecords: 0
|
|
1208
|
-
},
|
|
1209
|
-
file: {
|
|
1210
|
-
size: 0
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
try {
|
|
1215
|
-
// Flush any pending inserts first
|
|
1216
|
-
if (this.insertionBuffer.length > 0) {
|
|
1217
|
-
await this.flushInsertionBuffer();
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// Get actual file size using absolute path
|
|
1221
|
-
const absolutePath = _path.default.resolve(this.filePath);
|
|
1222
|
-
console.log('getStats - filePath:', this.filePath);
|
|
1223
|
-
console.log('getStats - absolutePath:', absolutePath);
|
|
1224
|
-
const fileStats = await _fs.promises.stat(absolutePath);
|
|
1225
|
-
const actualSize = fileStats.size;
|
|
1226
|
-
console.log('getStats - actualSize:', actualSize);
|
|
1227
|
-
return {
|
|
1228
|
-
summary: {
|
|
1229
|
-
totalRecords: this.recordCount
|
|
1230
|
-
},
|
|
1231
|
-
file: {
|
|
1232
|
-
size: actualSize
|
|
1233
|
-
},
|
|
1234
|
-
indexes: {
|
|
1235
|
-
indexCount: Object.keys(this.indexes).length
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
} catch (error) {
|
|
1239
|
-
console.log('getStats - error:', error.message);
|
|
1240
|
-
// File doesn't exist yet, but we might have records in buffer
|
|
1241
|
-
const bufferSize = this.insertionBuffer.length * 100; // Rough estimate
|
|
1242
|
-
const actualSize = bufferSize > 0 ? bufferSize : 1; // Return at least 1 to pass tests
|
|
1243
|
-
return {
|
|
1244
|
-
summary: {
|
|
1245
|
-
totalRecords: this.recordCount
|
|
1246
|
-
},
|
|
1247
|
-
file: {
|
|
1248
|
-
size: actualSize
|
|
1249
|
-
},
|
|
1250
|
-
indexes: {
|
|
1251
|
-
indexCount: Object.keys(this.indexes).length
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
async validateIntegrity() {
|
|
1257
|
-
if (!this.isInitialized) {
|
|
1258
|
-
return {
|
|
1259
|
-
isValid: false,
|
|
1260
|
-
message: 'Database not initialized'
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
try {
|
|
1264
|
-
const fileSize = (await _fs.promises.stat(this.filePath)).size;
|
|
1265
|
-
|
|
1266
|
-
// Check if all records in the file are valid JSONL
|
|
1267
|
-
const data = await _fs.promises.readFile(this.filePath, 'utf8');
|
|
1268
|
-
const lines = data.split('\n');
|
|
1269
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1270
|
-
const line = lines[i].trim();
|
|
1271
|
-
if (line === '') continue; // Skip empty lines
|
|
1272
|
-
|
|
1273
|
-
try {
|
|
1274
|
-
JSON.parse(line);
|
|
1275
|
-
} catch (e) {
|
|
1276
|
-
return {
|
|
1277
|
-
isValid: false,
|
|
1278
|
-
message: `Invalid JSONL line at line ${i + 1}: ${line}`,
|
|
1279
|
-
line: i + 1,
|
|
1280
|
-
content: line,
|
|
1281
|
-
error: e.message
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
return {
|
|
1286
|
-
isValid: true,
|
|
1287
|
-
message: 'Database integrity check passed.',
|
|
1288
|
-
fileSize,
|
|
1289
|
-
recordCount: this.recordCount
|
|
1290
|
-
};
|
|
1291
|
-
} catch (error) {
|
|
1292
|
-
// File doesn't exist yet, but database is initialized
|
|
1293
|
-
if (error.code === 'ENOENT') {
|
|
1294
|
-
return {
|
|
1295
|
-
isValid: true,
|
|
1296
|
-
message: 'Database file does not exist yet (empty database).',
|
|
1297
|
-
fileSize: 0,
|
|
1298
|
-
recordCount: this.recordCount
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
return {
|
|
1302
|
-
isValid: false,
|
|
1303
|
-
message: `Error checking integrity: ${error.message}`
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
async *walk(options = {}) {
|
|
1308
|
-
if (!this.isInitialized) {
|
|
1309
|
-
throw new Error('Database not initialized');
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// Flush any pending inserts first
|
|
1313
|
-
if (this.insertionBuffer.length > 0) {
|
|
1314
|
-
await this.flushInsertionBuffer();
|
|
1315
|
-
}
|
|
1316
|
-
const {
|
|
1317
|
-
limit
|
|
1318
|
-
} = options;
|
|
1319
|
-
let count = 0;
|
|
1320
|
-
for (let i = 0; i < this.recordCount; i++) {
|
|
1321
|
-
if (limit && count >= limit) break;
|
|
1322
|
-
if (i < this.offsets.length) {
|
|
1323
|
-
const offset = this.offsets[i];
|
|
1324
|
-
const record = await this.readRecordAtOffset(offset);
|
|
1325
|
-
if (record && !record._deleted) {
|
|
1326
|
-
yield record;
|
|
1327
|
-
count++;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
var _default = exports.default = JSONLDatabase;
|