apexbase 1.24.0

High-performance HTAP embedded database with Rust core
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
"""
Comprehensive test suite for ApexBase Context Manager and Lifecycle Management

This module tests:
- Context manager (__enter__, __exit__) functionality
- Automatic resource cleanup
- Exception handling in context managers
- Manual close() operations
- Instance registry and cleanup
- Force close scenarios
- Lifecycle state transitions
- Resource leak prevention
"""

import pytest
import tempfile
import shutil
from pathlib import Path
import sys
import os
import weakref
import gc
import threading
import time

# Add the apexbase python module to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'apexbase', 'python'))

try:
    from apexbase import ApexClient, FTS_AVAILABLE
except ImportError as e:
    pytest.skip(f"ApexBase not available: {e}", allow_module_level=True)


class TestContextManager:
    """Test context manager functionality"""
    
    def test_basic_context_manager(self):
        """Test basic context manager usage"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with ApexClient(dirpath=temp_dir) as client:
                client.create_table("default")
                assert not client._is_closed
                
                # Perform operations
                client.store({"name": "Alice", "age": 25})
                result = client.retrieve(1)
                assert result["name"] == "Alice"
            
            # Client should be closed after context
            assert client._is_closed
    
    def test_context_manager_with_exception(self):
        """Test context manager with exception inside"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with pytest.raises(ValueError, match="test exception"):
                with ApexClient(dirpath=temp_dir) as client:
                    client.create_table("default")
                    client.store({"name": "Alice", "age": 25})
                    
                    # Raise an exception
                    raise ValueError("test exception")
            
            # Client should still be closed despite exception
            assert client._is_closed
    
    def test_context_manager_chain_operations(self):
        """Test context manager with chain operations"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with ApexClient(dirpath=temp_dir) as client:
                client.create_table("default")
                client.init_fts(index_fields=['content'])
                assert client._is_fts_enabled()
                
                # Store searchable content
                client.store({"content": "Python programming"})
                
                # Search should work
                results = client.search_text("python")
                assert len(results) > 0
            
            # Client should be closed
            assert client._is_closed
    
    def test_context_manager_nested(self):
        """Test nested context managers"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with ApexClient(dirpath=temp_dir) as client1:
                client1.create_table("default")
                client1.store({"name": "Alice"})
                
                # Create second client with DIFFERENT path to avoid conflicts
                temp_dir2 = tempfile.mkdtemp()
                try:
                    with ApexClient(dirpath=temp_dir2) as client2:
                        client2.create_table("default")
                        client2.store({"name": "Bob"})
                        
                        # Both clients should be active with different paths
                        assert not client2._is_closed
                    
                    # Inner client should be closed
                    assert client2._is_closed
                finally:
                    import shutil
                    shutil.rmtree(temp_dir2, ignore_errors=True)
    
    def test_context_manager_return_value(self):
        """Test context manager return value"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with ApexClient(dirpath=temp_dir) as client:
                client.create_table("default")
                # __enter__ should return self
                assert client is not None
                assert hasattr(client, 'store')
                assert hasattr(client, 'query')
    
    def test_context_manager_exit_suppression(self):
        """Test that context manager doesn't suppress exceptions"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with pytest.raises(ValueError, match="test exception"):
                with ApexClient(dirpath=temp_dir) as client:
                    client.create_table("default")
                    client.store({"test": "data"})
                    raise ValueError("test exception")
            
            # Exception should not be suppressed
            assert client._is_closed


class TestManualCloseOperations:
    """Test manual close operations"""
    
    def test_manual_close_basic(self):
        """Test basic manual close operation"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            assert not client._is_closed
            
            # Perform operations
            client.store({"name": "Alice", "age": 25})
            result = client.retrieve(1)
            assert result["name"] == "Alice"
            
            # Manual close
            client.close()
            assert client._is_closed
            
            # Operations should fail after close
            with pytest.raises(RuntimeError, match="connection has been closed"):
                client.store({"test": "data"})
    
    def test_multiple_close_calls(self):
        """Test multiple close calls"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            client.store({"name": "Alice"})
            client.close()
            assert client._is_closed
            
            # Multiple close calls should not raise errors
            client.close()
            client.close()
            assert client._is_closed
    
    def test_force_close_operation(self):
        """Test force close operation"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            client.store({"name": "Alice"})
            
            # Force close (internal method)
            client._force_close()
            assert client._is_closed
            
            # Should handle gracefully
            client._force_close()
            assert client._is_closed
    
    def test_close_with_fts_enabled(self):
        """Test close with FTS enabled"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            client.init_fts(index_fields=['content'])
            
            client.store({"content": "Test content"})
            
            # Close should properly clean up FTS resources
            client.close()
            assert client._is_closed
            
            # FTS operations should fail after close
            with pytest.raises((RuntimeError, ValueError, AttributeError)):
                client.search_text("test")


