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,5 +1,9 @@
|
|
|
1
|
+
# lib/familia/features/relationships/indexing/unique_index_generators.rb
|
|
2
|
+
#
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
5
|
+
require_relative 'rebuild_strategies'
|
|
6
|
+
|
|
3
7
|
module Familia
|
|
4
8
|
module Features
|
|
5
9
|
module Relationships
|
|
@@ -49,11 +53,11 @@ module Familia
|
|
|
49
53
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
|
50
54
|
# @param field [Symbol] The field to index
|
|
51
55
|
# @param index_name [Symbol] Name of the index
|
|
52
|
-
# @param within [Class, Symbol, nil]
|
|
56
|
+
# @param within [Class, Symbol, nil] Scope class for instance-scoped index
|
|
53
57
|
# @param query [Boolean] Whether to generate query methods
|
|
54
58
|
def setup(indexed_class:, field:, index_name:, within:, query:)
|
|
55
59
|
# Normalize parameters and determine scope type
|
|
56
|
-
|
|
60
|
+
scope_class, scope_type = if within
|
|
57
61
|
k = Familia.resolve_class(within)
|
|
58
62
|
[k, :instance]
|
|
59
63
|
else
|
|
@@ -63,7 +67,8 @@ module Familia
|
|
|
63
67
|
# Store metadata for this indexing relationship
|
|
64
68
|
indexed_class.indexing_relationships << IndexingRelationship.new(
|
|
65
69
|
field: field,
|
|
66
|
-
|
|
70
|
+
scope_class: scope_class,
|
|
71
|
+
within: within,
|
|
67
72
|
index_name: index_name,
|
|
68
73
|
query: query,
|
|
69
74
|
cardinality: :unique,
|
|
@@ -73,10 +78,10 @@ module Familia
|
|
|
73
78
|
case scope_type
|
|
74
79
|
when :instance
|
|
75
80
|
# Instance-scoped index (within: Company)
|
|
76
|
-
if query &&
|
|
77
|
-
generate_query_methods_destination(indexed_class, field,
|
|
81
|
+
if query && scope_class.is_a?(Class)
|
|
82
|
+
generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
|
78
83
|
end
|
|
79
|
-
generate_mutation_methods_self(indexed_class, field,
|
|
84
|
+
generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
|
80
85
|
when :class
|
|
81
86
|
# Class-level index (no within:)
|
|
82
87
|
indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
|
|
@@ -85,7 +90,8 @@ module Familia
|
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
# Generates query methods ON THE
|
|
93
|
+
# Generates query methods ON THE SCOPE CLASS (Company when within: Company)
|
|
94
|
+
#
|
|
89
95
|
# - company.find_by_badge_number(badge) - find by field value
|
|
90
96
|
# - company.find_all_by_badge_number([badges]) - batch lookup
|
|
91
97
|
# - company.badge_index - DataType accessor
|
|
@@ -93,17 +99,20 @@ module Familia
|
|
|
93
99
|
#
|
|
94
100
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
|
95
101
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
|
96
|
-
# @param
|
|
102
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
|
97
103
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
|
98
|
-
def generate_query_methods_destination(indexed_class, field,
|
|
99
|
-
# Resolve
|
|
100
|
-
|
|
104
|
+
def generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
|
105
|
+
# Resolve scope class using Familia pattern
|
|
106
|
+
actual_scope_class = Familia.resolve_class(scope_class)
|
|
101
107
|
|
|
102
108
|
# Ensure the index field is declared (creates accessor that returns DataType)
|
|
103
|
-
|
|
109
|
+
actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
|
|
110
|
+
|
|
111
|
+
# Get scope_class_config for method naming (needed for rebuild methods)
|
|
112
|
+
scope_class_config = actual_scope_class.config_name
|
|
104
113
|
|
|
105
114
|
# Generate instance query method (e.g., company.find_by_badge_number)
|
|
106
|
-
|
|
115
|
+
actual_scope_class.class_eval do
|
|
107
116
|
define_method(:"find_by_#{field}") do |provided_value|
|
|
108
117
|
# Use declared field accessor instead of manual instantiation
|
|
109
118
|
index_hash = send(index_name)
|
|
@@ -121,7 +130,10 @@ module Familia
|
|
|
121
130
|
|
|
122
131
|
# Generate bulk query method (e.g., company.find_all_by_badge_number)
|
|
123
132
|
define_method(:"find_all_by_#{field}") do |provided_ids|
|
|
124
|
-
|
|
133
|
+
# Convert to array and filter nil inputs before querying Redis.
|
|
134
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
|
135
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
|
136
|
+
provided_ids = Array(provided_ids).compact
|
|
125
137
|
return [] if provided_ids.empty?
|
|
126
138
|
|
|
127
139
|
# Use declared field accessor instead of manual instantiation
|
|
@@ -129,7 +141,8 @@ module Familia
|
|
|
129
141
|
|
|
130
142
|
# Get all identifiers from the hash
|
|
131
143
|
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
|
132
|
-
|
|
144
|
+
|
|
145
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
|
133
146
|
record_ids.compact.map { |record_id|
|
|
134
147
|
indexed_class.find_by_identifier(record_id)
|
|
135
148
|
}
|
|
@@ -139,77 +152,166 @@ module Familia
|
|
|
139
152
|
# No need to manually define it here
|
|
140
153
|
|
|
141
154
|
# Generate method to rebuild the unique index for this parent instance
|
|
142
|
-
define_method(:"rebuild_#{index_name}") do
|
|
143
|
-
#
|
|
144
|
-
|
|
155
|
+
define_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
|
|
156
|
+
# Find the collection containing the indexed class.
|
|
157
|
+
#
|
|
158
|
+
# Strategy 1: Check if indexed_class has a participation relationship
|
|
159
|
+
# pointing back to this scope class. Participation relationships are
|
|
160
|
+
# stored on the PARTICIPANT class (indexed_class), not the target.
|
|
161
|
+
#
|
|
162
|
+
# Example: When RebuildTestEmployee.participates_in(RebuildTestCompany, :employees),
|
|
163
|
+
# the relationship is stored on RebuildTestEmployee, and we need to find it
|
|
164
|
+
# by matching target_class (RebuildTestCompany) with self.class.
|
|
165
|
+
collection = nil
|
|
166
|
+
if indexed_class.respond_to?(:participation_relationships)
|
|
167
|
+
participation = indexed_class.participation_relationships.find do |rel|
|
|
168
|
+
rel.target_class == self.class
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if participation
|
|
172
|
+
collection = send(participation.collection_name)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
145
175
|
|
|
146
|
-
#
|
|
147
|
-
|
|
176
|
+
# Strategy 2: Fallback to checking related_fields for explicit class: option
|
|
177
|
+
unless collection
|
|
178
|
+
if self.class.respond_to?(:related_fields)
|
|
179
|
+
self.class.related_fields&.each do |name, field_def|
|
|
180
|
+
# Check if this DataType's class option matches the indexed class
|
|
181
|
+
if field_def.opts[:class] == indexed_class
|
|
182
|
+
collection = send(name)
|
|
183
|
+
break
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
148
188
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
if collection
|
|
190
|
+
# Find the IndexingRelationship to get cardinality metadata
|
|
191
|
+
index_config = indexed_class.indexing_relationships.find { |rel| rel.index_name == index_name }
|
|
192
|
+
|
|
193
|
+
# Strategy 2: Use participation-based rebuild
|
|
194
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
195
|
+
self, # scope_instance (e.g., company)
|
|
196
|
+
indexed_class, # e.g., Employee
|
|
197
|
+
field, # e.g., :badge_number
|
|
198
|
+
:"add_to_#{scope_class_config}_#{index_name}", # e.g., :add_to_company_badge_index
|
|
199
|
+
collection,
|
|
200
|
+
index_config.cardinality, # :unique or :multi
|
|
201
|
+
batch_size: batch_size,
|
|
202
|
+
&progress_block
|
|
203
|
+
)
|
|
204
|
+
else
|
|
205
|
+
# Strategy 3: Fall back to SCAN with filtering
|
|
206
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
|
|
207
|
+
indexed_class,
|
|
208
|
+
field,
|
|
209
|
+
:"add_to_#{scope_class_config}_#{index_name}",
|
|
210
|
+
scope_instance: self,
|
|
211
|
+
batch_size: batch_size,
|
|
212
|
+
&progress_block
|
|
213
|
+
)
|
|
214
|
+
end
|
|
152
215
|
end
|
|
153
216
|
end
|
|
154
217
|
end
|
|
155
218
|
|
|
156
|
-
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
|
157
|
-
#
|
|
158
|
-
# -
|
|
219
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
|
220
|
+
#
|
|
221
|
+
# Instance methods for scope-scoped unique index operations:
|
|
222
|
+
# - employee.add_to_company_badge_index(company) - automatically validates uniqueness
|
|
159
223
|
# - employee.remove_from_company_badge_index(company)
|
|
160
224
|
# - employee.update_in_company_badge_index(company, old_badge)
|
|
225
|
+
# - employee.guard_unique_company_badge_index!(company) - manual validation
|
|
161
226
|
#
|
|
162
227
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
|
163
228
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
|
164
|
-
# @param
|
|
229
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
|
165
230
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
|
166
|
-
def generate_mutation_methods_self(indexed_class, field,
|
|
167
|
-
|
|
231
|
+
def generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
|
232
|
+
scope_class_config = scope_class.config_name
|
|
168
233
|
indexed_class.class_eval do
|
|
169
|
-
method_name = :"add_to_#{
|
|
170
|
-
Familia.
|
|
234
|
+
method_name = :"add_to_#{scope_class_config}_#{index_name}"
|
|
235
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
171
236
|
|
|
172
|
-
define_method(method_name) do |
|
|
173
|
-
return unless
|
|
237
|
+
define_method(method_name) do |scope_instance|
|
|
238
|
+
return unless scope_instance
|
|
174
239
|
|
|
175
240
|
field_value = send(field)
|
|
176
241
|
return unless field_value
|
|
177
242
|
|
|
178
|
-
#
|
|
179
|
-
|
|
243
|
+
# Automatically validate uniqueness before adding to index.
|
|
244
|
+
# Skip validation inside transactions since guard methods require read
|
|
245
|
+
# operations not available in MULTI/EXEC blocks.
|
|
246
|
+
unless Fiber[:familia_transaction]
|
|
247
|
+
guard_method = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
|
248
|
+
send(guard_method, scope_instance) if respond_to?(guard_method)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Use declared field accessor on scope instance
|
|
252
|
+
index_hash = scope_instance.send(index_name)
|
|
180
253
|
|
|
181
|
-
#
|
|
254
|
+
# Set the value (guard already validated uniqueness)
|
|
182
255
|
index_hash[field_value.to_s] = identifier
|
|
183
256
|
end
|
|
184
257
|
|
|
185
|
-
|
|
186
|
-
|
|
258
|
+
# Add a guard method to enforce unique constraint on this instance-scoped index
|
|
259
|
+
#
|
|
260
|
+
# @param scope_instance [Object] The scope instance providing uniqueness context (e.g., a Company)
|
|
261
|
+
# @raise [Familia::RecordExistsError] if a record with the same field value
|
|
262
|
+
# exists in the scope's index. Values are compared as strings.
|
|
263
|
+
# @return [void]
|
|
264
|
+
#
|
|
265
|
+
# @example
|
|
266
|
+
# employee.guard_unique_company_badge_index!(company)
|
|
267
|
+
#
|
|
268
|
+
method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
|
269
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
270
|
+
|
|
271
|
+
define_method(method_name) do |scope_instance|
|
|
272
|
+
return unless scope_instance
|
|
187
273
|
|
|
188
|
-
|
|
189
|
-
return unless
|
|
274
|
+
field_value = send(field)
|
|
275
|
+
return unless field_value
|
|
276
|
+
|
|
277
|
+
# Use declared field accessor on scope instance
|
|
278
|
+
index_hash = scope_instance.send(index_name)
|
|
279
|
+
existing_id = index_hash.get(field_value.to_s)
|
|
280
|
+
|
|
281
|
+
if existing_id && existing_id != identifier
|
|
282
|
+
raise Familia::RecordExistsError,
|
|
283
|
+
"#{self.class} exists in #{scope_instance.class} with #{field}=#{field_value}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
method_name = :"remove_from_#{scope_class_config}_#{index_name}"
|
|
288
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
289
|
+
|
|
290
|
+
define_method(method_name) do |scope_instance|
|
|
291
|
+
return unless scope_instance
|
|
190
292
|
|
|
191
293
|
field_value = send(field)
|
|
192
294
|
return unless field_value
|
|
193
295
|
|
|
194
|
-
# Use declared field accessor on
|
|
195
|
-
index_hash =
|
|
296
|
+
# Use declared field accessor on scope instance
|
|
297
|
+
index_hash = scope_instance.send(index_name)
|
|
196
298
|
|
|
197
299
|
# Remove using HashKey DataType method
|
|
198
300
|
index_hash.remove(field_value.to_s)
|
|
199
301
|
end
|
|
200
302
|
|
|
201
|
-
method_name = :"update_in_#{
|
|
202
|
-
Familia.
|
|
303
|
+
method_name = :"update_in_#{scope_class_config}_#{index_name}"
|
|
304
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
203
305
|
|
|
204
|
-
define_method(method_name) do |
|
|
205
|
-
return unless
|
|
306
|
+
define_method(method_name) do |scope_instance, old_field_value = nil|
|
|
307
|
+
return unless scope_instance
|
|
206
308
|
|
|
207
309
|
new_field_value = send(field)
|
|
208
310
|
|
|
209
311
|
# Use Familia's transaction method for atomicity with DataType abstraction
|
|
210
|
-
|
|
211
|
-
# Use declared field accessor on
|
|
212
|
-
index_hash =
|
|
312
|
+
scope_instance.transaction do |_tx|
|
|
313
|
+
# Use declared field accessor on scope instance
|
|
314
|
+
index_hash = scope_instance.send(index_name)
|
|
213
315
|
|
|
214
316
|
# Remove old value if provided
|
|
215
317
|
index_hash.remove(old_field_value.to_s) if old_field_value
|
|
@@ -228,10 +330,12 @@ module Familia
|
|
|
228
330
|
# - Employee.email_index
|
|
229
331
|
# - Employee.rebuild_email_index
|
|
230
332
|
def generate_query_methods_class(field, index_name, indexed_class)
|
|
333
|
+
# Generate class-level single record method
|
|
231
334
|
indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
|
|
232
|
-
index_hash = send(index_name) #
|
|
335
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
|
233
336
|
|
|
234
|
-
# Get the identifier from the
|
|
337
|
+
# Get the identifier from the db hashkey using .get method.
|
|
338
|
+
#
|
|
235
339
|
# We use .get instead of [] because it's part of the standard interface
|
|
236
340
|
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
|
237
341
|
# While unique indexes always use HashKey, using .get maintains consistency
|
|
@@ -245,12 +349,18 @@ module Familia
|
|
|
245
349
|
|
|
246
350
|
# Generate class-level bulk query method
|
|
247
351
|
indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
|
|
248
|
-
|
|
352
|
+
# Convert to array and filter nil inputs before querying Redis.
|
|
353
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
|
354
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
|
355
|
+
provided_ids = Array(provided_ids).compact
|
|
249
356
|
return [] if provided_ids.empty?
|
|
250
357
|
|
|
251
|
-
index_hash = send(index_name) #
|
|
358
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
|
359
|
+
|
|
360
|
+
# Get multiple identifiers from the db hashkey using .values_at
|
|
252
361
|
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
|
253
|
-
|
|
362
|
+
|
|
363
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
|
254
364
|
record_ids.compact.map { |record_id|
|
|
255
365
|
indexed_class.find_by_identifier(record_id)
|
|
256
366
|
}
|
|
@@ -260,15 +370,26 @@ module Familia
|
|
|
260
370
|
# No need to manually create it - Horreum handles this automatically
|
|
261
371
|
|
|
262
372
|
# Generate method to rebuild the class-level index
|
|
263
|
-
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
373
|
+
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
|
|
374
|
+
if respond_to?(:instances)
|
|
375
|
+
# Strategy 1: Use instances collection (fastest)
|
|
376
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_instances(
|
|
377
|
+
self, # indexed_class (e.g., User)
|
|
378
|
+
field, # e.g., :email
|
|
379
|
+
:"add_to_class_#{index_name}", # e.g., :add_to_class_email_lookup
|
|
380
|
+
batch_size: batch_size,
|
|
381
|
+
&progress_block
|
|
382
|
+
)
|
|
383
|
+
else
|
|
384
|
+
# Strategy 3: Fall back to SCAN
|
|
385
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
|
|
386
|
+
self,
|
|
387
|
+
field,
|
|
388
|
+
:"add_to_class_#{index_name}",
|
|
389
|
+
batch_size: batch_size,
|
|
390
|
+
&progress_block
|
|
391
|
+
)
|
|
392
|
+
end
|
|
272
393
|
end
|
|
273
394
|
end
|
|
274
395
|
|
|
@@ -285,9 +406,28 @@ module Familia
|
|
|
285
406
|
|
|
286
407
|
return unless field_value
|
|
287
408
|
|
|
409
|
+
# Just set the value - uniqueness should be validated before save
|
|
288
410
|
index_hash[field_value.to_s] = identifier
|
|
289
411
|
end
|
|
290
412
|
|
|
413
|
+
# Add a guard method to enforce unique constraint on this specific index
|
|
414
|
+
#
|
|
415
|
+
# @raise [Familia::RecordExistsError] if a record with the same
|
|
416
|
+
# field value exists. Values are compared as strings.
|
|
417
|
+
#
|
|
418
|
+
# @return [void]
|
|
419
|
+
define_method(:"guard_unique_#{index_name}!") do
|
|
420
|
+
field_value = send(field)
|
|
421
|
+
return unless field_value
|
|
422
|
+
|
|
423
|
+
index_hash = self.class.send(index_name)
|
|
424
|
+
existing_id = index_hash.get(field_value.to_s)
|
|
425
|
+
|
|
426
|
+
if existing_id && existing_id != identifier
|
|
427
|
+
raise Familia::RecordExistsError, "#{self.class} exists #{field}=#{field_value}"
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
291
431
|
define_method(:"remove_from_class_#{index_name}") do
|
|
292
432
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
|
293
433
|
field_value = send(field)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# lib/familia/features/relationships/indexing.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'indexing_relationship'
|
|
4
6
|
require_relative 'indexing/multi_index_generators'
|
|
5
7
|
require_relative 'indexing/unique_index_generators'
|
|
8
|
+
require_relative 'indexing/rebuild_strategies'
|
|
6
9
|
|
|
7
10
|
module Familia
|
|
8
11
|
module Features
|
|
@@ -50,7 +53,7 @@ module Familia
|
|
|
50
53
|
# Terminology:
|
|
51
54
|
# - unique_index: 1:1 field-to-object mapping (HashKey)
|
|
52
55
|
# - multi_index: 1:many field-to-objects mapping (UnsortedSet, no scores)
|
|
53
|
-
# - within:
|
|
56
|
+
# - within: scope class providing uniqueness boundary for instance-scoped indexes
|
|
54
57
|
# - query: whether to generate find_by_* methods (default: true)
|
|
55
58
|
#
|
|
56
59
|
# Key Patterns:
|
|
@@ -89,7 +92,7 @@ module Familia
|
|
|
89
92
|
#
|
|
90
93
|
# @param field [Symbol] The field to index on
|
|
91
94
|
# @param index_name [Symbol] Name of the index
|
|
92
|
-
# @param within [Class, Symbol] The
|
|
95
|
+
# @param within [Class, Symbol] The scope class providing uniqueness context
|
|
93
96
|
# @param query [Boolean] Whether to generate query methods
|
|
94
97
|
#
|
|
95
98
|
# @example Instance-scoped multi-value indexing
|
|
@@ -109,7 +112,7 @@ module Familia
|
|
|
109
112
|
#
|
|
110
113
|
# @param field [Symbol] The field to index on
|
|
111
114
|
# @param index_name [Symbol] Name of the index hash
|
|
112
|
-
# @param within [Class, Symbol] Optional
|
|
115
|
+
# @param within [Class, Symbol] Optional scope class for instance-scoped unique index
|
|
113
116
|
# @param query [Boolean] Whether to generate query methods
|
|
114
117
|
#
|
|
115
118
|
# @example Class-level unique index
|
|
@@ -136,70 +139,68 @@ module Familia
|
|
|
136
139
|
|
|
137
140
|
# Ensure proper DataType field is declared for index
|
|
138
141
|
# Similar to ensure_collection_field in participation system
|
|
139
|
-
def ensure_index_field(
|
|
140
|
-
return if
|
|
142
|
+
def ensure_index_field(scope_class, index_name, field_type)
|
|
143
|
+
return if scope_class.method_defined?(index_name) || scope_class.respond_to?(index_name)
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
scope_class.send(field_type, index_name)
|
|
143
146
|
end
|
|
144
147
|
end
|
|
145
148
|
|
|
146
149
|
# Instance methods for indexed objects
|
|
147
150
|
module ModelInstanceMethods
|
|
148
|
-
# Update all indexes for a given
|
|
149
|
-
# For class-level indexes (
|
|
150
|
-
# For
|
|
151
|
-
def update_all_indexes(old_values = {},
|
|
151
|
+
# Update all indexes for a given scope context
|
|
152
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
|
153
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
|
154
|
+
def update_all_indexes(old_values = {}, scope_context = nil)
|
|
152
155
|
return unless self.class.respond_to?(:indexing_relationships)
|
|
153
156
|
|
|
154
157
|
self.class.indexing_relationships.each do |config|
|
|
155
158
|
field = config.field
|
|
156
159
|
index_name = config.index_name
|
|
157
|
-
target_class = config.target_class
|
|
158
160
|
old_field_value = old_values[field]
|
|
159
161
|
|
|
160
162
|
# Determine which update method to call
|
|
161
|
-
if
|
|
163
|
+
if config.within.nil?
|
|
162
164
|
# Class-level index (unique_index without within:)
|
|
163
165
|
send("update_in_class_#{index_name}", old_field_value)
|
|
164
166
|
else
|
|
165
|
-
#
|
|
166
|
-
next unless
|
|
167
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
|
168
|
+
next unless scope_context
|
|
167
169
|
|
|
168
170
|
# Use config_name for method naming
|
|
169
|
-
|
|
170
|
-
send("update_in_#{
|
|
171
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
|
172
|
+
send("update_in_#{scope_class_config}_#{index_name}", scope_context, old_field_value)
|
|
171
173
|
end
|
|
172
174
|
end
|
|
173
175
|
end
|
|
174
176
|
|
|
175
|
-
# Remove from all indexes for a given
|
|
176
|
-
# For class-level indexes (
|
|
177
|
-
# For
|
|
178
|
-
def remove_from_all_indexes(
|
|
177
|
+
# Remove from all indexes for a given scope context
|
|
178
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
|
179
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
|
180
|
+
def remove_from_all_indexes(scope_context = nil)
|
|
179
181
|
return unless self.class.respond_to?(:indexing_relationships)
|
|
180
182
|
|
|
181
183
|
self.class.indexing_relationships.each do |config|
|
|
182
184
|
index_name = config.index_name
|
|
183
|
-
target_class = config.target_class
|
|
184
185
|
|
|
185
186
|
# Determine which remove method to call
|
|
186
|
-
if
|
|
187
|
+
if config.within.nil?
|
|
187
188
|
# Class-level index (unique_index without within:)
|
|
188
189
|
send("remove_from_class_#{index_name}")
|
|
189
190
|
else
|
|
190
|
-
#
|
|
191
|
-
next unless
|
|
191
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
|
192
|
+
next unless scope_context
|
|
192
193
|
|
|
193
194
|
# Use config_name for method naming
|
|
194
|
-
|
|
195
|
-
send("remove_from_#{
|
|
195
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
|
196
|
+
send("remove_from_#{scope_class_config}_#{index_name}", scope_context)
|
|
196
197
|
end
|
|
197
198
|
end
|
|
198
199
|
end
|
|
199
200
|
|
|
200
201
|
# Get all indexes this object appears in
|
|
201
|
-
# Note: For
|
|
202
|
-
# since
|
|
202
|
+
# Note: For instance-scoped indexes, this only shows class-level indexes
|
|
203
|
+
# since instance-scoped indexes require a specific scope instance
|
|
203
204
|
#
|
|
204
205
|
# @return [Array<Hash>] Array of index information
|
|
205
206
|
def current_indexings
|
|
@@ -210,19 +211,18 @@ module Familia
|
|
|
210
211
|
self.class.indexing_relationships.each do |config|
|
|
211
212
|
field = config.field
|
|
212
213
|
index_name = config.index_name
|
|
213
|
-
target_class = config.target_class
|
|
214
214
|
cardinality = config.cardinality
|
|
215
215
|
field_value = send(field)
|
|
216
216
|
|
|
217
217
|
next unless field_value
|
|
218
218
|
|
|
219
|
-
if
|
|
219
|
+
if config.within.nil?
|
|
220
220
|
# Class-level index (unique_index without within:) - check hash key using DataType
|
|
221
221
|
index_hash = self.class.send(index_name)
|
|
222
222
|
next unless index_hash.key?(field_value.to_s)
|
|
223
223
|
|
|
224
224
|
memberships << {
|
|
225
|
-
|
|
225
|
+
scope_class: 'class',
|
|
226
226
|
index_name: index_name,
|
|
227
227
|
field: field,
|
|
228
228
|
field_value: field_value,
|
|
@@ -231,17 +231,17 @@ module Familia
|
|
|
231
231
|
type: 'unique_index',
|
|
232
232
|
}
|
|
233
233
|
else
|
|
234
|
-
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without
|
|
235
|
-
# This would require scanning all possible
|
|
234
|
+
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without scope instance
|
|
235
|
+
# This would require scanning all possible scope instances
|
|
236
236
|
memberships << {
|
|
237
|
-
|
|
237
|
+
scope_class: config.scope_class_config_name,
|
|
238
238
|
index_name: index_name,
|
|
239
239
|
field: field,
|
|
240
240
|
field_value: field_value,
|
|
241
|
-
index_key: '
|
|
241
|
+
index_key: 'scope_dependent',
|
|
242
242
|
cardinality: cardinality,
|
|
243
243
|
type: cardinality == :unique ? 'unique_index' : 'multi_index',
|
|
244
|
-
note: 'Requires
|
|
244
|
+
note: 'Requires scope instance for verification',
|
|
245
245
|
}
|
|
246
246
|
end
|
|
247
247
|
end
|
|
@@ -249,9 +249,9 @@ module Familia
|
|
|
249
249
|
memberships
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
-
# Check if this object is indexed in a specific
|
|
252
|
+
# Check if this object is indexed in a specific scope
|
|
253
253
|
# For class-level indexes, checks the hash key
|
|
254
|
-
# For
|
|
254
|
+
# For instance-scoped indexes, returns false (requires scope instance)
|
|
255
255
|
def indexed_in?(index_name)
|
|
256
256
|
return false unless self.class.respond_to?(:indexing_relationships)
|
|
257
257
|
|
|
@@ -262,14 +262,12 @@ module Familia
|
|
|
262
262
|
field_value = send(field)
|
|
263
263
|
return false unless field_value
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if target_class == self.class
|
|
265
|
+
if config.within.nil?
|
|
268
266
|
# Class-level index (class_indexed_by) - check hash key using DataType
|
|
269
267
|
index_hash = self.class.send(index_name)
|
|
270
268
|
index_hash.key?(field_value.to_s)
|
|
271
269
|
else
|
|
272
|
-
#
|
|
270
|
+
# Instance-scoped index (with within:) - cannot verify without scope instance
|
|
273
271
|
false
|
|
274
272
|
end
|
|
275
273
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# lib/familia/features/relationships/indexing_relationship.rb
|
|
2
|
+
#
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
@@ -14,20 +16,30 @@ module Familia
|
|
|
14
16
|
# Similar to ParticipationRelationship but for attribute-based lookups
|
|
15
17
|
# rather than collection membership.
|
|
16
18
|
#
|
|
19
|
+
# Terminology:
|
|
20
|
+
# - `scope_class`: The class that provides the uniqueness boundary for
|
|
21
|
+
# instance-scoped indexes. For example, in `unique_index :badge_number,
|
|
22
|
+
# :badge_index, within: Company`, the Company is the scope class.
|
|
23
|
+
# - `within`: Preserves the original DSL parameter to explicitly distinguish
|
|
24
|
+
# class-level indexes (within: nil) from instance-scoped indexes (within:
|
|
25
|
+
# SomeClass). This avoids brittle class comparisons and prevents issues
|
|
26
|
+
# with inheritance scenarios.
|
|
27
|
+
#
|
|
17
28
|
IndexingRelationship = Data.define(
|
|
18
29
|
:field, # Symbol - field being indexed (e.g., :email, :department)
|
|
19
30
|
:index_name, # Symbol - name of the index (e.g., :email_index, :dept_index)
|
|
20
|
-
:
|
|
31
|
+
:scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
|
|
32
|
+
:within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
|
|
21
33
|
:cardinality, # Symbol - :unique (1:1) or :multi (1:many)
|
|
22
|
-
:query
|
|
34
|
+
:query, # Boolean - whether to generate query methods
|
|
23
35
|
) do
|
|
24
36
|
#
|
|
25
|
-
# Get the normalized config name for the
|
|
37
|
+
# Get the normalized config name for the scope class
|
|
26
38
|
#
|
|
27
39
|
# @return [String] The config name (e.g., "user", "company", "test_company")
|
|
28
40
|
#
|
|
29
|
-
def
|
|
30
|
-
|
|
41
|
+
def scope_class_config_name
|
|
42
|
+
scope_class.config_name
|
|
31
43
|
end
|
|
32
44
|
end
|
|
33
45
|
end
|