@push.rocks/smartmongo 2.2.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.
Files changed (126) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/index.d.ts +1 -1
  3. package/dist_ts/index.js +3 -3
  4. package/dist_ts/tsmdb/engine/AggregationEngine.js +189 -0
  5. package/dist_ts/{congodb → tsmdb}/engine/IndexEngine.d.ts +23 -3
  6. package/dist_ts/tsmdb/engine/IndexEngine.js +678 -0
  7. package/dist_ts/tsmdb/engine/QueryEngine.js +271 -0
  8. package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
  9. package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
  10. package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
  11. package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
  12. package/dist_ts/{congodb → tsmdb}/engine/TransactionEngine.d.ts +1 -1
  13. package/dist_ts/tsmdb/engine/TransactionEngine.js +287 -0
  14. package/dist_ts/tsmdb/engine/UpdateEngine.js +461 -0
  15. package/dist_ts/{congodb/errors/CongoErrors.d.ts → tsmdb/errors/TsmdbErrors.d.ts} +16 -16
  16. package/dist_ts/tsmdb/errors/TsmdbErrors.js +155 -0
  17. package/dist_ts/{congodb → tsmdb}/index.d.ts +11 -4
  18. package/dist_ts/tsmdb/index.js +31 -0
  19. package/dist_ts/tsmdb/server/CommandRouter.d.ts +87 -0
  20. package/dist_ts/tsmdb/server/CommandRouter.js +222 -0
  21. package/dist_ts/{congodb/server/CongoServer.d.ts → tsmdb/server/TsmdbServer.d.ts} +6 -6
  22. package/dist_ts/tsmdb/server/TsmdbServer.js +229 -0
  23. package/dist_ts/{congodb → tsmdb}/server/WireProtocol.d.ts +1 -1
  24. package/dist_ts/tsmdb/server/WireProtocol.js +298 -0
  25. package/dist_ts/{congodb → tsmdb}/server/handlers/AdminHandler.d.ts +1 -1
  26. package/dist_ts/tsmdb/server/handlers/AdminHandler.js +668 -0
  27. package/dist_ts/{congodb → tsmdb}/server/handlers/AggregateHandler.d.ts +1 -1
  28. package/dist_ts/tsmdb/server/handlers/AggregateHandler.js +277 -0
  29. package/dist_ts/{congodb → tsmdb}/server/handlers/DeleteHandler.d.ts +1 -1
  30. package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +95 -0
  31. package/dist_ts/{congodb → tsmdb}/server/handlers/FindHandler.d.ts +1 -1
  32. package/dist_ts/tsmdb/server/handlers/FindHandler.js +291 -0
  33. package/dist_ts/{congodb → tsmdb}/server/handlers/HelloHandler.d.ts +1 -1
  34. package/dist_ts/{congodb → tsmdb}/server/handlers/HelloHandler.js +2 -2
  35. package/dist_ts/{congodb → tsmdb}/server/handlers/IndexHandler.d.ts +1 -1
  36. package/dist_ts/tsmdb/server/handlers/IndexHandler.js +183 -0
  37. package/dist_ts/{congodb → tsmdb}/server/handlers/InsertHandler.d.ts +1 -1
  38. package/dist_ts/tsmdb/server/handlers/InsertHandler.js +79 -0
  39. package/dist_ts/{congodb → tsmdb}/server/handlers/UpdateHandler.d.ts +1 -1
  40. package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +296 -0
  41. package/dist_ts/tsmdb/server/handlers/index.js +10 -0
  42. package/dist_ts/{congodb → tsmdb}/server/index.d.ts +2 -2
  43. package/dist_ts/tsmdb/server/index.js +7 -0
  44. package/dist_ts/{congodb → tsmdb}/storage/FileStorageAdapter.d.ts +27 -3
  45. package/dist_ts/tsmdb/storage/FileStorageAdapter.js +465 -0
  46. package/dist_ts/{congodb → tsmdb}/storage/IStorageAdapter.d.ts +7 -2
  47. package/dist_ts/{congodb → tsmdb}/storage/IStorageAdapter.js +1 -1
  48. package/dist_ts/{congodb → tsmdb}/storage/MemoryStorageAdapter.d.ts +3 -2
  49. package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +378 -0
  50. package/dist_ts/{congodb → tsmdb}/storage/OpLog.d.ts +1 -1
  51. package/dist_ts/tsmdb/storage/OpLog.js +221 -0
  52. package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
  53. package/dist_ts/tsmdb/storage/WAL.js +286 -0
  54. package/dist_ts/tsmdb/tsmdb.plugins.js +14 -0
  55. package/dist_ts/{congodb → tsmdb}/types/interfaces.d.ts +3 -3
  56. package/dist_ts/{congodb → tsmdb}/types/interfaces.js +1 -1
  57. package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
  58. package/dist_ts/tsmdb/utils/checksum.js +77 -0
  59. package/dist_ts/tsmdb/utils/index.d.ts +1 -0
  60. package/dist_ts/tsmdb/utils/index.js +2 -0
  61. package/package.json +1 -1
  62. package/readme.hints.md +7 -12
  63. package/readme.md +25 -25
  64. package/ts/00_commitinfo_data.ts +1 -1
  65. package/ts/index.ts +2 -2
  66. package/ts/{congodb → tsmdb}/engine/AggregationEngine.ts +1 -1
  67. package/ts/tsmdb/engine/IndexEngine.ts +798 -0
  68. package/ts/{congodb → tsmdb}/engine/QueryEngine.ts +1 -1
  69. package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
  70. package/ts/tsmdb/engine/SessionEngine.ts +292 -0
  71. package/ts/{congodb → tsmdb}/engine/TransactionEngine.ts +12 -12
  72. package/ts/{congodb → tsmdb}/engine/UpdateEngine.ts +1 -1
  73. package/ts/{congodb/errors/CongoErrors.ts → tsmdb/errors/TsmdbErrors.ts} +34 -34
  74. package/ts/{congodb → tsmdb}/index.ts +16 -7
  75. package/ts/{congodb → tsmdb}/server/CommandRouter.ts +114 -5
  76. package/ts/{congodb/server/CongoServer.ts → tsmdb/server/TsmdbServer.ts} +11 -8
  77. package/ts/{congodb → tsmdb}/server/WireProtocol.ts +1 -1
  78. package/ts/{congodb → tsmdb}/server/handlers/AdminHandler.ts +116 -11
  79. package/ts/{congodb → tsmdb}/server/handlers/AggregateHandler.ts +1 -1
  80. package/ts/{congodb → tsmdb}/server/handlers/DeleteHandler.ts +18 -3
  81. package/ts/{congodb → tsmdb}/server/handlers/FindHandler.ts +43 -14
  82. package/ts/{congodb → tsmdb}/server/handlers/HelloHandler.ts +1 -1
  83. package/ts/{congodb → tsmdb}/server/handlers/IndexHandler.ts +1 -1
  84. package/ts/{congodb → tsmdb}/server/handlers/InsertHandler.ts +7 -1
  85. package/ts/{congodb → tsmdb}/server/handlers/UpdateHandler.ts +34 -5
  86. package/ts/{congodb → tsmdb}/server/index.ts +2 -2
  87. package/ts/{congodb → tsmdb}/storage/FileStorageAdapter.ts +90 -7
  88. package/ts/{congodb → tsmdb}/storage/IStorageAdapter.ts +8 -2
  89. package/ts/{congodb → tsmdb}/storage/MemoryStorageAdapter.ts +14 -2
  90. package/ts/{congodb → tsmdb}/storage/OpLog.ts +1 -1
  91. package/ts/tsmdb/storage/WAL.ts +375 -0
  92. package/ts/{congodb → tsmdb}/types/interfaces.ts +3 -3
  93. package/ts/tsmdb/utils/checksum.ts +88 -0
  94. package/ts/tsmdb/utils/index.ts +1 -0
  95. package/dist_ts/congodb/congodb.plugins.js +0 -14
  96. package/dist_ts/congodb/engine/AggregationEngine.js +0 -189
  97. package/dist_ts/congodb/engine/IndexEngine.js +0 -376
  98. package/dist_ts/congodb/engine/QueryEngine.js +0 -271
  99. package/dist_ts/congodb/engine/TransactionEngine.js +0 -287
  100. package/dist_ts/congodb/engine/UpdateEngine.js +0 -461
  101. package/dist_ts/congodb/errors/CongoErrors.js +0 -155
  102. package/dist_ts/congodb/index.js +0 -26
  103. package/dist_ts/congodb/server/CommandRouter.d.ts +0 -51
  104. package/dist_ts/congodb/server/CommandRouter.js +0 -132
  105. package/dist_ts/congodb/server/CongoServer.js +0 -227
  106. package/dist_ts/congodb/server/WireProtocol.js +0 -298
  107. package/dist_ts/congodb/server/handlers/AdminHandler.js +0 -568
  108. package/dist_ts/congodb/server/handlers/AggregateHandler.js +0 -277
  109. package/dist_ts/congodb/server/handlers/DeleteHandler.js +0 -83
  110. package/dist_ts/congodb/server/handlers/FindHandler.js +0 -261
  111. package/dist_ts/congodb/server/handlers/IndexHandler.js +0 -183
  112. package/dist_ts/congodb/server/handlers/InsertHandler.js +0 -76
  113. package/dist_ts/congodb/server/handlers/UpdateHandler.js +0 -270
  114. package/dist_ts/congodb/server/handlers/index.js +0 -10
  115. package/dist_ts/congodb/server/index.js +0 -7
  116. package/dist_ts/congodb/storage/FileStorageAdapter.js +0 -396
  117. package/dist_ts/congodb/storage/MemoryStorageAdapter.js +0 -367
  118. package/dist_ts/congodb/storage/OpLog.js +0 -221
  119. package/ts/congodb/engine/IndexEngine.ts +0 -479
  120. /package/dist_ts/{congodb → tsmdb}/engine/AggregationEngine.d.ts +0 -0
  121. /package/dist_ts/{congodb → tsmdb}/engine/QueryEngine.d.ts +0 -0
  122. /package/dist_ts/{congodb → tsmdb}/engine/UpdateEngine.d.ts +0 -0
  123. /package/dist_ts/{congodb → tsmdb}/server/handlers/index.d.ts +0 -0
  124. /package/dist_ts/{congodb/congodb.plugins.d.ts → tsmdb/tsmdb.plugins.d.ts} +0 -0
  125. /package/ts/{congodb → tsmdb}/server/handlers/index.ts +0 -0
  126. /package/ts/{congodb/congodb.plugins.ts → tsmdb/tsmdb.plugins.ts} +0 -0
@@ -0,0 +1,798 @@
1
+ import * as plugins from '../tsmdb.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 { TsmdbDuplicateKeyError, TsmdbIndexError } from '../errors/TsmdbErrors.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 TsmdbDuplicateKeyError(
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 TsmdbIndexError('cannot drop _id index');
313
+ }
314
+
315
+ if (!this.indexes.has(name)) {
316
+ throw new TsmdbIndexError(`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 TsmdbDuplicateKeyError(
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 TsmdbDuplicateKeyError(
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
+ }