@push.rocks/smartdb 1.0.1 → 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.
Files changed (126) hide show
  1. package/.smartconfig.json +18 -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/index.d.ts +1 -0
  6. package/dist_ts/ts_local/classes.localsmartdb.d.ts +5 -5
  7. package/dist_ts/ts_local/classes.localsmartdb.js +7 -9
  8. package/dist_ts/ts_local/plugins.d.ts +1 -2
  9. package/dist_ts/ts_local/plugins.js +3 -3
  10. package/dist_ts/ts_smartdb/index.d.ts +2 -24
  11. package/dist_ts/ts_smartdb/index.js +4 -29
  12. package/dist_ts/ts_smartdb/plugins.d.ts +2 -10
  13. package/dist_ts/ts_smartdb/plugins.js +3 -13
  14. package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +122 -0
  15. package/dist_ts/ts_smartdb/rust-db-bridge.js +113 -0
  16. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +39 -37
  17. package/dist_ts/ts_smartdb/server/SmartdbServer.js +87 -206
  18. package/dist_ts/ts_smartdb/server/index.d.ts +0 -4
  19. package/dist_ts/ts_smartdb/server/index.js +1 -5
  20. package/dist_ts_debugserver/bundled.d.ts +4 -0
  21. package/dist_ts_debugserver/bundled.js +12 -0
  22. package/dist_ts_debugserver/classes.debugserver.d.ts +36 -0
  23. package/dist_ts_debugserver/classes.debugserver.js +95 -0
  24. package/dist_ts_debugserver/index.d.ts +2 -0
  25. package/dist_ts_debugserver/index.js +2 -0
  26. package/dist_ts_debugserver/plugins.d.ts +2 -0
  27. package/dist_ts_debugserver/plugins.js +3 -0
  28. package/dist_ts_debugui/index.d.ts +2 -0
  29. package/dist_ts_debugui/index.js +2 -0
  30. package/dist_ts_debugui/plugins.d.ts +1 -0
  31. package/dist_ts_debugui/plugins.js +2 -0
  32. package/dist_ts_debugui/smartdb-debugui.d.ts +62 -0
  33. package/dist_ts_debugui/smartdb-debugui.js +1132 -0
  34. package/license +3 -1
  35. package/package.json +14 -13
  36. package/readme.md +209 -177
  37. package/ts/00_commitinfo_data.ts +2 -2
  38. package/ts/index.ts +11 -0
  39. package/ts/ts_local/classes.localsmartdb.ts +5 -6
  40. package/ts/ts_local/plugins.ts +1 -3
  41. package/ts/ts_smartdb/index.ts +14 -41
  42. package/ts/ts_smartdb/plugins.ts +2 -15
  43. package/ts/ts_smartdb/rust-db-bridge.ts +262 -0
  44. package/ts/ts_smartdb/server/SmartdbServer.ts +115 -246
  45. package/ts/ts_smartdb/server/index.ts +0 -7
  46. package/dist_ts/ts_smartdb/engine/AggregationEngine.d.ts +0 -66
  47. package/dist_ts/ts_smartdb/engine/AggregationEngine.js +0 -189
  48. package/dist_ts/ts_smartdb/engine/IndexEngine.d.ts +0 -97
  49. package/dist_ts/ts_smartdb/engine/IndexEngine.js +0 -678
  50. package/dist_ts/ts_smartdb/engine/QueryEngine.d.ts +0 -54
  51. package/dist_ts/ts_smartdb/engine/QueryEngine.js +0 -271
  52. package/dist_ts/ts_smartdb/engine/QueryPlanner.d.ts +0 -64
  53. package/dist_ts/ts_smartdb/engine/QueryPlanner.js +0 -308
  54. package/dist_ts/ts_smartdb/engine/SessionEngine.d.ts +0 -117
  55. package/dist_ts/ts_smartdb/engine/SessionEngine.js +0 -232
  56. package/dist_ts/ts_smartdb/engine/TransactionEngine.d.ts +0 -85
  57. package/dist_ts/ts_smartdb/engine/TransactionEngine.js +0 -287
  58. package/dist_ts/ts_smartdb/engine/UpdateEngine.d.ts +0 -47
  59. package/dist_ts/ts_smartdb/engine/UpdateEngine.js +0 -461
  60. package/dist_ts/ts_smartdb/errors/SmartdbErrors.d.ts +0 -100
  61. package/dist_ts/ts_smartdb/errors/SmartdbErrors.js +0 -155
  62. package/dist_ts/ts_smartdb/server/CommandRouter.d.ts +0 -87
  63. package/dist_ts/ts_smartdb/server/CommandRouter.js +0 -222
  64. package/dist_ts/ts_smartdb/server/WireProtocol.d.ts +0 -117
  65. package/dist_ts/ts_smartdb/server/WireProtocol.js +0 -298
  66. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.d.ts +0 -100
  67. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.js +0 -668
  68. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.d.ts +0 -31
  69. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.js +0 -277
  70. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.d.ts +0 -8
  71. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.js +0 -95
  72. package/dist_ts/ts_smartdb/server/handlers/FindHandler.d.ts +0 -31
  73. package/dist_ts/ts_smartdb/server/handlers/FindHandler.js +0 -291
  74. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.d.ts +0 -11
  75. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.js +0 -62
  76. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.d.ts +0 -20
  77. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.js +0 -183
  78. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.d.ts +0 -8
  79. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.js +0 -79
  80. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.d.ts +0 -24
  81. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.js +0 -296
  82. package/dist_ts/ts_smartdb/server/handlers/index.d.ts +0 -8
  83. package/dist_ts/ts_smartdb/server/handlers/index.js +0 -10
  84. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.d.ts +0 -85
  85. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.js +0 -465
  86. package/dist_ts/ts_smartdb/storage/IStorageAdapter.d.ts +0 -145
  87. package/dist_ts/ts_smartdb/storage/IStorageAdapter.js +0 -2
  88. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.d.ts +0 -67
  89. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.js +0 -378
  90. package/dist_ts/ts_smartdb/storage/OpLog.d.ts +0 -93
  91. package/dist_ts/ts_smartdb/storage/OpLog.js +0 -221
  92. package/dist_ts/ts_smartdb/storage/WAL.d.ts +0 -117
  93. package/dist_ts/ts_smartdb/storage/WAL.js +0 -286
  94. package/dist_ts/ts_smartdb/types/interfaces.d.ts +0 -363
  95. package/dist_ts/ts_smartdb/types/interfaces.js +0 -2
  96. package/dist_ts/ts_smartdb/utils/checksum.d.ts +0 -30
  97. package/dist_ts/ts_smartdb/utils/checksum.js +0 -77
  98. package/dist_ts/ts_smartdb/utils/index.d.ts +0 -1
  99. package/dist_ts/ts_smartdb/utils/index.js +0 -2
  100. package/ts/ts_smartdb/engine/AggregationEngine.ts +0 -283
  101. package/ts/ts_smartdb/engine/IndexEngine.ts +0 -798
  102. package/ts/ts_smartdb/engine/QueryEngine.ts +0 -301
  103. package/ts/ts_smartdb/engine/QueryPlanner.ts +0 -393
  104. package/ts/ts_smartdb/engine/SessionEngine.ts +0 -292
  105. package/ts/ts_smartdb/engine/TransactionEngine.ts +0 -351
  106. package/ts/ts_smartdb/engine/UpdateEngine.ts +0 -506
  107. package/ts/ts_smartdb/errors/SmartdbErrors.ts +0 -181
  108. package/ts/ts_smartdb/server/CommandRouter.ts +0 -289
  109. package/ts/ts_smartdb/server/WireProtocol.ts +0 -416
  110. package/ts/ts_smartdb/server/handlers/AdminHandler.ts +0 -719
  111. package/ts/ts_smartdb/server/handlers/AggregateHandler.ts +0 -342
  112. package/ts/ts_smartdb/server/handlers/DeleteHandler.ts +0 -115
  113. package/ts/ts_smartdb/server/handlers/FindHandler.ts +0 -330
  114. package/ts/ts_smartdb/server/handlers/HelloHandler.ts +0 -78
  115. package/ts/ts_smartdb/server/handlers/IndexHandler.ts +0 -207
  116. package/ts/ts_smartdb/server/handlers/InsertHandler.ts +0 -97
  117. package/ts/ts_smartdb/server/handlers/UpdateHandler.ts +0 -344
  118. package/ts/ts_smartdb/server/handlers/index.ts +0 -10
  119. package/ts/ts_smartdb/storage/FileStorageAdapter.ts +0 -562
  120. package/ts/ts_smartdb/storage/IStorageAdapter.ts +0 -208
  121. package/ts/ts_smartdb/storage/MemoryStorageAdapter.ts +0 -455
  122. package/ts/ts_smartdb/storage/OpLog.ts +0 -282
  123. package/ts/ts_smartdb/storage/WAL.ts +0 -375
  124. package/ts/ts_smartdb/types/interfaces.ts +0 -433
  125. package/ts/ts_smartdb/utils/checksum.ts +0 -88
  126. package/ts/ts_smartdb/utils/index.ts +0 -1
