@push.rocks/smartdb 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.smartconfig.json +7 -4
  2. package/dist_rust/rustdb_linux_amd64 +0 -0
  3. package/dist_rust/rustdb_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +3 -3
  5. package/dist_ts/ts_local/classes.localsmartdb.d.ts +5 -5
  6. package/dist_ts/ts_local/classes.localsmartdb.js +5 -6
  7. package/dist_ts/ts_local/plugins.d.ts +1 -2
  8. package/dist_ts/ts_local/plugins.js +3 -3
  9. package/dist_ts/ts_smartdb/index.d.ts +1 -24
  10. package/dist_ts/ts_smartdb/index.js +4 -29
  11. package/dist_ts/ts_smartdb/plugins.d.ts +2 -10
  12. package/dist_ts/ts_smartdb/plugins.js +3 -13
  13. package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +43 -0
  14. package/dist_ts/ts_smartdb/rust-db-bridge.js +98 -0
  15. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +8 -37
  16. package/dist_ts/ts_smartdb/server/SmartdbServer.js +49 -204
  17. package/dist_ts/ts_smartdb/server/index.d.ts +0 -4
  18. package/dist_ts/ts_smartdb/server/index.js +1 -5
  19. package/license +3 -1
  20. package/package.json +9 -12
  21. package/readme.md +84 -171
  22. package/ts/00_commitinfo_data.ts +2 -2
  23. package/ts/ts_local/classes.localsmartdb.ts +5 -6
  24. package/ts/ts_local/plugins.ts +1 -3
  25. package/ts/ts_smartdb/index.ts +3 -41
  26. package/ts/ts_smartdb/plugins.ts +2 -15
  27. package/ts/ts_smartdb/rust-db-bridge.ts +138 -0
  28. package/ts/ts_smartdb/server/SmartdbServer.ts +53 -248
  29. package/ts/ts_smartdb/server/index.ts +0 -7
  30. package/dist_ts/ts_smartdb/engine/AggregationEngine.d.ts +0 -66
  31. package/dist_ts/ts_smartdb/engine/AggregationEngine.js +0 -189
  32. package/dist_ts/ts_smartdb/engine/IndexEngine.d.ts +0 -97
  33. package/dist_ts/ts_smartdb/engine/IndexEngine.js +0 -678
  34. package/dist_ts/ts_smartdb/engine/QueryEngine.d.ts +0 -54
  35. package/dist_ts/ts_smartdb/engine/QueryEngine.js +0 -271
  36. package/dist_ts/ts_smartdb/engine/QueryPlanner.d.ts +0 -64
  37. package/dist_ts/ts_smartdb/engine/QueryPlanner.js +0 -308
  38. package/dist_ts/ts_smartdb/engine/SessionEngine.d.ts +0 -117
  39. package/dist_ts/ts_smartdb/engine/SessionEngine.js +0 -232
  40. package/dist_ts/ts_smartdb/engine/TransactionEngine.d.ts +0 -85
  41. package/dist_ts/ts_smartdb/engine/TransactionEngine.js +0 -287
  42. package/dist_ts/ts_smartdb/engine/UpdateEngine.d.ts +0 -47
  43. package/dist_ts/ts_smartdb/engine/UpdateEngine.js +0 -461
  44. package/dist_ts/ts_smartdb/errors/SmartdbErrors.d.ts +0 -100
  45. package/dist_ts/ts_smartdb/errors/SmartdbErrors.js +0 -155
  46. package/dist_ts/ts_smartdb/server/CommandRouter.d.ts +0 -87
  47. package/dist_ts/ts_smartdb/server/CommandRouter.js +0 -222
  48. package/dist_ts/ts_smartdb/server/WireProtocol.d.ts +0 -117
  49. package/dist_ts/ts_smartdb/server/WireProtocol.js +0 -298
  50. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.d.ts +0 -100
  51. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.js +0 -668
  52. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.d.ts +0 -31
  53. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.js +0 -277
  54. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.d.ts +0 -8
  55. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.js +0 -95
  56. package/dist_ts/ts_smartdb/server/handlers/FindHandler.d.ts +0 -31
  57. package/dist_ts/ts_smartdb/server/handlers/FindHandler.js +0 -291
  58. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.d.ts +0 -11
  59. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.js +0 -62
  60. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.d.ts +0 -20
  61. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.js +0 -183
  62. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.d.ts +0 -8
  63. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.js +0 -79
  64. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.d.ts +0 -24
  65. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.js +0 -296
  66. package/dist_ts/ts_smartdb/server/handlers/index.d.ts +0 -8
  67. package/dist_ts/ts_smartdb/server/handlers/index.js +0 -10
  68. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.d.ts +0 -85
  69. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.js +0 -465
  70. package/dist_ts/ts_smartdb/storage/IStorageAdapter.d.ts +0 -145
  71. package/dist_ts/ts_smartdb/storage/IStorageAdapter.js +0 -2
  72. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.d.ts +0 -67
  73. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.js +0 -378
  74. package/dist_ts/ts_smartdb/storage/OpLog.d.ts +0 -93
  75. package/dist_ts/ts_smartdb/storage/OpLog.js +0 -221
  76. package/dist_ts/ts_smartdb/storage/WAL.d.ts +0 -117
  77. package/dist_ts/ts_smartdb/storage/WAL.js +0 -286
  78. package/dist_ts/ts_smartdb/types/interfaces.d.ts +0 -363
  79. package/dist_ts/ts_smartdb/types/interfaces.js +0 -2
  80. package/dist_ts/ts_smartdb/utils/checksum.d.ts +0 -30
  81. package/dist_ts/ts_smartdb/utils/checksum.js +0 -77
  82. package/dist_ts/ts_smartdb/utils/index.d.ts +0 -1
  83. package/dist_ts/ts_smartdb/utils/index.js +0 -2
  84. package/ts/ts_smartdb/engine/AggregationEngine.ts +0 -283
  85. package/ts/ts_smartdb/engine/IndexEngine.ts +0 -798
  86. package/ts/ts_smartdb/engine/QueryEngine.ts +0 -301
  87. package/ts/ts_smartdb/engine/QueryPlanner.ts +0 -393
  88. package/ts/ts_smartdb/engine/SessionEngine.ts +0 -292
  89. package/ts/ts_smartdb/engine/TransactionEngine.ts +0 -351
  90. package/ts/ts_smartdb/engine/UpdateEngine.ts +0 -506
  91. package/ts/ts_smartdb/errors/SmartdbErrors.ts +0 -181
  92. package/ts/ts_smartdb/server/CommandRouter.ts +0 -289
  93. package/ts/ts_smartdb/server/WireProtocol.ts +0 -416
  94. package/ts/ts_smartdb/server/handlers/AdminHandler.ts +0 -719
  95. package/ts/ts_smartdb/server/handlers/AggregateHandler.ts +0 -342
  96. package/ts/ts_smartdb/server/handlers/DeleteHandler.ts +0 -115
  97. package/ts/ts_smartdb/server/handlers/FindHandler.ts +0 -330
  98. package/ts/ts_smartdb/server/handlers/HelloHandler.ts +0 -78
  99. package/ts/ts_smartdb/server/handlers/IndexHandler.ts +0 -207
  100. package/ts/ts_smartdb/server/handlers/InsertHandler.ts +0 -97
  101. package/ts/ts_smartdb/server/handlers/UpdateHandler.ts +0 -344
  102. package/ts/ts_smartdb/server/handlers/index.ts +0 -10
  103. package/ts/ts_smartdb/storage/FileStorageAdapter.ts +0 -562
  104. package/ts/ts_smartdb/storage/IStorageAdapter.ts +0 -208
  105. package/ts/ts_smartdb/storage/MemoryStorageAdapter.ts +0 -455
  106. package/ts/ts_smartdb/storage/OpLog.ts +0 -282
  107. package/ts/ts_smartdb/storage/WAL.ts +0 -375
  108. package/ts/ts_smartdb/types/interfaces.ts +0 -433
  109. package/ts/ts_smartdb/utils/checksum.ts +0 -88
  110. package/ts/ts_smartdb/utils/index.ts +0 -1
