familia 2.0.0.pre17 → 2.0.0.pre18
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 +60 -0
- data/CLAUDE.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- 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/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/horreum/database_commands.rb +1 -1
- data/lib/familia/horreum/definition.rb +6 -37
- data/lib/familia/horreum/management.rb +17 -12
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum/serialization.rb +91 -73
- data/lib/familia/horreum.rb +10 -6
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -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 +1 -1
- 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 +1 -1
- 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 +6 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- 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 +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- 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 +1 -1
- 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 +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- 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 +1 -1
- 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 +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- 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 +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- 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 +134 -125
- 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
@@ -3,7 +3,7 @@
|
|
3
3
|
# Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
|
4
4
|
# This test addresses gaps that allowed the exists? bug to go undetected
|
5
5
|
|
6
|
-
require_relative '../helpers/test_helpers'
|
6
|
+
require_relative '../support/helpers/test_helpers'
|
7
7
|
|
8
8
|
# Use a simple test class to isolate persistence behavior
|
9
9
|
class PersistenceTestModel < Familia::Horreum
|
@@ -0,0 +1,441 @@
|
|
1
|
+
# try/integration/relationships_persistence_round_trip_try.rb
|
2
|
+
#
|
3
|
+
# CRITICAL PRIORITY: Full Persistence Round-Trip Testing for Relationships
|
4
|
+
#
|
5
|
+
# PURPOSE:
|
6
|
+
# This test file addresses the critical gap that allowed the indexing bug to go undetected.
|
7
|
+
# Tests must verify that objects can be saved, indexed, found via relationships, and loaded
|
8
|
+
# with all fields intact - not just that the APIs work with in-memory objects.
|
9
|
+
#
|
10
|
+
# THE BUG PATTERN THIS EXPOSES:
|
11
|
+
# Previous tests created objects with .new(), added them to indexes/collections, and tested
|
12
|
+
# that the APIs worked - but never verified that find_by_* methods returned fully hydrated
|
13
|
+
# objects from the database. Production code failed because find_by_identifier calls hgetall
|
14
|
+
# which returns empty hashes for unsaved objects.
|
15
|
+
#
|
16
|
+
# BUGS DOCUMENTED IN THIS TEST:
|
17
|
+
# 1. SERIALIZATION BUG: All fields from Redis come back as strings
|
18
|
+
# - Integer 42 becomes "42"
|
19
|
+
# - Time timestamps become string representations
|
20
|
+
# - This breaks type expectations and equality comparisons
|
21
|
+
#
|
22
|
+
# 2. INCOMPLETE HYDRATION BUG: Multi-index queries return objects with only identifier
|
23
|
+
# - find_all_by_department returns RTPEmployee objects
|
24
|
+
# - But these objects only have @emp_id populated, email/department are nil
|
25
|
+
# - This suggests find_by_identifier isn't properly deserializing Redis hash fields
|
26
|
+
#
|
27
|
+
# 3. PARTICIPATION API BUG: Class-level participation methods have unclear signatures
|
28
|
+
# - add_to_class_all_domains method exists but requires unknown arguments
|
29
|
+
# - Documentation/API inconsistency prevents proper usage
|
30
|
+
#
|
31
|
+
# TESTING PHILOSOPHY - "The Persistence Contract":
|
32
|
+
# 1. Create object with .new() + field values
|
33
|
+
# 2. Save object with .save or .create
|
34
|
+
# 3. Index/Relate using relationship methods
|
35
|
+
# 4. Find via relationship query methods
|
36
|
+
# 5. Verify found object has ALL expected fields (not just identifier)
|
37
|
+
# 6. Modify object state
|
38
|
+
# 7. Re-save and verify updates persist
|
39
|
+
# 8. Destroy and verify cleanup
|
40
|
+
#
|
41
|
+
# TEST COVERAGE AREAS:
|
42
|
+
# - unique_index: Objects found via find_by_* should be fully hydrated (FAILS - serialization)
|
43
|
+
# - multi_index: Objects found via sample_from_*/find_all_by_* should be fully hydrated (FAILS - incomplete)
|
44
|
+
# - participates_in: Collection members should be loadable with all fields (PARTIAL)
|
45
|
+
# - Bulk operations: find_all_by_* should return fully hydrated objects (FAILS - incomplete)
|
46
|
+
# - Field equality: Loaded object fields should match original values (FAILS - serialization)
|
47
|
+
# - Nil fields vs missing fields: Correct handling after round-trip (FAILS - "" vs nil)
|
48
|
+
# - Type preservation: Field types should be preserved (FAILS - all strings)
|
49
|
+
#
|
50
|
+
# ANTI-PATTERNS THIS PREVENTS:
|
51
|
+
# - "Working by coincidence" - APIs work in memory but fail with persistence
|
52
|
+
# - Incomplete object loading - Objects with only identifier set
|
53
|
+
# - Silent field loss - Fields not persisted or not loaded
|
54
|
+
# - Type coercion bugs - String "123" becomes Integer 123 unexpectedly
|
55
|
+
#
|
56
|
+
# See: commit 802e80d0e5a0602e393468a9777b8e151ead11a6 for the bug this prevents
|
57
|
+
|
58
|
+
require_relative '../support/helpers/test_helpers'
|
59
|
+
|
60
|
+
# Test classes for persistence round-trip verification
|
61
|
+
class ::RTPUser < Familia::Horreum
|
62
|
+
feature :relationships
|
63
|
+
|
64
|
+
identifier_field :user_id
|
65
|
+
field :user_id
|
66
|
+
field :email
|
67
|
+
field :name
|
68
|
+
field :age
|
69
|
+
field :created_at
|
70
|
+
|
71
|
+
# Class-level unique indexing
|
72
|
+
unique_index :email, :email_index
|
73
|
+
unique_index :name, :name_index
|
74
|
+
end
|
75
|
+
|
76
|
+
class ::RTPCompany < Familia::Horreum
|
77
|
+
feature :relationships
|
78
|
+
|
79
|
+
identifier_field :company_id
|
80
|
+
field :company_id
|
81
|
+
field :name
|
82
|
+
field :industry
|
83
|
+
end
|
84
|
+
|
85
|
+
class ::RTPEmployee < Familia::Horreum
|
86
|
+
feature :relationships
|
87
|
+
|
88
|
+
identifier_field :emp_id
|
89
|
+
field :emp_id
|
90
|
+
field :email
|
91
|
+
field :department
|
92
|
+
field :badge_number
|
93
|
+
field :hire_date
|
94
|
+
|
95
|
+
# Instance-scoped unique indexing
|
96
|
+
unique_index :badge_number, :badge_index, within: RTPCompany
|
97
|
+
|
98
|
+
# Instance-scoped multi-value indexing
|
99
|
+
multi_index :department, :dept_index, within: RTPCompany
|
100
|
+
end
|
101
|
+
|
102
|
+
class ::RTPDomain < Familia::Horreum
|
103
|
+
feature :relationships
|
104
|
+
|
105
|
+
identifier_field :domain_id
|
106
|
+
field :domain_id
|
107
|
+
field :name
|
108
|
+
field :created_at
|
109
|
+
|
110
|
+
# Participation
|
111
|
+
participates_in RTPCompany, :domains, score: :created_at
|
112
|
+
class_participates_in :all_domains, score: :created_at
|
113
|
+
end
|
114
|
+
|
115
|
+
# Setup - create test data with known values
|
116
|
+
@test_user_id = "rtp_user_#{Familia.now.to_i}"
|
117
|
+
@test_email = "roundtrip@example.com"
|
118
|
+
@test_name = "Alice Roundtrip"
|
119
|
+
@test_age = 30
|
120
|
+
|
121
|
+
@test_company_id = "rtp_comp_#{Familia.now.to_i}"
|
122
|
+
@test_company_name = "Acme Corp"
|
123
|
+
@test_industry = "Technology"
|
124
|
+
|
125
|
+
@test_emp_id = "rtp_emp_#{Familia.now.to_i}"
|
126
|
+
@test_emp_email = "employee@acme.com"
|
127
|
+
@test_department = "engineering"
|
128
|
+
@test_badge = "BADGE_RTP_001"
|
129
|
+
@test_hire_date = Time.now.to_i
|
130
|
+
|
131
|
+
@test_domain_id = "rtp_dom_#{Familia.now.to_i}"
|
132
|
+
@test_domain_name = "example.com"
|
133
|
+
@test_domain_created = Familia.now.to_i
|
134
|
+
|
135
|
+
# =============================================
|
136
|
+
# 1. UNIQUE INDEX - Class-Level Round-Trip
|
137
|
+
# =============================================
|
138
|
+
|
139
|
+
## Create user with all fields populated
|
140
|
+
@user = RTPUser.new(
|
141
|
+
user_id: @test_user_id,
|
142
|
+
email: @test_email,
|
143
|
+
name: @test_name,
|
144
|
+
age: @test_age,
|
145
|
+
created_at: Familia.now.to_i
|
146
|
+
)
|
147
|
+
@user.user_id
|
148
|
+
#=> @test_user_id
|
149
|
+
|
150
|
+
## User does not exist before save
|
151
|
+
RTPUser.exists?(@test_user_id)
|
152
|
+
#=> false
|
153
|
+
|
154
|
+
## Save user to database (CRITICAL STEP)
|
155
|
+
@user.save
|
156
|
+
#==> true
|
157
|
+
|
158
|
+
## User exists after save
|
159
|
+
RTPUser.exists?(@test_user_id)
|
160
|
+
#=> true
|
161
|
+
|
162
|
+
## Add user to class-level email index
|
163
|
+
@user.add_to_class_email_index
|
164
|
+
@user.add_to_class_name_index
|
165
|
+
RTPUser.email_index.hgetall[@test_email]
|
166
|
+
#=> @test_user_id
|
167
|
+
|
168
|
+
## Find via unique_index returns fully hydrated object
|
169
|
+
@found_user = RTPUser.find_by_email(@test_email)
|
170
|
+
@found_user.class
|
171
|
+
#=> RTPUser
|
172
|
+
|
173
|
+
## Found user has correct user_id
|
174
|
+
@found_user.user_id
|
175
|
+
#=> @test_user_id
|
176
|
+
|
177
|
+
## Found user has correct email
|
178
|
+
@found_user.email
|
179
|
+
#=> @test_email
|
180
|
+
|
181
|
+
## Found user has correct name
|
182
|
+
@found_user.name
|
183
|
+
#=> @test_name
|
184
|
+
|
185
|
+
## Found user has correct age (Integer preserved)
|
186
|
+
@found_user.age
|
187
|
+
#=> @test_age
|
188
|
+
|
189
|
+
## created_at is correctly deserialized as Integer
|
190
|
+
@found_user.created_at
|
191
|
+
#=:> Integer
|
192
|
+
|
193
|
+
## All fields match original values (types preserved)
|
194
|
+
[@found_user.user_id, @found_user.email, @found_user.name, @found_user.age]
|
195
|
+
#=> [@user.user_id, @user.email, @user.name, @user.age]
|
196
|
+
|
197
|
+
## Find via second index also returns fully hydrated object
|
198
|
+
@found_by_name = RTPUser.find_by_name(@test_name)
|
199
|
+
@found_by_name.email
|
200
|
+
#=> @test_email
|
201
|
+
|
202
|
+
## Bulk find returns fully hydrated objects
|
203
|
+
@found_users = RTPUser.find_all_by_email([@test_email])
|
204
|
+
@found_users.first.name
|
205
|
+
#=> @test_name
|
206
|
+
|
207
|
+
|
208
|
+
# =============================================
|
209
|
+
# 2. UNIQUE INDEX - Instance-Scoped Round-Trip
|
210
|
+
# =============================================
|
211
|
+
|
212
|
+
## Create company and save it
|
213
|
+
@company = RTPCompany.new(
|
214
|
+
company_id: @test_company_id,
|
215
|
+
name: @test_company_name,
|
216
|
+
industry: @test_industry
|
217
|
+
)
|
218
|
+
@company.save
|
219
|
+
RTPCompany.exists?(@test_company_id)
|
220
|
+
#=> true
|
221
|
+
|
222
|
+
## Create employee and save it
|
223
|
+
@employee = RTPEmployee.new(
|
224
|
+
emp_id: @test_emp_id,
|
225
|
+
email: @test_emp_email,
|
226
|
+
department: @test_department,
|
227
|
+
badge_number: @test_badge,
|
228
|
+
hire_date: @test_hire_date
|
229
|
+
)
|
230
|
+
@employee.save
|
231
|
+
RTPEmployee.exists?(@test_emp_id)
|
232
|
+
#=> true
|
233
|
+
|
234
|
+
## Add employee to company's badge index
|
235
|
+
@employee.add_to_rtp_company_badge_index(@company)
|
236
|
+
@company.badge_index.hgetall[@test_badge]
|
237
|
+
#=> @test_emp_id
|
238
|
+
|
239
|
+
## Find employee via instance-scoped unique index
|
240
|
+
@found_emp = @company.find_by_badge_number(@test_badge)
|
241
|
+
@found_emp.class
|
242
|
+
#=> RTPEmployee
|
243
|
+
|
244
|
+
## Found employee has identifier (works)
|
245
|
+
@found_emp.emp_id
|
246
|
+
#=> @test_emp_id
|
247
|
+
|
248
|
+
## Found employee has correct email (string serialization)
|
249
|
+
@found_emp.email
|
250
|
+
#=> @test_emp_email
|
251
|
+
|
252
|
+
## Found employee has correct department (string serialization)
|
253
|
+
@found_emp.department
|
254
|
+
#=> @test_department
|
255
|
+
|
256
|
+
## Found employee has correct hire_date (Integer preserved)
|
257
|
+
@found_emp.hire_date
|
258
|
+
#=> @test_hire_date
|
259
|
+
|
260
|
+
## Bulk query via instance-scoped index returns hydrated objects
|
261
|
+
@found_emps = @company.find_all_by_badge_number([@test_badge])
|
262
|
+
@found_emps.first.email
|
263
|
+
#=> @test_emp_email
|
264
|
+
|
265
|
+
# =============================================
|
266
|
+
# 3. MULTI-VALUE INDEX - Round-Trip
|
267
|
+
# =============================================
|
268
|
+
|
269
|
+
## Add employee to department multi-value index
|
270
|
+
@employee.add_to_rtp_company_dept_index(@company)
|
271
|
+
@company.dept_index_for(@test_department).size
|
272
|
+
#=> 1
|
273
|
+
|
274
|
+
## Sample from multi-index returns array of objects
|
275
|
+
@sampled = @company.sample_from_department(@test_department, 1)
|
276
|
+
@sampled.class
|
277
|
+
#=> Array
|
278
|
+
|
279
|
+
## INCOMPLETE HYDRATION BUG: Sampled employee exists but missing fields
|
280
|
+
@sampled.first.class
|
281
|
+
#=> RTPEmployee
|
282
|
+
|
283
|
+
## FIXED: Objects now fully hydrated with all fields
|
284
|
+
[@sampled.first.emp_id, @sampled.first.email, @sampled.first.department]
|
285
|
+
#=> [@test_emp_id, @test_emp_email, @test_department]
|
286
|
+
|
287
|
+
## INCOMPLETE HYDRATION BUG: find_all_by returns objects missing fields
|
288
|
+
@dept_employees = @company.find_all_by_department(@test_department)
|
289
|
+
@dept_employees.length
|
290
|
+
#=> 1
|
291
|
+
|
292
|
+
## Multiple employees in same department
|
293
|
+
@emp2_id = "rtp_emp2_#{Familia.now.to_i}"
|
294
|
+
@emp2_badge = "BADGE_RTP_002"
|
295
|
+
@emp2 = RTPEmployee.new(
|
296
|
+
emp_id: @emp2_id,
|
297
|
+
email: "emp2@acme.com",
|
298
|
+
department: @test_department,
|
299
|
+
badge_number: @emp2_badge
|
300
|
+
)
|
301
|
+
@emp2.save
|
302
|
+
@emp2.add_to_rtp_company_dept_index(@company)
|
303
|
+
@company.find_all_by_department(@test_department).length
|
304
|
+
#=> 2
|
305
|
+
|
306
|
+
## FIXED: All multi-index objects fully hydrated with all fields
|
307
|
+
@all_eng = @company.find_all_by_department(@test_department)
|
308
|
+
@all_eng.all? { |e| e.is_a?(RTPEmployee) && e.emp_id && e.email }
|
309
|
+
#=> true
|
310
|
+
|
311
|
+
# =============================================
|
312
|
+
# 4. PARTICIPATION - Round-Trip
|
313
|
+
# =============================================
|
314
|
+
|
315
|
+
## Create domain and save it
|
316
|
+
@domain = RTPDomain.new(
|
317
|
+
domain_id: @test_domain_id,
|
318
|
+
name: @test_domain_name,
|
319
|
+
created_at: @test_domain_created
|
320
|
+
)
|
321
|
+
@domain.save
|
322
|
+
RTPDomain.exists?(@test_domain_id)
|
323
|
+
#=> true
|
324
|
+
|
325
|
+
## Add domain to company participation collection
|
326
|
+
@company.add_domain(@domain)
|
327
|
+
@company.domains.size
|
328
|
+
#=> 1
|
329
|
+
|
330
|
+
## Domain appears in company collection
|
331
|
+
@company.domains.members.include?(@test_domain_id)
|
332
|
+
#=> true
|
333
|
+
|
334
|
+
## Load domain from participation collection
|
335
|
+
@domain_ids = @company.domains.members
|
336
|
+
@loaded_domains = @domain_ids.map { |id| RTPDomain.find(id) }.compact
|
337
|
+
@loaded_domains.first.class
|
338
|
+
#=> RTPDomain
|
339
|
+
|
340
|
+
## Loaded domain has all fields
|
341
|
+
@loaded_domains.first.domain_id
|
342
|
+
#=> @test_domain_id
|
343
|
+
|
344
|
+
## Loaded domain has correct name
|
345
|
+
@loaded_domains.first.name
|
346
|
+
#=> @test_domain_name
|
347
|
+
|
348
|
+
## Loaded domain has correct created_at (Integer preserved)
|
349
|
+
@loaded_domains.first.created_at
|
350
|
+
#=> @test_domain_created
|
351
|
+
|
352
|
+
## Class-level participation requires manual addition - API unclear for add_to_class_all_domains
|
353
|
+
# Skip: @domain.add_to_class_all_domains - method signature unclear
|
354
|
+
RTPDomain.all_domains.size
|
355
|
+
#=> 0
|
356
|
+
|
357
|
+
## Domain cannot be loaded from class collection (expected - not added)
|
358
|
+
@class_domain_ids = RTPDomain.all_domains.members
|
359
|
+
@loaded_from_class = @class_domain_ids.map { |id| RTPDomain.find(id) }.compact
|
360
|
+
@loaded_from_class.any? { |d| d.domain_id == @test_domain_id }
|
361
|
+
#=> false
|
362
|
+
|
363
|
+
# =============================================
|
364
|
+
# 5. UPDATE PERSISTENCE - Round-Trip
|
365
|
+
# =============================================
|
366
|
+
|
367
|
+
## Modify user fields
|
368
|
+
@new_age = 31
|
369
|
+
@user.age = @new_age
|
370
|
+
@user.save
|
371
|
+
@user.age
|
372
|
+
#=> @new_age
|
373
|
+
|
374
|
+
## Updated age correctly preserved as Integer
|
375
|
+
@reloaded_user = RTPUser.find_by_email(@test_email)
|
376
|
+
@reloaded_user.age
|
377
|
+
#=> @new_age
|
378
|
+
|
379
|
+
## Update email and verify index updates
|
380
|
+
@new_email = "newemail@example.com"
|
381
|
+
@old_email = @user.email
|
382
|
+
@user.email = @new_email
|
383
|
+
@user.save
|
384
|
+
@user.update_in_class_email_index(@old_email)
|
385
|
+
RTPUser.find_by_email(@new_email)&.user_id
|
386
|
+
#=> @test_user_id
|
387
|
+
|
388
|
+
## Old email no longer finds user
|
389
|
+
RTPUser.find_by_email(@old_email)
|
390
|
+
#=> nil
|
391
|
+
|
392
|
+
# =============================================
|
393
|
+
# 6. NIL FIELDS - Round-Trip
|
394
|
+
# =============================================
|
395
|
+
|
396
|
+
## User with nil field saves correctly
|
397
|
+
@user_nil_age = RTPUser.new(
|
398
|
+
user_id: "rtp_nil_#{Familia.now.to_i}",
|
399
|
+
email: "nil@example.com",
|
400
|
+
name: "Nil Tester",
|
401
|
+
age: nil
|
402
|
+
)
|
403
|
+
@user_nil_age.save
|
404
|
+
@user_nil_age.age
|
405
|
+
#=> nil
|
406
|
+
|
407
|
+
## Nil fields correctly preserved as nil (not empty string)
|
408
|
+
@reloaded_nil = RTPUser.find(@user_nil_age.user_id)
|
409
|
+
@reloaded_nil.age
|
410
|
+
#=> nil
|
411
|
+
|
412
|
+
## Nil field vs missing field handled correctly, the field exists
|
413
|
+
@reloaded_nil.respond_to?(:age)
|
414
|
+
#=> true
|
415
|
+
|
416
|
+
## Nil field vs missing field handled correctly, the field does not exist
|
417
|
+
@reloaded_nil.respond_to?(:plop)
|
418
|
+
#=> false
|
419
|
+
|
420
|
+
# =============================================
|
421
|
+
# 7. TYPE PRESERVATION - Round-Trip
|
422
|
+
# =============================================
|
423
|
+
|
424
|
+
## Type preservation: Integer fields stay Integer after round-trip
|
425
|
+
@test_int_user = RTPUser.new(user_id: "rtp_int_#{Familia.now.to_i}", age: 42)
|
426
|
+
@test_int_user.save
|
427
|
+
@reloaded_int = RTPUser.find(@test_int_user.user_id)
|
428
|
+
@reloaded_int.age.class
|
429
|
+
#=> Integer
|
430
|
+
|
431
|
+
## String fields work correctly (expected behavior)
|
432
|
+
@reloaded_int.user_id.class
|
433
|
+
#=> String
|
434
|
+
|
435
|
+
# =============================================
|
436
|
+
# Cleanup
|
437
|
+
# =============================================
|
438
|
+
|
439
|
+
[@user, @company, @employee, @emp2, @domain, @user_nil_age, @test_int_user, @found_user, @reloaded_user].each do |obj|
|
440
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
441
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Performance benchmarks separate from stress tests
|
2
2
|
|
3
|
-
require_relative '../helpers/test_helpers'
|
3
|
+
require_relative '../support/helpers/test_helpers'
|
4
4
|
require 'benchmark'
|
5
5
|
|
6
6
|
## serialization performance comparison
|
@@ -17,7 +17,7 @@ json_time = Benchmark.realtime do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
familia_time = Benchmark.realtime do
|
20
|
-
100.times { Familia.
|
20
|
+
100.times { Familia::JsonSerializer.dump(large_data) }
|
21
21
|
end
|
22
22
|
|
23
23
|
json_time > 0 && familia_time > 0
|