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/definition.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'settings'
|
|
4
6
|
|
|
@@ -98,7 +100,7 @@ module Familia
|
|
|
98
100
|
@current_field_group = nil
|
|
99
101
|
end
|
|
100
102
|
else
|
|
101
|
-
Familia.
|
|
103
|
+
Familia.debug "[field_group] Created field group :#{name} but no block given"
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
field_groups[name.to_sym]
|
|
@@ -124,7 +126,10 @@ module Familia
|
|
|
124
126
|
# ]
|
|
125
127
|
#
|
|
126
128
|
def field_groups
|
|
127
|
-
@
|
|
129
|
+
@field_groups_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_groups')
|
|
130
|
+
@field_groups || @field_groups_mutex.synchronize do
|
|
131
|
+
@field_groups ||= {}
|
|
132
|
+
end
|
|
128
133
|
end
|
|
129
134
|
|
|
130
135
|
# Sets or retrieves the unique identifier field for the class.
|
|
@@ -215,8 +220,10 @@ module Familia
|
|
|
215
220
|
# Returns the list of field names defined for the class in the order
|
|
216
221
|
# that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
|
|
217
222
|
def fields
|
|
218
|
-
@
|
|
219
|
-
@fields
|
|
223
|
+
@fields_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('fields')
|
|
224
|
+
@fields || @fields_mutex.synchronize do
|
|
225
|
+
@fields ||= []
|
|
226
|
+
end
|
|
220
227
|
end
|
|
221
228
|
|
|
222
229
|
def class_related_fields
|
|
@@ -235,7 +242,10 @@ module Familia
|
|
|
235
242
|
|
|
236
243
|
# Storage for field type instances
|
|
237
244
|
def field_types
|
|
238
|
-
@
|
|
245
|
+
@field_types_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_types')
|
|
246
|
+
@field_types || @field_types_mutex.synchronize do
|
|
247
|
+
@field_types ||= {}
|
|
248
|
+
end
|
|
239
249
|
end
|
|
240
250
|
|
|
241
251
|
# Returns a hash mapping field names to method names for backward compatibility
|
|
@@ -467,7 +477,8 @@ module Familia
|
|
|
467
477
|
|
|
468
478
|
# If no value is provided to this fast attribute method, make a call
|
|
469
479
|
# to the db to return the current stored value of the hash field.
|
|
470
|
-
|
|
480
|
+
# Handle Redis::Future objects during transactions
|
|
481
|
+
return hget field_name if val.nil? || val.is_a?(Redis::Future)
|
|
471
482
|
|
|
472
483
|
begin
|
|
473
484
|
# Trace the operation if debugging is enabled.
|
|
@@ -475,7 +486,7 @@ module Familia
|
|
|
475
486
|
|
|
476
487
|
# Convert the provided value to a format suitable for Database storage.
|
|
477
488
|
prepared = serialize_value(val)
|
|
478
|
-
Familia.
|
|
489
|
+
Familia.debug "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
|
|
479
490
|
|
|
480
491
|
# Use the existing accessor method to set the attribute value.
|
|
481
492
|
send :"#{method_name}=", val
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/horreum/management.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
class Horreum
|
|
@@ -23,7 +25,7 @@ module Familia
|
|
|
23
25
|
# to the constructor.
|
|
24
26
|
# @param kwargs [Hash] Keyword arguments to be passed to the constructor.
|
|
25
27
|
# @return [Object] The newly created and persisted instance.
|
|
26
|
-
# @raise [Familia::
|
|
28
|
+
# @raise [Familia::RecordExistsError] If an instance with the same identifier already
|
|
27
29
|
# exists.
|
|
28
30
|
#
|
|
29
31
|
# This method serves as a factory method for creating and persisting new
|
|
@@ -35,7 +37,7 @@ module Familia
|
|
|
35
37
|
# - Keyword arguments (**kwargs) are passed as a hash to the constructor.
|
|
36
38
|
#
|
|
37
39
|
# After instantiation, the method checks if an object with the same
|
|
38
|
-
# identifier already exists. If it does, a Familia::
|
|
40
|
+
# identifier already exists. If it does, a Familia::RecordExistsError exception is
|
|
39
41
|
# raised to prevent overwriting existing data.
|
|
40
42
|
#
|
|
41
43
|
# Finally, the method saves the new instance returns it.
|
|
@@ -52,24 +54,27 @@ module Familia
|
|
|
52
54
|
# @see #new
|
|
53
55
|
# @see #exists?
|
|
54
56
|
# @see #save
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
def create!(...)
|
|
58
|
+
hobj = new(...)
|
|
59
|
+
hobj.save_if_not_exists!
|
|
60
|
+
|
|
61
|
+
# If a block is given, yield the created object
|
|
62
|
+
# This allows for additional operations on successful creation
|
|
63
|
+
yield hobj if block_given?
|
|
64
|
+
|
|
65
|
+
hobj
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
def multiget(
|
|
63
|
-
|
|
64
|
-
ids.filter_map { |json| from_json(json) }
|
|
68
|
+
def multiget(...)
|
|
69
|
+
rawmultiget(...).filter_map { |json| Familia::JsonSerializer.parse(json) }
|
|
65
70
|
end
|
|
66
71
|
|
|
67
|
-
def rawmultiget(*
|
|
68
|
-
|
|
69
|
-
return [] if
|
|
72
|
+
def rawmultiget(*hids)
|
|
73
|
+
hids.collect! { |hobjid| dbkey(hobjid) }
|
|
74
|
+
return [] if hids.compact.empty?
|
|
70
75
|
|
|
71
|
-
Familia.trace :MULTIGET, nil, "#{
|
|
72
|
-
dbclient.mget(*
|
|
76
|
+
Familia.trace :MULTIGET, nil, "#{hids.size}: #{hids}" if Familia.debug?
|
|
77
|
+
dbclient.mget(*hids)
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
# Converts the class name into a string that can be used to look up
|
|
@@ -101,56 +106,71 @@ module Familia
|
|
|
101
106
|
# key.
|
|
102
107
|
#
|
|
103
108
|
# @param objkey [String] The full dbkey for the object.
|
|
109
|
+
# @param check_exists [Boolean] Whether to check key existence before HGETALL
|
|
110
|
+
# (default: true). When false, skips EXISTS check for better performance
|
|
111
|
+
# but still returns nil for non-existent keys (detected via empty hash).
|
|
104
112
|
# @return [Object, nil] An instance of the class if the key exists, nil
|
|
105
113
|
# otherwise.
|
|
106
114
|
# @raise [ArgumentError] If the provided key is empty.
|
|
107
115
|
#
|
|
108
|
-
# This method
|
|
109
|
-
# instantiate objects:
|
|
116
|
+
# This method can operate in two modes:
|
|
110
117
|
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
118
|
+
# **Safe mode (check_exists: true, default):**
|
|
119
|
+
# 1. First checks if the key exists with EXISTS command
|
|
120
|
+
# 2. Returns nil immediately if key doesn't exist
|
|
121
|
+
# 3. If exists, retrieves data with HGETALL and instantiates object
|
|
122
|
+
# - Best for: Single object lookups, defensive code
|
|
123
|
+
# - Commands: 2 per object (EXISTS + HGETALL)
|
|
116
124
|
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
125
|
+
# **Optimized mode (check_exists: false):**
|
|
126
|
+
# 1. Directly calls HGETALL without EXISTS check
|
|
127
|
+
# 2. Returns nil if HGETALL returns empty hash (key doesn't exist)
|
|
128
|
+
# 3. Otherwise instantiates object with returned data
|
|
129
|
+
# - Best for: Bulk operations, performance-critical paths, when keys likely exist
|
|
130
|
+
# - Commands: 1 per object (HGETALL only)
|
|
131
|
+
# - Reduction: 50% fewer Redis commands
|
|
119
132
|
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# debugging.
|
|
133
|
+
# @example Safe mode (default)
|
|
134
|
+
# User.find_by_key("user:123") # 2 commands: EXISTS + HGETALL
|
|
123
135
|
#
|
|
124
|
-
# @example
|
|
125
|
-
# User.find_by_key("user:123") #
|
|
126
|
-
#
|
|
136
|
+
# @example Optimized mode (skip existence check)
|
|
137
|
+
# User.find_by_key("user:123", check_exists: false) # 1 command: HGETALL
|
|
138
|
+
#
|
|
139
|
+
# @note When check_exists: false, HGETALL on non-existent keys returns {}
|
|
140
|
+
# which we detect and return nil (not an empty object instance).
|
|
127
141
|
#
|
|
128
|
-
def find_by_dbkey(objkey)
|
|
142
|
+
def find_by_dbkey(objkey, check_exists: true)
|
|
129
143
|
raise ArgumentError, 'Empty key' if objkey.to_s.empty?
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
if check_exists
|
|
146
|
+
# Safe mode: Check existence first (original behavior)
|
|
147
|
+
# We use a lower-level method here b/c we're working with the
|
|
148
|
+
# full key and not just the identifier.
|
|
149
|
+
does_exist = dbclient.exists(objkey).positive?
|
|
150
|
+
|
|
151
|
+
Familia.debug "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
|
152
|
+
Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
|
|
153
|
+
|
|
154
|
+
# This is the reason for calling exists first. We want to definitively
|
|
155
|
+
# and without any ambiguity know if the object exists in the database. If it
|
|
156
|
+
# doesn't, we return nil. If it does, we proceed to load the object.
|
|
157
|
+
# Otherwise, hgetall will return an empty hash, which will be passed to
|
|
158
|
+
# the constructor, which will then be annoying to debug.
|
|
159
|
+
return unless does_exist
|
|
160
|
+
else
|
|
161
|
+
# Optimized mode: Skip existence check
|
|
162
|
+
Familia.debug "[find_by_key] #{self} from key #{objkey} (check_exists: false)"
|
|
163
|
+
Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
|
|
164
|
+
end
|
|
144
165
|
|
|
145
166
|
obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
|
|
146
167
|
Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
|
|
147
168
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
instance
|
|
152
|
-
|
|
153
|
-
instance
|
|
169
|
+
# If we skipped existence check and got empty hash, key doesn't exist
|
|
170
|
+
return nil if !check_exists && obj.empty?
|
|
171
|
+
|
|
172
|
+
# Create instance and deserialize fields using shared helper method
|
|
173
|
+
instantiate_from_hash(obj)
|
|
154
174
|
end
|
|
155
175
|
alias find_by_key find_by_dbkey
|
|
156
176
|
|
|
@@ -158,8 +178,10 @@ module Familia
|
|
|
158
178
|
#
|
|
159
179
|
# @param identifier [String, Integer] The unique identifier for the
|
|
160
180
|
# object.
|
|
161
|
-
# @param suffix [Symbol] The suffix to use in the dbkey (default:
|
|
162
|
-
#
|
|
181
|
+
# @param suffix [Symbol, nil] The suffix to use in the dbkey (default:
|
|
182
|
+
# class suffix). Keyword parameter for consistency with check_exists.
|
|
183
|
+
# @param check_exists [Boolean] Whether to check key existence before HGETALL
|
|
184
|
+
# (default: true). See find_by_dbkey for details.
|
|
163
185
|
# @return [Object, nil] An instance of the class if found, nil otherwise.
|
|
164
186
|
#
|
|
165
187
|
# This method constructs the full dbkey using the provided identifier
|
|
@@ -170,23 +192,153 @@ module Familia
|
|
|
170
192
|
# making it easier to retrieve objects when you only have their
|
|
171
193
|
# identifier.
|
|
172
194
|
#
|
|
173
|
-
# @example
|
|
174
|
-
# User.find_by_id(123) #
|
|
195
|
+
# @example Safe mode (default)
|
|
196
|
+
# User.find_by_id(123) # 2 commands: EXISTS + HGETALL
|
|
197
|
+
#
|
|
198
|
+
# @example Optimized mode
|
|
199
|
+
# User.find_by_id(123, check_exists: false) # 1 command: HGETALL
|
|
175
200
|
#
|
|
176
|
-
|
|
201
|
+
# @example Custom suffix
|
|
202
|
+
# Session.find_by_id('abc', suffix: :session)
|
|
203
|
+
#
|
|
204
|
+
def find_by_identifier(identifier, suffix: nil, check_exists: true)
|
|
177
205
|
suffix ||= self.suffix
|
|
178
206
|
return nil if identifier.to_s.empty?
|
|
179
207
|
|
|
180
208
|
objkey = dbkey(identifier, suffix)
|
|
181
209
|
|
|
182
|
-
Familia.
|
|
210
|
+
Familia.debug "[find_by_id] #{self} from key #{objkey})"
|
|
183
211
|
Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
|
|
184
|
-
find_by_dbkey objkey
|
|
212
|
+
find_by_dbkey objkey, check_exists: check_exists
|
|
185
213
|
end
|
|
186
214
|
alias find_by_id find_by_identifier
|
|
187
215
|
alias find find_by_id
|
|
188
216
|
alias load find_by_id
|
|
189
217
|
|
|
218
|
+
# Loads multiple objects by their identifiers using pipelined HGETALL commands.
|
|
219
|
+
#
|
|
220
|
+
# This method provides significant performance improvements for bulk loading by:
|
|
221
|
+
# 1. Batching all HGETALL commands into a single Redis pipeline
|
|
222
|
+
# 2. Eliminating network round-trip overhead
|
|
223
|
+
# 3. Skipping individual EXISTS checks (like check_exists: false)
|
|
224
|
+
#
|
|
225
|
+
# @param identifiers [Array<String, Integer>] Array of identifiers to load
|
|
226
|
+
# @param suffix [Symbol, nil] The suffix to use in dbkeys (default: class suffix)
|
|
227
|
+
# @return [Array<Object>] Array of instantiated objects (nils for non-existent)
|
|
228
|
+
#
|
|
229
|
+
# Performance characteristics:
|
|
230
|
+
# - Standard approach: N objects × 2 commands (EXISTS + HGETALL) = 2N round trips
|
|
231
|
+
# - check_exists: false: N objects × 1 command (HGETALL) = N round trips
|
|
232
|
+
# - load_multi: 1 pipeline with N commands = 1 round trip
|
|
233
|
+
# - Improvement: Up to 2N× faster for bulk operations
|
|
234
|
+
#
|
|
235
|
+
# @example Load multiple users efficiently
|
|
236
|
+
# users = User.load_multi([123, 456, 789])
|
|
237
|
+
# # 1 pipeline with 3 HGETALL commands instead of 6 individual commands
|
|
238
|
+
#
|
|
239
|
+
# @example Filter out nils
|
|
240
|
+
# existing_users = User.load_multi(ids).compact
|
|
241
|
+
#
|
|
242
|
+
# @note Returns nil for non-existent keys (maintains same contract as find_by_id)
|
|
243
|
+
# @note Objects are returned in the same order as input identifiers
|
|
244
|
+
# @note Empty/nil identifiers are skipped and return nil in result array
|
|
245
|
+
#
|
|
246
|
+
def load_multi(identifiers, suffix = nil)
|
|
247
|
+
suffix ||= self.suffix
|
|
248
|
+
return [] if identifiers.empty?
|
|
249
|
+
|
|
250
|
+
# Build list of valid keys and track their original positions
|
|
251
|
+
valid_keys = []
|
|
252
|
+
valid_positions = []
|
|
253
|
+
|
|
254
|
+
identifiers.each_with_index do |identifier, idx|
|
|
255
|
+
next if identifier.to_s.empty?
|
|
256
|
+
|
|
257
|
+
valid_keys << dbkey(identifier, suffix)
|
|
258
|
+
valid_positions << idx
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
Familia.trace :LOAD_MULTI, nil, "Loading #{identifiers.size} objects" if Familia.debug?
|
|
262
|
+
|
|
263
|
+
# Pipeline all HGETALL commands
|
|
264
|
+
multi_result = pipelined do |pipeline|
|
|
265
|
+
valid_keys.each do |objkey|
|
|
266
|
+
pipeline.hgetall(objkey)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Extract results array from MultiResult
|
|
271
|
+
results = multi_result.results
|
|
272
|
+
|
|
273
|
+
# Map results back to original positions
|
|
274
|
+
objects = Array.new(identifiers.size)
|
|
275
|
+
valid_positions.each_with_index do |pos, result_idx|
|
|
276
|
+
obj_hash = results[result_idx]
|
|
277
|
+
|
|
278
|
+
# Skip empty hashes (non-existent keys)
|
|
279
|
+
next if obj_hash.nil? || obj_hash.empty?
|
|
280
|
+
|
|
281
|
+
# Instantiate object using shared helper method
|
|
282
|
+
objects[pos] = instantiate_from_hash(obj_hash)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
objects
|
|
286
|
+
end
|
|
287
|
+
alias load_batch load_multi
|
|
288
|
+
|
|
289
|
+
# Loads multiple objects by their full dbkeys using pipelined HGETALL commands.
|
|
290
|
+
#
|
|
291
|
+
# This is a lower-level variant of load_multi that works directly with dbkeys
|
|
292
|
+
# instead of identifiers. Useful when you already have the full keys.
|
|
293
|
+
#
|
|
294
|
+
# @param objkeys [Array<String>] Array of full dbkeys to load
|
|
295
|
+
# @return [Array<Object>] Array of instantiated objects (nils for non-existent)
|
|
296
|
+
#
|
|
297
|
+
# @example Load objects by full keys
|
|
298
|
+
# keys = ["user:123:object", "user:456:object"]
|
|
299
|
+
# users = User.load_multi_by_keys(keys)
|
|
300
|
+
#
|
|
301
|
+
# @note Returns nil for empty/nil keys, maintaining position alignment with input array
|
|
302
|
+
#
|
|
303
|
+
# @see load_multi For loading by identifiers
|
|
304
|
+
#
|
|
305
|
+
def load_multi_by_keys(objkeys)
|
|
306
|
+
return [] if objkeys.empty?
|
|
307
|
+
|
|
308
|
+
Familia.trace :LOAD_MULTI_BY_KEYS, nil, "Loading #{objkeys.size} objects" if Familia.debug?
|
|
309
|
+
|
|
310
|
+
# Track which positions have valid keys to maintain result array alignment
|
|
311
|
+
valid_positions = []
|
|
312
|
+
objkeys.each_with_index do |objkey, idx|
|
|
313
|
+
valid_positions << idx unless objkey.to_s.empty?
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Pipeline all HGETALL commands for valid keys
|
|
317
|
+
multi_result = pipelined do |pipeline|
|
|
318
|
+
objkeys.each do |objkey|
|
|
319
|
+
next if objkey.to_s.empty?
|
|
320
|
+
pipeline.hgetall(objkey)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Extract results array from MultiResult
|
|
325
|
+
results = multi_result.results
|
|
326
|
+
|
|
327
|
+
# Map results back to original positions
|
|
328
|
+
objects = Array.new(objkeys.size)
|
|
329
|
+
valid_positions.each_with_index do |pos, result_idx|
|
|
330
|
+
obj_hash = results[result_idx]
|
|
331
|
+
|
|
332
|
+
# Skip empty hashes (non-existent keys)
|
|
333
|
+
next if obj_hash.nil? || obj_hash.empty?
|
|
334
|
+
|
|
335
|
+
# Instantiate object using shared helper method
|
|
336
|
+
objects[pos] = instantiate_from_hash(obj_hash)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
objects
|
|
340
|
+
end
|
|
341
|
+
|
|
190
342
|
# Checks if an object with the given identifier exists in the database.
|
|
191
343
|
#
|
|
192
344
|
# @param identifier [String, Integer] The unique identifier for the object.
|
|
@@ -209,6 +361,9 @@ module Familia
|
|
|
209
361
|
ret = dbclient.exists objkey
|
|
210
362
|
Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
|
|
211
363
|
|
|
364
|
+
# Handle Redis::Future objects during transactions
|
|
365
|
+
return ret if ret.is_a?(Redis::Future)
|
|
366
|
+
|
|
212
367
|
ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
|
|
213
368
|
end
|
|
214
369
|
|
|
@@ -311,6 +466,27 @@ module Familia
|
|
|
311
466
|
end
|
|
312
467
|
alias size matching_keys_count
|
|
313
468
|
alias length matching_keys_count
|
|
469
|
+
|
|
470
|
+
# Instantiates an object from a hash of field values.
|
|
471
|
+
#
|
|
472
|
+
# This is an internal helper method used by find_by_dbkey, load_multi, and
|
|
473
|
+
# load_multi_by_keys to eliminate code duplication. Not intended for direct use.
|
|
474
|
+
#
|
|
475
|
+
# @param obj_hash [Hash] Hash of field names to serialized values from Redis
|
|
476
|
+
# @return [Object] Instantiated object with deserialized fields
|
|
477
|
+
#
|
|
478
|
+
# @note This method:
|
|
479
|
+
# 1. Allocates a new instance without calling initialize
|
|
480
|
+
# 2. Initializes related DataType fields
|
|
481
|
+
# 3. Deserializes and assigns field values from the hash
|
|
482
|
+
#
|
|
483
|
+
# @api private
|
|
484
|
+
def instantiate_from_hash(obj_hash)
|
|
485
|
+
instance = allocate
|
|
486
|
+
instance.send(:initialize_relatives)
|
|
487
|
+
instance.send(:initialize_with_keyword_args_deserialize_value, **obj_hash)
|
|
488
|
+
instance
|
|
489
|
+
end
|
|
314
490
|
end
|
|
315
491
|
end
|
|
316
492
|
end
|