@joystick.js/db-canary 0.0.0-canary.2251 → 0.0.0-canary.2252

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 (97) hide show
  1. package/dist/client/database.js +1 -1
  2. package/dist/client/index.js +1 -1
  3. package/dist/server/cluster/master.js +4 -4
  4. package/dist/server/cluster/worker.js +1 -1
  5. package/dist/server/index.js +1 -1
  6. package/dist/server/lib/auto_index_manager.js +1 -1
  7. package/dist/server/lib/backup_manager.js +1 -1
  8. package/dist/server/lib/index_manager.js +1 -1
  9. package/dist/server/lib/operation_dispatcher.js +1 -1
  10. package/dist/server/lib/operations/admin.js +1 -1
  11. package/dist/server/lib/operations/bulk_write.js +1 -1
  12. package/dist/server/lib/operations/create_index.js +1 -1
  13. package/dist/server/lib/operations/delete_many.js +1 -1
  14. package/dist/server/lib/operations/delete_one.js +1 -1
  15. package/dist/server/lib/operations/find.js +1 -1
  16. package/dist/server/lib/operations/find_one.js +1 -1
  17. package/dist/server/lib/operations/insert_one.js +1 -1
  18. package/dist/server/lib/operations/update_one.js +1 -1
  19. package/dist/server/lib/send_response.js +1 -1
  20. package/dist/server/lib/tcp_protocol.js +1 -1
  21. package/package.json +2 -2
  22. package/src/client/database.js +92 -119
  23. package/src/client/index.js +279 -345
  24. package/src/server/cluster/master.js +265 -156
  25. package/src/server/cluster/worker.js +26 -18
  26. package/src/server/index.js +553 -330
  27. package/src/server/lib/auto_index_manager.js +85 -23
  28. package/src/server/lib/backup_manager.js +117 -70
  29. package/src/server/lib/index_manager.js +63 -25
  30. package/src/server/lib/operation_dispatcher.js +339 -168
  31. package/src/server/lib/operations/admin.js +343 -205
  32. package/src/server/lib/operations/bulk_write.js +458 -194
  33. package/src/server/lib/operations/create_index.js +127 -34
  34. package/src/server/lib/operations/delete_many.js +204 -67
  35. package/src/server/lib/operations/delete_one.js +164 -52
  36. package/src/server/lib/operations/find.js +552 -319
  37. package/src/server/lib/operations/find_one.js +530 -304
  38. package/src/server/lib/operations/insert_one.js +147 -52
  39. package/src/server/lib/operations/update_one.js +334 -93
  40. package/src/server/lib/send_response.js +37 -17
  41. package/src/server/lib/tcp_protocol.js +158 -53
  42. package/test_data_api_key_1758233848259_cglfjzhou/data.mdb +0 -0
  43. package/test_data_api_key_1758233848259_cglfjzhou/lock.mdb +0 -0
  44. package/test_data_api_key_1758233848502_urlje2utd/data.mdb +0 -0
  45. package/test_data_api_key_1758233848502_urlje2utd/lock.mdb +0 -0
  46. package/test_data_api_key_1758233848738_mtcpfe5ns/data.mdb +0 -0
  47. package/test_data_api_key_1758233848738_mtcpfe5ns/lock.mdb +0 -0
  48. package/test_data_api_key_1758233848856_9g97p6gag/data.mdb +0 -0
  49. package/test_data_api_key_1758233848856_9g97p6gag/lock.mdb +0 -0
  50. package/test_data_api_key_1758233857008_0tl9zzhj8/data.mdb +0 -0
  51. package/test_data_api_key_1758233857008_0tl9zzhj8/lock.mdb +0 -0
  52. package/test_data_api_key_1758233857120_60c2f2uhu/data.mdb +0 -0
  53. package/test_data_api_key_1758233857120_60c2f2uhu/lock.mdb +0 -0
  54. package/test_data_api_key_1758233857232_aw7fkqgd9/data.mdb +0 -0
  55. package/test_data_api_key_1758233857232_aw7fkqgd9/lock.mdb +0 -0
  56. package/test_data_api_key_1758234881285_4aeflubjb/data.mdb +0 -0
  57. package/test_data_api_key_1758234881285_4aeflubjb/lock.mdb +0 -0
  58. package/test_data_api_key_1758234881520_kb0amvtqb/data.mdb +0 -0
  59. package/test_data_api_key_1758234881520_kb0amvtqb/lock.mdb +0 -0
  60. package/test_data_api_key_1758234881756_k04gfv2va/data.mdb +0 -0
  61. package/test_data_api_key_1758234881756_k04gfv2va/lock.mdb +0 -0
  62. package/test_data_api_key_1758234881876_wn90dpo1z/data.mdb +0 -0
  63. package/test_data_api_key_1758234881876_wn90dpo1z/lock.mdb +0 -0
  64. package/test_data_api_key_1758234889461_26xz3dmbr/data.mdb +0 -0
  65. package/test_data_api_key_1758234889461_26xz3dmbr/lock.mdb +0 -0
  66. package/test_data_api_key_1758234889572_uziz7e0p5/data.mdb +0 -0
  67. package/test_data_api_key_1758234889572_uziz7e0p5/lock.mdb +0 -0
  68. package/test_data_api_key_1758234889684_5f9wmposh/data.mdb +0 -0
  69. package/test_data_api_key_1758234889684_5f9wmposh/lock.mdb +0 -0
  70. package/test_data_api_key_1758235657729_prwgm6mxr/data.mdb +0 -0
  71. package/test_data_api_key_1758235657729_prwgm6mxr/lock.mdb +0 -0
  72. package/test_data_api_key_1758235657961_rc2da0dc2/data.mdb +0 -0
  73. package/test_data_api_key_1758235657961_rc2da0dc2/lock.mdb +0 -0
  74. package/test_data_api_key_1758235658193_oqqxm0sny/data.mdb +0 -0
  75. package/test_data_api_key_1758235658193_oqqxm0sny/lock.mdb +0 -0
  76. package/test_data_api_key_1758235658309_vggac1pj6/data.mdb +0 -0
  77. package/test_data_api_key_1758235658309_vggac1pj6/lock.mdb +0 -0
  78. package/test_data_api_key_1758235665968_61ko07dd1/data.mdb +0 -0
  79. package/test_data_api_key_1758235665968_61ko07dd1/lock.mdb +0 -0
  80. package/test_data_api_key_1758235666082_50lrt6sq8/data.mdb +0 -0
  81. package/test_data_api_key_1758235666082_50lrt6sq8/lock.mdb +0 -0
  82. package/test_data_api_key_1758235666194_ykvauwlzh/data.mdb +0 -0
  83. package/test_data_api_key_1758235666194_ykvauwlzh/lock.mdb +0 -0
  84. package/test_data_api_key_1758236187207_9c4paeh09/data.mdb +0 -0
  85. package/test_data_api_key_1758236187207_9c4paeh09/lock.mdb +0 -0
  86. package/test_data_api_key_1758236187441_4n3o3gkkl/data.mdb +0 -0
  87. package/test_data_api_key_1758236187441_4n3o3gkkl/lock.mdb +0 -0
  88. package/test_data_api_key_1758236187672_jt6b21ye0/data.mdb +0 -0
  89. package/test_data_api_key_1758236187672_jt6b21ye0/lock.mdb +0 -0
  90. package/test_data_api_key_1758236187788_oo84fz9u6/data.mdb +0 -0
  91. package/test_data_api_key_1758236187788_oo84fz9u6/lock.mdb +0 -0
  92. package/test_data_api_key_1758236195507_o9zeznwlm/data.mdb +0 -0
  93. package/test_data_api_key_1758236195507_o9zeznwlm/lock.mdb +0 -0
  94. package/test_data_api_key_1758236195619_qsqd60y41/data.mdb +0 -0
  95. package/test_data_api_key_1758236195619_qsqd60y41/lock.mdb +0 -0
  96. package/test_data_api_key_1758236195731_im13iq284/data.mdb +0 -0
  97. package/test_data_api_key_1758236195731_im13iq284/lock.mdb +0 -0
