@joystick.js/db-canary 0.0.0-canary.2271 → 0.0.0-canary.2272

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.
@@ -48,7 +48,9 @@ test.afterEach(async () => {
48
48
 
49
49
  if (server) {
50
50
  await server.cleanup();
51
- server.close();
51
+ await new Promise((resolve) => {
52
+ server.close(resolve);
53
+ });
52
54
  server = null;
53
55
  }
54
56
 
@@ -57,7 +57,9 @@ test.before(async () => {
57
57
  test.after.always(async () => {
58
58
  if (server) {
59
59
  await server.cleanup();
60
- server.close();
60
+ await new Promise((resolve) => {
61
+ server.close(resolve);
62
+ });
61
63
  }
62
64
  });
63
65
 
@@ -0,0 +1,523 @@
1
+ /**
2
+ * @fileoverview Comprehensive tests for JoystickDB bulk insert optimizer.
3
+ * Tests performance optimizations, safety guarantees, and concurrent read capabilities.
4
+ */
5
+
6
+ import test from 'ava';
7
+ import { rmSync, existsSync } from 'fs';
8
+ import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
9
+ import {
10
+ bulk_insert_optimized,
11
+ bulk_insert_with_metrics,
12
+ non_blocking_bulk_insert,
13
+ calculate_average_document_size,
14
+ calculate_bulk_map_size,
15
+ create_size_based_batches,
16
+ sort_documents_by_key,
17
+ pre_encode_documents
18
+ } from '../../../src/server/lib/bulk_insert_optimizer.js';
19
+ import find_one from '../../../src/server/lib/operations/find_one.js';
20
+ import find from '../../../src/server/lib/operations/find.js';
21
+
22
+ const TEST_DB_PATH = './test_data/bulk_optimizer_test';
23
+ const TEST_DATABASE = 'test_db';
24
+ const TEST_COLLECTION = 'test_collection';
25
+
26
+ /**
27
+ * Generates test documents for bulk insert testing.
28
+ * @param {number} count - Number of documents to generate
29
+ * @param {Object} [options={}] - Generation options
30
+ * @returns {Array<Object>} Array of test documents
31
+ */
32
+ const generate_test_documents = (count, options = {}) => {
33
+ const {
34
+ include_id = false,
35
+ base_size = 100,
36
+ variable_size = false
37
+ } = options;
38
+
39
+ const documents = [];
40
+
41
+ for (let i = 0; i < count; i++) {
42
+ const doc = {
43
+ name: `Test Document ${i}`,
44
+ index: i,
45
+ category: `category_${i % 10}`,
46
+ active: i % 2 === 0,
47
+ metadata: {
48
+ created_by: 'test_user',
49
+ tags: [`tag_${i % 5}`, `tag_${(i + 1) % 5}`],
50
+ priority: i % 3
51
+ }
52
+ };
53
+
54
+ if (include_id) {
55
+ doc._id = `test_doc_${i.toString().padStart(6, '0')}`;
56
+ }
57
+
58
+ // Add variable size content for testing
59
+ if (variable_size) {
60
+ const extra_content_size = Math.floor(Math.random() * base_size);
61
+ doc.content = 'x'.repeat(extra_content_size);
62
+ }
63
+
64
+ documents.push(doc);
65
+ }
66
+
67
+ return documents;
68
+ };
69
+
70
+ /**
71
+ * Sets up test database before each test.
72
+ */
73
+ test.beforeEach(async () => {
74
+ // Clean up any existing test database
75
+ if (existsSync(TEST_DB_PATH)) {
76
+ rmSync(TEST_DB_PATH, { recursive: true, force: true });
77
+ }
78
+
79
+ // Initialize fresh database
80
+ initialize_database(TEST_DB_PATH);
81
+ });
82
+
83
+ /**
84
+ * Cleans up test database after each test.
85
+ */
86
+ test.afterEach(async () => {
87
+ await cleanup_database(true);
88
+ });
89
+
90
+ // Utility function tests
91
+ test('calculate_average_document_size should calculate correct average', t => {
92
+ const documents = [
93
+ { name: 'doc1', data: 'small' },
94
+ { name: 'doc2', data: 'medium_sized_content' },
95
+ { name: 'doc3', data: 'large_content_with_more_data' }
96
+ ];
97
+
98
+ const avg_size = calculate_average_document_size(documents);
99
+
100
+ t.true(avg_size > 0);
101
+ t.true(typeof avg_size === 'number');
102
+ });
103
+
104
+ test('calculate_bulk_map_size should return appropriate map size', t => {
105
+ const document_count = 100000;
106
+ const avg_document_size = 500;
107
+
108
+ const map_size = calculate_bulk_map_size(document_count, avg_document_size);
109
+
110
+ // Should be at least 2x the estimated size
111
+ const estimated_size = document_count * avg_document_size;
112
+ t.true(map_size >= estimated_size * 2);
113
+
114
+ // Should be at least 10GB minimum
115
+ const minimum_size = 1024 * 1024 * 1024 * 10;
116
+ t.true(map_size >= minimum_size);
117
+ });
118
+
119
+ test('create_size_based_batches should create appropriate batches', t => {
120
+ const documents = generate_test_documents(1000);
121
+ const target_size = 50 * 1024; // 50KB target
122
+
123
+ const batches = create_size_based_batches(documents, target_size);
124
+
125
+ t.true(batches.length > 0);
126
+ t.true(Array.isArray(batches));
127
+
128
+ // Verify all documents are included
129
+ const total_docs = batches.reduce((sum, batch) => sum + batch.length, 0);
130
+ t.is(total_docs, documents.length);
131
+ });
132
+
133
+ test('sort_documents_by_key should sort documents correctly', t => {
134
+ const documents = generate_test_documents(100);
135
+
136
+ const sorted_docs = sort_documents_by_key(documents, TEST_DATABASE, TEST_COLLECTION);
137
+
138
+ t.is(sorted_docs.length, documents.length);
139
+
140
+ // Verify all documents have IDs
141
+ sorted_docs.forEach(doc => {
142
+ t.truthy(doc._id);
143
+ });
144
+
145
+ // Verify sorting (keys should be in ascending order)
146
+ for (let i = 1; i < sorted_docs.length; i++) {
147
+ const key_a = `${TEST_DATABASE}:${TEST_COLLECTION}:${sorted_docs[i-1]._id}`;
148
+ const key_b = `${TEST_DATABASE}:${TEST_COLLECTION}:${sorted_docs[i]._id}`;
149
+ t.true(key_a.localeCompare(key_b) <= 0);
150
+ }
151
+ });
152
+
153
+ test('pre_encode_documents should encode documents correctly', t => {
154
+ const documents = generate_test_documents(10, { include_id: true });
155
+
156
+ const encoded_docs = pre_encode_documents(documents, TEST_DATABASE, TEST_COLLECTION);
157
+
158
+ t.is(encoded_docs.length, documents.length);
159
+
160
+ encoded_docs.forEach((encoded, index) => {
161
+ t.truthy(encoded.key);
162
+ t.truthy(encoded.value);
163
+ t.truthy(encoded.document_id);
164
+ t.is(encoded.document_id, documents[index]._id);
165
+
166
+ // Verify key format
167
+ t.true(encoded.key.startsWith(`${TEST_DATABASE}:${TEST_COLLECTION}:`));
168
+
169
+ // Verify value is valid JSON
170
+ const parsed = JSON.parse(encoded.value);
171
+ t.truthy(parsed._created_at);
172
+ t.truthy(parsed._updated_at);
173
+ });
174
+ });
175
+
176
+ // Basic bulk insert tests
177
+ test('bulk_insert_optimized should insert small dataset successfully', async t => {
178
+ const documents = generate_test_documents(100);
179
+
180
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
181
+
182
+ t.true(result.acknowledged);
183
+ t.is(result.inserted_count, 100);
184
+ t.is(result.inserted_ids.length, 100);
185
+ t.truthy(result.performance);
186
+ t.true(result.performance.duration_ms > 0);
187
+ t.true(result.performance.documents_per_second > 0);
188
+ });
189
+
190
+ test('bulk_insert_optimized should insert medium dataset successfully', async t => {
191
+ const documents = generate_test_documents(10000);
192
+
193
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
194
+
195
+ t.true(result.acknowledged);
196
+ t.is(result.inserted_count, 10000);
197
+ t.is(result.inserted_ids.length, 10000);
198
+ t.truthy(result.performance);
199
+
200
+ // Performance should be reasonable
201
+ t.true(result.performance.documents_per_second > 1000);
202
+ });
203
+
204
+ test('bulk_insert_optimized should handle large dataset efficiently', async t => {
205
+ const documents = generate_test_documents(100000);
206
+
207
+ const start_time = Date.now();
208
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
209
+ const duration = Date.now() - start_time;
210
+
211
+ t.true(result.acknowledged);
212
+ t.is(result.inserted_count, 100000);
213
+ t.is(result.inserted_ids.length, 100000);
214
+
215
+ // Should complete in reasonable time (less than 30 seconds)
216
+ t.true(duration < 30000);
217
+
218
+ // Should achieve good throughput
219
+ t.true(result.performance.documents_per_second > 3000);
220
+ });
221
+
222
+ // Performance optimization tests
223
+ test('bulk_insert_optimized with streaming should handle memory efficiently', async t => {
224
+ const documents = generate_test_documents(50000, { variable_size: true, base_size: 1000 });
225
+
226
+ const start_memory = process.memoryUsage().heapUsed;
227
+
228
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
229
+ stream_processing: true,
230
+ batch_size: 1000
231
+ });
232
+
233
+ const end_memory = process.memoryUsage().heapUsed;
234
+ const memory_delta = end_memory - start_memory;
235
+
236
+ t.true(result.acknowledged);
237
+ t.is(result.inserted_count, 50000);
238
+
239
+ // Memory usage should be reasonable (less than 500MB delta)
240
+ t.true(memory_delta < 500 * 1024 * 1024);
241
+ });
242
+
243
+ test('bulk_insert_optimized with key sorting should improve performance', async t => {
244
+ const documents = generate_test_documents(10000);
245
+
246
+ // Test with sorting enabled
247
+ const sorted_result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
248
+ sort_keys: true
249
+ });
250
+
251
+ // Clean up for second test
252
+ await cleanup_database(true);
253
+ initialize_database(TEST_DB_PATH);
254
+
255
+ // Test with sorting disabled
256
+ const unsorted_result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
257
+ sort_keys: false
258
+ });
259
+
260
+ t.true(sorted_result.acknowledged);
261
+ t.true(unsorted_result.acknowledged);
262
+ t.is(sorted_result.inserted_count, 10000);
263
+ t.is(unsorted_result.inserted_count, 10000);
264
+
265
+ // Sorted version should generally be faster or similar
266
+ // (This is a performance hint, not a strict requirement)
267
+ t.true(sorted_result.performance.duration_ms <= unsorted_result.performance.duration_ms * 1.5);
268
+ });
269
+
270
+ // Error handling tests
271
+ test('bulk_insert_optimized should reject invalid parameters', async t => {
272
+ await t.throwsAsync(
273
+ () => bulk_insert_optimized('', TEST_COLLECTION, []),
274
+ { message: /Database name and collection name are required/ }
275
+ );
276
+
277
+ await t.throwsAsync(
278
+ () => bulk_insert_optimized(TEST_DATABASE, '', []),
279
+ { message: /Database name and collection name are required/ }
280
+ );
281
+
282
+ await t.throwsAsync(
283
+ () => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, []),
284
+ { message: /Documents must be a non-empty array/ }
285
+ );
286
+
287
+ await t.throwsAsync(
288
+ () => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, null),
289
+ { message: /Documents must be a non-empty array/ }
290
+ );
291
+ });
292
+
293
+ test('bulk_insert_optimized should handle duplicate IDs correctly', async t => {
294
+ const documents = [
295
+ { _id: 'duplicate_id', name: 'Document 1' },
296
+ { _id: 'duplicate_id', name: 'Document 2' }
297
+ ];
298
+
299
+ await t.throwsAsync(
300
+ () => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents),
301
+ { message: /Document with _id duplicate_id already exists/ }
302
+ );
303
+ });
304
+
305
+ // Data integrity tests
306
+ test('bulk_insert_optimized should preserve document data integrity', async t => {
307
+ const original_documents = generate_test_documents(1000, { include_id: true });
308
+
309
+ await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, original_documents);
310
+
311
+ // Verify all documents were inserted correctly
312
+ for (const original_doc of original_documents) {
313
+ const retrieved_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { _id: original_doc._id });
314
+
315
+ t.truthy(retrieved_doc);
316
+ t.is(retrieved_doc.name, original_doc.name);
317
+ t.is(retrieved_doc.index, original_doc.index);
318
+ t.is(retrieved_doc.category, original_doc.category);
319
+ t.is(retrieved_doc.active, original_doc.active);
320
+ t.deepEqual(retrieved_doc.metadata, original_doc.metadata);
321
+ t.truthy(retrieved_doc._created_at);
322
+ t.truthy(retrieved_doc._updated_at);
323
+ }
324
+ });
325
+
326
+ // Concurrent read safety tests
327
+ test('bulk_insert_optimized should allow concurrent reads', async t => {
328
+ // Insert initial data
329
+ const initial_docs = generate_test_documents(1000, { include_id: true });
330
+ await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, initial_docs);
331
+
332
+ // Start bulk insert of additional data
333
+ const additional_docs = generate_test_documents(10000);
334
+ const bulk_insert_promise = bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, additional_docs);
335
+
336
+ // Perform concurrent reads while bulk insert is running
337
+ const read_promises = [];
338
+ for (let i = 0; i < 10; i++) {
339
+ read_promises.push(
340
+ find_one(TEST_DATABASE, TEST_COLLECTION, { _id: initial_docs[i]._id })
341
+ );
342
+ }
343
+
344
+ // Wait for both bulk insert and reads to complete
345
+ const [bulk_result, ...read_results] = await Promise.all([
346
+ bulk_insert_promise,
347
+ ...read_promises
348
+ ]);
349
+
350
+ // Verify bulk insert succeeded
351
+ t.true(bulk_result.acknowledged);
352
+ t.is(bulk_result.inserted_count, 10000);
353
+
354
+ // Verify all reads succeeded and returned correct data
355
+ read_results.forEach((doc, index) => {
356
+ t.truthy(doc);
357
+ t.is(doc._id, initial_docs[index]._id);
358
+ t.is(doc.name, initial_docs[index].name);
359
+ });
360
+ });
361
+
362
+ // Performance monitoring tests
363
+ test('bulk_insert_with_metrics should provide detailed performance metrics', async t => {
364
+ const documents = generate_test_documents(5000);
365
+
366
+ const result = await bulk_insert_with_metrics(TEST_DATABASE, TEST_COLLECTION, documents);
367
+
368
+ t.true(result.acknowledged);
369
+ t.is(result.inserted_count, 5000);
370
+ t.truthy(result.performance);
371
+ t.truthy(result.performance.memory_usage);
372
+
373
+ // Verify performance metrics structure
374
+ t.true(typeof result.performance.duration_ms === 'number');
375
+ t.true(typeof result.performance.documents_per_second === 'number');
376
+ t.true(typeof result.performance.memory_usage.start_heap_mb === 'number');
377
+ t.true(typeof result.performance.memory_usage.end_heap_mb === 'number');
378
+ t.true(typeof result.performance.memory_usage.delta_heap_mb === 'number');
379
+ t.true(typeof result.performance.memory_usage.peak_heap_mb === 'number');
380
+ });
381
+
382
+ // Non-blocking bulk insert tests
383
+ test('non_blocking_bulk_insert should process in chunks', async t => {
384
+ const documents = generate_test_documents(25000);
385
+
386
+ const result = await non_blocking_bulk_insert(TEST_DATABASE, TEST_COLLECTION, documents, {
387
+ chunk_size: 5000
388
+ });
389
+
390
+ t.true(result.acknowledged);
391
+ t.is(result.inserted_count, 25000);
392
+ t.is(result.inserted_ids.length, 25000);
393
+ t.truthy(result.performance);
394
+ t.true(result.performance.duration_ms > 0);
395
+ t.true(result.performance.documents_per_second > 0);
396
+ });
397
+
398
+ // Stress tests
399
+ test('bulk_insert_optimized should handle very large documents', async t => {
400
+ const large_documents = [];
401
+ for (let i = 0; i < 1000; i++) {
402
+ large_documents.push({
403
+ name: `Large Document ${i}`,
404
+ large_content: 'x'.repeat(10000), // 10KB per document
405
+ nested_data: {
406
+ level1: {
407
+ level2: {
408
+ level3: {
409
+ data: Array.from({ length: 100 }, (_, j) => ({ id: j, value: `value_${j}` }))
410
+ }
411
+ }
412
+ }
413
+ }
414
+ });
415
+ }
416
+
417
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, large_documents);
418
+
419
+ t.true(result.acknowledged);
420
+ t.is(result.inserted_count, 1000);
421
+ t.true(result.performance.documents_per_second > 100);
422
+ });
423
+
424
+ test('bulk_insert_optimized should maintain performance with mixed document sizes', async t => {
425
+ const mixed_documents = [];
426
+
427
+ // Small documents
428
+ for (let i = 0; i < 5000; i++) {
429
+ mixed_documents.push({
430
+ type: 'small',
431
+ index: i,
432
+ data: 'small_content'
433
+ });
434
+ }
435
+
436
+ // Medium documents
437
+ for (let i = 0; i < 3000; i++) {
438
+ mixed_documents.push({
439
+ type: 'medium',
440
+ index: i,
441
+ data: 'x'.repeat(1000),
442
+ metadata: Array.from({ length: 50 }, (_, j) => ({ key: `key_${j}`, value: `value_${j}` }))
443
+ });
444
+ }
445
+
446
+ // Large documents
447
+ for (let i = 0; i < 2000; i++) {
448
+ mixed_documents.push({
449
+ type: 'large',
450
+ index: i,
451
+ data: 'x'.repeat(5000),
452
+ large_array: Array.from({ length: 200 }, (_, j) => ({ id: j, content: `content_${j}` }))
453
+ });
454
+ }
455
+
456
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, mixed_documents);
457
+
458
+ t.true(result.acknowledged);
459
+ t.is(result.inserted_count, 10000);
460
+ t.true(result.performance.documents_per_second > 2000);
461
+
462
+ // Verify data integrity for different document types
463
+ const small_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'small' });
464
+ const medium_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'medium' });
465
+ const large_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'large' });
466
+
467
+ t.truthy(small_doc);
468
+ t.is(small_doc.type, 'small');
469
+ t.truthy(medium_doc);
470
+ t.is(medium_doc.type, 'medium');
471
+ t.is(medium_doc.metadata.length, 50);
472
+ t.truthy(large_doc);
473
+ t.is(large_doc.type, 'large');
474
+ t.is(large_doc.large_array.length, 200);
475
+ });
476
+
477
+ // Edge case tests
478
+ test('bulk_insert_optimized should handle documents with special characters', async t => {
479
+ const special_documents = [
480
+ { name: 'Document with "quotes"', content: 'Content with \n newlines and \t tabs' },
481
+ { name: 'Document with émojis 🚀', content: 'Unicode content: café, naïve, résumé' },
482
+ { name: 'Document with JSON', content: '{"nested": "json", "array": [1, 2, 3]}' },
483
+ { name: 'Document with HTML', content: '<div>HTML content</div>' },
484
+ { name: 'Document with null values', nullable_field: null, undefined_field: undefined }
485
+ ];
486
+
487
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, special_documents);
488
+
489
+ t.true(result.acknowledged);
490
+ t.is(result.inserted_count, 5);
491
+
492
+ // Verify special characters are preserved
493
+ const docs = await find(TEST_DATABASE, TEST_COLLECTION, {});
494
+ t.is(docs.length, 5);
495
+
496
+ const emoji_doc = docs.find(doc => doc.name.includes('émojis'));
497
+ t.truthy(emoji_doc);
498
+ t.true(emoji_doc.content.includes('café'));
499
+ });
500
+
501
+ test('bulk_insert_optimized should handle empty and minimal documents', async t => {
502
+ const minimal_documents = [
503
+ {},
504
+ { single_field: 'value' },
505
+ { _id: 'custom_id_1' },
506
+ { _id: 'custom_id_2', empty_object: {}, empty_array: [] }
507
+ ];
508
+
509
+ const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, minimal_documents);
510
+
511
+ t.true(result.acknowledged);
512
+ t.is(result.inserted_count, 4);
513
+
514
+ // Verify all documents were inserted with proper timestamps
515
+ const docs = await find(TEST_DATABASE, TEST_COLLECTION, {});
516
+ t.is(docs.length, 4);
517
+
518
+ docs.forEach(doc => {
519
+ t.truthy(doc._id);
520
+ t.truthy(doc._created_at);
521
+ t.truthy(doc._updated_at);
522
+ });
523
+ });
@@ -44,7 +44,9 @@ test.beforeEach(async () => {
44
44
  test.afterEach(async () => {
45
45
  if (server) {
46
46
  await server.cleanup();
47
- server.close();
47
+ await new Promise((resolve) => {
48
+ server.close(resolve);
49
+ });
48
50
  server = null;
49
51
  }
50
52