@@ -1,798 +0,0 @@
1
- import * as plugins from '../plugins.js';
2
- import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
3
-
4
- // Simple B-Tree implementation for range queries
5
- // Since sorted-btree has ESM/CJS interop issues, we use a simple custom implementation
6
- class SimpleBTree<K, V> {
7
- private entries: Map<string, { key: K; value: V }> = new Map();
8
- private sortedKeys: K[] = [];
9
- private comparator: (a: K, b: K) => number;
10
-
11
- constructor(_unused?: undefined, comparator?: (a: K, b: K) => number) {
12
- this.comparator = comparator || ((a: K, b: K) => {
13
- if (a < b) return -1;
14
- if (a > b) return 1;
15
- return 0;
16
- });
17
- }
18
-
19
- private keyToString(key: K): string {
20
- return JSON.stringify(key);
21
- }
22
-
23
- set(key: K, value: V): boolean {
24
- const keyStr = this.keyToString(key);
25
- const existed = this.entries.has(keyStr);
26
- this.entries.set(keyStr, { key, value });
27
-
28
- if (!existed) {
29
- // Insert in sorted order
30
- const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) > 0);
31
- if (idx === -1) {
32
- this.sortedKeys.push(key);
33
- } else {
34
- this.sortedKeys.splice(idx, 0, key);
35
- }
36
- }
37
- return !existed;
38
- }
39
-
40
- get(key: K): V | undefined {
41
- const entry = this.entries.get(this.keyToString(key));
42
- return entry?.value;
43
- }
44
-
45
- delete(key: K): boolean {
46
- const keyStr = this.keyToString(key);
47
- if (this.entries.has(keyStr)) {
48
- this.entries.delete(keyStr);
49
- const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) === 0);
50
- if (idx !== -1) {
51
- this.sortedKeys.splice(idx, 1);
52
- }
53
- return true;
54
- }
55
- return false;
56
- }
57
-
58
- forRange(
59
- lowKey: K | undefined,
60
- highKey: K | undefined,
61
- lowInclusive: boolean,
62
- highInclusive: boolean,
63
- callback: (value: V, key: K) => void
64
- ): void {
65
- for (const key of this.sortedKeys) {
66
- // Check low bound
67
- if (lowKey !== undefined) {
68
- const cmp = this.comparator(key, lowKey);
69
- if (cmp < 0) continue;
70
- if (cmp === 0 && !lowInclusive) continue;
71
- }
72
-
73
- // Check high bound
74
- if (highKey !== undefined) {
75
- const cmp = this.comparator(key, highKey);
76
- if (cmp > 0) break;
77
- if (cmp === 0 && !highInclusive) break;
78
- }
79
-
80
- const entry = this.entries.get(this.keyToString(key));
81
- if (entry) {
82
- callback(entry.value, key);
83
- }
84
- }
85
- }
86
- }
87
- import type {
88
- Document,
89
- IStoredDocument,
90
- IIndexSpecification,
91
- IIndexInfo,
92
- ICreateIndexOptions,
93
- } from '../types/interfaces.js';
94
- import { SmartdbDuplicateKeyError, SmartdbIndexError } from '../errors/SmartdbErrors.js';
95
- import { QueryEngine } from './QueryEngine.js';
96
-
97
- /**
98
- * Comparator for B-Tree that handles mixed types consistently
99
- */
100
- function indexKeyComparator(a: any, b: any): number {
101
- // Handle null/undefined
102
- if (a === null || a === undefined) {
103
- if (b === null || b === undefined) return 0;
104
- return -1;
105
- }
106
- if (b === null || b === undefined) return 1;
107
-
108
- // Handle arrays (compound keys)
109
- if (Array.isArray(a) && Array.isArray(b)) {
110
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
111
- const cmp = indexKeyComparator(a[i], b[i]);
112
- if (cmp !== 0) return cmp;
113
- }
114
- return 0;
115
- }
116
-
117
- // Handle ObjectId
118
- if (a instanceof plugins.bson.ObjectId && b instanceof plugins.bson.ObjectId) {
119
- return a.toHexString().localeCompare(b.toHexString());
120
- }
121
-
122
- // Handle Date
123
- if (a instanceof Date && b instanceof Date) {
124
- return a.getTime() - b.getTime();
125
- }
126
-
127
- // Handle different types - use type ordering (null < number < string < object)
128
- const typeOrder = (v: any): number => {
129
- if (v === null || v === undefined) return 0;
130
- if (typeof v === 'number') return 1;
131
- if (typeof v === 'string') return 2;
132
- if (typeof v === 'boolean') return 3;
133
- if (v instanceof Date) return 4;
134
- if (v instanceof plugins.bson.ObjectId) return 5;
135
- return 6;
136
- };
137
-
138
- const typeA = typeOrder(a);
139
- const typeB = typeOrder(b);
140
- if (typeA !== typeB) return typeA - typeB;
141
-
142
- // Same type comparison
143
- if (typeof a === 'number') return a - b;
144
- if (typeof a === 'string') return a.localeCompare(b);
145
- if (typeof a === 'boolean') return (a ? 1 : 0) - (b ? 1 : 0);
146
-
147
- // Fallback to string comparison
148
- return String(a).localeCompare(String(b));
149
- }
150
-
151
- /**
152
- * Index data structure using B-Tree for range queries
153
- */
154
- interface IIndexData {
155
- name: string;
156
- key: Record<string, 1 | -1 | string>;
157
- unique: boolean;
158
- sparse: boolean;
159
- expireAfterSeconds?: number;
160
- // B-Tree for ordered index lookups (supports range queries)
161
- btree: SimpleBTree<any, Set<string>>;
162
- // Hash map for fast equality lookups
163
- hashMap: Map<string, Set<string>>;
164
- }
165
-
166
- /**
167
- * Index engine for managing indexes and query optimization
168
- */
169
- export class IndexEngine {
170
- private dbName: string;
171
- private collName: string;
172
- private storage: IStorageAdapter;
173
- private indexes: Map<string, IIndexData> = new Map();
174
- private initialized = false;
175
-
176
- constructor(dbName: string, collName: string, storage: IStorageAdapter) {
177
- this.dbName = dbName;
178
- this.collName = collName;
179
- this.storage = storage;
180
- }
181
-
182
- /**
183
- * Initialize indexes from storage
184
- */
185
- async initialize(): Promise<void> {
186
- if (this.initialized) return;
187
-
188
- const storedIndexes = await this.storage.getIndexes(this.dbName, this.collName);
189
- const documents = await this.storage.findAll(this.dbName, this.collName);
190
-
191
- for (const indexSpec of storedIndexes) {
192
- const indexData: IIndexData = {
193
- name: indexSpec.name,
194
- key: indexSpec.key,
195
- unique: indexSpec.unique || false,
196
- sparse: indexSpec.sparse || false,
197
- expireAfterSeconds: indexSpec.expireAfterSeconds,
198
- btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
199
- hashMap: new Map(),
200
- };
201
-
202
- // Build index entries
203
- for (const doc of documents) {
204
- const keyValue = this.extractKeyValue(doc, indexSpec.key);
205
- if (keyValue !== null || !indexData.sparse) {
206
- const keyStr = JSON.stringify(keyValue);
207
-
208
- // Add to hash map
209
- if (!indexData.hashMap.has(keyStr)) {
210
- indexData.hashMap.set(keyStr, new Set());
211
- }
212
- indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
213
-
214
- // Add to B-tree
215
- const existing = indexData.btree.get(keyValue);
216
- if (existing) {
217
- existing.add(doc._id.toHexString());
218
- } else {
219
- indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
220
- }
221
- }
222
- }
223
-
224
- this.indexes.set(indexSpec.name, indexData);
225
- }
226
-
227
- this.initialized = true;
228
- }
229
-
230
- /**
231
- * Create a new index
232
- */
233
- async createIndex(
234
- key: Record<string, 1 | -1 | 'text' | '2dsphere'>,
235
- options?: ICreateIndexOptions
236
- ): Promise<string> {
237
- await this.initialize();
238
-
239
- // Generate index name if not provided
240
- const name = options?.name || this.generateIndexName(key);
241
-
242
- // Check if index already exists
243
- if (this.indexes.has(name)) {
244
- return name;
245
- }
246
-
247
- // Create index data structure
248
- const indexData: IIndexData = {
249
- name,
250
- key: key as Record<string, 1 | -1 | string>,
251
- unique: options?.unique || false,
252
- sparse: options?.sparse || false,
253
- expireAfterSeconds: options?.expireAfterSeconds,
254
- btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
255
- hashMap: new Map(),
256
- };
257
-
258
- // Build index from existing documents
259
- const documents = await this.storage.findAll(this.dbName, this.collName);
260
-
261
- for (const doc of documents) {
262
- const keyValue = this.extractKeyValue(doc, key);
263
-
264
- if (keyValue === null && indexData.sparse) {
265
- continue;
266
- }
267
-
268
- const keyStr = JSON.stringify(keyValue);
269
-
270
- if (indexData.unique && indexData.hashMap.has(keyStr)) {
271
- throw new SmartdbDuplicateKeyError(
272
- `E11000 duplicate key error index: ${this.dbName}.${this.collName}.$${name}`,
273
- key as Record<string, 1>,
274
- keyValue
275
- );
276
- }
277
-
278
- // Add to hash map
279
- if (!indexData.hashMap.has(keyStr)) {
280
- indexData.hashMap.set(keyStr, new Set());
281
- }
282
- indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
283
-
284
- // Add to B-tree
285
- const existing = indexData.btree.get(keyValue);
286
- if (existing) {
287
- existing.add(doc._id.toHexString());
288
- } else {
289
- indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
290
- }
291
- }
292
-
293
- // Store index
294
- this.indexes.set(name, indexData);
295
- await this.storage.saveIndex(this.dbName, this.collName, name, {
296
- key,
297
- unique: options?.unique,
298
- sparse: options?.sparse,
299
- expireAfterSeconds: options?.expireAfterSeconds,
300
- });
301
-
302
- return name;
303
- }
304
-
305
- /**
306
- * Drop an index
307
- */
308
- async dropIndex(name: string): Promise<void> {
309
- await this.initialize();
310
-
311
- if (name === '_id_') {
312
- throw new SmartdbIndexError('cannot drop _id index');
313
- }
314
-
315
- if (!this.indexes.has(name)) {
316
- throw new SmartdbIndexError(`index not found: ${name}`);
317
- }
318
-
319
- this.indexes.delete(name);
320
- await this.storage.dropIndex(this.dbName, this.collName, name);
321
- }
322
-
323
- /**
324
- * Drop all indexes except _id
325
- */
326
- async dropAllIndexes(): Promise<void> {
327
- await this.initialize();
328
-
329
- const names = Array.from(this.indexes.keys()).filter(n => n !== '_id_');
330
- for (const name of names) {
331
- this.indexes.delete(name);
332
- await this.storage.dropIndex(this.dbName, this.collName, name);
333
- }
334
- }
335
-
336
- /**
337
- * List all indexes
338
- */
339
- async listIndexes(): Promise<IIndexInfo[]> {
340
- await this.initialize();
341
-
342
- return Array.from(this.indexes.values()).map(idx => ({
343
- v: 2,
344
- key: idx.key,
345
- name: idx.name,
346
- unique: idx.unique || undefined,
347
- sparse: idx.sparse || undefined,
348
- expireAfterSeconds: idx.expireAfterSeconds,
349
- }));
350
- }
351
-
352
- /**
353
- * Check if an index exists
354
- */
355
- async indexExists(name: string): Promise<boolean> {
356
- await this.initialize();
357
- return this.indexes.has(name);
358
- }
359
-
360
- /**
361
- * Update index entries after document insert
362
- */
363
- async onInsert(doc: IStoredDocument): Promise<void> {
364
- await this.initialize();
365
-
366
- for (const [name, indexData] of this.indexes) {
367
- const keyValue = this.extractKeyValue(doc, indexData.key);
368
-
369
- if (keyValue === null && indexData.sparse) {
370
- continue;
371
- }
372
-
373
- const keyStr = JSON.stringify(keyValue);
374
-
375
- // Check unique constraint
376
- if (indexData.unique) {
377
- const existing = indexData.hashMap.get(keyStr);
378
- if (existing && existing.size > 0) {
379
- throw new SmartdbDuplicateKeyError(
380
- `E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
381
- indexData.key as Record<string, 1>,
382
- keyValue
383
- );
384
- }
385
- }
386
-
387
- // Add to hash map
388
- if (!indexData.hashMap.has(keyStr)) {
389
- indexData.hashMap.set(keyStr, new Set());
390
- }
391
- indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
392
-
393
- // Add to B-tree
394
- const btreeSet = indexData.btree.get(keyValue);
395
- if (btreeSet) {
396
- btreeSet.add(doc._id.toHexString());
397
- } else {
398
- indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
399
- }
400
- }
401
- }
402
-
403
- /**
404
- * Update index entries after document update
405
- */
406
- async onUpdate(oldDoc: IStoredDocument, newDoc: IStoredDocument): Promise<void> {
407
- await this.initialize();
408
-
409
- for (const [name, indexData] of this.indexes) {
410
- const oldKeyValue = this.extractKeyValue(oldDoc, indexData.key);
411
- const newKeyValue = this.extractKeyValue(newDoc, indexData.key);
412
- const oldKeyStr = JSON.stringify(oldKeyValue);
413
- const newKeyStr = JSON.stringify(newKeyValue);
414
-
415
- // Remove old entry if key changed
416
- if (oldKeyStr !== newKeyStr) {
417
- if (oldKeyValue !== null || !indexData.sparse) {
418
- // Remove from hash map
419
- const oldHashSet = indexData.hashMap.get(oldKeyStr);
420
- if (oldHashSet) {
421
- oldHashSet.delete(oldDoc._id.toHexString());
422
- if (oldHashSet.size === 0) {
423
- indexData.hashMap.delete(oldKeyStr);
424
- }
425
- }
426
-
427
- // Remove from B-tree
428
- const oldBtreeSet = indexData.btree.get(oldKeyValue);
429
- if (oldBtreeSet) {
430
- oldBtreeSet.delete(oldDoc._id.toHexString());
431
- if (oldBtreeSet.size === 0) {
432
- indexData.btree.delete(oldKeyValue);
433
- }
434
- }
435
- }
436
-
437
- // Add new entry
438
- if (newKeyValue !== null || !indexData.sparse) {
439
- // Check unique constraint
440
- if (indexData.unique) {
441
- const existing = indexData.hashMap.get(newKeyStr);
442
- if (existing && existing.size > 0) {
443
- throw new SmartdbDuplicateKeyError(
444
- `E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
445
- indexData.key as Record<string, 1>,
446
- newKeyValue
447
- );
448
- }
449
- }
450
-
451
- // Add to hash map
452
- if (!indexData.hashMap.has(newKeyStr)) {
453
- indexData.hashMap.set(newKeyStr, new Set());
454
- }
455
- indexData.hashMap.get(newKeyStr)!.add(newDoc._id.toHexString());
456
-
457
- // Add to B-tree
458
- const newBtreeSet = indexData.btree.get(newKeyValue);
459
- if (newBtreeSet) {
460
- newBtreeSet.add(newDoc._id.toHexString());
461
- } else {
462
- indexData.btree.set(newKeyValue, new Set([newDoc._id.toHexString()]));
463
- }
464
- }
465
- }
466
- }
467
- }
468
-
469
- /**
470
- * Update index entries after document delete
471
- */
472
- async onDelete(doc: IStoredDocument): Promise<void> {
473
- await this.initialize();
474
-
475
- for (const indexData of this.indexes.values()) {
476
- const keyValue = this.extractKeyValue(doc, indexData.key);
477
-
478
- if (keyValue === null && indexData.sparse) {
479
- continue;
480
- }
481
-
482
- const keyStr = JSON.stringify(keyValue);
483
-
484
- // Remove from hash map
485
- const hashSet = indexData.hashMap.get(keyStr);
486
- if (hashSet) {
487
- hashSet.delete(doc._id.toHexString());
488
- if (hashSet.size === 0) {
489
- indexData.hashMap.delete(keyStr);
490
- }
491
- }
492
-
493
- // Remove from B-tree
494
- const btreeSet = indexData.btree.get(keyValue);
495
- if (btreeSet) {
496
- btreeSet.delete(doc._id.toHexString());
497
- if (btreeSet.size === 0) {
498
- indexData.btree.delete(keyValue);
499
- }
500
- }
501
- }
502
- }
503
-
504
- /**
505
- * Find the best index for a query
506
- */
507
- selectIndex(filter: Document): { name: string; data: IIndexData } | null {
508
- if (!filter || Object.keys(filter).length === 0) {
509
- return null;
510
- }
511
-
512
- // Get filter fields and operators
513
- const filterInfo = this.analyzeFilter(filter);
514
-
515
- // Score each index
516
- let bestIndex: { name: string; data: IIndexData } | null = null;
517
- let bestScore = 0;
518
-
519
- for (const [name, indexData] of this.indexes) {
520
- const indexFields = Object.keys(indexData.key);
521
- let score = 0;
522
-
523
- // Count how many index fields can be used
524
- for (const field of indexFields) {
525
- const info = filterInfo.get(field);
526
- if (!info) break;
527
-
528
- // Equality is best
529
- if (info.equality) {
530
- score += 2;
531
- } else if (info.range) {
532
- // Range queries can use B-tree
533
- score += 1;
534
- } else if (info.in) {
535
- score += 1.5;
536
- } else {
537
- break;
538
- }
539
- }
540
-
541
- // Prefer unique indexes
542
- if (indexData.unique && score > 0) {
543
- score += 0.5;
544
- }
545
-
546
- if (score > bestScore) {
547
- bestScore = score;
548
- bestIndex = { name, data: indexData };
549
- }
550
- }
551
-
552
- return bestIndex;
553
- }
554
-
555
- /**
556
- * Analyze filter to extract field operators
557
- */
558
- private analyzeFilter(filter: Document): Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }> {
559
- const result = new Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }>();
560
-
561
- for (const [key, value] of Object.entries(filter)) {
562
- if (key.startsWith('$')) continue;
563
-
564
- const info = { equality: false, range: false, in: false, ops: {} as Record<string, any> };
565
-
566
- if (typeof value !== 'object' || value === null || value instanceof plugins.bson.ObjectId || value instanceof Date) {
567
- info.equality = true;
568
- info.ops['$eq'] = value;
569
- } else {
570
- const ops = value as Record<string, any>;
571
- if (ops.$eq !== undefined) {
572
- info.equality = true;
573
- info.ops['$eq'] = ops.$eq;
574
- }
575
- if (ops.$in !== undefined) {
576
- info.in = true;
577
- info.ops['$in'] = ops.$in;
578
- }
579
- if (ops.$gt !== undefined || ops.$gte !== undefined || ops.$lt !== undefined || ops.$lte !== undefined) {
580
- info.range = true;
581
- if (ops.$gt !== undefined) info.ops['$gt'] = ops.$gt;
582
- if (ops.$gte !== undefined) info.ops['$gte'] = ops.$gte;
583
- if (ops.$lt !== undefined) info.ops['$lt'] = ops.$lt;
584
- if (ops.$lte !== undefined) info.ops['$lte'] = ops.$lte;
585
- }
586
- }
587
-
588
- result.set(key, info);
589
- }
590
-
591
- return result;
592
- }
593
-
594
- /**
595
- * Use index to find candidate document IDs (supports range queries with B-tree)
596
- */
597
- async findCandidateIds(filter: Document): Promise<Set<string> | null> {
598
- await this.initialize();
599
-
600
- const index = this.selectIndex(filter);
601
- if (!index) return null;
602
-
603
- const filterInfo = this.analyzeFilter(filter);
604
- const indexFields = Object.keys(index.data.key);
605
-
606
- // For single-field indexes with range queries, use B-tree
607
- if (indexFields.length === 1) {
608
- const field = indexFields[0];
609
- const info = filterInfo.get(field);
610
-
611
- if (info) {
612
- // Handle equality using hash map (faster)
613
- if (info.equality) {
614
- const keyStr = JSON.stringify(info.ops['$eq']);
615
- return index.data.hashMap.get(keyStr) || new Set();
616
- }
617
-
618
- // Handle $in using hash map
619
- if (info.in) {
620
- const results = new Set<string>();
621
- for (const val of info.ops['$in']) {
622
- const keyStr = JSON.stringify(val);
623
- const ids = index.data.hashMap.get(keyStr);
624
- if (ids) {
625
- for (const id of ids) {
626
- results.add(id);
627
- }
628
- }
629
- }
630
- return results;
631
- }
632
-
633
- // Handle range queries using B-tree
634
- if (info.range) {
635
- return this.findRangeCandidates(index.data, info.ops);
636
- }
637
- }
638
- } else {
639
- // For compound indexes, use hash map with partial key matching
640
- const equalityValues: Record<string, any> = {};
641
-
642
- for (const field of indexFields) {
643
- const info = filterInfo.get(field);
644
- if (!info) break;
645
-
646
- if (info.equality) {
647
- equalityValues[field] = info.ops['$eq'];
648
- } else if (info.in) {
649
- // Handle $in with multiple lookups
650
- const results = new Set<string>();
651
- for (const val of info.ops['$in']) {
652
- equalityValues[field] = val;
653
- const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
654
- const ids = index.data.hashMap.get(keyStr);
655
- if (ids) {
656
- for (const id of ids) {
657
- results.add(id);
658
- }
659
- }
660
- }
661
- return results;
662
- } else {
663
- break; // Non-equality/in operator, stop here
664
- }
665
- }
666
-
667
- if (Object.keys(equalityValues).length > 0) {
668
- const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
669
- return index.data.hashMap.get(keyStr) || new Set();
670
- }
671
- }
672
-
673
- return null;
674
- }
675
-
676
- /**
677
- * Find candidates using B-tree range scan
678
- */
679
- private findRangeCandidates(indexData: IIndexData, ops: Record<string, any>): Set<string> {
680
- const results = new Set<string>();
681
-
682
- let lowKey: any = undefined;
683
- let highKey: any = undefined;
684
- let lowInclusive = true;
685
- let highInclusive = true;
686
-
687
- if (ops['$gt'] !== undefined) {
688
- lowKey = ops['$gt'];
689
- lowInclusive = false;
690
- }
691
- if (ops['$gte'] !== undefined) {
692
- lowKey = ops['$gte'];
693
- lowInclusive = true;
694
- }
695
- if (ops['$lt'] !== undefined) {
696
- highKey = ops['$lt'];
697
- highInclusive = false;
698
- }
699
- if (ops['$lte'] !== undefined) {
700
- highKey = ops['$lte'];
701
- highInclusive = true;
702
- }
703
-
704
- // Use B-tree range iteration
705
- indexData.btree.forRange(lowKey, highKey, lowInclusive, highInclusive, (value, key) => {
706
- if (value) {
707
- for (const id of value) {
708
- results.add(id);
709
- }
710
- }
711
- });
712
-
713
- return results;
714
- }
715
-
716
- // ============================================================================
717
- // Helper Methods
718
- // ============================================================================
719
-
720
- private generateIndexName(key: Record<string, any>): string {
721
- return Object.entries(key)
722
- .map(([field, dir]) => `${field}_${dir}`)
723
- .join('_');
724
- }
725
-
726
- private extractKeyValue(doc: Document, key: Record<string, any>): any {
727
- const values: any[] = [];
728
-
729
- for (const field of Object.keys(key)) {
730
- const value = QueryEngine.getNestedValue(doc, field);
731
- values.push(value === undefined ? null : value);
732
- }
733
-
734
- // For single-field index, return the value directly
735
- if (values.length === 1) {
736
- return values[0];
737
- }
738
-
739
- return values;
740
- }
741
-
742
- private buildKeyValue(values: Record<string, any>, key: Record<string, any>): any {
743
- const result: any[] = [];
744
-
745
- for (const field of Object.keys(key)) {
746
- result.push(values[field] !== undefined ? values[field] : null);
747
- }
748
-
749
- if (result.length === 1) {
750
- return result[0];
751
- }
752
-
753
- return result;
754
- }
755
-
756
- private getFilterFields(filter: Document, prefix = ''): string[] {
757
- const fields: string[] = [];
758
-
759
- for (const [key, value] of Object.entries(filter)) {
760
- if (key.startsWith('$')) {
761
- // Logical operator
762
- if (key === '$and' || key === '$or' || key === '$nor') {
763
- for (const subFilter of value as Document[]) {
764
- fields.push(...this.getFilterFields(subFilter, prefix));
765
- }
766
- }
767
- } else {
768
- const fullKey = prefix ? `${prefix}.${key}` : key;
769
- fields.push(fullKey);
770
-
771
- // Check for nested filters
772
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
773
- const subKeys = Object.keys(value);
774
- if (subKeys.length > 0 && !subKeys[0].startsWith('$')) {
775
- fields.push(...this.getFilterFields(value, fullKey));
776
- }
777
- }
778
- }
779
- }
780
-
781
- return fields;
782
- }
783
-
784
- private getFilterValue(filter: Document, field: string): any {
785
- // Handle dot notation
786
- const parts = field.split('.');
787
- let current: any = filter;
788
-
789
- for (const part of parts) {
790
- if (current === null || current === undefined) {
791
- return undefined;
792
- }
793
- current = current[part];
794
- }
795
-
796
- return current;
797
- }
798
- }