familia 2.0.0.pre18 → 2.0.0.pre21
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/.github/workflows/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +205 -88
- data/CLAUDE.md +62 -10
- data/Gemfile +3 -3
- data/Gemfile.lock +27 -62
- data/README.md +39 -0
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +177 -133
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +282 -0
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +254 -0
- data/lib/familia/connection/handlers.rb +97 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +3 -1
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
- data/lib/familia/connection/transaction_core.rb +75 -9
- data/lib/familia/connection.rb +21 -5
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +153 -7
- data/lib/familia/data_type/database_commands.rb +9 -4
- data/lib/familia/data_type/serialization.rb +10 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +8 -6
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +53 -14
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +11 -11
- data/lib/familia/features/expiration.rb +29 -21
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
- data/lib/familia/features/relationships/indexing.rb +40 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/connection.rb +28 -36
- data/lib/familia/horreum/database_commands.rb +131 -10
- data/lib/familia/horreum/definition.rb +18 -7
- data/lib/familia/horreum/management.rb +233 -57
- data/lib/familia/horreum/persistence.rb +314 -122
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +26 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -8
- data/lib/familia/horreum.rb +46 -13
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +94 -37
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +9 -7
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +325 -129
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +6 -4
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +5 -1
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +30 -4
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +6 -4
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +5 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +26 -22
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
- data/try/integration/data_types/datatype_transactions_try.rb +251 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +9 -1
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +5 -1
- data/try/integration/persistence_operations_try.rb +166 -10
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +5 -1
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +5 -1
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
- data/try/unit/horreum/base_try.rb +5 -1
- data/try/unit/horreum/class_methods_try.rb +6 -2
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +5 -1
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +8 -4
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +6 -2
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +81 -14
- data/.github/workflows/code-quality.yml +0 -138
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/horreum/persistence.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
# Familia::Horreum
|
|
@@ -35,128 +37,187 @@ module Familia
|
|
|
35
37
|
# Handles conversion between Ruby objects and Valkey hash storage
|
|
36
38
|
#
|
|
37
39
|
module Persistence
|
|
38
|
-
# Persists
|
|
40
|
+
# Persists object state to storage with timestamps, validation, and indexing.
|
|
39
41
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
42
|
+
# Performs a complete save operation in an atomic transaction:
|
|
43
|
+
# - Sets created/updated timestamps
|
|
44
|
+
# - Validates unique index constraints
|
|
45
|
+
# - Persists all fields
|
|
46
|
+
# - Updates expiration (optional)
|
|
47
|
+
# - Updates class-level indexes
|
|
48
|
+
# - Adds to instances collection
|
|
43
49
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
50
|
+
# ## Transaction Safety
|
|
51
|
+
#
|
|
52
|
+
# This method CANNOT be called within a transaction context. The save process
|
|
53
|
+
# requires reading current state to validate unique constraints, which would
|
|
54
|
+
# return uninspectable Redis::Future objects inside transactions.
|
|
55
|
+
#
|
|
56
|
+
# ### Correct Pattern:
|
|
57
|
+
# customer = Customer.new(email: 'test@example.com')
|
|
58
|
+
# customer.save # Validates unique constraints here
|
|
46
59
|
#
|
|
47
|
-
#
|
|
60
|
+
# customer.transaction do
|
|
61
|
+
# # Perform other atomic operations
|
|
62
|
+
# customer.increment(:login_count)
|
|
63
|
+
# customer.hset(:last_login, Time.now.to_i)
|
|
64
|
+
# end
|
|
48
65
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
66
|
+
# ### Incorrect Pattern:
|
|
67
|
+
# Customer.transaction do
|
|
68
|
+
# customer = Customer.new(email: 'test@example.com')
|
|
69
|
+
# customer.save # Raises Familia::OperationModeError
|
|
70
|
+
# end
|
|
53
71
|
#
|
|
54
|
-
# @
|
|
55
|
-
#
|
|
56
|
-
# # => true
|
|
72
|
+
# @param update_expiration [Boolean] Whether to refresh key expiration (default: true)
|
|
73
|
+
# @return [Boolean] true on success
|
|
57
74
|
#
|
|
58
|
-
# @
|
|
59
|
-
#
|
|
75
|
+
# @raise [Familia::OperationModeError] If called within a transaction
|
|
76
|
+
# @raise [Familia::RecordExistsError] If unique index constraint violated
|
|
60
77
|
#
|
|
61
|
-
# @
|
|
78
|
+
# @example Basic usage
|
|
79
|
+
# user = User.new(email: "john@example.com")
|
|
80
|
+
# user.save # => true
|
|
81
|
+
#
|
|
82
|
+
# @see #save_if_not_exists! For conditional saves
|
|
83
|
+
# @see #transaction For atomic operations after save
|
|
62
84
|
#
|
|
63
85
|
def save(update_expiration: true)
|
|
64
|
-
Familia.
|
|
65
|
-
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ret = commit_fields(update_expiration: update_expiration)
|
|
73
|
-
|
|
74
|
-
# Auto-index for class-level indexes after successful save
|
|
75
|
-
# Use transaction to ensure atomicity with the save operation
|
|
76
|
-
if ret
|
|
77
|
-
transaction do |conn|
|
|
78
|
-
auto_update_class_indexes
|
|
79
|
-
# Add to class-level instances collection after successful save
|
|
80
|
-
self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
|
|
81
|
-
end
|
|
86
|
+
start_time = Familia.now_in_μs if Familia.debug?
|
|
87
|
+
|
|
88
|
+
# Prevent save within transaction - unique index guards require read operations
|
|
89
|
+
# which are not available in Redis MULTI/EXEC blocks
|
|
90
|
+
if Fiber[:familia_transaction]
|
|
91
|
+
raise Familia::OperationModeError, <<~ERROR_MESSAGE
|
|
92
|
+
Cannot call save within a transaction. Save operations must be called outside transactions to ensure unique constraints can be validated.
|
|
93
|
+
ERROR_MESSAGE
|
|
82
94
|
end
|
|
83
95
|
|
|
84
|
-
Familia.
|
|
96
|
+
Familia.trace :SAVE, nil, self.class.uri if Familia.debug?
|
|
85
97
|
|
|
86
|
-
#
|
|
87
|
-
|
|
98
|
+
# Prepare object for persistence (timestamps, validation)
|
|
99
|
+
prepare_for_save
|
|
100
|
+
|
|
101
|
+
# Everything in ONE transaction for complete atomicity
|
|
102
|
+
result = transaction do |_conn|
|
|
103
|
+
persist_to_storage(update_expiration)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Structured lifecycle logging and instrumentation
|
|
107
|
+
if Familia.debug? && start_time
|
|
108
|
+
duration = Familia.now_in_μs - start_time
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
fields_count = to_h_for_storage.size
|
|
112
|
+
rescue => e
|
|
113
|
+
Familia.error "Failed to serialize fields for logging",
|
|
114
|
+
error: e.message,
|
|
115
|
+
class: self.class.name,
|
|
116
|
+
identifier: (identifier rescue nil)
|
|
117
|
+
fields_count = 0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
Familia.debug "Horreum saved",
|
|
121
|
+
class: self.class.name,
|
|
122
|
+
identifier: identifier,
|
|
123
|
+
duration: duration,
|
|
124
|
+
fields_count: fields_count,
|
|
125
|
+
update_expiration: update_expiration
|
|
126
|
+
|
|
127
|
+
Familia::Instrumentation.notify_lifecycle(:save, self,
|
|
128
|
+
duration: duration,
|
|
129
|
+
update_expiration: update_expiration,
|
|
130
|
+
fields_count: fields_count
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Return boolean indicating success
|
|
135
|
+
!result.nil?
|
|
88
136
|
end
|
|
89
137
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# Conditionally persists the object to Valkey storage by first checking if the
|
|
93
|
-
# identifier field already exists. If the object already exists in storage,
|
|
94
|
-
# raises an error. Otherwise, proceeds with a normal save operation including
|
|
95
|
-
# automatic timestamping.
|
|
96
|
-
#
|
|
97
|
-
# This method provides atomic conditional creation to prevent duplicate objects
|
|
98
|
-
# from being saved when uniqueness is required based on the identifier field.
|
|
99
|
-
#
|
|
100
|
-
# @param update_expiration [Boolean] Whether to update the key's expiration
|
|
101
|
-
# time after saving. Defaults to true.
|
|
102
|
-
#
|
|
103
|
-
# @return [Boolean] true if the save operation was successful
|
|
138
|
+
# Conditionally persists object only if it doesn't already exist in storage.
|
|
104
139
|
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
140
|
+
# Uses optimistic locking (WATCH) to atomically check existence and save.
|
|
141
|
+
# If the object doesn't exist, performs identical operations as save.
|
|
142
|
+
# If it exists, raises an error with retry logic for optimistic lock failures.
|
|
107
143
|
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
# # => true
|
|
121
|
-
#
|
|
122
|
-
# @note This method uses HSETNX to atomically check and set the identifier
|
|
123
|
-
# field, ensuring race-condition-free conditional creation.
|
|
144
|
+
# `save_if_not_exists` doesn't call save because of the gap between checking
|
|
145
|
+
# existence and persisting the data. We can't check for existence inside the
|
|
146
|
+
# transaction because commands are queued and not executed until EXEC
|
|
147
|
+
# is called (if you try you get a Redis::Future object). So here we use a
|
|
148
|
+
# WATCH + MULTI/EXEC pattern to fail the transaction if the key is created
|
|
149
|
+
# (or modified in any way) to avoid silent data corruption♀︎.
|
|
150
|
+
|
|
151
|
+
# ♀︎ Additional note about WATCH + MULTI/EXEC in Valkey/Redis or any two
|
|
152
|
+
# step existence check in any database: although it is more cautious,
|
|
153
|
+
# it is not atomic. The only way to do that is if the database process
|
|
154
|
+
# can determine itself whether the record already exists or not. For
|
|
155
|
+
# Valkey/Redis, that means writing the lua to do that.
|
|
124
156
|
#
|
|
125
|
-
# @
|
|
157
|
+
# @param update_expiration [Boolean] Whether to refresh key expiration (default: true)
|
|
158
|
+
# @return [Boolean] true on successful save
|
|
126
159
|
#
|
|
127
|
-
#
|
|
160
|
+
# @raise [Familia::RecordExistsError] If object already exists
|
|
161
|
+
# @raise [Familia::OptimisticLockError] If retries exhausted (max 3 attempts)
|
|
162
|
+
# @raise [Familia::OperationModeError] If called within a transaction
|
|
128
163
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
def save_if_not_exists(update_expiration: true)
|
|
164
|
+
# @example
|
|
165
|
+
# user = User.new(id: 123)
|
|
166
|
+
# user.save_if_not_exists! # => true or raises
|
|
167
|
+
def save_if_not_exists!(update_expiration: true)
|
|
168
|
+
# Prevent save_if_not_exists! within transaction - needs to read existence state
|
|
169
|
+
if Fiber[:familia_transaction]
|
|
170
|
+
raise Familia::OperationModeError, <<~ERROR_MESSAGE
|
|
171
|
+
Cannot call save_if_not_exists! within a transaction. This method
|
|
172
|
+
must be called outside transactions to properly check existence.
|
|
173
|
+
ERROR_MESSAGE
|
|
174
|
+
end
|
|
175
|
+
|
|
133
176
|
identifier_field = self.class.identifier_field
|
|
134
177
|
|
|
135
|
-
Familia.
|
|
136
|
-
Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
|
|
178
|
+
Familia.debug "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
|
|
179
|
+
Familia.trace :SAVE_IF_NOT_EXISTS, nil, self.class.uri if Familia.debug?
|
|
137
180
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
dbclient.unwatch
|
|
141
|
-
raise Familia::RecordExistsError, dbkey
|
|
142
|
-
end
|
|
181
|
+
# Prepare object for persistence (timestamps, validation)
|
|
182
|
+
prepare_for_save
|
|
143
183
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
184
|
+
attempts = 0
|
|
185
|
+
begin
|
|
186
|
+
attempts += 1
|
|
147
187
|
|
|
148
|
-
result
|
|
149
|
-
|
|
188
|
+
result = watch do
|
|
189
|
+
raise Familia::RecordExistsError, dbkey if exists?
|
|
150
190
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
txn_result = transaction do |_multi|
|
|
192
|
+
persist_to_storage(update_expiration)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
Familia.debug "[save_if_not_exists]: txn_result=#{txn_result.inspect}"
|
|
196
|
+
|
|
197
|
+
txn_result
|
|
156
198
|
end
|
|
199
|
+
|
|
200
|
+
Familia.debug "[save_if_not_exists]: result=#{result.inspect}"
|
|
201
|
+
|
|
202
|
+
# Return boolean indicating success (consistent with save method)
|
|
203
|
+
!result.nil?
|
|
204
|
+
rescue OptimisticLockError => e
|
|
205
|
+
Familia.debug "[save_if_not_exists]: OptimisticLockError (#{attempts}): #{e.message}"
|
|
206
|
+
raise if attempts >= 3
|
|
207
|
+
|
|
208
|
+
sleep(0.001 * (2**attempts))
|
|
209
|
+
retry
|
|
157
210
|
end
|
|
211
|
+
end
|
|
158
212
|
|
|
159
|
-
|
|
213
|
+
# Non-raising variant of save_if_not_exists!
|
|
214
|
+
#
|
|
215
|
+
# @return [Boolean] true on success, false if object exists
|
|
216
|
+
# @raise [Familia::OptimisticLockError] If concurrency conflict persists after retries
|
|
217
|
+
def save_if_not_exists(...)
|
|
218
|
+
save_if_not_exists!(...)
|
|
219
|
+
rescue RecordExistsError
|
|
220
|
+
false
|
|
160
221
|
end
|
|
161
222
|
|
|
162
223
|
# Commits object fields to the DB storage.
|
|
@@ -186,16 +247,17 @@ module Familia
|
|
|
186
247
|
#
|
|
187
248
|
def commit_fields(update_expiration: true)
|
|
188
249
|
prepared_value = to_h_for_storage
|
|
189
|
-
Familia.
|
|
250
|
+
Familia.debug "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})"
|
|
190
251
|
|
|
191
|
-
|
|
252
|
+
transaction do |_conn|
|
|
253
|
+
# Set all fields atomically
|
|
254
|
+
result = hmset(prepared_value)
|
|
192
255
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# this will be a no-op that simply logs the attempt.
|
|
196
|
-
update_expiration(default_expiration: nil) if update_expiration
|
|
256
|
+
# Update expiration in same transaction to ensure atomicity
|
|
257
|
+
self.update_expiration if result && update_expiration
|
|
197
258
|
|
|
198
|
-
|
|
259
|
+
result
|
|
260
|
+
end
|
|
199
261
|
end
|
|
200
262
|
|
|
201
263
|
# Updates multiple fields atomically in a Database transaction.
|
|
@@ -216,20 +278,57 @@ module Familia
|
|
|
216
278
|
|
|
217
279
|
Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug?
|
|
218
280
|
|
|
219
|
-
|
|
281
|
+
transaction do |_conn|
|
|
282
|
+
# 1. Update all fields atomically
|
|
220
283
|
fields.each do |field, value|
|
|
221
284
|
prepared_value = serialize_value(value)
|
|
222
|
-
|
|
285
|
+
hset field, prepared_value
|
|
223
286
|
# Update instance variable to keep object in sync
|
|
224
287
|
send("#{field}=", value) if respond_to?("#{field}=")
|
|
225
288
|
end
|
|
289
|
+
|
|
290
|
+
# 2. Update expiration in same transaction
|
|
291
|
+
self.update_expiration if update_expiration
|
|
226
292
|
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Persists only the specified fields to Redis.
|
|
296
|
+
#
|
|
297
|
+
# Saves the current in-memory values of specified fields to Redis without
|
|
298
|
+
# modifying them first. Fields must already be set on the instance.
|
|
299
|
+
#
|
|
300
|
+
# @param field_names [Array<Symbol, String>] Names of fields to persist
|
|
301
|
+
# @param update_expiration [Boolean] Whether to refresh key expiration
|
|
302
|
+
# @return [self] Returns self for method chaining
|
|
303
|
+
#
|
|
304
|
+
# @example Persist only passphrase fields after updating them
|
|
305
|
+
# customer.update_passphrase('secret').save_fields(:passphrase, :passphrase_encryption)
|
|
306
|
+
#
|
|
307
|
+
def save_fields(*field_names, update_expiration: true)
|
|
308
|
+
raise ArgumentError, 'No fields specified' if field_names.empty?
|
|
227
309
|
|
|
228
|
-
|
|
229
|
-
self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
|
|
310
|
+
Familia.trace :SAVE_FIELDS, nil, field_names if Familia.debug?
|
|
230
311
|
|
|
231
|
-
|
|
232
|
-
|
|
312
|
+
transaction do |_conn|
|
|
313
|
+
# Build hash of field names to serialized values
|
|
314
|
+
fields_hash = {}
|
|
315
|
+
field_names.each do |field|
|
|
316
|
+
field_sym = field.to_sym
|
|
317
|
+
raise ArgumentError, "Unknown field: #{field}" unless respond_to?(field_sym)
|
|
318
|
+
|
|
319
|
+
value = send(field_sym)
|
|
320
|
+
prepared_value = serialize_value(value)
|
|
321
|
+
fields_hash[field] = prepared_value
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Set all fields at once using hmset
|
|
325
|
+
hmset(fields_hash)
|
|
326
|
+
|
|
327
|
+
# Update expiration in same transaction
|
|
328
|
+
self.update_expiration if update_expiration
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
self
|
|
233
332
|
end
|
|
234
333
|
|
|
235
334
|
# Updates the object by applying multiple field values.
|
|
@@ -279,25 +378,35 @@ module Familia
|
|
|
279
378
|
# @see #delete! The underlying method that performs the key deletion
|
|
280
379
|
#
|
|
281
380
|
def destroy!
|
|
282
|
-
Familia.trace :DESTROY
|
|
381
|
+
Familia.trace :DESTROY!, dbkey, self.class.uri
|
|
283
382
|
|
|
284
383
|
# Execute all deletion operations within a transaction
|
|
285
|
-
transaction do |
|
|
384
|
+
result = transaction do |_conn|
|
|
286
385
|
# Delete the main object key
|
|
287
|
-
|
|
386
|
+
delete!
|
|
288
387
|
|
|
289
388
|
# Delete all related fields if present
|
|
290
389
|
if self.class.relations?
|
|
291
390
|
Familia.trace :DELETE_RELATED_FIELDS!, nil,
|
|
292
391
|
"#{self.class} has relations: #{self.class.related_fields.keys}"
|
|
293
392
|
|
|
294
|
-
self.class.related_fields.
|
|
393
|
+
self.class.related_fields.each_key do |name|
|
|
295
394
|
obj = send(name)
|
|
296
395
|
Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})"
|
|
297
|
-
|
|
396
|
+
obj.delete!
|
|
298
397
|
end
|
|
299
398
|
end
|
|
300
399
|
end
|
|
400
|
+
|
|
401
|
+
# Structured lifecycle logging and instrumentation
|
|
402
|
+
Familia.debug "Horreum destroyed",
|
|
403
|
+
class: self.class.name,
|
|
404
|
+
identifier: identifier,
|
|
405
|
+
key: dbkey
|
|
406
|
+
|
|
407
|
+
Familia::Instrumentation.notify_lifecycle(:destroy, self, key: dbkey)
|
|
408
|
+
|
|
409
|
+
result
|
|
301
410
|
end
|
|
302
411
|
|
|
303
412
|
# Clears all fields by setting them to nil.
|
|
@@ -318,6 +427,7 @@ module Familia
|
|
|
318
427
|
# after clear_fields! if you want to persist the cleared state.
|
|
319
428
|
#
|
|
320
429
|
def clear_fields!
|
|
430
|
+
Familia.trace :CLEAR_FIELDS!, dbkey, self.class.uri
|
|
321
431
|
self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) }
|
|
322
432
|
end
|
|
323
433
|
|
|
@@ -343,11 +453,11 @@ module Familia
|
|
|
343
453
|
# no authoritative source in Valkey storage.
|
|
344
454
|
#
|
|
345
455
|
def refresh!
|
|
346
|
-
Familia.trace :REFRESH, nil, uri if Familia.debug?
|
|
456
|
+
Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
|
|
347
457
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
|
348
458
|
|
|
349
459
|
fields = hgetall
|
|
350
|
-
Familia.
|
|
460
|
+
Familia.debug "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}"
|
|
351
461
|
|
|
352
462
|
# Reset transient fields to nil for semantic clarity and ORM consistency
|
|
353
463
|
# Transient fields have no authoritative source, so they should return to
|
|
@@ -378,6 +488,11 @@ module Familia
|
|
|
378
488
|
self
|
|
379
489
|
end
|
|
380
490
|
|
|
491
|
+
# Convenience methods that forward to the class method of the same name
|
|
492
|
+
def transaction(...) = self.class.transaction(...)
|
|
493
|
+
def pipelined(...) = self.class.pipelined(...)
|
|
494
|
+
def dbclient(...) = self.class.dbclient(...)
|
|
495
|
+
|
|
381
496
|
private
|
|
382
497
|
|
|
383
498
|
# Reset all transient fields to nil
|
|
@@ -398,16 +513,50 @@ module Familia
|
|
|
398
513
|
|
|
399
514
|
# UnsortedSet the transient field back to nil
|
|
400
515
|
send("#{field_type.method_name}=", nil)
|
|
401
|
-
Familia.
|
|
516
|
+
Familia.debug "[reset_transient_fields!] Reset #{field_name} to nil"
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Validates that unique index constraints are satisfied before saving
|
|
521
|
+
# This must be called OUTSIDE of transactions to allow reading current values
|
|
522
|
+
#
|
|
523
|
+
# @raise [Familia::RecordExistsError] If a unique index constraint is violated
|
|
524
|
+
# for any class-level unique_index relationships
|
|
525
|
+
#
|
|
526
|
+
# @note Only validates class-level unique indexes (without within: parameter).
|
|
527
|
+
# Instance-scoped indexes (with within:) are validated automatically when
|
|
528
|
+
# calling add_to_*_index methods:
|
|
529
|
+
#
|
|
530
|
+
# @example Instance-scoped indexes need to be called explicitly but when
|
|
531
|
+
# called they will perform the validation automatically:
|
|
532
|
+
# employee.add_to_company_badge_index(company) # raises on duplicate
|
|
533
|
+
#
|
|
534
|
+
# @return [void]
|
|
535
|
+
#
|
|
536
|
+
def guard_unique_indexes!
|
|
537
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
|
538
|
+
|
|
539
|
+
self.class.indexing_relationships.each do |rel|
|
|
540
|
+
# Only validate unique indexes (not multi_index)
|
|
541
|
+
next unless rel.cardinality == :unique
|
|
542
|
+
|
|
543
|
+
# Only validate class-level indexes (skip instance-scoped)
|
|
544
|
+
next if rel.within
|
|
545
|
+
|
|
546
|
+
# Call the validation method if it exists
|
|
547
|
+
validate_method = :"guard_unique_#{rel.index_name}!"
|
|
548
|
+
send(validate_method) if respond_to?(validate_method)
|
|
402
549
|
end
|
|
550
|
+
|
|
551
|
+
nil # Explicit nil return as documented
|
|
403
552
|
end
|
|
404
553
|
|
|
405
554
|
# Automatically update class-level indexes after save
|
|
406
555
|
#
|
|
407
556
|
# Iterates through class-level indexing relationships and calls their
|
|
408
557
|
# corresponding add_to_class_* methods to populate indexes. Only processes
|
|
409
|
-
# class-level indexes (where
|
|
410
|
-
#
|
|
558
|
+
# class-level indexes (where within is nil), skipping instance-scoped
|
|
559
|
+
# indexes which require scope context.
|
|
411
560
|
#
|
|
412
561
|
# Uses idempotent Redis commands (HSET for unique_index) so repeated calls
|
|
413
562
|
# are safe and have negligible performance overhead. Note that multi_index
|
|
@@ -434,12 +583,12 @@ module Familia
|
|
|
434
583
|
return unless self.class.respond_to?(:indexing_relationships)
|
|
435
584
|
|
|
436
585
|
self.class.indexing_relationships.each do |rel|
|
|
437
|
-
# Skip instance-scoped indexes (require
|
|
586
|
+
# Skip instance-scoped indexes (require scope context)
|
|
438
587
|
# Instance-scoped indexes must be manually populated because they need
|
|
439
|
-
# the
|
|
440
|
-
|
|
441
|
-
Familia.
|
|
442
|
-
[auto_update_class_indexes] Skipping #{rel.index_name} (requires
|
|
588
|
+
# the scope instance reference (e.g., employee.add_to_company_badge_index(company))
|
|
589
|
+
if rel.within
|
|
590
|
+
Familia.debug <<~LOG_MESSAGE
|
|
591
|
+
[auto_update_class_indexes] Skipping #{rel.index_name} (requires scope context)
|
|
443
592
|
LOG_MESSAGE
|
|
444
593
|
next
|
|
445
594
|
end
|
|
@@ -450,6 +599,49 @@ module Familia
|
|
|
450
599
|
end
|
|
451
600
|
end
|
|
452
601
|
|
|
602
|
+
# Prepares the object for persistence by setting timestamps and validating constraints
|
|
603
|
+
#
|
|
604
|
+
# This method is called by both save and save_if_not_exists to ensure consistent
|
|
605
|
+
# preparation logic. It updates created/updated timestamps and validates unique
|
|
606
|
+
# indexes before the transaction begins.
|
|
607
|
+
#
|
|
608
|
+
# @return [void]
|
|
609
|
+
#
|
|
610
|
+
def prepare_for_save
|
|
611
|
+
# Update timestamp fields before saving
|
|
612
|
+
self.created ||= Familia.now if respond_to?(:created)
|
|
613
|
+
self.updated = Familia.now if respond_to?(:updated)
|
|
614
|
+
|
|
615
|
+
# Validate unique indexes BEFORE the transaction
|
|
616
|
+
guard_unique_indexes!
|
|
617
|
+
end
|
|
618
|
+
private :prepare_for_save
|
|
619
|
+
|
|
620
|
+
# Persists the object's data to storage within a transaction
|
|
621
|
+
#
|
|
622
|
+
# This method contains the core persistence logic shared by both save and
|
|
623
|
+
# save_if_not_exists. It must be called within a transaction block.
|
|
624
|
+
#
|
|
625
|
+
# @param update_expiration [Boolean] Whether to update the key's expiration
|
|
626
|
+
# @return [Object] The result of the hmset operation
|
|
627
|
+
#
|
|
628
|
+
def persist_to_storage(update_expiration)
|
|
629
|
+
# 1. Save all fields to hashkey at once
|
|
630
|
+
prepared_h = to_h_for_storage
|
|
631
|
+
hmset_result = hmset(prepared_h)
|
|
632
|
+
|
|
633
|
+
# 2. Set expiration in same transaction
|
|
634
|
+
self.update_expiration if update_expiration
|
|
635
|
+
|
|
636
|
+
# 3. Update class-level indexes
|
|
637
|
+
auto_update_class_indexes
|
|
638
|
+
|
|
639
|
+
# 4. Add to instances collection if available
|
|
640
|
+
self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
|
|
641
|
+
|
|
642
|
+
hmset_result
|
|
643
|
+
end
|
|
644
|
+
private :persist_to_storage
|
|
453
645
|
end
|
|
454
646
|
end
|
|
455
647
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/horreum/serialization.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
class Horreum
|
|
@@ -30,7 +32,7 @@ module Familia
|
|
|
30
32
|
next unless field_type.loggable
|
|
31
33
|
|
|
32
34
|
val = send(field_type.method_name)
|
|
33
|
-
Familia.
|
|
35
|
+
Familia.debug " [to_h] field: #{field} val: #{val.class}"
|
|
34
36
|
|
|
35
37
|
# Use string key for external API compatibility
|
|
36
38
|
# Return Ruby values, not JSON-encoded strings
|
|
@@ -63,7 +65,7 @@ module Familia
|
|
|
63
65
|
prepared = serialize_value(val)
|
|
64
66
|
|
|
65
67
|
if Familia.debug?
|
|
66
|
-
Familia.
|
|
68
|
+
Familia.debug " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
# Use string key for database compatibility
|
|
@@ -96,7 +98,7 @@ module Familia
|
|
|
96
98
|
|
|
97
99
|
method_name = field_type.method_name
|
|
98
100
|
val = send(method_name)
|
|
99
|
-
Familia.
|
|
101
|
+
Familia.debug " [to_a] field: #{field} method: #{method_name} val: #{val.class}"
|
|
100
102
|
|
|
101
103
|
# Return actual Ruby values, including nil to maintain array positions
|
|
102
104
|
val
|
|
@@ -159,6 +161,9 @@ module Familia
|
|
|
159
161
|
def deserialize_value(val, symbolize: false, field_name: nil)
|
|
160
162
|
return nil if val.nil? || val == ''
|
|
161
163
|
|
|
164
|
+
# Handle Redis::Future objects during transactions
|
|
165
|
+
return val if val.is_a?(Redis::Future)
|
|
166
|
+
|
|
162
167
|
begin
|
|
163
168
|
Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
|
164
169
|
rescue Familia::SerializerError
|
|
@@ -179,7 +184,24 @@ module Familia
|
|
|
179
184
|
"Legacy plain string in #{context}: #{val.inspect} (#{dbkey_info})"
|
|
180
185
|
end
|
|
181
186
|
|
|
182
|
-
|
|
187
|
+
# Structured error logging with instrumentation
|
|
188
|
+
error_type = looks_like_json?(val) ? :corrupted_json : :legacy_string
|
|
189
|
+
Familia.error msg,
|
|
190
|
+
error_type: error_type,
|
|
191
|
+
field: field_name,
|
|
192
|
+
value_preview: val.to_s[0...50],
|
|
193
|
+
object_class: self.class.name,
|
|
194
|
+
identifier: (identifier rescue nil),
|
|
195
|
+
key: dbkey_info
|
|
196
|
+
|
|
197
|
+
# Notify instrumentation hooks
|
|
198
|
+
Familia::Instrumentation.notify_error(
|
|
199
|
+
StandardError.new(msg),
|
|
200
|
+
operation: :deserialization,
|
|
201
|
+
error_type: error_type,
|
|
202
|
+
field: field_name,
|
|
203
|
+
object_class: self.class.name
|
|
204
|
+
)
|
|
183
205
|
end
|
|
184
206
|
|
|
185
207
|
def looks_like_json?(val)
|