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,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
|
@@ -0,0 +1,180 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Benchmark: Field deserialization strategies in find_by_dbkey
|
4
|
+
#
|
5
|
+
# Compares different approaches to deserializing Redis hash values
|
6
|
+
# when loading Horreum objects from the database.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# $ ruby try/support/benchmarks/deserialization_benchmark.rb
|
11
|
+
# ======================================================================
|
12
|
+
# RESULTS SUMMARY (baseline: direct assignment)
|
13
|
+
# ======================================================================
|
14
|
+
#
|
15
|
+
# Baseline (no deserialization): 0.151312s
|
16
|
+
#
|
17
|
+
# Deserialization strategies:
|
18
|
+
# 1. Selective (only JSON-like strings): 0.174016s (1.15x baseline, +15.0% overhead) 🏆 FASTEST
|
19
|
+
# 2. Bulk JSON round-trip (parse/dump): 0.183926s (1.22x baseline, +21.6% overhead)
|
20
|
+
# 3. Cached instance + transform: 0.320223s (2.12x baseline, +111.6% overhead)
|
21
|
+
# 4. Current (field-by-field transform_values): 0.49568s (3.28x baseline, +227.6% overhead)
|
22
|
+
|
23
|
+
require_relative '../../../lib/familia'
|
24
|
+
require 'benchmark'
|
25
|
+
require 'json'
|
26
|
+
|
27
|
+
# Setup Redis connection
|
28
|
+
Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
|
29
|
+
|
30
|
+
# Sample model with various field types
|
31
|
+
class BenchmarkUser < Familia::Horreum
|
32
|
+
identifier_field :user_id
|
33
|
+
|
34
|
+
field :user_id
|
35
|
+
field :name
|
36
|
+
field :email
|
37
|
+
field :age
|
38
|
+
field :active
|
39
|
+
field :metadata # Will store JSON hash
|
40
|
+
field :tags # Will store JSON array
|
41
|
+
field :created_at # Will store timestamp
|
42
|
+
field :score # Will store float
|
43
|
+
field :simple_string
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create sample data with realistic values
|
47
|
+
sample_data = {
|
48
|
+
'user_id' => 'user_12345',
|
49
|
+
'name' => 'John Doe',
|
50
|
+
'email' => 'john.doe@example.com',
|
51
|
+
'age' => '35',
|
52
|
+
'active' => 'true',
|
53
|
+
'metadata' => '{"role":"admin","department":"engineering","level":5}',
|
54
|
+
'tags' => '["ruby","redis","performance","optimization"]',
|
55
|
+
'created_at' => Time.now.to_i.to_s,
|
56
|
+
'score' => '98.7',
|
57
|
+
'simple_string' => 'Just a plain string value',
|
58
|
+
}
|
59
|
+
|
60
|
+
# Persist sample data to Redis
|
61
|
+
user = BenchmarkUser.new(**sample_data)
|
62
|
+
user.save
|
63
|
+
|
64
|
+
# Get the raw hash data directly from Redis (what find_by_dbkey gets)
|
65
|
+
raw_hash = BenchmarkUser.dbclient.hgetall(user.dbkey)
|
66
|
+
|
67
|
+
puts 'Benchmarking deserialization strategies'
|
68
|
+
puts "Sample data fields: #{raw_hash.keys.size}"
|
69
|
+
puts "Raw hash: #{raw_hash.inspect}"
|
70
|
+
puts "\n"
|
71
|
+
|
72
|
+
# Strategy 1: Current field-by-field with transform_values
|
73
|
+
def strategy_current(fields, klass)
|
74
|
+
deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
|
75
|
+
klass.new(**deserialized)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Strategy 2: Bulk JSON round-trip
|
79
|
+
def strategy_bulk_json(fields, klass)
|
80
|
+
parsed = JSON.parse(JSON.dump(fields))
|
81
|
+
klass.new(**parsed)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Strategy 3: Direct assignment without deserialization
|
85
|
+
def strategy_direct(fields, klass)
|
86
|
+
klass.new(**fields)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Strategy 4: Selective deserialization (only JSON-looking strings)
|
90
|
+
def strategy_selective(fields, klass)
|
91
|
+
deserialized = fields.transform_values do |value|
|
92
|
+
if value.to_s.start_with?('{', '[')
|
93
|
+
begin
|
94
|
+
JSON.parse(value, symbolize_names: true)
|
95
|
+
rescue JSON::ParserError
|
96
|
+
value
|
97
|
+
end
|
98
|
+
else
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
klass.new(**deserialized)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Strategy 5: Cached instance + transform
|
106
|
+
def strategy_cached_instance(fields, klass)
|
107
|
+
instance = klass.new
|
108
|
+
deserialized = fields.transform_values { |value| instance.deserialize_value(value) }
|
109
|
+
klass.new(**deserialized)
|
110
|
+
end
|
111
|
+
|
112
|
+
iterations = 10_000
|
113
|
+
|
114
|
+
puts "Running #{iterations} iterations per strategy...\n\n"
|
115
|
+
|
116
|
+
strategies = {
|
117
|
+
'Current (field-by-field transform_values)' => :strategy_current,
|
118
|
+
'Bulk JSON round-trip (parse/dump)' => :strategy_bulk_json,
|
119
|
+
'Direct (no deserialization)' => :strategy_direct,
|
120
|
+
'Selective (only parse JSON-like strings)' => :strategy_selective,
|
121
|
+
'Cached instance + transform' => :strategy_cached_instance,
|
122
|
+
}
|
123
|
+
|
124
|
+
results = {}
|
125
|
+
|
126
|
+
strategies.each do |name, method_name|
|
127
|
+
time = Benchmark.measure do
|
128
|
+
iterations.times do
|
129
|
+
send(method_name, raw_hash, BenchmarkUser)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
results[name] = time.real
|
133
|
+
puts "#{name}: #{time.real.round(6)} seconds (#{(iterations / time.real).round(0)} ops/sec)"
|
134
|
+
end
|
135
|
+
|
136
|
+
puts "\n" + ('=' * 70)
|
137
|
+
puts 'RESULTS SUMMARY (baseline: direct assignment)'
|
138
|
+
puts '=' * 70
|
139
|
+
|
140
|
+
# Use direct assignment as baseline (obviously fastest but incorrect)
|
141
|
+
baseline = results['Direct (no deserialization)']
|
142
|
+
|
143
|
+
# Sort deserialization strategies only (exclude baseline)
|
144
|
+
deserialization_strategies = results.reject { |name, _| name == 'Direct (no deserialization)' }
|
145
|
+
sorted = deserialization_strategies.sort_by { |_, time| time }
|
146
|
+
|
147
|
+
puts "
|
148
|
+
Baseline (no deserialization): #{baseline.round(6)}s"
|
149
|
+
puts "
|
150
|
+
Deserialization strategies:
|
151
|
+
"
|
152
|
+
|
153
|
+
sorted.each_with_index do |(name, time), index|
|
154
|
+
overhead = ((time / baseline - 1) * 100).round(1)
|
155
|
+
vs_baseline = (time / baseline).round(2)
|
156
|
+
marker = index == 0 ? '🏆 FASTEST' : ''
|
157
|
+
puts "#{index + 1}. #{name}: #{time.round(6)}s (#{vs_baseline}x baseline, +#{overhead}% overhead) #{marker}"
|
158
|
+
end
|
159
|
+
|
160
|
+
puts "
|
161
|
+
" + ('=' * 70)
|
162
|
+
puts 'RECOMMENDATIONS'
|
163
|
+
puts '=' * 70
|
164
|
+
|
165
|
+
puts 'Best strategy depends on your data:'
|
166
|
+
puts ' • Mostly simple strings → Direct or Selective'
|
167
|
+
puts ' • Mixed types with JSON → Current (field-by-field)'
|
168
|
+
puts ' • Heavy JSON payloads → Consider lazy deserialization'
|
169
|
+
|
170
|
+
# Cleanup
|
171
|
+
user.destroy!
|
172
|
+
|
173
|
+
__END__
|
174
|
+
|
175
|
+
# Example output expectations:
|
176
|
+
#
|
177
|
+
# Current approach should be moderately fast
|
178
|
+
# Bulk JSON round-trip should be slower (extra serialization step)
|
179
|
+
# Direct assignment should be fastest but incorrect for complex types
|
180
|
+
# Selective should be fast for simple data, slower for JSON-heavy data
|