class TestInstanceRegistry:
    """Test instance registry and automatic cleanup"""
    
    def test_registry_registration(self):
        """Test automatic registration in instance registry"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Client should be registered
            db_path = str(client._db_path)
            assert db_path in client._registry._instances
            
            # Close should unregister
            client.close()
            assert db_path not in client._registry._instances
    
    def test_registry_auto_cleanup(self):
        """Test automatic cleanup through registry"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Store some data
            client.store({"name": "Alice", "age": 25})
            
            # Get reference to client
            client_ref = weakref.ref(client)
            
            # Delete client reference
            del client
            
            # Force garbage collection
            gc.collect()
            time.sleep(0.1)  # Allow cleanup to happen
            
            # Client should be cleaned up (weak reference should be dead)
            assert client_ref() is None
    
    def test_registry_close_all(self):
        """Test close all functionality"""
        # Create multiple clients with different paths
        temp_dirs = [tempfile.mkdtemp() for _ in range(3)]
        clients = []
        
        try:
            for i, td in enumerate(temp_dirs):
                client = ApexClient(dirpath=td)
                client.create_table("default")
                client.store({"id": i, "name": f"Client_{i}"})
                clients.append(client)
            
            # Close all through registry
            if clients:
                clients[0]._registry.close_all()
            
            # All should be closed
            for client in clients:
                assert client._is_closed
        finally:
            import shutil
            for td in temp_dirs:
                shutil.rmtree(td, ignore_errors=True)
    
    def test_registry_duplicate_paths(self):
        """Test registry handling of duplicate paths"""
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create first client
            client1 = ApexClient(dirpath=temp_dir)
            client1.create_table("default")
            client1.store({"name": "First"})
            
            # Create second client with same path - both clients can coexist
            # With multi-client support, both clients share the storage
            client2 = ApexClient(dirpath=temp_dir)
            
            # Both clients should be active
            assert not client1._is_closed
            assert not client2._is_closed
            
            # Second client uses shared storage, needs to select the table
            client2.use_table("default")
            
            # Second client can access existing data
            result = client2.execute("SELECT * FROM default")
            assert len(result) == 1
            
            # Second client can insert data
            client2.store({"name": "Second"})
            
            # Both clients should see the data
            result1 = client1.execute("SELECT * FROM default")
            result2 = client2.execute("SELECT * FROM default")
            assert len(result1) == 2
            assert len(result2) == 2
            
            client1.close()
            client2.close()
    
    def test_registry_disabled_auto_manage(self):
        """Test registry with auto management disabled"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir, _auto_manage=False)
            client.create_table("default")
            
            # Should not be in registry
            db_path = str(client._db_path)
            assert db_path not in client._registry._instances
            
            # Close should work normally
            client.close()
            assert client._is_closed


class TestLifecycleStateTransitions:
    """Test lifecycle state transitions"""
    
    def test_state_transition_sequence(self):
        """Test proper state transition sequence"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Initial state: not closed
            assert not client._is_closed
            
            # Operations should work
            client.store({"test": "data"})
            
            # Close state
            client.close()
            assert client._is_closed
            
            # Operations should fail
            with pytest.raises(RuntimeError):
                client.store({"test": "data"})
    
    def test_state_persistence_across_operations(self):
        """Test state persistence across various operations"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # State should persist across operations
            operations = [
                lambda: client.store({"test": "data"}),
                lambda: client.query(),
                lambda: client.retrieve(1),
                lambda: client.list_tables(),
                lambda: client.count_rows(),
            ]
            
            for op in operations:
                assert not client._is_closed
                op()
                assert not client._is_closed
            
            client.close()
            assert client._is_closed
    
    def test_state_with_table_operations(self):
        """Test state with table operations"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Table operations should maintain state
            client.create_table("test_table")
            assert not client._is_closed
            
            client.use_table("test_table")
            assert not client._is_closed
            
            client.drop_table("test_table")
            assert not client._is_closed
            
            client.close()
            assert client._is_closed


