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
|
@@ -28,7 +28,8 @@ end
|
|
|
28
28
|
|
|
29
29
|
**Key Methods:**
|
|
30
30
|
- `save` - Persist object to Valkey/Redis
|
|
31
|
-
- `save_if_not_exists` - Conditional persistence
|
|
31
|
+
- `save_if_not_exists` - Conditional persistence, returns false if exists
|
|
32
|
+
- `save_if_not_exists!` - Conditional persistence, raises RecordExistsError if exists
|
|
32
33
|
- `load` - Load object from Valkey/Redis
|
|
33
34
|
- `exists?` - Check if object exists in Valkey/Redis
|
|
34
35
|
- `destroy` - Remove object from Valkey/Redis
|
|
@@ -103,7 +104,7 @@ user = User.new(name: "Alice", email: "alice@example.com", password_hash: "secre
|
|
|
103
104
|
user.safe_dump # => {"name" => "Alice", "email" => "alice@example.com"}
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
#### 3. Encrypted Fields Feature
|
|
107
|
+
#### 3. Encrypted Fields Feature
|
|
107
108
|
Transparent field-level encryption with multiple providers.
|
|
108
109
|
|
|
109
110
|
```ruby
|
|
@@ -146,13 +147,9 @@ vault.secret_key # => "super-secret-123" (decrypted on access)
|
|
|
146
147
|
class CriticalData < Familia::Horreum
|
|
147
148
|
feature :encrypted_fields
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
encrypted_field :credit_card, aad_fields: [:user_id, :created_at]
|
|
154
|
-
encrypted_field :ssn, provider: :xchacha20_poly1305 # Force specific provider
|
|
155
|
-
encrypted_field :notes # Uses default provider
|
|
150
|
+
encrypted_field :credit_card
|
|
151
|
+
encrypted_field :ssn
|
|
152
|
+
encrypted_field :notes
|
|
156
153
|
end
|
|
157
154
|
|
|
158
155
|
# Key versioning and rotation
|
|
@@ -163,44 +160,20 @@ Familia.configure do |config|
|
|
|
163
160
|
v3: ENV['NEW_KEY'] # New key for rotation
|
|
164
161
|
}
|
|
165
162
|
config.current_key_version = :v2
|
|
166
|
-
|
|
167
|
-
# Provider configuration
|
|
168
|
-
config.encryption_providers = {
|
|
169
|
-
xchacha20_poly1305: {
|
|
170
|
-
key_size: 32,
|
|
171
|
-
nonce_size: 24,
|
|
172
|
-
require_gem: 'rbnacl'
|
|
173
|
-
},
|
|
174
|
-
aes_gcm: {
|
|
175
|
-
key_size: 32,
|
|
176
|
-
iv_size: 12,
|
|
177
|
-
tag_size: 16
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Request-level key caching for performance
|
|
183
|
-
Familia::Encryption.with_request_cache do
|
|
184
|
-
1000.times do |i|
|
|
185
|
-
record = CriticalData.new(
|
|
186
|
-
credit_card: "4111-1111-1111-#{i.to_s.rjust(4, '0')}",
|
|
187
|
-
ssn: "123-45-#{i.to_s.rjust(4, '0')}",
|
|
188
|
-
notes: "Customer record #{i}"
|
|
189
|
-
)
|
|
190
|
-
record.save # Reuses derived keys for performance
|
|
191
|
-
end
|
|
163
|
+
config.encryption_personalization = 'MyApp-2024' # Optional (XChaCha20 only)
|
|
192
164
|
end
|
|
193
165
|
|
|
194
|
-
#
|
|
195
|
-
CriticalData.
|
|
196
|
-
|
|
197
|
-
|
|
166
|
+
# Operations with encrypted fields
|
|
167
|
+
record = CriticalData.new(
|
|
168
|
+
credit_card: "4111-1111-1111-1234",
|
|
169
|
+
ssn: "123-45-6789",
|
|
170
|
+
notes: "Customer notes"
|
|
171
|
+
)
|
|
172
|
+
record.save
|
|
198
173
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# => {credit_card: {encrypted: true, key_version: :v2, provider: :xchacha20_poly1305}}
|
|
203
|
-
end
|
|
174
|
+
# Verify encryption status
|
|
175
|
+
puts "Record #{record.identifier}: #{status}"
|
|
176
|
+
# => {credit_card: {encrypted: true, algorithm: "xchacha20poly1305", cleared: false}}
|
|
204
177
|
```
|
|
205
178
|
|
|
206
179
|
**ConcealedString Security Features:**
|
|
@@ -226,7 +199,7 @@ user_data = user.to_json
|
|
|
226
199
|
# All encrypted fields show as "[CONCEALED]" in JSON
|
|
227
200
|
```
|
|
228
201
|
|
|
229
|
-
#### 4. Transient Fields Feature
|
|
202
|
+
#### 4. Transient Fields Feature
|
|
230
203
|
Non-persistent fields with memory-safe handling.
|
|
231
204
|
|
|
232
205
|
```ruby
|
|
@@ -252,7 +225,7 @@ form.password.reveal # => "secret123" (explicit access)
|
|
|
252
225
|
- `reveal` method for explicit access
|
|
253
226
|
- Safe for logging and serialization
|
|
254
227
|
|
|
255
|
-
#### 5. Relationships Feature
|
|
228
|
+
#### 5. Relationships Feature
|
|
256
229
|
Comprehensive object relationship system with automatic management, clean Ruby-idiomatic syntax, and simplified method generation.
|
|
257
230
|
|
|
258
231
|
```ruby
|
|
@@ -329,12 +302,14 @@ active_domains = Domain.active_domains.members
|
|
|
329
302
|
- **Performance**: O(1) hash lookups and efficient sorted set operations
|
|
330
303
|
- **Flexibility**: Supports class-level and relationship-scoped indexing patterns
|
|
331
304
|
|
|
332
|
-
#### 6. Object Identifier Feature
|
|
305
|
+
#### 6. Object Identifier Feature
|
|
333
306
|
Automatic generation of unique object identifiers with configurable strategies.
|
|
334
307
|
|
|
308
|
+
Default generator is `:uuid_v7` (UUID version 7 with embedded timestamp).
|
|
309
|
+
|
|
335
310
|
```ruby
|
|
336
311
|
class Document < Familia::Horreum
|
|
337
|
-
feature :object_identifier
|
|
312
|
+
feature :object_identifier # Uses default :uuid_v7
|
|
338
313
|
|
|
339
314
|
field :title, :content, :created_at
|
|
340
315
|
end
|
|
@@ -358,91 +333,69 @@ end
|
|
|
358
333
|
```
|
|
359
334
|
|
|
360
335
|
**Generator Types:**
|
|
336
|
+
- `:uuid_v7` - UUID version 7 with embedded timestamp (default, 36 characters)
|
|
361
337
|
- `:uuid_v4` - Standard UUID v4 format (36 characters)
|
|
362
|
-
- `:hex` -
|
|
363
|
-
-
|
|
338
|
+
- `:hex` - High-entropy hexadecimal strings (256-bit via SecureIdentifier)
|
|
339
|
+
- Proc/Lambda - Custom generation logic provided as a callable
|
|
364
340
|
|
|
365
341
|
**Technical Implementation:**
|
|
366
342
|
```ruby
|
|
367
343
|
# Auto-generated on object creation
|
|
368
344
|
doc = Document.create(title: "My Document")
|
|
369
|
-
doc.objid # => "
|
|
345
|
+
doc.objid # => "01234567-89ab-7def-8000-123456789abc" # UUID v7 format
|
|
370
346
|
|
|
371
347
|
session = Session.create(user_id: "123")
|
|
372
348
|
session.objid # => "a1b2c3d4e5f67890"
|
|
373
349
|
|
|
374
|
-
# Custom identifier
|
|
350
|
+
# Custom identifier with proc
|
|
375
351
|
api_key = ApiKey.create(name: "Production API")
|
|
376
352
|
api_key.objid # => "ak_Xy9ZaBcD3fG8HjKlMnOpQrStUvWxYz12"
|
|
377
353
|
|
|
378
|
-
#
|
|
354
|
+
# Feature options per-class isolation
|
|
379
355
|
Document.feature_options(:object_identifier)
|
|
380
|
-
#=> {generator: :
|
|
356
|
+
#=> {generator: :uuid_v7}
|
|
381
357
|
```
|
|
382
358
|
|
|
383
|
-
#### 7. External Identifier Feature
|
|
384
|
-
|
|
359
|
+
#### 7. External Identifier Feature
|
|
360
|
+
Derives deterministic external identifiers from object identifiers.
|
|
385
361
|
|
|
386
362
|
```ruby
|
|
387
363
|
class ExternalUser < Familia::Horreum
|
|
388
364
|
feature :external_identifier
|
|
389
365
|
|
|
390
|
-
identifier_field :
|
|
391
|
-
field :
|
|
366
|
+
identifier_field :id
|
|
367
|
+
field :id, :name
|
|
392
368
|
|
|
393
|
-
# External
|
|
394
|
-
|
|
395
|
-
external_id.present? && external_id.match?(/^ext_\d{6,}$/)
|
|
396
|
-
end
|
|
369
|
+
# External ID is automatically derived from objid
|
|
370
|
+
# Format: 'ext_' + base36(truncated_hash(objid))
|
|
397
371
|
end
|
|
398
372
|
|
|
399
|
-
class
|
|
400
|
-
feature :external_identifier,
|
|
373
|
+
class APIKey < Familia::Horreum
|
|
374
|
+
feature :external_identifier, format: 'api-%{id}'
|
|
401
375
|
|
|
402
|
-
field :
|
|
403
|
-
|
|
404
|
-
# Custom validation logic
|
|
405
|
-
def valid_external_id?
|
|
406
|
-
legacy_account_id.present? &&
|
|
407
|
-
legacy_account_id.match?(/^LAC[A-Z]{2}\d{8}$/)
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# Bidirectional mapping
|
|
411
|
-
def self.find_by_legacy_id(legacy_id)
|
|
412
|
-
mapping = external_id_mapping.get(legacy_id)
|
|
413
|
-
mapping ? load(mapping) : nil
|
|
414
|
-
end
|
|
376
|
+
field :name, :permissions
|
|
415
377
|
end
|
|
416
378
|
```
|
|
417
379
|
|
|
418
380
|
**External ID Management:**
|
|
419
381
|
```ruby
|
|
420
|
-
#
|
|
421
|
-
user = ExternalUser.new(
|
|
422
|
-
internal_id: SecureRandom.uuid,
|
|
423
|
-
external_id: "ext_123456",
|
|
424
|
-
name: "John Doe"
|
|
425
|
-
)
|
|
426
|
-
user.save # Automatically creates bidirectional mapping
|
|
427
|
-
|
|
428
|
-
# Lookup by external ID (custom implementation)
|
|
429
|
-
found_user = nil
|
|
430
|
-
ExternalUser.all.each do |user|
|
|
431
|
-
if user.external_id == "ext_123456"
|
|
432
|
-
found_user = user
|
|
433
|
-
break
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# Sync status tracking (custom implementation)
|
|
438
|
-
user.sync_status = "pending"
|
|
439
|
-
user.save
|
|
440
|
-
user.sync_status = "completed"
|
|
382
|
+
# External ID is deterministically derived from objid
|
|
383
|
+
user = ExternalUser.new(name: "John Doe")
|
|
441
384
|
user.save
|
|
385
|
+
user.objid # => "01234567-89ab-7def-8000-123456789abc"
|
|
386
|
+
user.extid # => "ext_abc123def456ghi789" # Deterministic from objid
|
|
387
|
+
|
|
388
|
+
# Same objid always produces same extid
|
|
389
|
+
user2 = ExternalUser.new(objid: user.objid, name: "John Doe")
|
|
390
|
+
user2.extid # => "ext_abc123def456ghi789" # Identical
|
|
391
|
+
|
|
392
|
+
# Custom format with APIKey
|
|
393
|
+
key = APIKey.new(name: "Production")
|
|
394
|
+
key.extid # => "api-xyz789abc123"
|
|
442
395
|
```
|
|
443
396
|
|
|
444
|
-
#### 8. Quantization Feature
|
|
445
|
-
|
|
397
|
+
#### 8. Quantization Feature
|
|
398
|
+
Time-based data bucketing for analytics and caching.
|
|
446
399
|
|
|
447
400
|
```ruby
|
|
448
401
|
class DailyMetric < Familia::Horreum
|
|
@@ -473,26 +426,24 @@ metric.counter.increment
|
|
|
473
426
|
|
|
474
427
|
---
|
|
475
428
|
|
|
476
|
-
## Advanced Feature System Architecture
|
|
429
|
+
## Advanced Feature System Architecture
|
|
477
430
|
|
|
478
|
-
### Feature Autoloader for
|
|
479
|
-
|
|
431
|
+
### Feature Autoloader for Project Organization
|
|
432
|
+
Automatically load features from directory structure.
|
|
480
433
|
|
|
481
434
|
```ruby
|
|
482
435
|
# app/models/customer.rb - Main model file
|
|
483
436
|
class Customer < Familia::Horreum
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
# Automatically loads all .rb files from app/models/customer/features/
|
|
487
|
-
end
|
|
437
|
+
include Familia::Features::Autoloader
|
|
438
|
+
# Automatically loads all .rb files from app/models/customer/*.rb
|
|
488
439
|
|
|
489
440
|
# Core model definition
|
|
490
441
|
identifier_field :custid
|
|
491
442
|
field :custid, :name, :email, :created_at
|
|
492
443
|
end
|
|
493
444
|
|
|
494
|
-
# app/models/customer/
|
|
495
|
-
|
|
445
|
+
# app/models/customer/notifications.rb
|
|
446
|
+
class Customer < Familia::Horreum
|
|
496
447
|
def send_welcome_email
|
|
497
448
|
NotificationService.send_template(
|
|
498
449
|
email: email,
|
|
@@ -510,16 +461,9 @@ module Customer::Features::Notifications
|
|
|
510
461
|
end
|
|
511
462
|
end
|
|
512
463
|
|
|
513
|
-
# app/models/customer/
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
included do
|
|
518
|
-
# Add analytics tracking to core model
|
|
519
|
-
feature :relationships
|
|
520
|
-
class_participates_in :customer_analytics, score: :created_at
|
|
521
|
-
end
|
|
522
|
-
|
|
464
|
+
# app/models/customer/analytics.rb
|
|
465
|
+
class Customer < Familia::Horreum
|
|
466
|
+
# Analytics methods added to Customer
|
|
523
467
|
def track_activity(activity_type, metadata = {})
|
|
524
468
|
activity_data = {
|
|
525
469
|
custid: custid,
|
|
@@ -539,56 +483,20 @@ module Customer::Features::Analytics
|
|
|
539
483
|
end
|
|
540
484
|
```
|
|
541
485
|
|
|
542
|
-
### Feature Dependencies
|
|
543
|
-
|
|
486
|
+
### Feature Dependencies
|
|
487
|
+
Features can declare dependencies that are automatically resolved.
|
|
544
488
|
|
|
545
489
|
```ruby
|
|
546
|
-
#
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def self.depends_on
|
|
551
|
-
[:encrypted_fields, :safe_dump] # Required features
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def self.included(base)
|
|
555
|
-
base.extend ClassMethods
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
module ClassMethods
|
|
559
|
-
def encrypt_all_fields!
|
|
560
|
-
# Batch encrypt all existing records
|
|
561
|
-
all_records.each(&:re_encrypt_fields!)
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
def encryption_health_check
|
|
565
|
-
# Validate encryption across all records
|
|
566
|
-
failed_records = []
|
|
567
|
-
all_records.each do |record|
|
|
568
|
-
unless record.encrypted_fields_status.all? { |_, status| status[:encrypted] }
|
|
569
|
-
failed_records << record.identifier
|
|
570
|
-
end
|
|
571
|
-
end
|
|
572
|
-
failed_records
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
def secure_export
|
|
577
|
-
# Combine safe_dump with additional security
|
|
578
|
-
exported = safe_dump
|
|
579
|
-
exported[:export_timestamp] = Familia.now.to_i
|
|
580
|
-
exported[:checksum] = Digest::SHA256.hexdigest(exported.to_json)
|
|
581
|
-
exported
|
|
582
|
-
end
|
|
490
|
+
# External identifier depends on object_identifier
|
|
491
|
+
class User < Familia::Horreum
|
|
492
|
+
feature :external_identifier # Automatically includes :object_identifier
|
|
493
|
+
field :name
|
|
583
494
|
end
|
|
584
495
|
|
|
585
|
-
#
|
|
586
|
-
|
|
587
|
-
feature :advanced_encryption # Automatically includes dependencies
|
|
496
|
+
# Feature dependency resolution
|
|
497
|
+
Familia::Base.add_feature ExternalIdentifier, :external_identifier, depends_on: [:object_identifier]
|
|
588
498
|
|
|
589
|
-
|
|
590
|
-
encrypted_field :api_key, :private_notes
|
|
591
|
-
safe_dump_field :name, :email
|
|
499
|
+
# When external_identifier is included, object_identifier is automatically loaded first
|
|
592
500
|
end
|
|
593
501
|
```
|
|
594
502
|
|
|
@@ -597,82 +505,64 @@ Each class maintains independent feature options.
|
|
|
597
505
|
|
|
598
506
|
```ruby
|
|
599
507
|
class PrimaryCache < Familia::Horreum
|
|
600
|
-
feature :expiration
|
|
601
|
-
feature :
|
|
508
|
+
feature :expiration
|
|
509
|
+
feature :object_identifier, generator: :uuid_v7
|
|
602
510
|
|
|
603
511
|
field :cache_key, :value, :hit_count
|
|
604
512
|
default_expiration 24.hours
|
|
605
513
|
end
|
|
606
514
|
|
|
607
515
|
class SecondaryCache < Familia::Horreum
|
|
608
|
-
feature :expiration
|
|
609
|
-
feature :
|
|
516
|
+
feature :expiration
|
|
517
|
+
feature :object_identifier, generator: :hex # Different generator
|
|
610
518
|
|
|
611
519
|
field :cache_key, :backup_value, :backup_timestamp
|
|
612
520
|
default_expiration 7.days
|
|
613
521
|
end
|
|
614
522
|
|
|
615
523
|
# Feature options are completely isolated
|
|
616
|
-
PrimaryCache.feature_options(:
|
|
617
|
-
#=> {
|
|
524
|
+
PrimaryCache.feature_options(:object_identifier)
|
|
525
|
+
#=> {generator: :uuid_v7}
|
|
618
526
|
|
|
619
|
-
SecondaryCache.feature_options(:
|
|
620
|
-
#=> {
|
|
621
|
-
|
|
622
|
-
PrimaryCache.feature_options(:quantization)
|
|
623
|
-
#=> {time_buckets: [3600, 21600, 86400]}
|
|
624
|
-
|
|
625
|
-
SecondaryCache.feature_options(:quantization)
|
|
626
|
-
#=> {time_buckets: [86400, 604800]}
|
|
527
|
+
SecondaryCache.feature_options(:object_identifier)
|
|
528
|
+
#=> {generator: :hex}
|
|
627
529
|
```
|
|
628
530
|
|
|
629
|
-
### Runtime Feature
|
|
630
|
-
|
|
531
|
+
### Runtime Feature Checking
|
|
532
|
+
Check which features are enabled on a class.
|
|
631
533
|
|
|
632
534
|
```ruby
|
|
633
|
-
class
|
|
535
|
+
class SecureModel < Familia::Horreum
|
|
536
|
+
feature :expiration
|
|
537
|
+
feature :encrypted_fields
|
|
538
|
+
feature :safe_dump
|
|
539
|
+
|
|
634
540
|
field :name, :status
|
|
541
|
+
encrypted_field :api_key
|
|
542
|
+
safe_dump_field :name
|
|
543
|
+
end
|
|
635
544
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
feature :expiration
|
|
640
|
-
feature :safe_dump
|
|
641
|
-
when :secure
|
|
642
|
-
feature :expiration
|
|
643
|
-
feature :encrypted_fields
|
|
644
|
-
feature :safe_dump
|
|
645
|
-
when :analytics
|
|
646
|
-
feature :expiration
|
|
647
|
-
feature :relationships
|
|
648
|
-
feature :quantization
|
|
649
|
-
end
|
|
650
|
-
end
|
|
545
|
+
# Check enabled features
|
|
546
|
+
SecureModel.features_enabled
|
|
547
|
+
#=> [:expiration, :encrypted_fields, :safe_dump]
|
|
651
548
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
549
|
+
# Check feature options
|
|
550
|
+
SecureModel.feature_options(:encrypted_fields)
|
|
551
|
+
#=> {} # Default options
|
|
655
552
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
end
|
|
553
|
+
# Each class tracks its own features
|
|
554
|
+
class BasicModel < Familia::Horreum
|
|
555
|
+
feature :expiration
|
|
556
|
+
field :name
|
|
661
557
|
end
|
|
662
558
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
DynamicModel.feature_enabled?(:relationships) # => true
|
|
666
|
-
|
|
667
|
-
# Conditional feature usage
|
|
668
|
-
if DynamicModel.feature_enabled?(:encrypted_fields)
|
|
669
|
-
DynamicModel.encrypted_field :sensitive_data
|
|
670
|
-
end
|
|
559
|
+
BasicModel.features_enabled
|
|
560
|
+
#=> [:expiration]
|
|
671
561
|
```
|
|
672
562
|
|
|
673
563
|
---
|
|
674
564
|
|
|
675
|
-
## Connection Management
|
|
565
|
+
## Connection Management
|
|
676
566
|
|
|
677
567
|
### Connection Provider Pattern
|
|
678
568
|
Flexible connection pooling with provider-based architecture.
|
|
@@ -680,13 +570,14 @@ Flexible connection pooling with provider-based architecture.
|
|
|
680
570
|
```ruby
|
|
681
571
|
# Basic Valkey/Redis connection
|
|
682
572
|
Familia.configure do |config|
|
|
683
|
-
config.
|
|
573
|
+
config.uri = "redis://localhost:6379/0"
|
|
684
574
|
end
|
|
685
575
|
|
|
686
|
-
#
|
|
576
|
+
# Custom connection provider with pooling
|
|
687
577
|
require 'connection_pool'
|
|
688
578
|
|
|
689
579
|
Familia.connection_provider = lambda do |uri|
|
|
580
|
+
# Provider MUST return connection already on correct database
|
|
690
581
|
parsed = URI.parse(uri)
|
|
691
582
|
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
|
692
583
|
|
|
@@ -695,14 +586,11 @@ Familia.connection_provider = lambda do |uri|
|
|
|
695
586
|
Redis.new(
|
|
696
587
|
host: parsed.host,
|
|
697
588
|
port: parsed.port,
|
|
698
|
-
db: parsed.db || 0
|
|
699
|
-
connect_timeout: 1,
|
|
700
|
-
read_timeout: 1,
|
|
701
|
-
write_timeout: 1
|
|
589
|
+
db: parsed.db || 0
|
|
702
590
|
)
|
|
703
591
|
end
|
|
704
592
|
|
|
705
|
-
@pools[pool_key].with { |conn|
|
|
593
|
+
@pools[pool_key].with { |conn| conn }
|
|
706
594
|
end
|
|
707
595
|
```
|
|
708
596
|
|
|
@@ -728,68 +616,129 @@ end
|
|
|
728
616
|
|
|
729
617
|
---
|
|
730
618
|
|
|
731
|
-
##
|
|
619
|
+
## Testing and Debugging
|
|
732
620
|
|
|
733
|
-
###
|
|
734
|
-
|
|
621
|
+
### Database Command Logging
|
|
622
|
+
Monitor all Redis commands with DatabaseLogger middleware.
|
|
735
623
|
|
|
736
624
|
```ruby
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
identifier_field :doc_id
|
|
741
|
-
field :doc_id, :title, :content
|
|
625
|
+
# Enable command logging (middleware registered automatically)
|
|
626
|
+
Familia.enable_database_logging = true
|
|
742
627
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
ADMIN = 8 # 1000
|
|
628
|
+
# Capture commands in tests
|
|
629
|
+
commands = DatabaseLogger.capture_commands do
|
|
630
|
+
user = User.create(name: "Test User")
|
|
631
|
+
user.save
|
|
748
632
|
end
|
|
749
633
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
def self.encode_score(timestamp, permissions)
|
|
753
|
-
"#{timestamp}.#{permissions}".to_f
|
|
754
|
-
end
|
|
634
|
+
puts commands.first.command # => "SET user:123 {...}"
|
|
635
|
+
puts commands.first.μs # => 567 (microseconds)
|
|
755
636
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
timestamp = parts[0].to_i
|
|
759
|
-
permissions = parts[1] ? parts[1].to_i : 0
|
|
760
|
-
[timestamp, permissions]
|
|
761
|
-
end
|
|
637
|
+
# Enable sampling for production
|
|
638
|
+
DatabaseLogger.sample_rate = 0.01 # Log 1% of commands
|
|
762
639
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
640
|
+
# Structured logging format
|
|
641
|
+
DatabaseLogger.structured_logging = true
|
|
642
|
+
# => "Redis command cmd=SET args=[key, value] duration_ms=0.42 db=0"
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Debug Mode
|
|
646
|
+
Enable comprehensive debugging output.
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
# Via environment variable
|
|
650
|
+
ENV['FAMILIA_DEBUG'] = '1'
|
|
651
|
+
ENV['FAMILIA_TRACE'] = '1'
|
|
652
|
+
|
|
653
|
+
# Via configuration
|
|
654
|
+
Familia.configure do |config|
|
|
655
|
+
config.debug = true
|
|
767
656
|
end
|
|
768
657
|
|
|
769
|
-
#
|
|
770
|
-
|
|
771
|
-
doc_id = "doc456"
|
|
772
|
-
timestamp = Familia.now.to_i
|
|
658
|
+
# Check database contents
|
|
659
|
+
Familia.dbclient.keys('user:*')
|
|
773
660
|
|
|
774
|
-
#
|
|
775
|
-
|
|
776
|
-
|
|
661
|
+
# Trace specific operations
|
|
662
|
+
Familia.trace :LOAD, redis_client, "user:123", "from cache"
|
|
663
|
+
```
|
|
777
664
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
user_documents.add(doc_id, score)
|
|
665
|
+
### Connection Chain Debugging
|
|
666
|
+
Understand connection resolution order.
|
|
781
667
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
668
|
+
```ruby
|
|
669
|
+
# Connection resolution order:
|
|
670
|
+
# 1. Instance @dbclient if set
|
|
671
|
+
# 2. Class logical_database if configured
|
|
672
|
+
# 3. Connection provider if set
|
|
673
|
+
# 4. Global Familia connection
|
|
674
|
+
|
|
675
|
+
class DebugModel < Familia::Horreum
|
|
676
|
+
logical_database 3
|
|
677
|
+
field :name
|
|
786
678
|
end
|
|
679
|
+
|
|
680
|
+
model = DebugModel.new(name: "test")
|
|
681
|
+
# Uses database 3 from logical_database
|
|
682
|
+
|
|
683
|
+
model.dbclient = custom_connection
|
|
684
|
+
# Now uses custom_connection instead
|
|
787
685
|
```
|
|
788
686
|
|
|
789
|
-
|
|
790
|
-
|
|
687
|
+
## Performance Considerations
|
|
688
|
+
|
|
689
|
+
### Encryption Performance
|
|
690
|
+
- XChaCha20-Poly1305 is ~2x faster than AES-256-GCM
|
|
691
|
+
- Key derivation is NOT cached by default for security
|
|
692
|
+
- Use request-level caching for bulk operations:
|
|
791
693
|
|
|
792
694
|
```ruby
|
|
695
|
+
Familia::Encryption.with_request_cache do
|
|
696
|
+
# Bulk operations with cached key derivation
|
|
697
|
+
1000.times do |i|
|
|
698
|
+
User.create(email: "user#{i}@example.com")
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Connection Pooling
|
|
704
|
+
- Use connection_provider for multi-threaded environments
|
|
705
|
+
- Pool size should match thread count
|
|
706
|
+
- Connections are thread-safe when using provider pattern
|
|
707
|
+
|
|
708
|
+
### Feature Performance Impact
|
|
709
|
+
- **Encryption**: ~10-20% overhead for field access
|
|
710
|
+
- **Relationships**: O(1) for indexed lookups
|
|
711
|
+
- **Quantization**: Minimal overhead, improves storage efficiency
|
|
712
|
+
- **Safe Dump**: Lazy evaluation, only computed when called
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Migration Guide
|
|
717
|
+
|
|
718
|
+
### From Familia v1.x to v2.0
|
|
719
|
+
- Replace `redis_uri` with `uri` in configuration
|
|
720
|
+
- Update feature syntax from mixins to `feature` declarations
|
|
721
|
+
- Migrate from `global_` prefix to `class_` for class-level methods
|
|
722
|
+
- Update encryption configuration to new provider system
|
|
723
|
+
|
|
724
|
+
### Connection Provider Pattern
|
|
725
|
+
- Familia v2.0 uses redis-rb gem internally
|
|
726
|
+
- Connection providers must return Redis connections
|
|
727
|
+
- Uses RedisClient middleware architecture internally via redis-rb
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Summary
|
|
732
|
+
|
|
733
|
+
Familia v2.0.0-pre series provides a comprehensive ORM for Valkey/Redis with:
|
|
734
|
+
- **Modular Feature System**: Isolated, configurable features per class
|
|
735
|
+
- **Advanced Security**: Field-level encryption with multiple providers
|
|
736
|
+
- **Flexible Relationships**: Automatic management with clean Ruby syntax
|
|
737
|
+
- **Performance Optimized**: Connection pooling, sampling, and caching
|
|
738
|
+
- **Production Ready**: Debug logging, monitoring, and thread safety
|
|
739
|
+
|
|
740
|
+
For additional documentation and examples, see the [Familia GitHub repository](https://github.com/delano/familia).
|
|
741
|
+
|
|
793
742
|
class ActivityTracker < Familia::Horreum
|
|
794
743
|
feature :relationships
|
|
795
744
|
|
|
@@ -799,7 +748,7 @@ class ActivityTracker < Familia::Horreum
|
|
|
799
748
|
# Class-level tracking with automatic management
|
|
800
749
|
class_participates_in :user_activities, score: :created_at
|
|
801
750
|
class_participates_in :activity_by_type,
|
|
802
|
-
score: ->
|
|
751
|
+
score: -> { "#{activity_type}:#{created_at}".hash }
|
|
803
752
|
end
|
|
804
753
|
|
|
805
754
|
# Create and save activity (automatic tracking)
|
|
@@ -814,12 +763,14 @@ activity.save # Automatically added to both tracking collections
|
|
|
814
763
|
# Query recent activities (last hour)
|
|
815
764
|
hour_ago = (Familia.now - 1.hour).to_i
|
|
816
765
|
recent_activities = ActivityTracker.user_activities.range_by_score(
|
|
817
|
-
hour_ago, '+inf'
|
|
766
|
+
hour_ago, '+inf'
|
|
818
767
|
)
|
|
819
768
|
|
|
820
769
|
# Get activities by type in time range
|
|
770
|
+
login_hash_start = "login:#{hour_ago}".hash
|
|
771
|
+
login_hash_end = "login:#{Familia.now.to_i}".hash
|
|
821
772
|
login_activities = ActivityTracker.activity_by_type.range_by_score(
|
|
822
|
-
|
|
773
|
+
login_hash_start, login_hash_end
|
|
823
774
|
)
|
|
824
775
|
```
|
|
825
776
|
|
|
@@ -827,7 +778,7 @@ login_activities = ActivityTracker.activity_by_type.range_by_score(
|
|
|
827
778
|
|
|
828
779
|
## Data Type Usage Patterns
|
|
829
780
|
|
|
830
|
-
### Advanced Sorted
|
|
781
|
+
### Advanced Sorted Set Operations
|
|
831
782
|
Leverage Valkey/Redis sorted sets for rankings, time series, and scored data.
|
|
832
783
|
|
|
833
784
|
```ruby
|
|
@@ -840,19 +791,20 @@ end
|
|
|
840
791
|
leaderboard = Leaderboard.new(game_id: "game1", name: "Daily Challenge")
|
|
841
792
|
|
|
842
793
|
# Add player scores
|
|
843
|
-
leaderboard.scores.add("player1"
|
|
844
|
-
leaderboard.scores.add("player2"
|
|
845
|
-
leaderboard.scores.add("player3"
|
|
794
|
+
leaderboard.scores.add(1500, "player1")
|
|
795
|
+
leaderboard.scores.add(2300, "player2")
|
|
796
|
+
leaderboard.scores.add(1800, "player3")
|
|
846
797
|
|
|
847
|
-
# Get top 10 players
|
|
848
|
-
top_players = leaderboard.scores.
|
|
798
|
+
# Get top 10 players (highest scores first)
|
|
799
|
+
top_players = leaderboard.scores.revrange(0, 9, withscores: true)
|
|
849
800
|
# => [["player2", 2300.0], ["player3", 1800.0], ["player1", 1500.0]]
|
|
850
801
|
|
|
851
|
-
# Get player rank
|
|
852
|
-
rank = leaderboard.scores.rank("player1"
|
|
802
|
+
# Get player rank (0-indexed, lower scores = lower rank)
|
|
803
|
+
rank = leaderboard.scores.rank("player1") # => 0
|
|
804
|
+
rev_rank = leaderboard.scores.revrank("player1") # => 2 (highest to lowest)
|
|
853
805
|
|
|
854
806
|
# Get score range
|
|
855
|
-
mid_tier = leaderboard.scores.
|
|
807
|
+
mid_tier = leaderboard.scores.rangebyscore(1000, 2000, withscores: true)
|
|
856
808
|
|
|
857
809
|
# Increment score atomically
|
|
858
810
|
leaderboard.scores.increment("player1", 100) # Add 100 to existing score
|
|
@@ -913,23 +865,23 @@ end
|
|
|
913
865
|
prefs = UserPreferences.new(user_id: "user123")
|
|
914
866
|
|
|
915
867
|
# UnsortedSet individual preferences
|
|
916
|
-
prefs.settings
|
|
917
|
-
prefs.settings
|
|
918
|
-
prefs.settings
|
|
868
|
+
prefs.settings["theme"] = "dark"
|
|
869
|
+
prefs.settings["notifications"] = "true"
|
|
870
|
+
prefs.settings["timezone"] = "UTC-5"
|
|
919
871
|
|
|
920
872
|
# Batch set multiple values
|
|
921
|
-
prefs.feature_flags.update(
|
|
873
|
+
prefs.feature_flags.update(
|
|
922
874
|
"beta_ui" => "true",
|
|
923
875
|
"new_dashboard" => "false",
|
|
924
876
|
"advanced_features" => "true"
|
|
925
|
-
|
|
877
|
+
)
|
|
926
878
|
|
|
927
879
|
# Get preferences
|
|
928
|
-
theme = prefs.settings
|
|
929
|
-
all_settings = prefs.settings.
|
|
880
|
+
theme = prefs.settings["theme"] # => "dark"
|
|
881
|
+
all_settings = prefs.settings.to_h # => Hash of all settings
|
|
930
882
|
|
|
931
883
|
# Check feature flags
|
|
932
|
-
beta_enabled = prefs.feature_flags
|
|
884
|
+
beta_enabled = prefs.feature_flags["beta_ui"] == "true"
|
|
933
885
|
```
|
|
934
886
|
|
|
935
887
|
---
|
|
@@ -953,7 +905,7 @@ class ResilientService < Familia::Horreum
|
|
|
953
905
|
sleep(0.1 * (4 - retries)) # Exponential backoff
|
|
954
906
|
retry
|
|
955
907
|
else
|
|
956
|
-
Familia.warn "
|
|
908
|
+
Familia.warn "Database operation failed after retries: #{e.message}"
|
|
957
909
|
nil # Return nil or handle gracefully
|
|
958
910
|
end
|
|
959
911
|
end
|
|
@@ -1029,12 +981,11 @@ users = []
|
|
|
1029
981
|
users << user
|
|
1030
982
|
end
|
|
1031
983
|
|
|
1032
|
-
# Use
|
|
1033
|
-
User.
|
|
984
|
+
# Use transactions for batch saves
|
|
985
|
+
User.pipelined do
|
|
1034
986
|
users.each do |user|
|
|
1035
|
-
# All operations batched in
|
|
1036
|
-
user.
|
|
1037
|
-
User.email_index.set(user.email, user.identifier)
|
|
987
|
+
# All operations batched in pipeline
|
|
988
|
+
user.save
|
|
1038
989
|
end
|
|
1039
990
|
end
|
|
1040
991
|
```
|
|
@@ -1073,15 +1024,15 @@ Configure connection pools based on application needs.
|
|
|
1073
1024
|
# High-throughput application
|
|
1074
1025
|
Familia.connection_provider = lambda do |uri|
|
|
1075
1026
|
ConnectionPool.new(size: 25, timeout: 5) do
|
|
1076
|
-
Redis.new(
|
|
1077
|
-
end.with { |conn|
|
|
1027
|
+
Redis.new(url: uri)
|
|
1028
|
+
end.with { |conn| conn }
|
|
1078
1029
|
end
|
|
1079
1030
|
|
|
1080
1031
|
# Memory-constrained environment
|
|
1081
1032
|
Familia.connection_provider = lambda do |uri|
|
|
1082
1033
|
ConnectionPool.new(size: 5, timeout: 10) do
|
|
1083
|
-
Redis.new(
|
|
1084
|
-
end.with { |conn|
|
|
1034
|
+
Redis.new(url: uri)
|
|
1035
|
+
end.with { |conn| conn }
|
|
1085
1036
|
end
|
|
1086
1037
|
```
|
|
1087
1038
|
|
|
@@ -1142,16 +1093,16 @@ class User < Familia::Horreum
|
|
|
1142
1093
|
|
|
1143
1094
|
# Temporary migration method
|
|
1144
1095
|
def migrate_api_key!
|
|
1145
|
-
if raw_api_key =
|
|
1146
|
-
self.api_key = raw_api_key
|
|
1147
|
-
|
|
1096
|
+
if raw_api_key = dbclient.hget(dbkey, "api_key") # Read old plaintext
|
|
1097
|
+
self.api_key = raw_api_key # Write as encrypted
|
|
1098
|
+
dbclient.hdel(dbkey, "api_key") # Remove plaintext
|
|
1148
1099
|
save
|
|
1149
1100
|
end
|
|
1150
1101
|
end
|
|
1151
1102
|
end
|
|
1152
1103
|
|
|
1153
|
-
# Step 3: Run migration for
|
|
1154
|
-
User.
|
|
1104
|
+
# Step 3: Run migration for existing users
|
|
1105
|
+
User.instances.each(&:migrate_api_key!)
|
|
1155
1106
|
```
|
|
1156
1107
|
|
|
1157
1108
|
---
|
|
@@ -1167,17 +1118,17 @@ require 'familia'
|
|
|
1167
1118
|
|
|
1168
1119
|
# Use separate Valkey/Redis database for tests
|
|
1169
1120
|
Familia.configure do |config|
|
|
1170
|
-
config.
|
|
1121
|
+
config.uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:2525/3')
|
|
1171
1122
|
end
|
|
1172
1123
|
|
|
1173
1124
|
module TestHelpers
|
|
1174
1125
|
def setup_redis
|
|
1175
1126
|
# Clear test database
|
|
1176
|
-
Familia.
|
|
1127
|
+
Familia.dbclient.flushdb
|
|
1177
1128
|
end
|
|
1178
1129
|
|
|
1179
1130
|
def teardown_redis
|
|
1180
|
-
Familia.
|
|
1131
|
+
Familia.dbclient.flushdb
|
|
1181
1132
|
end
|
|
1182
1133
|
|
|
1183
1134
|
def create_test_user(**attrs)
|
|
@@ -1208,9 +1159,11 @@ class UserTest < Minitest::Test
|
|
|
1208
1159
|
assert user.exists?
|
|
1209
1160
|
assert_equal "Alice", user.name
|
|
1210
1161
|
|
|
1211
|
-
# Test automatic indexing
|
|
1212
|
-
|
|
1213
|
-
|
|
1162
|
+
# Test automatic indexing (if using unique_index)
|
|
1163
|
+
if User.respond_to?(:find_by_email)
|
|
1164
|
+
found_user = User.find_by_email(user.email)
|
|
1165
|
+
assert_equal user.identifier, found_user.identifier
|
|
1166
|
+
end
|
|
1214
1167
|
end
|
|
1215
1168
|
|
|
1216
1169
|
def test_relationships_with_clean_syntax
|
|
@@ -1285,23 +1238,17 @@ class UserTest < Minitest::Test
|
|
|
1285
1238
|
end
|
|
1286
1239
|
|
|
1287
1240
|
def test_external_identifier_mapping
|
|
1288
|
-
user = ExternalUser.new(
|
|
1289
|
-
internal_id: SecureRandom.uuid,
|
|
1290
|
-
external_id: "ext_123456",
|
|
1291
|
-
name: "External User"
|
|
1292
|
-
)
|
|
1241
|
+
user = ExternalUser.new(name: "External User")
|
|
1293
1242
|
user.save
|
|
1294
1243
|
|
|
1295
|
-
# Test
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
# Test sync status tracking
|
|
1300
|
-
user.mark_sync_pending
|
|
1301
|
-
assert_equal "pending", user.sync_status
|
|
1244
|
+
# Test external ID is derived from objid
|
|
1245
|
+
assert_not_nil user.objid
|
|
1246
|
+
assert_not_nil user.extid
|
|
1247
|
+
assert user.extid.start_with?("ext_")
|
|
1302
1248
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1249
|
+
# Test deterministic generation
|
|
1250
|
+
user2 = ExternalUser.new(objid: user.objid, name: "External User")
|
|
1251
|
+
assert_equal user.extid, user2.extid
|
|
1305
1252
|
end
|
|
1306
1253
|
|
|
1307
1254
|
private
|
|
@@ -1330,7 +1277,7 @@ Essential configuration options for Familia v2.0.0-pre.
|
|
|
1330
1277
|
```ruby
|
|
1331
1278
|
Familia.configure do |config|
|
|
1332
1279
|
# Basic Valkey/Redis connection
|
|
1333
|
-
config.
|
|
1280
|
+
config.uri = ENV['REDIS_URL'] || 'redis://localhost:6379/0'
|
|
1334
1281
|
|
|
1335
1282
|
# Connection provider for pooling (optional)
|
|
1336
1283
|
config.connection_provider = MyConnectionProvider
|