@@ -1,301 +0,0 @@
1
- import * as plugins from '../plugins.js';
2
- import type { Document, IStoredDocument, ISortSpecification, ISortDirection } from '../types/interfaces.js';
3
-
4
- // Import mingo Query class
5
- import { Query } from 'mingo';
6
-
7
- /**
8
- * Query engine using mingo for MongoDB-compatible query matching
9
- */
10
- export class QueryEngine {
11
- /**
12
- * Filter documents by a MongoDB query filter
13
- */
14
- static filter(documents: IStoredDocument[], filter: Document): IStoredDocument[] {
15
- if (!filter || Object.keys(filter).length === 0) {
16
- return documents;
17
- }
18
-
19
- const query = new Query(filter);
20
- return documents.filter(doc => query.test(doc));
21
- }
22
-
23
- /**
24
- * Test if a single document matches a filter
25
- */
26
- static matches(document: Document, filter: Document): boolean {
27
- if (!filter || Object.keys(filter).length === 0) {
28
- return true;
29
- }
30
-
31
- const query = new Query(filter);
32
- return query.test(document);
33
- }
34
-
35
- /**
36
- * Find a single document matching the filter
37
- */
38
- static findOne(documents: IStoredDocument[], filter: Document): IStoredDocument | null {
39
- if (!filter || Object.keys(filter).length === 0) {
40
- return documents[0] || null;
41
- }
42
-
43
- const query = new Query(filter);
44
- for (const doc of documents) {
45
- if (query.test(doc)) {
46
- return doc;
47
- }
48
- }
49
- return null;
50
- }
51
-
52
- /**
53
- * Sort documents by a sort specification
54
- */
55
- static sort(documents: IStoredDocument[], sort: ISortSpecification): IStoredDocument[] {
56
- if (!sort) {
57
- return documents;
58
- }
59
-
60
- // Normalize sort specification to array of [field, direction] pairs
61
- const sortFields: Array<[string, number]> = [];
62
-
63
- if (Array.isArray(sort)) {
64
- for (const [field, direction] of sort) {
65
- sortFields.push([field, this.normalizeDirection(direction)]);
66
- }
67
- } else {
68
- for (const [field, direction] of Object.entries(sort)) {
69
- sortFields.push([field, this.normalizeDirection(direction)]);
70
- }
71
- }
72
-
73
- return [...documents].sort((a, b) => {
74
- for (const [field, direction] of sortFields) {
75
- const aVal = this.getNestedValue(a, field);
76
- const bVal = this.getNestedValue(b, field);
77
-
78
- const comparison = this.compareValues(aVal, bVal);
79
- if (comparison !== 0) {
80
- return comparison * direction;
81
- }
82
- }
83
- return 0;
84
- });
85
- }
86
-
87
- /**
88
- * Apply projection to documents
89
- */
90
- static project(documents: IStoredDocument[], projection: Document): Document[] {
91
- if (!projection || Object.keys(projection).length === 0) {
92
- return documents;
93
- }
94
-
95
- // Determine if this is inclusion or exclusion projection
96
- const keys = Object.keys(projection);
97
- const hasInclusion = keys.some(k => k !== '_id' && projection[k] === 1);
98
- const hasExclusion = keys.some(k => k !== '_id' && projection[k] === 0);
99
-
100
- // Can't mix inclusion and exclusion (except for _id)
101
- if (hasInclusion && hasExclusion) {
102
- throw new Error('Cannot mix inclusion and exclusion in projection');
103
- }
104
-
105
- return documents.map(doc => {
106
- if (hasInclusion) {
107
- // Inclusion projection
108
- const result: Document = {};
109
-
110
- // Handle _id
111
- if (projection._id !== 0 && projection._id !== false) {
112
- result._id = doc._id;
113
- }
114
-
115
- for (const key of keys) {
116
- if (key === '_id') continue;
117
- if (projection[key] === 1 || projection[key] === true) {
118
- const value = this.getNestedValue(doc, key);
119
- if (value !== undefined) {
120
- this.setNestedValue(result, key, value);
121
- }
122
- }
123
- }
124
-
125
- return result;
126
- } else {
127
- // Exclusion projection - start with copy and remove fields
128
- const result = { ...doc };
129
-
130
- for (const key of keys) {
131
- if (projection[key] === 0 || projection[key] === false) {
132
- this.deleteNestedValue(result, key);
133
- }
134
- }
135
-
136
- return result;
137
- }
138
- });
139
- }
140
-
141
- /**
142
- * Get distinct values for a field
143
- */
144
- static distinct(documents: IStoredDocument[], field: string, filter?: Document): any[] {
145
- let docs = documents;
146
- if (filter && Object.keys(filter).length > 0) {
147
- docs = this.filter(documents, filter);
148
- }
149
-
150
- const values = new Set<any>();
151
- for (const doc of docs) {
152
- const value = this.getNestedValue(doc, field);
153
- if (value !== undefined) {
154
- if (Array.isArray(value)) {
155
- // For arrays, add each element
156
- for (const v of value) {
157
- values.add(this.toComparable(v));
158
- }
159
- } else {
160
- values.add(this.toComparable(value));
161
- }
162
- }
163
- }
164
-
165
- return Array.from(values);
166
- }
167
-
168
- /**
169
- * Normalize sort direction to 1 or -1
170
- */
171
- private static normalizeDirection(direction: ISortDirection): number {
172
- if (typeof direction === 'number') {
173
- return direction > 0 ? 1 : -1;
174
- }
175
- if (direction === 'asc' || direction === 'ascending') {
176
- return 1;
177
- }
178
- return -1;
179
- }
180
-
181
- /**
182
- * Get a nested value from an object using dot notation
183
- */
184
- static getNestedValue(obj: any, path: string): any {
185
- const parts = path.split('.');
186
- let current = obj;
187
-
188
- for (const part of parts) {
189
- if (current === null || current === undefined) {
190
- return undefined;
191
- }
192
- if (Array.isArray(current)) {
193
- // Handle array access
194
- const index = parseInt(part, 10);
195
- if (!isNaN(index)) {
196
- current = current[index];
197
- } else {
198
- // Get the field from all array elements
199
- return current.map(item => this.getNestedValue(item, part)).flat();
200
- }
201
- } else {
202
- current = current[part];
203
- }
204
- }
205
-
206
- return current;
207
- }
208
-
209
- /**
210
- * Set a nested value in an object using dot notation
211
- */
212
- private static setNestedValue(obj: any, path: string, value: any): void {
213
- const parts = path.split('.');
214
- let current = obj;
215
-
216
- for (let i = 0; i < parts.length - 1; i++) {
217
- const part = parts[i];
218
- if (!(part in current)) {
219
- current[part] = {};
220
- }
221
- current = current[part];
222
- }
223
-
224
- current[parts[parts.length - 1]] = value;
225
- }
226
-
227
- /**
228
- * Delete a nested value from an object using dot notation
229
- */
230
- private static deleteNestedValue(obj: any, path: string): void {
231
- const parts = path.split('.');
232
- let current = obj;
233
-
234
- for (let i = 0; i < parts.length - 1; i++) {
235
- const part = parts[i];
236
- if (!(part in current)) {
237
- return;
238
- }
239
- current = current[part];
240
- }
241
-
242
- delete current[parts[parts.length - 1]];
243
- }
244
-
245
- /**
246
- * Compare two values for sorting
247
- */
248
- private static compareValues(a: any, b: any): number {
249
- // Handle undefined/null
250
- if (a === undefined && b === undefined) return 0;
251
- if (a === undefined) return -1;
252
- if (b === undefined) return 1;
253
- if (a === null && b === null) return 0;
254
- if (a === null) return -1;
255
- if (b === null) return 1;
256
-
257
- // Handle ObjectId
258
- if (a instanceof plugins.bson.ObjectId && b instanceof plugins.bson.ObjectId) {
259
- return a.toHexString().localeCompare(b.toHexString());
260
- }
261
-
262
- // Handle dates
263
- if (a instanceof Date && b instanceof Date) {
264
- return a.getTime() - b.getTime();
265
- }
266
-
267
- // Handle numbers
268
- if (typeof a === 'number' && typeof b === 'number') {
269
- return a - b;
270
- }
271
-
272
- // Handle strings
273
- if (typeof a === 'string' && typeof b === 'string') {
274
- return a.localeCompare(b);
275
- }
276
-
277
- // Handle booleans
278
- if (typeof a === 'boolean' && typeof b === 'boolean') {
279
- return (a ? 1 : 0) - (b ? 1 : 0);
280
- }
281
-
282
- // Fall back to string comparison
283
- return String(a).localeCompare(String(b));
284
- }
285
-
286
- /**
287
- * Convert a value to a comparable form (for distinct)
288
- */
289
- private static toComparable(value: any): any {
290
- if (value instanceof plugins.bson.ObjectId) {
291
- return value.toHexString();
292
- }
293
- if (value instanceof Date) {
294
- return value.toISOString();
295
- }
296
- if (typeof value === 'object' && value !== null) {
297
- return JSON.stringify(value);
298
- }
299
- return value;
300
- }
301
- }
@@ -1,393 +0,0 @@
1
- import * as plugins from '../plugins.js';
2
- import type { Document, IStoredDocument } from '../types/interfaces.js';
3
- import { IndexEngine } from './IndexEngine.js';
4
-
5
- /**
6
- * Query execution plan types
7
- */
8
- export type TQueryPlanType = 'IXSCAN' | 'COLLSCAN' | 'FETCH' | 'IXSCAN_RANGE';
9
-
10
- /**
11
- * Represents a query execution plan
12
- */
13
- export interface IQueryPlan {
14
- /** The type of scan used */
15
- type: TQueryPlanType;
16
- /** Index name if using an index */
17
- indexName?: string;
18
- /** Index key specification */
19
- indexKey?: Record<string, 1 | -1 | string>;
20
- /** Whether the query can be fully satisfied by the index */
21
- indexCovering: boolean;
22
- /** Estimated selectivity (0-1, lower is more selective) */
23
- selectivity: number;
24
- /** Whether range operators are used */
25
- usesRange: boolean;
26
- /** Fields used from the index */
27
- indexFieldsUsed: string[];
28
- /** Filter conditions that must be applied post-index lookup */
29
- residualFilter?: Document;
30
- /** Explanation for debugging */
31
- explanation: string;
32
- }
33
-
34
- /**
35
- * Filter operator analysis
36
- */
37
- interface IFilterOperatorInfo {
38
- field: string;
39
- operators: string[];
40
- equality: boolean;
41
- range: boolean;
42
- in: boolean;
43
- exists: boolean;
44
- regex: boolean;
45
- values: Record<string, any>;
46
- }
47
-
48
- /**
49
- * QueryPlanner - Analyzes queries and selects optimal execution plans
50
- */
51
- export class QueryPlanner {
52
- private indexEngine: IndexEngine;
53
-
54
- constructor(indexEngine: IndexEngine) {
55
- this.indexEngine = indexEngine;
56
- }
57
-
58
- /**
59
- * Generate an execution plan for a query filter
60
- */
61
- async plan(filter: Document): Promise<IQueryPlan> {
62
- await this.indexEngine['initialize']();
63
-
64
- // Empty filter = full collection scan
65
- if (!filter || Object.keys(filter).length === 0) {
66
- return {
67
- type: 'COLLSCAN',
68
- indexCovering: false,
69
- selectivity: 1.0,
70
- usesRange: false,
71
- indexFieldsUsed: [],
72
- explanation: 'No filter specified, full collection scan required',
73
- };
74
- }
75
-
76
- // Analyze the filter
77
- const operatorInfo = this.analyzeFilter(filter);
78
-
79
- // Get available indexes
80
- const indexes = await this.indexEngine.listIndexes();
81
-
82
- // Score each index
83
- let bestPlan: IQueryPlan | null = null;
84
- let bestScore = -1;
85
-
86
- for (const index of indexes) {
87
- const plan = this.scoreIndex(index, operatorInfo, filter);
88
- if (plan.selectivity < 1.0) {
89
- const score = this.calculateScore(plan);
90
- if (score > bestScore) {
91
- bestScore = score;
92
- bestPlan = plan;
93
- }
94
- }
95
- }
96
-
97
- // If no suitable index found, fall back to collection scan
98
- if (!bestPlan || bestScore <= 0) {
99
- return {
100
- type: 'COLLSCAN',
101
- indexCovering: false,
102
- selectivity: 1.0,
103
- usesRange: false,
104
- indexFieldsUsed: [],
105
- explanation: 'No suitable index found for this query',
106
- };
107
- }
108
-
109
- return bestPlan;
110
- }
111
-
112
- /**
113
- * Analyze filter to extract operator information per field
114
- */
115
- private analyzeFilter(filter: Document, prefix = ''): Map<string, IFilterOperatorInfo> {
116
- const result = new Map<string, IFilterOperatorInfo>();
117
-
118
- for (const [key, value] of Object.entries(filter)) {
119
- // Skip logical operators at the top level
120
- if (key.startsWith('$')) {
121
- if (key === '$and' && Array.isArray(value)) {
122
- // Merge $and conditions
123
- for (const subFilter of value) {
124
- const subInfo = this.analyzeFilter(subFilter, prefix);
125
- for (const [field, info] of subInfo) {
126
- if (result.has(field)) {
127
- // Merge operators
128
- const existing = result.get(field)!;
129
- existing.operators.push(...info.operators);
130
- existing.equality = existing.equality || info.equality;
131
- existing.range = existing.range || info.range;
132
- existing.in = existing.in || info.in;
133
- Object.assign(existing.values, info.values);
134
- } else {
135
- result.set(field, info);
136
- }
137
- }
138
- }
139
- }
140
- continue;
141
- }
142
-
143
- const fullKey = prefix ? `${prefix}.${key}` : key;
144
- const info: IFilterOperatorInfo = {
145
- field: fullKey,
146
- operators: [],
147
- equality: false,
148
- range: false,
149
- in: false,
150
- exists: false,
151
- regex: false,
152
- values: {},
153
- };
154
-
155
- if (typeof value !== 'object' || value === null || value instanceof plugins.bson.ObjectId || value instanceof Date) {
156
- // Direct equality
157
- info.equality = true;
158
- info.operators.push('$eq');
159
- info.values['$eq'] = value;
160
- } else if (Array.isArray(value)) {
161
- // Array equality (rare, but possible)
162
- info.equality = true;
163
- info.operators.push('$eq');
164
- info.values['$eq'] = value;
165
- } else {
166
- // Operator object
167
- for (const [op, opValue] of Object.entries(value)) {
168
- if (op.startsWith('$')) {
169
- info.operators.push(op);
170
- info.values[op] = opValue;
171
-
172
- switch (op) {
173
- case '$eq':
174
- info.equality = true;
175
- break;
176
- case '$ne':
177
- case '$not':
178
- // These can use indexes but with low selectivity
179
- break;
180
- case '$in':
181
- info.in = true;
182
- break;
183
- case '$nin':
184
- // Can't efficiently use indexes
185
- break;
186
- case '$gt':
187
- case '$gte':
188
- case '$lt':
189
- case '$lte':
190
- info.range = true;
191
- break;
192
- case '$exists':
193
- info.exists = true;
194
- break;
195
- case '$regex':
196
- info.regex = true;
197
- break;
198
- }
199
- } else {
200
- // Nested object - recurse
201
- const nestedInfo = this.analyzeFilter({ [op]: opValue }, fullKey);
202
- for (const [nestedField, nestedFieldInfo] of nestedInfo) {
203
- result.set(nestedField, nestedFieldInfo);
204
- }
205
- }
206
- }
207
- }
208
-
209
- if (info.operators.length > 0) {
210
- result.set(fullKey, info);
211
- }
212
- }
213
-
214
- return result;
215
- }
216
-
217
- /**
218
- * Score an index for the given filter
219
- */
220
- private scoreIndex(
221
- index: { name: string; key: Record<string, any>; unique?: boolean; sparse?: boolean },
222
- operatorInfo: Map<string, IFilterOperatorInfo>,
223
- filter: Document
224
- ): IQueryPlan {
225
- const indexFields = Object.keys(index.key);
226
- const usedFields: string[] = [];
227
- let usesRange = false;
228
- let canUseIndex = true;
229
- let selectivity = 1.0;
230
- let residualFilter: Document | undefined;
231
-
232
- // Check each index field in order
233
- for (const field of indexFields) {
234
- const info = operatorInfo.get(field);
235
- if (!info) {
236
- // Index field not in filter - stop here
237
- break;
238
- }
239
-
240
- usedFields.push(field);
241
-
242
- // Calculate selectivity based on operator
243
- if (info.equality) {
244
- // Equality has high selectivity
245
- selectivity *= 0.01; // Assume 1% match
246
- } else if (info.in) {
247
- // $in selectivity depends on array size
248
- const inValues = info.values['$in'];
249
- if (Array.isArray(inValues)) {
250
- selectivity *= Math.min(0.5, inValues.length * 0.01);
251
- } else {
252
- selectivity *= 0.1;
253
- }
254
- } else if (info.range) {
255
- // Range queries have moderate selectivity
256
- selectivity *= 0.25;
257
- usesRange = true;
258
- // After range, can't use more index fields efficiently
259
- break;
260
- } else if (info.exists) {
261
- // $exists can use sparse indexes
262
- selectivity *= 0.5;
263
- } else {
264
- // Other operators may not be indexable
265
- canUseIndex = false;
266
- break;
267
- }
268
- }
269
-
270
- if (!canUseIndex || usedFields.length === 0) {
271
- return {
272
- type: 'COLLSCAN',
273
- indexCovering: false,
274
- selectivity: 1.0,
275
- usesRange: false,
276
- indexFieldsUsed: [],
277
- explanation: `Index ${index.name} cannot be used for this query`,
278
- };
279
- }
280
-
281
- // Build residual filter for conditions not covered by index
282
- const coveredFields = new Set(usedFields);
283
- const residualConditions: Record<string, any> = {};
284
- for (const [field, info] of operatorInfo) {
285
- if (!coveredFields.has(field)) {
286
- // This field isn't covered by the index
287
- if (info.equality) {
288
- residualConditions[field] = info.values['$eq'];
289
- } else {
290
- residualConditions[field] = info.values;
291
- }
292
- }
293
- }
294
-
295
- if (Object.keys(residualConditions).length > 0) {
296
- residualFilter = residualConditions;
297
- }
298
-
299
- // Unique indexes have better selectivity for equality
300
- if (index.unique && usedFields.length === indexFields.length) {
301
- selectivity = Math.min(selectivity, 0.001); // At most 1 document
302
- }
303
-
304
- return {
305
- type: usesRange ? 'IXSCAN_RANGE' : 'IXSCAN',
306
- indexName: index.name,
307
- indexKey: index.key,
308
- indexCovering: Object.keys(residualConditions).length === 0,
309
- selectivity,
310
- usesRange,
311
- indexFieldsUsed: usedFields,
312
- residualFilter,
313
- explanation: `Using index ${index.name} on fields [${usedFields.join(', ')}]`,
314
- };
315
- }
316
-
317
- /**
318
- * Calculate overall score for a plan (higher is better)
319
- */
320
- private calculateScore(plan: IQueryPlan): number {
321
- let score = 0;
322
-
323
- // Lower selectivity is better (fewer documents to fetch)
324
- score += (1 - plan.selectivity) * 100;
325
-
326
- // Index covering queries are best
327
- if (plan.indexCovering) {
328
- score += 50;
329
- }
330
-
331
- // More index fields used is better
332
- score += plan.indexFieldsUsed.length * 10;
333
-
334
- // Equality scans are better than range scans
335
- if (!plan.usesRange) {
336
- score += 20;
337
- }
338
-
339
- return score;
340
- }
341
-
342
- /**
343
- * Explain a query - returns detailed plan information
344
- */
345
- async explain(filter: Document): Promise<{
346
- queryPlanner: {
347
- plannerVersion: number;
348
- namespace: string;
349
- indexFilterSet: boolean;
350
- winningPlan: IQueryPlan;
351
- rejectedPlans: IQueryPlan[];
352
- };
353
- }> {
354
- await this.indexEngine['initialize']();
355
-
356
- // Analyze the filter
357
- const operatorInfo = this.analyzeFilter(filter);
358
-
359
- // Get available indexes
360
- const indexes = await this.indexEngine.listIndexes();
361
-
362
- // Score all indexes
363
- const plans: IQueryPlan[] = [];
364
-
365
- for (const index of indexes) {
366
- const plan = this.scoreIndex(index, operatorInfo, filter);
367
- plans.push(plan);
368
- }
369
-
370
- // Add collection scan as fallback
371
- plans.push({
372
- type: 'COLLSCAN',
373
- indexCovering: false,
374
- selectivity: 1.0,
375
- usesRange: false,
376
- indexFieldsUsed: [],
377
- explanation: 'Full collection scan',
378
- });
379
-
380
- // Sort by score (best first)
381
- plans.sort((a, b) => this.calculateScore(b) - this.calculateScore(a));
382
-
383
- return {
384
- queryPlanner: {
385
- plannerVersion: 1,
386
- namespace: `${this.indexEngine['dbName']}.${this.indexEngine['collName']}`,
387
- indexFilterSet: false,
388
- winningPlan: plans[0],
389
- rejectedPlans: plans.slice(1),
390
- },
391
- };
392
- }
393
- }