class TestResourceLeakPrevention:
    """Test resource leak prevention"""
    
    def test_file_handle_cleanup(self):
        """Test file handle cleanup on close"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Store data to ensure file handles are used
            for i in range(100):
                client.store({"id": i, "data": f"test_{i}"})
            
            # Close should clean up file handles
            client.close()
            
            # Try to remove directory (should work if handles are cleaned up)
            try:
                shutil.rmtree(temp_dir)
                # If we get here, handles were cleaned up properly
                # Recreate for cleanup
                os.makedirs(temp_dir)
            except OSError as e:
                # If directory can't be removed, handles might not be cleaned up
                pytest.fail(f"File handle leak detected: {e}")
    
    def test_memory_cleanup_on_close(self):
        """Test memory cleanup on close"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Store large data to use memory
            large_data = [{"id": i, "data": "x" * 1000} for i in range(1000)]
            client.store(large_data)
            
            # Get memory usage before close
            client_ref = weakref.ref(client)
            
            # Close client
            client.close()
            assert client._is_closed
            
            # Delete reference and force garbage collection
            del client
            gc.collect()
            time.sleep(0.1)
            
            # Memory should be cleaned up
            assert client_ref() is None
    
    def test_fts_resource_cleanup(self):
        """Test FTS resource cleanup"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            client.init_fts(index_fields=['content'])
            
            # Store searchable data
            for i in range(100):
                client.store({"content": f"Searchable content {i}"})
            
            # Perform searches to ensure FTS resources are used
            results = client.search_text("content")
            assert len(results) > 0
            
            # Close should clean up FTS resources
            client.close()
            assert client._is_closed
            
            # FTS files should be cleaned up or properly closed
            fts_dir = Path(temp_dir) / "fts_indexes"
            if fts_dir.exists():
                # Directory should exist but files should be properly closed
                assert fts_dir.is_dir()


class TestExceptionHandling:
    """Test exception handling in lifecycle operations"""
    
    def test_exception_during_init(self):
        """Test exception during initialization"""
        with tempfile.TemporaryDirectory() as temp_dir:
            # Try to create client with invalid parameters
            try:
                client = ApexClient(dirpath=temp_dir, durability="invalid")
                client.create_table("default")
            except ValueError:
                pass  # Expected
            
            # Should not leave resources in inconsistent state
            # (hard to test directly, but should not crash)
    
    def test_exception_during_operation(self):
        """Test exception during operation"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Cause an exception during operation
            try:
                # Try to delete non-existent record
                client.delete(999999)
            except Exception:
                pass  # Expected
            
            # Client should still be usable
            client.store({"recovery": "test"})
            result = client.retrieve(1)
            assert result["recovery"] == "test"
            
            client.close()
    
    def test_exception_in_context_manager(self):
        """Test exception handling in context manager"""
        with tempfile.TemporaryDirectory() as temp_dir:
            exception_caught = False
            
            try:
                with ApexClient(dirpath=temp_dir) as client:
                    client.create_table("default")
                    client.store({"test": "data"})
                    raise ValueError("Test exception")
            except ValueError:
                exception_caught = True
            
            assert exception_caught
            assert client._is_closed
    
    def test_nested_exception_handling(self):
        """Test nested exception handling"""
        temp_dir1 = tempfile.mkdtemp()
        temp_dir2 = tempfile.mkdtemp()
        
        try:
            with ApexClient(dirpath=temp_dir1) as client1:
                client1.create_table("default")
                client1.store({"name": "Alice"})
                
                try:
                    # Use different path to avoid conflicts
                    with ApexClient(dirpath=temp_dir2) as client2:
                        client2.create_table("default")
                        client2.store({"name": "Bob"})
                        raise RuntimeError("Inner exception")
                except RuntimeError:
                    pass  # Expected
                
                # Outer client should still work
                client1.store({"name": "Charlie"})
                
        except Exception as e:
            print(f"Nested exception test: {e}")
        finally:
            import shutil
            shutil.rmtree(temp_dir1, ignore_errors=True)
            shutil.rmtree(temp_dir2, ignore_errors=True)