@@ -11,72 +11,105 @@ import create_logger from '../logger.js';
11
11
 
12
12
  const { create_context_logger } = create_logger('find');
13
13
 
14
+ /**
15
+ * Splits field path into parts.
16
+ * @param {string} field_path - Dot-separated field path
17
+ * @returns {Array<string>} Array of field parts
18
+ */
19
+ const split_field_path = (field_path) => {
20
+ return field_path.split('.');
21
+ };
22
+
23
+ /**
24
+ * Checks if value is null or undefined.
25
+ * @param {any} value - Value to check
26
+ * @returns {boolean} True if null or undefined
27
+ */
28
+ const is_null_or_undefined = (value) => {
29
+ return value === null || value === undefined;
30
+ };
31
+
32
+ /**
33
+ * Collects nested values from array elements.
34
+ * @param {Array} array_value - Array to process
35
+ * @param {string} remaining_path - Remaining field path
36
+ * @returns {Array|undefined} Collected nested values or undefined
37
+ */
38
+ const collect_nested_values_from_array = (array_value, remaining_path) => {
39
+ const nested_values = [];
40
+
41
+ for (let j = 0; j < array_value.length; j++) {
42
+ const item = array_value[j];
43
+ if (typeof item === 'object' && item !== null) {
44
+ const nested_value = get_field_value(item, remaining_path);
45
+ if (nested_value !== undefined) {
46
+ if (Array.isArray(nested_value)) {
47
+ nested_values.push(...nested_value);
48
+ } else {
49
+ nested_values.push(nested_value);
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return nested_values.length > 0 ? nested_values : undefined;
56
+ };
57
+
14
58
  /**
15
59
  * Extracts field value from document using dot notation path.
16
- * Handles nested array object queries like 'reviews.rating' where reviews is an array of objects.
17
60
  * @param {Object} document - Document to extract value from
18
- * @param {string} field_path - Dot-separated field path (e.g., 'user.profile.name', 'reviews.rating')
19
- * @returns {*} Field value, array of values for nested array queries, or undefined if path doesn't exist
61
+ * @param {string} field_path - Dot-separated field path
62
+ * @returns {any} Field value or undefined if path doesn't exist
20
63
  */
21
64
  const get_field_value = (document, field_path) => {
22
- const parts = field_path.split('.');
65
+ const parts = split_field_path(field_path);
23
66
  let value = document;
24
67
 
25
68
  for (let i = 0; i < parts.length; i++) {
26
69
  const part = parts[i];
27
70
 
28
- if (value === null || value === undefined) {
71
+ if (is_null_or_undefined(value)) {
29
72
  return undefined;
30
73
  }
31
74
 
32
75
  value = value[part];
33
76
 
34
- // If we got an array and there are more parts to traverse
35
77
  if (Array.isArray(value) && i < parts.length - 1) {
36
- // Extract the remaining path
37
78
  const remaining_path = parts.slice(i + 1).join('.');
38
-
39
- // Collect all matching values from array elements
40
- const nested_values = [];
41
- for (let j = 0; j < value.length; j++) {
42
- const item = value[j];
43
- if (typeof item === 'object' && item !== null) {
44
- const nested_value = get_field_value(item, remaining_path);
45
- if (nested_value !== undefined) {
46
- if (Array.isArray(nested_value)) {
47
- nested_values.push(...nested_value);
48
- } else {
49
- nested_values.push(nested_value);
50
- }
51
- }
52
- }
53
- }
54
-
55
- // Return array of values for nested array queries
56
- return nested_values.length > 0 ? nested_values : undefined;
79
+ return collect_nested_values_from_array(value, remaining_path);
57
80
  }
58
81
  }
59
82
 
60
83
  return value;
61
84
  };
62
85
 
86
+ /**
87
+ * Checks if current object has the specified property.
88
+ * @param {Object} current - Current object
89
+ * @param {string} property - Property name
90
+ * @returns {boolean} True if property exists
91
+ */
92
+ const has_property = (current, property) => {
93
+ return current.hasOwnProperty(property);
94
+ };
95
+
63
96
  /**
64
97
  * Checks if a field exists in a document using dot notation path.
65
98
  * @param {Object} document - Document to check
66
99
  * @param {string} field_path - Dot-separated field path
67
- * @returns {boolean} True if field exists, false otherwise
100
+ * @returns {boolean} True if field exists
68
101
  */
69
102
  const field_exists = (document, field_path) => {
70
- const parts = field_path.split('.');
103
+ const parts = split_field_path(field_path);
71
104
  let current = document;
72
105
 
73
106
  for (let i = 0; i < parts.length; i++) {
74
- if (current === null || current === undefined || typeof current !== 'object') {
107
+ if (is_null_or_undefined(current) || typeof current !== 'object') {
75
108
  return false;
76
109
  }
77
110
 
78
111
  if (i === parts.length - 1) {
79
- return current.hasOwnProperty(parts[i]);
112
+ return has_property(current, parts[i]);
80
113
  }
81
114
 
82
115
  current = current[parts[i]];
@@ -85,32 +118,264 @@ const field_exists = (document, field_path) => {
85
118
  return false;
86
119
  };
87
120
 
121
+ /**
122
+ * Checks if array contains value.
123
+ * @param {Array} field_value - Array field value
124
+ * @param {any} operand - Value to check
125
+ * @returns {boolean} True if array contains value
126
+ */
127
+ const array_contains_value = (field_value, operand) => {
128
+ return field_value.includes(operand);
129
+ };
130
+
131
+ /**
132
+ * Checks if any array element satisfies condition.
133
+ * @param {Array} field_value - Array field value
134
+ * @param {any} operand - Value to compare
135
+ * @param {Function} comparator - Comparison function
136
+ * @returns {boolean} True if any element satisfies condition
137
+ */
138
+ const any_array_element_satisfies = (field_value, operand, comparator) => {
139
+ for (let i = 0; i < field_value.length; i++) {
140
+ if (comparator(field_value[i], operand)) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ };
146
+
147
+ /**
148
+ * Handles equality operator matching.
149
+ * @param {any} field_value - Field value to test
150
+ * @param {any} operand - Value to match
151
+ * @returns {boolean} True if matches
152
+ */
153
+ const handle_equality_operator = (field_value, operand) => {
154
+ if (Array.isArray(field_value)) {
155
+ return array_contains_value(field_value, operand);
156
+ }
157
+ return field_value === operand;
158
+ };
159
+
160
+ /**
161
+ * Handles not equal operator matching.
162
+ * @param {any} field_value - Field value to test
163
+ * @param {any} operand - Value to match
164
+ * @returns {boolean} True if does not match
165
+ */
166
+ const handle_not_equal_operator = (field_value, operand) => {
167
+ if (Array.isArray(field_value)) {
168
+ return !array_contains_value(field_value, operand);
169
+ }
170
+ return field_value !== operand;
171
+ };
172
+
173
+ /**
174
+ * Handles greater than operator matching.
175
+ * @param {any} field_value - Field value to test
176
+ * @param {any} operand - Value to compare
177
+ * @returns {boolean} True if greater than
178
+ */
179
+ const handle_greater_than_operator = (field_value, operand) => {
180
+ if (Array.isArray(field_value)) {
181
+ return any_array_element_satisfies(field_value, operand, (val, op) => val > op);
182
+ }
183
+ return field_value > operand;
184
+ };
185
+
186
+ /**
187
+ * Handles greater than or equal operator matching.
188
+ * @param {any} field_value - Field value to test
189
+ * @param {any} operand - Value to compare
190
+ * @returns {boolean} True if greater than or equal
191
+ */
192
+ const handle_greater_than_equal_operator = (field_value, operand) => {
193
+ if (Array.isArray(field_value)) {
194
+ return any_array_element_satisfies(field_value, operand, (val, op) => val >= op);
195
+ }
196
+ return field_value >= operand;
197
+ };
198
+
199
+ /**
200
+ * Handles less than operator matching.
201
+ * @param {any} field_value - Field value to test
202
+ * @param {any} operand - Value to compare
203
+ * @returns {boolean} True if less than
204
+ */
205
+ const handle_less_than_operator = (field_value, operand) => {
206
+ if (Array.isArray(field_value)) {
207
+ return any_array_element_satisfies(field_value, operand, (val, op) => val < op);
208
+ }
209
+ return field_value < operand;
210
+ };
211
+
212
+ /**
213
+ * Handles less than or equal operator matching.
214
+ * @param {any} field_value - Field value to test
215
+ * @param {any} operand - Value to compare
216
+ * @returns {boolean} True if less than or equal
217
+ */
218
+ const handle_less_than_equal_operator = (field_value, operand) => {
219
+ if (Array.isArray(field_value)) {
220
+ return any_array_element_satisfies(field_value, operand, (val, op) => val <= op);
221
+ }
222
+ return field_value <= operand;
223
+ };
224
+
225
+ /**
226
+ * Handles in operator matching.
227
+ * @param {any} field_value - Field value to test
228
+ * @param {Array} operand - Array of values to match
229
+ * @returns {boolean} True if value is in array
230
+ */
231
+ const handle_in_operator = (field_value, operand) => {
232
+ if (!Array.isArray(operand)) {
233
+ return false;
234
+ }
235
+
236
+ if (Array.isArray(field_value)) {
237
+ return any_array_element_satisfies(field_value, operand, (val, op) => op.includes(val));
238
+ }
239
+
240
+ return operand.includes(field_value);
241
+ };
242
+
243
+ /**
244
+ * Handles not in operator matching.
245
+ * @param {any} field_value - Field value to test
246
+ * @param {Array} operand - Array of values to exclude
247
+ * @returns {boolean} True if value is not in array
248
+ */
249
+ const handle_not_in_operator = (field_value, operand) => {
250
+ if (!Array.isArray(operand)) {
251
+ return false;
252
+ }
253
+
254
+ if (Array.isArray(field_value)) {
255
+ for (let i = 0; i < field_value.length; i++) {
256
+ if (operand.includes(field_value[i])) {
257
+ return false;
258
+ }
259
+ }
260
+ return true;
261
+ }
262
+
263
+ return !operand.includes(field_value);
264
+ };
265
+
266
+ /**
267
+ * Handles exists operator matching.
268
+ * @param {Object} document - Document to check
269
+ * @param {string} field - Field name
270
+ * @param {boolean} operand - Whether field should exist
271
+ * @returns {boolean} True if existence matches expectation
272
+ */
273
+ const handle_exists_operator = (document, field, operand) => {
274
+ const exists = field_exists(document, field);
275
+ return operand ? exists : !exists;
276
+ };
277
+
278
+ /**
279
+ * Handles regex operator matching.
280
+ * @param {any} field_value - Field value to test
281
+ * @param {string} pattern - Regex pattern
282
+ * @param {string} options - Regex options
283
+ * @returns {boolean} True if matches regex
284
+ */
285
+ const handle_regex_operator = (field_value, pattern, options = '') => {
286
+ const regex = new RegExp(pattern, options);
287
+
288
+ if (Array.isArray(field_value)) {
289
+ return any_array_element_satisfies(field_value, regex, (val, reg) => {
290
+ return typeof val === 'string' && reg.test(val);
291
+ });
292
+ }
293
+
294
+ return regex.test(field_value);
295
+ };
296
+
297
+ /**
298
+ * Processes query operators for a field.
299
+ * @param {Object} document - Document to test
300
+ * @param {string} field - Field name
301
+ * @param {any} field_value - Field value
302
+ * @param {Object} value - Query value with operators
303
+ * @returns {boolean} True if all operators match
304
+ */
305
+ const process_query_operators = (document, field, field_value, value) => {
306
+ for (const [operator, operand] of Object.entries(value)) {
307
+ switch (operator) {
308
+ case '$eq':
309
+ if (!handle_equality_operator(field_value, operand)) return false;
310
+ break;
311
+ case '$ne':
312
+ if (!handle_not_equal_operator(field_value, operand)) return false;
313
+ break;
314
+ case '$gt':
315
+ if (!handle_greater_than_operator(field_value, operand)) return false;
316
+ break;
317
+ case '$gte':
318
+ if (!handle_greater_than_equal_operator(field_value, operand)) return false;
319
+ break;
320
+ case '$lt':
321
+ if (!handle_less_than_operator(field_value, operand)) return false;
322
+ break;
323
+ case '$lte':
324
+ if (!handle_less_than_equal_operator(field_value, operand)) return false;
325
+ break;
326
+ case '$in':
327
+ if (!handle_in_operator(field_value, operand)) return false;
328
+ break;
329
+ case '$nin':
330
+ if (!handle_not_in_operator(field_value, operand)) return false;
331
+ break;
332
+ case '$exists':
333
+ if (!handle_exists_operator(document, field, operand)) return false;
334
+ break;
335
+ case '$regex':
336
+ const regex_options = value.$options || '';
337
+ if (!handle_regex_operator(field_value, operand, regex_options)) return false;
338
+ break;
339
+ case '$options':
340
+ break;
341
+ default:
342
+ throw new Error(`Unsupported query operator: ${operator}`);
343
+ }
344
+ }
345
+ return true;
346
+ };
347
+
348
+ /**
349
+ * Handles OR operator matching.
350
+ * @param {Object} document - Document to test
351
+ * @param {Array} or_conditions - Array of OR conditions
352
+ * @returns {boolean} True if any condition matches
353
+ */
354
+ const handle_or_operator = (document, or_conditions) => {
355
+ for (let i = 0; i < or_conditions.length; i++) {
356
+ if (matches_filter(document, or_conditions[i])) {
357
+ return true;
358
+ }
359
+ }
360
+ return false;
361
+ };
362
+
88
363
  /**
89
364
  * Checks if a document matches the provided filter criteria.
90
- * Supports MongoDB-like query operators with optimized array field matching.
91
365
  * @param {Object} document - Document to test against filter
92
366
  * @param {Object} filter - Filter criteria with field names and values/operators
93
- * @returns {boolean} True if document matches filter, false otherwise
94
- * @throws {Error} When unsupported query operator is used
367
+ * @returns {boolean} True if document matches filter
95
368
  */
96
369
  const matches_filter = (document, filter) => {
97
370
  if (!filter || Object.keys(filter).length === 0) {
98
371
  return true;
99
372
  }
100
373
 
101
- // Handle $or operator at the top level
102
374
  if (filter.$or && Array.isArray(filter.$or)) {
103
- const or_conditions = filter.$or;
104
- let or_match = false;
105
- for (let i = 0; i < or_conditions.length; i++) {
106
- if (matches_filter(document, or_conditions[i])) {
107
- or_match = true;
108
- break;
109
- }
375
+ if (!handle_or_operator(document, filter.$or)) {
376
+ return false;
110
377
  }
111
- if (!or_match) return false;
112
378
 
113
- // Continue checking other conditions (if any) outside of $or
114
379
  const remaining_filter = { ...filter };
115
380
  delete remaining_filter.$or;
116
381
  if (Object.keys(remaining_filter).length > 0) {
@@ -123,143 +388,11 @@ const matches_filter = (document, filter) => {
123
388
  const field_value = get_field_value(document, field);
124
389
 
125
390
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
126
- for (const [operator, operand] of Object.entries(value)) {
127
- switch (operator) {
128
- case '$eq':
129
- // Handle array field matching for $eq
130
- if (Array.isArray(field_value)) {
131
- if (!field_value.includes(operand)) return false;
132
- } else if (field_value !== operand) {
133
- return false;
134
- }
135
- break;
136
- case '$ne':
137
- // Handle array field matching for $ne
138
- if (Array.isArray(field_value)) {
139
- if (field_value.includes(operand)) return false;
140
- } else if (field_value === operand) {
141
- return false;
142
- }
143
- break;
144
- case '$gt':
145
- if (Array.isArray(field_value)) {
146
- let found = false;
147
- for (let i = 0; i < field_value.length; i++) {
148
- if (field_value[i] > operand) {
149
- found = true;
150
- break;
151
- }
152
- }
153
- if (!found) return false;
154
- } else if (field_value <= operand) {
155
- return false;
156
- }
157
- break;
158
- case '$gte':
159
- if (Array.isArray(field_value)) {
160
- let found = false;
161
- for (let i = 0; i < field_value.length; i++) {
162
- if (field_value[i] >= operand) {
163
- found = true;
164
- break;
165
- }
166
- }
167
- if (!found) return false;
168
- } else if (field_value < operand) {
169
- return false;
170
- }
171
- break;
172
- case '$lt':
173
- if (Array.isArray(field_value)) {
174
- let found = false;
175
- for (let i = 0; i < field_value.length; i++) {
176
- if (field_value[i] < operand) {
177
- found = true;
178
- break;
179
- }
180
- }
181
- if (!found) return false;
182
- } else if (field_value >= operand) {
183
- return false;
184
- }
185
- break;
186
- case '$lte':
187
- if (Array.isArray(field_value)) {
188
- let found = false;
189
- for (let i = 0; i < field_value.length; i++) {
190
- if (field_value[i] <= operand) {
191
- found = true;
192
- break;
193
- }
194
- }
195
- if (!found) return false;
196
- } else if (field_value > operand) {
197
- return false;
198
- }
199
- break;
200
- case '$in':
201
- if (!Array.isArray(operand)) return false;
202
- if (Array.isArray(field_value)) {
203
- // Check if any array element is in the operand array
204
- let found = false;
205
- for (let i = 0; i < field_value.length; i++) {
206
- if (operand.includes(field_value[i])) {
207
- found = true;
208
- break;
209
- }
210
- }
211
- if (!found) return false;
212
- } else if (!operand.includes(field_value)) {
213
- return false;
214
- }
215
- break;
216
- case '$nin':
217
- if (!Array.isArray(operand)) return false;
218
- if (Array.isArray(field_value)) {
219
- // Check if any array element is in the operand array
220
- for (let i = 0; i < field_value.length; i++) {
221
- if (operand.includes(field_value[i])) {
222
- return false;
223
- }
224
- }
225
- } else if (operand.includes(field_value)) {
226
- return false;
227
- }
228
- break;
229
- case '$exists':
230
- const exists = field_exists(document, field);
231
- if (operand && !exists) return false;
232
- if (!operand && exists) return false;
233
- break;
234
- case '$regex':
235
- // Handle $options parameter for regex flags
236
- const regex_options = value.$options || '';
237
- const regex = new RegExp(operand, regex_options);
238
- if (Array.isArray(field_value)) {
239
- let found = false;
240
- for (let i = 0; i < field_value.length; i++) {
241
- if (typeof field_value[i] === 'string' && regex.test(field_value[i])) {
242
- found = true;
243
- break;
244
- }
245
- }
246
- if (!found) return false;
247
- } else if (!regex.test(field_value)) {
248
- return false;
249
- }
250
- break;
251
- case '$options':
252
- // $options is handled as part of $regex, skip it here
253
- break;
254
- default:
255
- throw new Error(`Unsupported query operator: ${operator}`);
256
- }
391
+ if (!process_query_operators(document, field, field_value, value)) {
392
+ return false;
257
393
  }
258
394
  } else {
259
- // Direct field matching with array support
260
- if (Array.isArray(field_value)) {
261
- if (!field_value.includes(value)) return false;
262
- } else if (field_value !== value) {
395
+ if (!handle_equality_operator(field_value, value)) {
263
396
  return false;
264
397
  }
265
398
  }
@@ -269,48 +402,90 @@ const matches_filter = (document, filter) => {
269
402
  };
270
403
 
271
404
  /**
272
- * Applies projection to a document, including or excluding specified fields.
273
- * Supports both inclusion and exclusion projections with MongoDB-like syntax.
405
+ * Determines if projection is inclusion-based.
406
+ * @param {Object} projection - Projection specification
407
+ * @returns {boolean} True if inclusion-based
408
+ */
409
+ const is_inclusion_projection = (projection) => {
410
+ return Object.values(projection).some(value => value === 1 || value === true);
411
+ };
412
+
413
+ /**
414
+ * Applies inclusion projection to document.
274
415
  * @param {Object} document - Document to project
275
- * @param {Object} projection - Projection specification with field names and 1/0 or true/false values
276
- * @returns {Object} Projected document with only specified fields
416
+ * @param {Object} projection - Projection specification
417
+ * @returns {Object} Projected document
418
+ */
419
+ const apply_inclusion_projection = (document, projection) => {
420
+ const projected_document = { _id: document._id };
421
+
422
+ for (const [field, include] of Object.entries(projection)) {
423
+ if (field === '_id' && (include === 0 || include === false)) {
424
+ delete projected_document._id;
425
+ } else if (include === 1 || include === true) {
426
+ projected_document[field] = document[field];
427
+ }
428
+ }
429
+
430
+ return projected_document;
431
+ };
432
+
433
+ /**
434
+ * Applies exclusion projection to document.
435
+ * @param {Object} document - Document to project
436
+ * @param {Object} projection - Projection specification
437
+ * @returns {Object} Projected document
438
+ */
439
+ const apply_exclusion_projection = (document, projection) => {
440
+ const projected_document = { ...document };
441
+
442
+ for (const [field, exclude] of Object.entries(projection)) {
443
+ if (exclude === 0 || exclude === false) {
444
+ delete projected_document[field];
445
+ }
446
+ }
447
+
448
+ return projected_document;
449
+ };
450
+
451
+ /**
452
+ * Applies projection to a document.
453
+ * @param {Object} document - Document to project
454
+ * @param {Object} projection - Projection specification
455
+ * @returns {Object} Projected document
277
456
  */
278
457
  const apply_projection = (document, projection) => {
279
458
  if (!projection || Object.keys(projection).length === 0) {
280
459
  return document;
281
460
  }
282
461
 
283
- const is_inclusion = Object.values(projection).some(value => value === 1 || value === true);
284
- const projected_document = {};
285
-
286
- if (is_inclusion) {
287
- projected_document._id = document._id;
288
-
289
- for (const [field, include] of Object.entries(projection)) {
290
- if (field === '_id' && (include === 0 || include === false)) {
291
- delete projected_document._id;
292
- } else if (include === 1 || include === true) {
293
- projected_document[field] = document[field];
294
- }
295
- }
462
+ if (is_inclusion_projection(projection)) {
463
+ return apply_inclusion_projection(document, projection);
296
464
  } else {
297
- Object.assign(projected_document, document);
298
-
299
- for (const [field, exclude] of Object.entries(projection)) {
300
- if (exclude === 0 || exclude === false) {
301
- delete projected_document[field];
302
- }
303
- }
465
+ return apply_exclusion_projection(document, projection);
304
466
  }
467
+ };
468
+
469
+ /**
470
+ * Compares two values for sorting.
471
+ * @param {any} a_value - First value
472
+ * @param {any} b_value - Second value
473
+ * @param {number} direction - Sort direction (1 or -1)
474
+ * @returns {number} Comparison result
475
+ */
476
+ const compare_values_for_sort = (a_value, b_value, direction) => {
477
+ if (a_value === b_value) return 0;
478
+ if (a_value === undefined) return 1;
479
+ if (b_value === undefined) return -1;
305
480
 
306
- return projected_document;
481
+ const comparison = a_value < b_value ? -1 : a_value > b_value ? 1 : 0;
482
+ return direction === -1 ? -comparison : comparison;
307
483
  };
308
484
 
309
485
  /**
310
486
  * Sorts an array of documents based on sort specification.
311
- * Supports multi-field sorting with ascending (1) and descending (-1) directions.
312
487
  * @param {Array<Object>} documents - Array of documents to sort
313
- * @param {Object} sort - Sort specification with field names and 1/-1 direction values
488
+ * @param {Object} sort - Sort specification
314
489
  * @returns {Array<Object>} Sorted array of documents
315
490
  */
316
491
  const apply_sort = (documents, sort) => {
@@ -320,44 +495,22 @@ const apply_sort = (documents, sort) => {
320
495
 
321
496
  return documents.sort((a, b) => {
322
497
  for (const [field, direction] of Object.entries(sort)) {
323
- const a_value = a[field];
324
- const b_value = b[field];
325
-
326
- if (a_value === b_value) continue;
327
-
328
- if (a_value === undefined) return 1;
329
- if (b_value === undefined) return -1;
330
-
331
- const comparison = a_value < b_value ? -1 : a_value > b_value ? 1 : 0;
332
-
333
- if (direction === -1) {
334
- return -comparison;
498
+ const comparison = compare_values_for_sort(a[field], b[field], direction);
499
+ if (comparison !== 0) {
500
+ return comparison;
335
501
  }
336
-
337
- return comparison;
338
502
  }
339
-
340
503
  return 0;
341
504
  });
342
505
  };
343
506
 
344
507
  /**
345
- * Finds documents in a collection matching the specified filter criteria.
346
- * Supports MongoDB-like querying with automatic index optimization, projection, sorting, and pagination.
347
- * @param {string} database_name - Name of the database
348
- * @param {string} collection_name - Name of the collection to search
349
- * @param {Object} [filter={}] - Filter criteria for matching documents
350
- * @param {Object} [options={}] - Query options
351
- * @param {Object} [options.projection] - Fields to include/exclude in results
352
- * @param {Object} [options.sort] - Sort specification for ordering results
353
- * @param {number} [options.limit] - Maximum number of documents to return
354
- * @param {number} [options.skip=0] - Number of documents to skip
355
- * @returns {Promise<Array<Object>>} Promise resolving to array of matching documents
356
- * @throws {Error} When database or collection name is missing or query execution fails
508
+ * Validates required parameters.
509
+ * @param {string} database_name - Database name
510
+ * @param {string} collection_name - Collection name
511
+ * @throws {Error} When parameters are missing
357
512
  */
358
- const find = async (database_name, collection_name, filter = {}, options = {}) => {
359
- const log = create_context_logger();
360
-
513
+ const validate_find_parameters = (database_name, collection_name) => {
361
514
  if (!database_name) {
362
515
  throw new Error('Database name is required');
363
516
  }
@@ -365,6 +518,152 @@ const find = async (database_name, collection_name, filter = {}, options = {}) =
365
518
  if (!collection_name) {
366
519
  throw new Error('Collection name is required');
367
520
  }
521
+ };
522
+
523
+ /**
524
+ * Searches for documents using index.
525
+ * @param {Object} db - Database instance
526
+ * @param {string} database_name - Database name
527
+ * @param {string} collection_name - Collection name
528
+ * @param {Object} filter - Query filter
529
+ * @param {Object} index_info - Index information
530
+ * @returns {Array<Object>} Found documents
531
+ */
532
+ const search_using_index = (db, database_name, collection_name, filter, index_info) => {
533
+ const { field, operators } = index_info;
534
+ const field_filter = filter[field];
535
+ const matching_documents = [];
536
+
537
+ record_index_usage(database_name, collection_name, field);
538
+
539
+ let document_ids = null;
540
+
541
+ if (typeof field_filter === 'object' && field_filter !== null && !Array.isArray(field_filter)) {
542
+ for (const operator of operators) {
543
+ if (field_filter[operator] !== undefined) {
544
+ document_ids = find_documents_by_index(database_name, collection_name, field, operator, field_filter[operator]);
545
+ break;
546
+ }
547
+ }
548
+ } else if (operators.includes('eq')) {
549
+ document_ids = find_documents_by_index(database_name, collection_name, field, 'eq', field_filter);
550
+ }
551
+
552
+ if (document_ids) {
553
+ for (const document_id of document_ids) {
554
+ const collection_key = build_collection_key(database_name, collection_name, document_id);
555
+ const document_data = db.get(collection_key);
556
+
557
+ if (document_data) {
558
+ const document = JSON.parse(document_data);
559
+ if (matches_filter(document, filter)) {
560
+ matching_documents.push(document);
561
+ }
562
+ }
563
+ }
564
+ }
565
+
566
+ return matching_documents;
567
+ };
568
+
569
+ /**
570
+ * Searches collection using full scan.
571
+ * @param {Object} db - Database instance
572
+ * @param {string} database_name - Database name
573
+ * @param {string} collection_name - Collection name
574
+ * @param {Object} filter - Query filter
575
+ * @returns {Array<Object>} Found documents
576
+ */
577
+ const search_using_full_scan = (db, database_name, collection_name, filter) => {
578
+ const matching_documents = [];
579
+ const collection_prefix = `${database_name}:${collection_name}:`;
580
+ const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
581
+
582
+ for (const { key, value: document_data } of range) {
583
+ const document = JSON.parse(document_data);
584
+ if (matches_filter(document, filter)) {
585
+ matching_documents.push(document);
586
+ }
587
+ }
588
+
589
+ return matching_documents;
590
+ };
591
+
592
+ /**
593
+ * Applies pagination to documents.
594
+ * @param {Array<Object>} documents - Documents to paginate
595
+ * @param {number} skip - Number of documents to skip
596
+ * @param {number} limit - Maximum number of documents to return
597
+ * @returns {Array<Object>} Paginated documents
598
+ */
599
+ const apply_pagination = (documents, skip, limit) => {
600
+ let result = documents;
601
+
602
+ if (skip > 0) {
603
+ result = result.slice(skip);
604
+ }
605
+
606
+ if (limit && limit > 0) {
607
+ result = result.slice(0, limit);
608
+ }
609
+
610
+ return result;
611
+ };
612
+
613
+ /**
614
+ * Records query for auto-indexing analysis.
615
+ * @param {Function} log - Logger function
616
+ * @param {string} collection_name - Collection name
617
+ * @param {Object} filter - Query filter
618
+ * @param {number} execution_time - Execution time in milliseconds
619
+ * @param {boolean} used_index - Whether index was used
620
+ * @param {string} indexed_field - Field that was indexed
621
+ */
622
+ const record_query_for_auto_indexing = (log, collection_name, filter, execution_time, used_index, indexed_field) => {
623
+ try {
624
+ record_query(collection_name, filter, execution_time, used_index, indexed_field);
625
+ } catch (auto_index_error) {
626
+ log.warn('Failed to record query for auto-indexing', {
627
+ error: auto_index_error.message
628
+ });
629
+ }
630
+ };
631
+
632
+ /**
633
+ * Logs find operation completion.
634
+ * @param {Function} log - Logger function
635
+ * @param {string} database_name - Database name
636
+ * @param {string} collection_name - Collection name
637
+ * @param {number} documents_found - Number of documents found
638
+ * @param {number} total_matching - Total matching documents
639
+ * @param {boolean} used_index - Whether index was used
640
+ * @param {string} indexed_field - Field that was indexed
641
+ * @param {number} execution_time - Execution time in milliseconds
642
+ */
643
+ const log_find_completion = (log, database_name, collection_name, documents_found, total_matching, used_index, indexed_field, execution_time) => {
644
+ log.info('Find operation completed', {
645
+ database: database_name,
646
+ collection: collection_name,
647
+ documents_found,
648
+ total_matching,
649
+ used_index,
650
+ indexed_field,
651
+ execution_time_ms: execution_time
652
+ });
653
+ };
654
+
655
+ /**
656
+ * Finds documents in a collection matching the specified filter criteria.
657
+ * @param {string} database_name - Name of the database
658
+ * @param {string} collection_name - Name of the collection to search
659
+ * @param {Object} filter - Filter criteria for matching documents
660
+ * @param {Object} options - Query options
661
+ * @returns {Promise<Array<Object>>} Array of matching documents
662
+ */
663
+ const find = async (database_name, collection_name, filter = {}, options = {}) => {
664
+ const log = create_context_logger();
665
+
666
+ validate_find_parameters(database_name, collection_name);
368
667
 
369
668
  const db = get_database();
370
669
  const { projection, sort, limit, skip = 0 } = options;
@@ -378,101 +677,35 @@ const find = async (database_name, collection_name, filter = {}, options = {}) =
378
677
  const index_info = can_use_index(database_name, collection_name, filter);
379
678
 
380
679
  if (index_info) {
381
- const { field, operators } = index_info;
382
- const field_filter = filter[field];
383
- indexed_field = field;
384
-
385
- if (typeof field_filter === 'object' && field_filter !== null && !Array.isArray(field_filter)) {
386
- for (const operator of operators) {
387
- if (field_filter[operator] !== undefined) {
388
- const document_ids = find_documents_by_index(database_name, collection_name, field, operator, field_filter[operator]);
389
-
390
- if (document_ids) {
391
- used_index = true;
392
- record_index_usage(database_name, collection_name, field);
393
-
394
- for (const document_id of document_ids) {
395
- const collection_key = build_collection_key(database_name, collection_name, document_id);
396
- const document_data = db.get(collection_key);
397
-
398
- if (document_data) {
399
- const document = JSON.parse(document_data);
400
- if (matches_filter(document, filter)) {
401
- matching_documents.push(document);
402
- }
403
- }
404
- }
405
- break;
406
- }
407
- }
408
- }
409
- } else if (operators.includes('eq')) {
410
- const document_ids = find_documents_by_index(database_name, collection_name, field, 'eq', field_filter);
411
-
412
- if (document_ids) {
413
- used_index = true;
414
- record_index_usage(database_name, collection_name, field);
415
-
416
- for (const document_id of document_ids) {
417
- const collection_key = build_collection_key(database_name, collection_name, document_id);
418
- const document_data = db.get(collection_key);
419
-
420
- if (document_data) {
421
- const document = JSON.parse(document_data);
422
- if (matches_filter(document, filter)) {
423
- matching_documents.push(document);
424
- }
425
- }
426
- }
427
- }
428
- }
680
+ indexed_field = index_info.field;
681
+ matching_documents = search_using_index(db, database_name, collection_name, filter, index_info);
682
+ used_index = matching_documents.length > 0;
429
683
  }
430
684
 
431
685
  if (!used_index) {
432
- const collection_prefix = `${database_name}:${collection_name}:`;
433
- const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
434
-
435
- for (const { key, value: document_data } of range) {
436
- const document = JSON.parse(document_data);
437
- if (matches_filter(document, filter)) {
438
- matching_documents.push(document);
439
- }
440
- }
441
- }
442
-
443
- let sorted_documents = apply_sort(matching_documents, sort);
444
-
445
- if (skip > 0) {
446
- sorted_documents = sorted_documents.slice(skip);
447
- }
448
-
449
- if (limit && limit > 0) {
450
- sorted_documents = sorted_documents.slice(0, limit);
686
+ matching_documents = search_using_full_scan(db, database_name, collection_name, filter);
451
687
  }
452
688
 
453
- const projected_documents = sorted_documents.map(document =>
689
+ const sorted_documents = apply_sort(matching_documents, sort);
690
+ const paginated_documents = apply_pagination(sorted_documents, skip, limit);
691
+ const projected_documents = paginated_documents.map(document =>
454
692
  apply_projection(document, projection)
455
693
  );
456
694
 
457
695
  const execution_time = Date.now() - start_time;
458
696
 
459
- try {
460
- record_query(collection_name, filter, execution_time, used_index, indexed_field);
461
- } catch (auto_index_error) {
462
- log.warn('Failed to record query for auto-indexing', {
463
- error: auto_index_error.message
464
- });
465
- }
697
+ record_query_for_auto_indexing(log, collection_name, filter, execution_time, used_index, indexed_field);
466
698
 
467
- log.info('Find operation completed', {
468
- database: database_name,
469
- collection: collection_name,
470
- documents_found: projected_documents.length,
471
- total_matching: matching_documents.length,
472
- used_index,
473
- indexed_field,
474
- execution_time_ms: execution_time
475
- });
699
+ log_find_completion(
700
+ log,
701
+ database_name,
702
+ collection_name,
703
+ projected_documents.length,
704
+ matching_documents.length,
705
+ used_index,
706
+ indexed_field,
707
+ execution_time
708
+ );
476
709
 
477
710
  return projected_documents;
478
711
  } catch (error) {