familia 2.0.0.pre17 → 2.0.0.pre19
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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,376 @@
|
|
1
|
+
# Testing edge cases for unique index validation
|
2
|
+
# lib/familia/features/relationships/indexing/unique_index_generators.rb
|
3
|
+
# lib/familia/horreum/persistence.rb
|
4
|
+
|
5
|
+
require_relative '../../../lib/familia'
|
6
|
+
|
7
|
+
Familia.debug = false
|
8
|
+
|
9
|
+
# ========================================
|
10
|
+
# Setup: Define test models
|
11
|
+
# ========================================
|
12
|
+
|
13
|
+
class EdgeCaseCompany < Familia::Horreum
|
14
|
+
feature :relationships
|
15
|
+
|
16
|
+
identifier_field :company_id
|
17
|
+
field :company_id
|
18
|
+
field :company_name
|
19
|
+
|
20
|
+
# init receives no arguments - fields already set from new()
|
21
|
+
# Use ||= to apply defaults if needed
|
22
|
+
def init
|
23
|
+
# No defaults needed for this class
|
24
|
+
# Could add: @company_name ||= 'Unknown Company'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class EdgeCaseEmployee < Familia::Horreum
|
29
|
+
feature :relationships
|
30
|
+
|
31
|
+
identifier_field :emp_id
|
32
|
+
field :emp_id
|
33
|
+
field :email
|
34
|
+
field :badge_number
|
35
|
+
field :department
|
36
|
+
field :status
|
37
|
+
|
38
|
+
# Class-level unique index for email (auto-populates on save)
|
39
|
+
unique_index :email, :email_index
|
40
|
+
|
41
|
+
# Instance-scoped unique index for badge_number within Company
|
42
|
+
unique_index :badge_number, :badge_index, within: EdgeCaseCompany
|
43
|
+
|
44
|
+
# Multi-index for department (1:many) within Company
|
45
|
+
multi_index :department, :dept_index, within: EdgeCaseCompany
|
46
|
+
|
47
|
+
# init receives no arguments - fields already set from new()
|
48
|
+
# Use ||= to apply defaults if needed
|
49
|
+
def init
|
50
|
+
@status ||= 'active' # Apply default status if not provided
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class EdgeCaseProduct < Familia::Horreum
|
55
|
+
feature :relationships
|
56
|
+
|
57
|
+
identifier_field :product_id
|
58
|
+
field :product_id
|
59
|
+
field :sku
|
60
|
+
|
61
|
+
# Allow empty strings in unique index
|
62
|
+
unique_index :sku, :sku_index
|
63
|
+
|
64
|
+
# init receives no arguments - fields already set from new()
|
65
|
+
# Use ||= to apply defaults if needed
|
66
|
+
def init
|
67
|
+
# No defaults needed for this class
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Clear all indexes before starting
|
72
|
+
EdgeCaseEmployee.email_index.clear
|
73
|
+
EdgeCaseProduct.sku_index.clear
|
74
|
+
|
75
|
+
# ========================================
|
76
|
+
# Test 1: Duplicate instance-scoped index values
|
77
|
+
# ========================================
|
78
|
+
|
79
|
+
## Setup companies and employees
|
80
|
+
@company1 = EdgeCaseCompany.new(company_id: 'c1', company_name: 'Acme Corp')
|
81
|
+
@company2 = EdgeCaseCompany.new(company_id: 'c2', company_name: 'Tech Inc')
|
82
|
+
@company1.save
|
83
|
+
@company2.save
|
84
|
+
#=> true
|
85
|
+
|
86
|
+
## Create employee with badge_number in company1
|
87
|
+
@emp1 = EdgeCaseEmployee.new(emp_id: 'e1', email: 'john@test.com', badge_number: 'B12345')
|
88
|
+
@emp1.save # This auto-populates email index
|
89
|
+
@emp1.add_to_edge_case_company_badge_index(@company1) # Manual for instance-scoped
|
90
|
+
@company1.find_by_badge_number('B12345')&.emp_id
|
91
|
+
#=> 'e1'
|
92
|
+
|
93
|
+
## Create another employee with same badge_number - guard should detect duplicate
|
94
|
+
@emp2 = EdgeCaseEmployee.new(emp_id: 'e2', email: 'jane@test.com', badge_number: 'B12345')
|
95
|
+
@emp2.save # Different email is OK
|
96
|
+
begin
|
97
|
+
@emp2.guard_unique_edge_case_company_badge_index!(@company1)
|
98
|
+
false
|
99
|
+
rescue Familia::RecordExistsError
|
100
|
+
true
|
101
|
+
end
|
102
|
+
#=> true
|
103
|
+
|
104
|
+
## Same badge_number should work in different company (different scope)
|
105
|
+
@emp2.add_to_edge_case_company_badge_index(@company2)
|
106
|
+
@company2.find_by_badge_number('B12345')&.emp_id
|
107
|
+
#=> 'e2'
|
108
|
+
|
109
|
+
## Verify badge exists in both companies with different employees
|
110
|
+
[@company1.find_by_badge_number('B12345')&.emp_id, @company2.find_by_badge_number('B12345')&.emp_id]
|
111
|
+
#=> ['e1', 'e2']
|
112
|
+
|
113
|
+
## Cleanup for next test
|
114
|
+
EdgeCaseEmployee.email_index.clear
|
115
|
+
@company1.badge_index.clear
|
116
|
+
@company2.badge_index.clear
|
117
|
+
#=> 1
|
118
|
+
|
119
|
+
# ========================================
|
120
|
+
# Test 2: Field updates with auto-index cleanup
|
121
|
+
# ========================================
|
122
|
+
|
123
|
+
## Create employee with email (save auto-populates index)
|
124
|
+
@emp3 = EdgeCaseEmployee.new(emp_id: 'e3', email: 'original@test.com')
|
125
|
+
@emp3.save
|
126
|
+
EdgeCaseEmployee.find_by_email('original@test.com')&.emp_id
|
127
|
+
#=> 'e3'
|
128
|
+
|
129
|
+
## Update email - must manually update index (no automatic cleanup on field change)
|
130
|
+
old_email = @emp3.email
|
131
|
+
@emp3.email = 'updated@test.com'
|
132
|
+
@emp3.update_in_class_email_index(old_email) # Manual update required
|
133
|
+
EdgeCaseEmployee.find_by_email('original@test.com')
|
134
|
+
#=> nil
|
135
|
+
|
136
|
+
## New email should resolve to employee
|
137
|
+
EdgeCaseEmployee.find_by_email('updated@test.com')&.emp_id
|
138
|
+
#=> 'e3'
|
139
|
+
|
140
|
+
## Update instance-scoped index
|
141
|
+
@emp3.badge_number = 'B99999'
|
142
|
+
@emp3.add_to_edge_case_company_badge_index(@company1)
|
143
|
+
@company1.find_by_badge_number('B99999')&.emp_id
|
144
|
+
#=> 'e3'
|
145
|
+
|
146
|
+
## Change badge and update index
|
147
|
+
old_badge = @emp3.badge_number
|
148
|
+
@emp3.badge_number = 'B11111'
|
149
|
+
@emp3.update_in_edge_case_company_badge_index(@company1, old_badge)
|
150
|
+
@company1.find_by_badge_number('B99999')
|
151
|
+
#=> nil
|
152
|
+
|
153
|
+
## New badge should work
|
154
|
+
@company1.find_by_badge_number('B11111')&.emp_id
|
155
|
+
#=> 'e3'
|
156
|
+
|
157
|
+
## Cleanup
|
158
|
+
EdgeCaseEmployee.email_index.clear
|
159
|
+
@company1.badge_index.clear
|
160
|
+
#=> 1
|
161
|
+
|
162
|
+
# ========================================
|
163
|
+
# Test 3: Save within explicit transactions (validation bypass)
|
164
|
+
# ========================================
|
165
|
+
|
166
|
+
## Create first employee successfully
|
167
|
+
@emp4 = EdgeCaseEmployee.new(emp_id: 'e4', email: 'txn@test.com')
|
168
|
+
@emp4.save
|
169
|
+
EdgeCaseEmployee.find_by_email('txn@test.com')&.emp_id
|
170
|
+
#=> 'e4'
|
171
|
+
|
172
|
+
## Save cannot be called inside transaction - it raises OperationModeError
|
173
|
+
@emp5 = EdgeCaseEmployee.new(emp_id: 'e5', email: 'txn@test.com')
|
174
|
+
error_raised = false
|
175
|
+
begin
|
176
|
+
EdgeCaseEmployee.transaction do |tx|
|
177
|
+
@emp5.save # This will raise
|
178
|
+
end
|
179
|
+
rescue Familia::OperationModeError => e
|
180
|
+
error_raised = e.message.include?("Cannot call save within a transaction")
|
181
|
+
end
|
182
|
+
error_raised
|
183
|
+
#=> true
|
184
|
+
|
185
|
+
## However, we can bypass validation by manually adding to index inside transaction
|
186
|
+
result = EdgeCaseEmployee.transaction do |tx|
|
187
|
+
# Manually add without validation (dangerous!)
|
188
|
+
EdgeCaseEmployee.email_index['txn_bypass@test.com'] = 'e5'
|
189
|
+
'manual_bypass'
|
190
|
+
end
|
191
|
+
result.successful?
|
192
|
+
#=> true
|
193
|
+
|
194
|
+
## After transaction, the manual entry exists (no validation occurred)
|
195
|
+
EdgeCaseEmployee.email_index['txn_bypass@test.com']
|
196
|
+
#=> 'e5'
|
197
|
+
|
198
|
+
## After transaction, the manual entry exists (no validation occurred)
|
199
|
+
EdgeCaseEmployee.email_index['txn_bypass@test.com']
|
200
|
+
#=> 'e5'
|
201
|
+
|
202
|
+
## Cleanup
|
203
|
+
EdgeCaseEmployee.email_index.clear
|
204
|
+
#=> 1
|
205
|
+
|
206
|
+
# ========================================
|
207
|
+
# Test 4: Multiple empty string values in same index
|
208
|
+
# ========================================
|
209
|
+
|
210
|
+
## Create product with empty SKU
|
211
|
+
@prod1 = EdgeCaseProduct.new(product_id: 'p1', sku: '')
|
212
|
+
@prod1.save
|
213
|
+
EdgeCaseProduct.find_by_sku('')&.product_id
|
214
|
+
#=> 'p1'
|
215
|
+
|
216
|
+
## Try to create another product with empty SKU - should fail
|
217
|
+
@prod2 = EdgeCaseProduct.new(product_id: 'p2', sku: '')
|
218
|
+
begin
|
219
|
+
@prod2.save
|
220
|
+
false
|
221
|
+
rescue Familia::RecordExistsError => e
|
222
|
+
e.message.include?('sku=')
|
223
|
+
end
|
224
|
+
#=> true
|
225
|
+
|
226
|
+
## nil values should be skipped (not indexed)
|
227
|
+
@prod3 = EdgeCaseProduct.new(product_id: 'p3', sku: nil)
|
228
|
+
@prod3.save # Should succeed - nil values aren't indexed
|
229
|
+
@prod3.identifier
|
230
|
+
#=> 'p3'
|
231
|
+
|
232
|
+
## Verify nil doesn't exist in index (empty string != nil)
|
233
|
+
EdgeCaseProduct.sku_index[''] # Empty string key
|
234
|
+
#=> 'p1'
|
235
|
+
|
236
|
+
## nil is not indexed
|
237
|
+
EdgeCaseProduct.sku_index.keys.include?(nil)
|
238
|
+
#=> false
|
239
|
+
|
240
|
+
## Cleanup
|
241
|
+
EdgeCaseProduct.sku_index.clear
|
242
|
+
#=> 1
|
243
|
+
|
244
|
+
# ========================================
|
245
|
+
# Test 5: Concurrent saves with same unique value
|
246
|
+
# ========================================
|
247
|
+
|
248
|
+
## Setup fresh index
|
249
|
+
EdgeCaseEmployee.email_index.clear
|
250
|
+
#=> 0
|
251
|
+
|
252
|
+
## Create two employees with same email (simulating race condition)
|
253
|
+
@emp6 = EdgeCaseEmployee.new(emp_id: 'e6', email: 'race@test.com')
|
254
|
+
@emp7 = EdgeCaseEmployee.new(emp_id: 'e7', email: 'race@test.com')
|
255
|
+
@emp7.emp_id
|
256
|
+
#=> 'e7'
|
257
|
+
|
258
|
+
## First save succeeds
|
259
|
+
@emp6.save
|
260
|
+
EdgeCaseEmployee.find_by_email('race@test.com')&.emp_id
|
261
|
+
#=> 'e6'
|
262
|
+
|
263
|
+
## Second save fails due to validation
|
264
|
+
begin
|
265
|
+
@emp7.save
|
266
|
+
false
|
267
|
+
rescue Familia::RecordExistsError
|
268
|
+
true
|
269
|
+
end
|
270
|
+
#=> true
|
271
|
+
|
272
|
+
## Simulate race condition: both check validation, then both write
|
273
|
+
EdgeCaseEmployee.email_index.clear
|
274
|
+
@emp8 = EdgeCaseEmployee.new(emp_id: 'e8', email: 'race2@test.com')
|
275
|
+
@emp9 = EdgeCaseEmployee.new(emp_id: 'e9', email: 'race2@test.com')
|
276
|
+
[@emp8.emp_id, @emp9.emp_id]
|
277
|
+
#=> ['e8', 'e9']
|
278
|
+
|
279
|
+
## Both pass validation check (index is empty)
|
280
|
+
begin
|
281
|
+
@emp8.guard_unique_email_index!
|
282
|
+
@emp9.guard_unique_email_index!
|
283
|
+
true
|
284
|
+
rescue
|
285
|
+
false
|
286
|
+
end
|
287
|
+
#=> true
|
288
|
+
|
289
|
+
## Both write to index (last write wins in Redis)
|
290
|
+
@emp8.add_to_class_email_index
|
291
|
+
@emp9.add_to_class_email_index
|
292
|
+
# Verify the index contains the identifier (orphaned entry - wastes space but harmless)
|
293
|
+
EdgeCaseEmployee.email_index['race2@test.com']
|
294
|
+
#=> 'e9'
|
295
|
+
|
296
|
+
## find_by returns nil for orphaned index entries (object never saved)
|
297
|
+
# This is correct behavior - orphaned entries degrade gracefully to nil
|
298
|
+
EdgeCaseEmployee.find_by_email('race2@test.com')
|
299
|
+
#=> nil
|
300
|
+
|
301
|
+
## To properly handle concurrent saves, check existence inside transaction
|
302
|
+
# Note: Can't read inside MULTI block, so need WATCH/MULTI pattern
|
303
|
+
result = nil
|
304
|
+
EdgeCaseEmployee.dbclient.watch('edge_case_employee:email_index') do
|
305
|
+
if EdgeCaseEmployee.email_index['race3@test.com'].nil?
|
306
|
+
EdgeCaseEmployee.transaction do |tx|
|
307
|
+
EdgeCaseEmployee.email_index['race3@test.com'] = 'e10'
|
308
|
+
result = 'success'
|
309
|
+
end
|
310
|
+
else
|
311
|
+
result = 'duplicate'
|
312
|
+
end
|
313
|
+
end
|
314
|
+
result
|
315
|
+
#=> 'success'
|
316
|
+
|
317
|
+
## Cleanup
|
318
|
+
EdgeCaseEmployee.email_index.clear
|
319
|
+
#=> 1
|
320
|
+
|
321
|
+
# ========================================
|
322
|
+
# Edge Case: Update with validation in compound operation
|
323
|
+
# ========================================
|
324
|
+
|
325
|
+
## Test compound index updates in transaction
|
326
|
+
@company3 = EdgeCaseCompany.new(company_id: 'c3', company_name: 'Test Corp')
|
327
|
+
@company3.save
|
328
|
+
#=> true
|
329
|
+
|
330
|
+
## Create employee
|
331
|
+
@emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
|
332
|
+
@emp11.save
|
333
|
+
@emp11.add_to_edge_case_company_badge_index(@company3)
|
334
|
+
@emp11.emp_id
|
335
|
+
#=> 'e11'
|
336
|
+
|
337
|
+
## Update multiple indexed fields atomically
|
338
|
+
@emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
|
339
|
+
@emp11.save
|
340
|
+
@emp11.add_to_edge_case_company_badge_index(@company3)
|
341
|
+
|
342
|
+
old_email = @emp11.email
|
343
|
+
old_badge = @emp11.badge_number
|
344
|
+
@emp11.email = 'compound_new@test.com'
|
345
|
+
@emp11.badge_number = 'B666'
|
346
|
+
|
347
|
+
# Update both indexes in single transaction
|
348
|
+
result = EdgeCaseEmployee.transaction do |tx|
|
349
|
+
@emp11.update_in_class_email_index(old_email)
|
350
|
+
@emp11.update_in_edge_case_company_badge_index(@company3, old_badge)
|
351
|
+
'updated'
|
352
|
+
end
|
353
|
+
result.successful?
|
354
|
+
#=> true
|
355
|
+
|
356
|
+
## Verify updates succeeded
|
357
|
+
[EdgeCaseEmployee.find_by_email('compound_new@test.com')&.emp_id, @company3.find_by_badge_number('B666')&.emp_id]
|
358
|
+
#=> ['e11', 'e11']
|
359
|
+
|
360
|
+
## Old values should be gone
|
361
|
+
[EdgeCaseEmployee.find_by_email('compound@test.com'), @company3.find_by_badge_number('B555')]
|
362
|
+
#=> [nil, nil]
|
363
|
+
|
364
|
+
|
365
|
+
# Final cleanup
|
366
|
+
EdgeCaseEmployee.email_index.clear
|
367
|
+
if @company3&.respond_to?(:badge_index) && @company3.badge_index.respond_to?(:clear)
|
368
|
+
@company3.badge_index.clear
|
369
|
+
end
|
370
|
+
|
371
|
+
# Clean up test objects - check if they still exist before destroying
|
372
|
+
[@company1, @company2, @company3].compact.each do |obj|
|
373
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
374
|
+
end
|
375
|
+
|
376
|
+
puts "All edge case tests completed"
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# try/unit/horreum/unique_index_guard_validation_try.rb
|
2
|
+
|
3
|
+
#
|
4
|
+
# Unique index guard validation tests
|
5
|
+
# Tests the guard_unique_*! methods for both class-level and instance-scoped indexes
|
6
|
+
#
|
7
|
+
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
9
|
+
|
10
|
+
# Test classes for unique index guard validation
|
11
|
+
class ::GuardUser < Familia::Horreum
|
12
|
+
feature :relationships
|
13
|
+
|
14
|
+
identifier_field :user_id
|
15
|
+
field :user_id
|
16
|
+
field :email
|
17
|
+
field :username
|
18
|
+
|
19
|
+
# Class-level unique indexes (auto-validated on save)
|
20
|
+
unique_index :email, :email_index
|
21
|
+
unique_index :username, :username_index
|
22
|
+
end
|
23
|
+
|
24
|
+
class ::GuardCompany < Familia::Horreum
|
25
|
+
feature :relationships
|
26
|
+
|
27
|
+
identifier_field :company_id
|
28
|
+
field :company_id
|
29
|
+
field :name
|
30
|
+
end
|
31
|
+
|
32
|
+
class ::GuardEmployee < Familia::Horreum
|
33
|
+
feature :relationships
|
34
|
+
|
35
|
+
identifier_field :emp_id
|
36
|
+
field :emp_id
|
37
|
+
field :badge_number
|
38
|
+
field :email
|
39
|
+
|
40
|
+
# Instance-scoped unique index (manually validated)
|
41
|
+
unique_index :badge_number, :badge_index, within: GuardCompany
|
42
|
+
|
43
|
+
# Class-level unique index (auto-validated)
|
44
|
+
unique_index :email, :email_index
|
45
|
+
end
|
46
|
+
|
47
|
+
# Setup
|
48
|
+
@user_id1 = "user_#{rand(1000000)}"
|
49
|
+
@user_id2 = "user_#{rand(1000000)}"
|
50
|
+
@company_id = "comp_#{rand(1000000)}"
|
51
|
+
@emp_id1 = "emp_#{rand(1000000)}"
|
52
|
+
@emp_id2 = "emp_#{rand(1000000)}"
|
53
|
+
|
54
|
+
@company = GuardCompany.new(company_id: @company_id, name: 'Test Corp')
|
55
|
+
@company.save
|
56
|
+
|
57
|
+
# =============================================
|
58
|
+
# 1. Class-Level Unique Index Guard Methods
|
59
|
+
# =============================================
|
60
|
+
|
61
|
+
## Guard method exists for class-level unique index
|
62
|
+
@user1 = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
|
63
|
+
@user1.respond_to?(:guard_unique_email_index!)
|
64
|
+
#=> true
|
65
|
+
|
66
|
+
## Guard passes when no conflict exists
|
67
|
+
@user1.guard_unique_email_index!
|
68
|
+
#=> nil
|
69
|
+
|
70
|
+
## Save succeeds after guard passes
|
71
|
+
@user1.save
|
72
|
+
#=> true
|
73
|
+
|
74
|
+
## Guard fails when duplicate email exists
|
75
|
+
@user2 = GuardUser.new(user_id: @user_id2, email: 'test@example.com', username: 'different')
|
76
|
+
begin
|
77
|
+
@user2.guard_unique_email_index!
|
78
|
+
false
|
79
|
+
rescue Familia::RecordExistsError => e
|
80
|
+
e.message.include?('GuardUser exists email=test@example.com')
|
81
|
+
end
|
82
|
+
#=> true
|
83
|
+
|
84
|
+
## Save automatically calls guard and raises error
|
85
|
+
begin
|
86
|
+
@user2.save
|
87
|
+
false
|
88
|
+
rescue Familia::RecordExistsError
|
89
|
+
true
|
90
|
+
end
|
91
|
+
#=> true
|
92
|
+
|
93
|
+
## Guard allows same identifier (updating existing record)
|
94
|
+
@user1_copy = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
|
95
|
+
@user1_copy.guard_unique_email_index!
|
96
|
+
#=> nil
|
97
|
+
|
98
|
+
## Guard handles nil field values gracefully
|
99
|
+
@user_nil = GuardUser.new(user_id: "user_nil_#{rand(1000000)}", email: nil, username: 'niluser')
|
100
|
+
@user_nil.guard_unique_email_index!
|
101
|
+
#=> nil
|
102
|
+
|
103
|
+
## Guard handles empty string field values
|
104
|
+
@user_empty1 = GuardUser.new(user_id: "user_empty1_#{rand(1000000)}", email: '', username: 'empty1')
|
105
|
+
@user_empty1.save
|
106
|
+
@user_empty2 = GuardUser.new(user_id: "user_empty2_#{rand(1000000)}", email: '', username: 'empty2')
|
107
|
+
begin
|
108
|
+
@user_empty2.save
|
109
|
+
false
|
110
|
+
rescue Familia::RecordExistsError => e
|
111
|
+
e.message.include?('GuardUser exists email=')
|
112
|
+
end
|
113
|
+
#=> true
|
114
|
+
|
115
|
+
# =============================================
|
116
|
+
# 2. Instance-Scoped Unique Index Guard Methods
|
117
|
+
# =============================================
|
118
|
+
|
119
|
+
## Guard method exists for instance-scoped unique index
|
120
|
+
@emp1 = GuardEmployee.new(emp_id: @emp_id1, badge_number: 'BADGE123', email: 'emp1@example.com')
|
121
|
+
@emp1.respond_to?(:guard_unique_guard_company_badge_index!)
|
122
|
+
#=> true
|
123
|
+
|
124
|
+
## Guard method requires parent instance parameter
|
125
|
+
@emp1.method(:guard_unique_guard_company_badge_index!).arity
|
126
|
+
#=> 1
|
127
|
+
|
128
|
+
## Guard passes when no conflict exists in parent's index
|
129
|
+
@emp1.guard_unique_guard_company_badge_index!(@company)
|
130
|
+
#=> nil
|
131
|
+
|
132
|
+
## Can add to index after guard passes
|
133
|
+
@emp1.add_to_guard_company_badge_index(@company)
|
134
|
+
@company.badge_index.has_key?('BADGE123')
|
135
|
+
#=> true
|
136
|
+
|
137
|
+
## Guard fails when duplicate badge exists in same company
|
138
|
+
@emp2 = GuardEmployee.new(emp_id: @emp_id2, badge_number: 'BADGE123', email: 'emp2@example.com')
|
139
|
+
begin
|
140
|
+
@emp2.guard_unique_guard_company_badge_index!(@company)
|
141
|
+
false
|
142
|
+
rescue Familia::RecordExistsError => e
|
143
|
+
e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE123')
|
144
|
+
end
|
145
|
+
#=> true
|
146
|
+
|
147
|
+
## Guard allows same employee to re-add (idempotent)
|
148
|
+
@emp1.guard_unique_guard_company_badge_index!(@company)
|
149
|
+
#=> nil
|
150
|
+
|
151
|
+
## Guard passes for different company (different scope)
|
152
|
+
@company2_id = "comp_#{rand(1000000)}"
|
153
|
+
@company2 = GuardCompany.new(company_id: @company2_id, name: 'Other Corp')
|
154
|
+
@company2.save
|
155
|
+
@emp2.guard_unique_guard_company_badge_index!(@company2)
|
156
|
+
#=> nil
|
157
|
+
|
158
|
+
## Can add same badge to different company
|
159
|
+
@emp2.add_to_guard_company_badge_index(@company2)
|
160
|
+
@company2.badge_index.has_key?('BADGE123')
|
161
|
+
#=> true
|
162
|
+
|
163
|
+
## Guard handles nil parent instance gracefully
|
164
|
+
@emp3 = GuardEmployee.new(emp_id: "emp_#{rand(1000000)}", badge_number: 'BADGE456', email: 'emp3@example.com')
|
165
|
+
@emp3.guard_unique_guard_company_badge_index!(nil)
|
166
|
+
#=> nil
|
167
|
+
|
168
|
+
## Guard handles nil badge_number gracefully
|
169
|
+
@emp_nil = GuardEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
|
170
|
+
@emp_nil.guard_unique_guard_company_badge_index!(@company)
|
171
|
+
#=> nil
|
172
|
+
|
173
|
+
# =============================================
|
174
|
+
# 3. Mixed Class and Instance-Scoped Validation
|
175
|
+
# =============================================
|
176
|
+
|
177
|
+
## Employee has both class-level and instance-scoped indexes
|
178
|
+
@emp4_id = "emp_#{rand(1000000)}"
|
179
|
+
@emp4 = GuardEmployee.new(emp_id: @emp4_id, badge_number: 'BADGE789', email: 'unique@example.com')
|
180
|
+
@emp4.class
|
181
|
+
#=> GuardEmployee
|
182
|
+
|
183
|
+
## Class-level email index auto-validates on save
|
184
|
+
@emp4.save
|
185
|
+
GuardEmployee.find_by_email('unique@example.com')&.emp_id
|
186
|
+
#=> @emp4_id
|
187
|
+
|
188
|
+
## Instance-scoped badge index must be manually validated and added
|
189
|
+
@emp4.guard_unique_guard_company_badge_index!(@company)
|
190
|
+
@emp4.add_to_guard_company_badge_index(@company)
|
191
|
+
@company.badge_index.has_key?('BADGE789')
|
192
|
+
#=> true
|
193
|
+
|
194
|
+
## Duplicate class-level index caught by save
|
195
|
+
@emp5_id = "emp_#{rand(1000000)}"
|
196
|
+
@emp5 = GuardEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'unique@example.com')
|
197
|
+
begin
|
198
|
+
@emp5.save
|
199
|
+
false
|
200
|
+
rescue Familia::RecordExistsError => e
|
201
|
+
e.message.include?('GuardEmployee exists email=unique@example.com')
|
202
|
+
end
|
203
|
+
#=> true
|
204
|
+
|
205
|
+
## Duplicate instance-scoped index requires manual guard
|
206
|
+
@emp6_id = "emp_#{rand(1000000)}"
|
207
|
+
@emp6 = GuardEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE789', email: 'emp6@example.com')
|
208
|
+
@emp6.save # Succeeds - no auto-validation of instance-scoped indexes
|
209
|
+
begin
|
210
|
+
@emp6.guard_unique_guard_company_badge_index!(@company)
|
211
|
+
false
|
212
|
+
rescue Familia::RecordExistsError => e
|
213
|
+
e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE789')
|
214
|
+
end
|
215
|
+
#=> true
|
216
|
+
|
217
|
+
# =============================================
|
218
|
+
# 4. Guard Method Error Messages
|
219
|
+
# =============================================
|
220
|
+
|
221
|
+
## Class-level guard error includes class and field
|
222
|
+
@user_dup = GuardUser.new(user_id: "user_dup_#{rand(1000000)}", email: 'test@example.com', username: 'dupuser')
|
223
|
+
begin
|
224
|
+
@user_dup.guard_unique_email_index!
|
225
|
+
rescue Familia::RecordExistsError => e
|
226
|
+
[e.message.include?('GuardUser'), e.message.include?('email=test@example.com')]
|
227
|
+
end
|
228
|
+
#=> [true, true]
|
229
|
+
|
230
|
+
## Instance-scoped guard error includes both classes and field
|
231
|
+
begin
|
232
|
+
@emp2.guard_unique_guard_company_badge_index!(@company)
|
233
|
+
rescue Familia::RecordExistsError => e
|
234
|
+
[e.message.include?('GuardEmployee'), e.message.include?('GuardCompany'), e.message.include?('badge_number=BADGE123')]
|
235
|
+
end
|
236
|
+
#=> [true, true, true]
|
237
|
+
|
238
|
+
## RecordExistsError is correct type
|
239
|
+
begin
|
240
|
+
@emp2.guard_unique_guard_company_badge_index!(@company)
|
241
|
+
rescue => e
|
242
|
+
e.class
|
243
|
+
end
|
244
|
+
#=> Familia::RecordExistsError
|
245
|
+
|
246
|
+
# =============================================
|
247
|
+
# 5. Transaction Context Behavior
|
248
|
+
# =============================================
|
249
|
+
|
250
|
+
## Guard works outside transaction
|
251
|
+
@user_tx = GuardUser.new(user_id: "user_tx_#{rand(1000000)}", email: 'tx@example.com', username: 'txuser')
|
252
|
+
@user_tx.guard_unique_email_index!
|
253
|
+
#=> nil
|
254
|
+
|
255
|
+
## Guard must be called outside transaction (new rule)
|
256
|
+
unique_timestamp = Time.now.to_i
|
257
|
+
unique_rand = rand(1000000)
|
258
|
+
email = "tx_unique_#{unique_timestamp}_#{unique_rand}@example.com"
|
259
|
+
@user_tx_unique = GuardUser.new(user_id: "user_tx_unique_#{unique_rand}", email: email, username: "txuser_#{unique_rand}")
|
260
|
+
|
261
|
+
# Guards should be called outside transactions
|
262
|
+
@user_tx_unique.send(:guard_unique_indexes!)
|
263
|
+
#=> nil
|
264
|
+
|
265
|
+
# Teardown - clean up test objects
|
266
|
+
[@user1, @user2, @user_nil, @user_empty1, @user_empty2, @user_dup, @user_tx, @user_tx_unique].each do |obj|
|
267
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
268
|
+
end
|
269
|
+
|
270
|
+
[@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6].each do |obj|
|
271
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
272
|
+
end
|
273
|
+
|
274
|
+
[@company, @company2].each do |obj|
|
275
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
276
|
+
end
|
277
|
+
|
278
|
+
# Clean up class-level indexes
|
279
|
+
[GuardUser.email_index, GuardUser.username_index, GuardEmployee.email_index].each do |index|
|
280
|
+
index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
|
281
|
+
end
|