class TestAtexitCleanup:
    """Test atexit cleanup functionality"""
    
    def test_atexit_registration(self):
        """Test that clients are registered for atexit cleanup"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Client should be registered for cleanup
            # (Hard to test directly without actually exiting)
            
            # Manual close should unregister
            client.close()
            assert client._is_closed
    
    def test_cleanup_on_interpreter_shutdown(self):
        """Test cleanup behavior on interpreter shutdown simulation"""
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create client
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            client.store({"test": "data"})
            
            # Simulate interpreter shutdown by calling registry cleanup
            client._registry.close_all()
            
            # Client should be closed
            assert client._is_closed


class TestConcurrentLifecycle:
    """Test concurrent lifecycle operations"""
    
    def test_concurrent_close(self):
        """Test concurrent close operations"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            
            # Store some data
            for i in range(100):
                client.store({"id": i, "data": f"test_{i}"})
            
            def close_client():
                try:
                    client.close()
                    return True
                except Exception as e:
                    print(f"Close error: {e}")
                    return False
            
            # Try to close from multiple threads (only one should succeed)
            import threading
            threads = []
            results = []
            
            for i in range(5):
                thread = threading.Thread(target=lambda: results.append(close_client()))
                threads.append(thread)
                thread.start()
            
            for thread in threads:
                thread.join()
            
            # Client should be closed
            assert client._is_closed
            # At least one close should succeed
            assert any(results)
    
    def test_concurrent_creation_cleanup(self):
        """Test concurrent client creation and cleanup with file locking
        
        With reader-writer file locking, concurrent WRITE operations from different
        clients will require exclusive locks. This test verifies that:
        1. File locking prevents concurrent write access (serialized writes)
        2. At least some operations succeed
        3. No data corruption occurs
        """
        with tempfile.TemporaryDirectory() as temp_dir:
            def create_and_close():
                try:
                    client = ApexClient(dirpath=temp_dir)
                    client.create_table("default")
                    client.store({"test": "data"})
                    client.close()
                    return True
                except Exception as e:
                    # Expected: "Database is locked" errors for concurrent write access
                    if "locked" in str(e).lower():
                        return False
                    print(f"Create/close error: {e}")
                    return False
            
            # Run multiple create/close cycles
            import threading
            threads = []
            results = []
            
            for i in range(10):
                thread = threading.Thread(target=lambda: results.append(create_and_close()))
                threads.append(thread)
                thread.start()
            
            for thread in threads:
                thread.join()
            
            # With file locking, at least one should succeed (the first to acquire the lock)
            # Other concurrent writes may fail with lock errors - this is expected behavior
            success_count = sum(results)
            assert success_count >= 1, f"At least one concurrent operation should succeed, got {success_count}"


class TestLifecycleWithFTS:
    """Test lifecycle management with FTS"""
    
    def test_fts_lifecycle_integration(self):
        """Test FTS integration with lifecycle management"""
        with tempfile.TemporaryDirectory() as temp_dir:
            with ApexClient(dirpath=temp_dir) as client:
                client.create_table("default")
                # Initialize FTS
                client.init_fts(index_fields=['content'])
                
                # Store searchable data
                client.store({"content": "Python programming"})
                
                # Search should work
                results = client.search_text("python")
                # May or may not find results depending on indexing
                
                # Modify data - behavior may vary
                try:
                    client.replace(1, {"content": "JavaScript programming"})
                except Exception as e:
                    print(f"FTS lifecycle replace: {e}")
            
            # FTS resources should be cleaned up
            assert client._is_closed
    
    def test_fts_cleanup_on_close(self):
        """Test FTS cleanup on close"""
        with tempfile.TemporaryDirectory() as temp_dir:
            client = ApexClient(dirpath=temp_dir)
            client.create_table("default")
            client.init_fts(index_fields=['content'])
            
            # Store and search data
            client.store({"content": "Test content"})
            results = client.search_text("test")
            # Results may vary
            
            # Close should clean up FTS
            client.close()
            assert client._is_closed
            
            # FTS operations should fail after close
            with pytest.raises((RuntimeError, ValueError, AttributeError)):
                client.search_text("test")


if __name__ == "__main__":
    pytest.main([__file__, "-v"])