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