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
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# try/features/relationships/indexing_rebuild_try.rb
|
|
2
|
+
#
|
|
3
|
+
# Comprehensive tests for index rebuild functionality
|
|
4
|
+
# Tests both unique_index (class-level and instance-scoped) and multi_index rebuild
|
|
5
|
+
|
|
6
|
+
require_relative '../../support/helpers/test_helpers'
|
|
7
|
+
|
|
8
|
+
Familia.enable_database_logging = true
|
|
9
|
+
|
|
10
|
+
# Test classes for class-level unique index rebuild
|
|
11
|
+
class ::RebuildTestUser < Familia::Horreum
|
|
12
|
+
feature :relationships
|
|
13
|
+
|
|
14
|
+
identifier_field :user_id
|
|
15
|
+
field :user_id
|
|
16
|
+
field :email
|
|
17
|
+
field :username
|
|
18
|
+
field :department
|
|
19
|
+
|
|
20
|
+
unique_index :email, :email_lookup
|
|
21
|
+
unique_index :username, :username_lookup
|
|
22
|
+
|
|
23
|
+
class_sorted_set :instances, reference: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Test classes for instance-scoped unique index rebuild
|
|
27
|
+
# Define Company first so Employee can reference it
|
|
28
|
+
class ::RebuildTestCompany < Familia::Horreum
|
|
29
|
+
feature :relationships
|
|
30
|
+
|
|
31
|
+
identifier_field :company_id
|
|
32
|
+
field :company_id
|
|
33
|
+
field :name
|
|
34
|
+
|
|
35
|
+
sorted_set :employees
|
|
36
|
+
|
|
37
|
+
class_sorted_set :instances, reference: true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class ::RebuildTestEmployee < Familia::Horreum
|
|
41
|
+
feature :relationships
|
|
42
|
+
|
|
43
|
+
identifier_field :emp_id
|
|
44
|
+
field :emp_id
|
|
45
|
+
field :email
|
|
46
|
+
field :badge_number
|
|
47
|
+
field :department
|
|
48
|
+
|
|
49
|
+
unique_index :badge_number, :badge_index, within: RebuildTestCompany
|
|
50
|
+
multi_index :department, :dept_index, within: RebuildTestCompany
|
|
51
|
+
|
|
52
|
+
participates_in RebuildTestCompany, :employees, score: :emp_id
|
|
53
|
+
|
|
54
|
+
class_sorted_set :instances, reference: true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Setup: Create test data for class-level unique index
|
|
58
|
+
@user1 = RebuildTestUser.new(user_id: "user_1", email: "user1@test.com", username: "user1")
|
|
59
|
+
@user1.save
|
|
60
|
+
@user2 = RebuildTestUser.new(user_id: "user_2", email: "user2@test.com", username: "user2")
|
|
61
|
+
@user2.save
|
|
62
|
+
@user3 = RebuildTestUser.new(user_id: "user_3", email: "user3@test.com", username: "user3")
|
|
63
|
+
@user3.save
|
|
64
|
+
|
|
65
|
+
# Setup: Create test data for instance-scoped indexes
|
|
66
|
+
@company = RebuildTestCompany.new(company_id: "company_1", name: "Acme Corp")
|
|
67
|
+
@company.save
|
|
68
|
+
|
|
69
|
+
@emp1 = RebuildTestEmployee.new(emp_id: "emp_1", email: "emp1@acme.com", badge_number: "BADGE001", department: "engineering")
|
|
70
|
+
@emp1.save
|
|
71
|
+
@emp1.add_to_rebuild_test_company_employees(@company)
|
|
72
|
+
|
|
73
|
+
@emp2 = RebuildTestEmployee.new(emp_id: "emp_2", email: "emp2@acme.com", badge_number: "BADGE002", department: "sales")
|
|
74
|
+
@emp2.save
|
|
75
|
+
@emp2.add_to_rebuild_test_company_employees(@company)
|
|
76
|
+
|
|
77
|
+
@emp3 = RebuildTestEmployee.new(emp_id: "emp_3", email: "emp3@acme.com", badge_number: "BADGE003", department: "engineering")
|
|
78
|
+
@emp3.save
|
|
79
|
+
@emp3.add_to_rebuild_test_company_employees(@company)
|
|
80
|
+
|
|
81
|
+
# =============================================
|
|
82
|
+
# 1. Class-Level Unique Index Rebuild Tests
|
|
83
|
+
# =============================================
|
|
84
|
+
|
|
85
|
+
## Class-level rebuild method exists
|
|
86
|
+
RebuildTestUser.respond_to?(:rebuild_email_lookup)
|
|
87
|
+
#=> true
|
|
88
|
+
|
|
89
|
+
## Index starts empty before rebuild
|
|
90
|
+
RebuildTestUser.email_lookup.clear
|
|
91
|
+
RebuildTestUser.email_lookup.size
|
|
92
|
+
#=> 0
|
|
93
|
+
|
|
94
|
+
## Rebuild returns count of indexed objects
|
|
95
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
96
|
+
count
|
|
97
|
+
#=> 3
|
|
98
|
+
|
|
99
|
+
## Index size matches object count after rebuild
|
|
100
|
+
RebuildTestUser.email_lookup.size
|
|
101
|
+
#=> 3
|
|
102
|
+
|
|
103
|
+
## Find by email works after rebuild
|
|
104
|
+
found = RebuildTestUser.find_by_email("user1@test.com")
|
|
105
|
+
found&.user_id
|
|
106
|
+
#=> "user_1"
|
|
107
|
+
|
|
108
|
+
## All users are findable after rebuild
|
|
109
|
+
found = RebuildTestUser.find_by_email("user2@test.com")
|
|
110
|
+
found&.user_id
|
|
111
|
+
#=> "user_2"
|
|
112
|
+
|
|
113
|
+
## Third user is findable after rebuild
|
|
114
|
+
found = RebuildTestUser.find_by_email("user3@test.com")
|
|
115
|
+
found&.user_id
|
|
116
|
+
#=> "user_3"
|
|
117
|
+
|
|
118
|
+
## Bulk query works after rebuild
|
|
119
|
+
emails = ["user1@test.com", "user3@test.com"]
|
|
120
|
+
found_users = RebuildTestUser.find_all_by_email(emails)
|
|
121
|
+
found_users.map(&:user_id).sort
|
|
122
|
+
#=> ["user_1", "user_3"]
|
|
123
|
+
|
|
124
|
+
## Rebuild can be called multiple times
|
|
125
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
126
|
+
count
|
|
127
|
+
#=> 3
|
|
128
|
+
|
|
129
|
+
## Index remains consistent after multiple rebuilds
|
|
130
|
+
RebuildTestUser.email_lookup.size
|
|
131
|
+
#=> 3
|
|
132
|
+
|
|
133
|
+
## Rebuild for second unique index works independently
|
|
134
|
+
RebuildTestUser.username_lookup.clear
|
|
135
|
+
count = RebuildTestUser.rebuild_username_lookup
|
|
136
|
+
count
|
|
137
|
+
#=> 3
|
|
138
|
+
|
|
139
|
+
## Second index is populated correctly
|
|
140
|
+
found = RebuildTestUser.find_by_username("user2")
|
|
141
|
+
found&.user_id
|
|
142
|
+
#=> "user_2"
|
|
143
|
+
|
|
144
|
+
# =============================================
|
|
145
|
+
# 2. Class-Level Index Rebuild Edge Cases
|
|
146
|
+
# =============================================
|
|
147
|
+
|
|
148
|
+
## Rebuild with nil field values skips those objects
|
|
149
|
+
@user_nil = RebuildTestUser.new(user_id: "user_nil", email: nil, username: "user_nil")
|
|
150
|
+
@user_nil.save
|
|
151
|
+
RebuildTestUser.email_lookup.clear
|
|
152
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
153
|
+
count
|
|
154
|
+
#=> 3
|
|
155
|
+
|
|
156
|
+
## Nil field values are not in index
|
|
157
|
+
RebuildTestUser.find_by_email("")
|
|
158
|
+
#=> nil
|
|
159
|
+
|
|
160
|
+
## Non-nil fields are still indexed for object with nil field
|
|
161
|
+
found = RebuildTestUser.find_by_username("user_nil")
|
|
162
|
+
found&.user_id
|
|
163
|
+
#=> "user_nil"
|
|
164
|
+
|
|
165
|
+
## Rebuild with empty string field values skips those objects
|
|
166
|
+
@user_empty = RebuildTestUser.new(user_id: "user_empty", email: "", username: "user_empty")
|
|
167
|
+
@user_empty.save
|
|
168
|
+
RebuildTestUser.email_lookup.clear
|
|
169
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
170
|
+
count
|
|
171
|
+
#=> 3
|
|
172
|
+
|
|
173
|
+
## Empty string field values are not in index
|
|
174
|
+
RebuildTestUser.find_by_email("")
|
|
175
|
+
#=> nil
|
|
176
|
+
|
|
177
|
+
## Rebuild with whitespace-only field values skips those objects
|
|
178
|
+
@user_whitespace = RebuildTestUser.new(user_id: "user_ws", email: " ", username: "user_ws")
|
|
179
|
+
@user_whitespace.save
|
|
180
|
+
RebuildTestUser.email_lookup.clear
|
|
181
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
182
|
+
count
|
|
183
|
+
#=> 3
|
|
184
|
+
|
|
185
|
+
## Whitespace-only field values are not in index
|
|
186
|
+
RebuildTestUser.find_by_email(" ")
|
|
187
|
+
#=> nil
|
|
188
|
+
|
|
189
|
+
## Rebuild handles stale object IDs gracefully
|
|
190
|
+
RebuildTestUser.instances.add("stale_user_id")
|
|
191
|
+
RebuildTestUser.email_lookup.clear
|
|
192
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
193
|
+
count
|
|
194
|
+
#=> 3
|
|
195
|
+
|
|
196
|
+
## Index size is correct despite stale ID
|
|
197
|
+
RebuildTestUser.email_lookup.size
|
|
198
|
+
#=> 3
|
|
199
|
+
|
|
200
|
+
## Rebuild with no instances returns zero
|
|
201
|
+
RebuildTestUser.instances.clear
|
|
202
|
+
RebuildTestUser.email_lookup.clear
|
|
203
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
204
|
+
count
|
|
205
|
+
#=> 0
|
|
206
|
+
|
|
207
|
+
## Index is empty after rebuilding with no instances
|
|
208
|
+
RebuildTestUser.email_lookup.size
|
|
209
|
+
#=> 0
|
|
210
|
+
|
|
211
|
+
# Restore instances for remaining tests
|
|
212
|
+
## Check instances key
|
|
213
|
+
RebuildTestUser.instances.dbkey
|
|
214
|
+
#=> "rebuild_test_user:instances"
|
|
215
|
+
|
|
216
|
+
## Manually populate instances for testing
|
|
217
|
+
result1 = RebuildTestUser.instances.add("user_1")
|
|
218
|
+
result2 = RebuildTestUser.instances.add("user_2")
|
|
219
|
+
result3 = RebuildTestUser.instances.add("user_3")
|
|
220
|
+
[result1, result2, result3]
|
|
221
|
+
#=> [true, true, true]
|
|
222
|
+
|
|
223
|
+
## Instances restored successfully
|
|
224
|
+
RebuildTestUser.instances.size
|
|
225
|
+
#=> 3
|
|
226
|
+
|
|
227
|
+
# =============================================
|
|
228
|
+
# 3. Instance-Scoped Unique Index Rebuild
|
|
229
|
+
# =============================================
|
|
230
|
+
|
|
231
|
+
## Instance-scoped rebuild method exists
|
|
232
|
+
@company.respond_to?(:rebuild_badge_index)
|
|
233
|
+
#=> true
|
|
234
|
+
|
|
235
|
+
## Instance-scoped index starts empty before rebuild
|
|
236
|
+
@company.badge_index.clear
|
|
237
|
+
@company.badge_index.size
|
|
238
|
+
#=> 0
|
|
239
|
+
|
|
240
|
+
## Instance-scoped rebuild returns count
|
|
241
|
+
count = @company.rebuild_badge_index
|
|
242
|
+
count
|
|
243
|
+
#=> 3
|
|
244
|
+
|
|
245
|
+
## Instance-scoped index size matches after rebuild
|
|
246
|
+
@company.badge_index.size
|
|
247
|
+
#=> 3
|
|
248
|
+
|
|
249
|
+
## Find by badge works after instance-scoped rebuild
|
|
250
|
+
found = @company.find_by_badge_number("BADGE001")
|
|
251
|
+
found&.emp_id
|
|
252
|
+
#=> "emp_1"
|
|
253
|
+
|
|
254
|
+
## All employees findable after instance-scoped rebuild
|
|
255
|
+
found = @company.find_by_badge_number("BADGE002")
|
|
256
|
+
found&.emp_id
|
|
257
|
+
#=> "emp_2"
|
|
258
|
+
|
|
259
|
+
## Third employee findable after instance-scoped rebuild
|
|
260
|
+
found = @company.find_by_badge_number("BADGE003")
|
|
261
|
+
found&.emp_id
|
|
262
|
+
#=> "emp_3"
|
|
263
|
+
|
|
264
|
+
## Bulk query works after instance-scoped rebuild
|
|
265
|
+
badges = ["BADGE001", "BADGE003"]
|
|
266
|
+
found_emps = @company.find_all_by_badge_number(badges)
|
|
267
|
+
found_emps.map(&:emp_id).sort
|
|
268
|
+
#=> ["emp_1", "emp_3"]
|
|
269
|
+
|
|
270
|
+
## Instance-scoped rebuild only indexes employees in that company
|
|
271
|
+
@company2 = RebuildTestCompany.new(company_id: "company_2", name: "TechCo")
|
|
272
|
+
@company2.save
|
|
273
|
+
@company2.badge_index.clear
|
|
274
|
+
count = @company2.rebuild_badge_index
|
|
275
|
+
count
|
|
276
|
+
#=> 0
|
|
277
|
+
|
|
278
|
+
## Second company has empty index after rebuild
|
|
279
|
+
@company2.badge_index.size
|
|
280
|
+
#=> 0
|
|
281
|
+
|
|
282
|
+
## First company still has correct index
|
|
283
|
+
@company.badge_index.size
|
|
284
|
+
#=> 3
|
|
285
|
+
|
|
286
|
+
# =============================================
|
|
287
|
+
# 4. Multi-Value Index Rebuild
|
|
288
|
+
# =============================================
|
|
289
|
+
|
|
290
|
+
## Multi-index rebuild method exists
|
|
291
|
+
@company.respond_to?(:rebuild_dept_index)
|
|
292
|
+
#=> true
|
|
293
|
+
|
|
294
|
+
## Multi-index starts empty before rebuild
|
|
295
|
+
engineering_set = @company.dept_index_for("engineering")
|
|
296
|
+
engineering_set.clear
|
|
297
|
+
sales_set = @company.dept_index_for("sales")
|
|
298
|
+
sales_set.clear
|
|
299
|
+
engineering_set.size
|
|
300
|
+
#=> 0
|
|
301
|
+
|
|
302
|
+
## Multi-index rebuild returns processed count
|
|
303
|
+
count = @company.rebuild_dept_index
|
|
304
|
+
count
|
|
305
|
+
#=> 3
|
|
306
|
+
|
|
307
|
+
## Manually populate departments to test structure
|
|
308
|
+
@emp1.add_to_rebuild_test_company_dept_index(@company)
|
|
309
|
+
@emp2.add_to_rebuild_test_company_dept_index(@company)
|
|
310
|
+
@emp3.add_to_rebuild_test_company_dept_index(@company)
|
|
311
|
+
engineering_count = @company.dept_index_for("engineering").size
|
|
312
|
+
engineering_count
|
|
313
|
+
#=> 2
|
|
314
|
+
|
|
315
|
+
## Sales department has correct count
|
|
316
|
+
sales_count = @company.dept_index_for("sales").size
|
|
317
|
+
sales_count
|
|
318
|
+
#=> 1
|
|
319
|
+
|
|
320
|
+
## Find all by department works for engineering
|
|
321
|
+
eng_emps = @company.find_all_by_department("engineering")
|
|
322
|
+
eng_emps.map(&:emp_id).sort
|
|
323
|
+
#=> ["emp_1", "emp_3"]
|
|
324
|
+
|
|
325
|
+
## Find all by department works for sales
|
|
326
|
+
sales_emps = @company.find_all_by_department("sales")
|
|
327
|
+
sales_emps.map(&:emp_id)
|
|
328
|
+
#=> ["emp_2"]
|
|
329
|
+
|
|
330
|
+
## Sample from department returns employees
|
|
331
|
+
sample = @company.sample_from_department("engineering", 1)
|
|
332
|
+
["emp_1", "emp_3"].include?(sample.first&.emp_id)
|
|
333
|
+
#=> true
|
|
334
|
+
|
|
335
|
+
# =============================================
|
|
336
|
+
# 5. Rebuild Progress Callbacks
|
|
337
|
+
# =============================================
|
|
338
|
+
|
|
339
|
+
## Instances collection has users before rebuild
|
|
340
|
+
RebuildTestUser.instances.size
|
|
341
|
+
#=> 3
|
|
342
|
+
|
|
343
|
+
## Rebuild accepts batch_size parameter
|
|
344
|
+
RebuildTestUser.email_lookup.clear
|
|
345
|
+
count = RebuildTestUser.rebuild_email_lookup(batch_size: 1)
|
|
346
|
+
count
|
|
347
|
+
#=> 3
|
|
348
|
+
|
|
349
|
+
## Index works correctly with small batch size
|
|
350
|
+
found = RebuildTestUser.find_by_email("user1@test.com")
|
|
351
|
+
found&.user_id
|
|
352
|
+
#=> "user_1"
|
|
353
|
+
|
|
354
|
+
## Rebuild accepts large batch_size parameter
|
|
355
|
+
RebuildTestUser.email_lookup.clear
|
|
356
|
+
count = RebuildTestUser.rebuild_email_lookup(batch_size: 1000)
|
|
357
|
+
count
|
|
358
|
+
#=> 3
|
|
359
|
+
|
|
360
|
+
## Index works correctly with large batch size
|
|
361
|
+
found = RebuildTestUser.find_by_email("user2@test.com")
|
|
362
|
+
found&.user_id
|
|
363
|
+
#=> "user_2"
|
|
364
|
+
|
|
365
|
+
## Rebuild accepts progress callback block
|
|
366
|
+
@progress_updates = []
|
|
367
|
+
RebuildTestUser.email_lookup.clear
|
|
368
|
+
count = RebuildTestUser.rebuild_email_lookup { |progress| @progress_updates << progress }
|
|
369
|
+
count
|
|
370
|
+
#=> 3
|
|
371
|
+
|
|
372
|
+
## Progress callback receives updates
|
|
373
|
+
@progress_updates.size > 0
|
|
374
|
+
#=> true
|
|
375
|
+
|
|
376
|
+
## Progress callback includes completed count
|
|
377
|
+
@progress_updates.last[:completed]
|
|
378
|
+
#=> 3
|
|
379
|
+
|
|
380
|
+
## Progress callback includes total count
|
|
381
|
+
@progress_updates.last[:total]
|
|
382
|
+
#=> 3
|
|
383
|
+
|
|
384
|
+
## Progress callback includes rate information
|
|
385
|
+
@progress_updates.last.key?(:rate)
|
|
386
|
+
#=> true
|
|
387
|
+
|
|
388
|
+
## Progress updates are incremental
|
|
389
|
+
completed_values = @progress_updates.map { |p| p[:completed] }
|
|
390
|
+
completed_values.sort == completed_values
|
|
391
|
+
#=> true
|
|
392
|
+
|
|
393
|
+
# =============================================
|
|
394
|
+
# 6. Rebuild with Modified Data
|
|
395
|
+
# =============================================
|
|
396
|
+
|
|
397
|
+
## Rebuild reflects updated field values
|
|
398
|
+
@user1.email = "updated1@test.com"
|
|
399
|
+
@user1.save
|
|
400
|
+
RebuildTestUser.email_lookup.clear
|
|
401
|
+
RebuildTestUser.rebuild_email_lookup
|
|
402
|
+
found = RebuildTestUser.find_by_email("updated1@test.com")
|
|
403
|
+
found&.user_id
|
|
404
|
+
#=> "user_1"
|
|
405
|
+
|
|
406
|
+
## Old email is not in index after rebuild
|
|
407
|
+
RebuildTestUser.find_by_email("user1@test.com")
|
|
408
|
+
#=> nil
|
|
409
|
+
|
|
410
|
+
## Rebuild after deleting object removes from index
|
|
411
|
+
@user3.destroy if @user3.respond_to?(:destroy)
|
|
412
|
+
RebuildTestUser.instances.remove(@user3.identifier)
|
|
413
|
+
RebuildTestUser.email_lookup.clear
|
|
414
|
+
count = RebuildTestUser.rebuild_email_lookup
|
|
415
|
+
count
|
|
416
|
+
#=> 2
|
|
417
|
+
|
|
418
|
+
## Deleted object not findable after rebuild
|
|
419
|
+
RebuildTestUser.find_by_email("user3@test.com")
|
|
420
|
+
#=> nil
|
|
421
|
+
|
|
422
|
+
## Remaining objects still findable
|
|
423
|
+
found = RebuildTestUser.find_by_email("user2@test.com")
|
|
424
|
+
found&.user_id
|
|
425
|
+
#=> "user_2"
|
|
426
|
+
|
|
427
|
+
# =============================================
|
|
428
|
+
# 7. Instance-Scoped Rebuild with Batch Sizes
|
|
429
|
+
# =============================================
|
|
430
|
+
|
|
431
|
+
## Instance-scoped rebuild accepts batch_size
|
|
432
|
+
@company.badge_index.clear
|
|
433
|
+
count = @company.rebuild_badge_index(batch_size: 1)
|
|
434
|
+
count
|
|
435
|
+
#=> 3
|
|
436
|
+
|
|
437
|
+
## Instance-scoped index works with small batch
|
|
438
|
+
found = @company.find_by_badge_number("BADGE001")
|
|
439
|
+
found&.emp_id
|
|
440
|
+
#=> "emp_1"
|
|
441
|
+
|
|
442
|
+
## Instance-scoped rebuild with large batch_size
|
|
443
|
+
@company.badge_index.clear
|
|
444
|
+
count = @company.rebuild_badge_index(batch_size: 100)
|
|
445
|
+
count
|
|
446
|
+
#=> 3
|
|
447
|
+
|
|
448
|
+
## Instance-scoped index works with large batch
|
|
449
|
+
found = @company.find_by_badge_number("BADGE002")
|
|
450
|
+
found&.emp_id
|
|
451
|
+
#=> "emp_2"
|
|
452
|
+
|
|
453
|
+
# =============================================
|
|
454
|
+
# 8. Concurrent Rebuilds (Thread Safety)
|
|
455
|
+
# =============================================
|
|
456
|
+
|
|
457
|
+
## Multiple rebuilds don't corrupt index
|
|
458
|
+
RebuildTestUser.email_lookup.clear
|
|
459
|
+
counts = 3.times.map { RebuildTestUser.rebuild_email_lookup }
|
|
460
|
+
counts
|
|
461
|
+
#=> [2, 2, 2]
|
|
462
|
+
|
|
463
|
+
## Index remains consistent after concurrent rebuilds
|
|
464
|
+
RebuildTestUser.email_lookup.size
|
|
465
|
+
#=> 2
|
|
466
|
+
|
|
467
|
+
## All expected objects findable after concurrent rebuilds
|
|
468
|
+
found1 = RebuildTestUser.find_by_email("updated1@test.com")
|
|
469
|
+
found2 = RebuildTestUser.find_by_email("user2@test.com")
|
|
470
|
+
[found1&.user_id, found2&.user_id].sort
|
|
471
|
+
#=> ["user_1", "user_2"]
|
|
472
|
+
|
|
473
|
+
# =============================================
|
|
474
|
+
# 9. Orphaned Data Cleanup (SCAN-based)
|
|
475
|
+
# =============================================
|
|
476
|
+
|
|
477
|
+
## Clear all dept indexes from earlier tests
|
|
478
|
+
["engineering", "sales", "marketing", "finance"].each do |dept|
|
|
479
|
+
@company.dept_index_for(dept).clear rescue nil
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
## Manually create orphaned stale data in finance dept
|
|
483
|
+
@company.dept_index_for("finance").add("emp_1")
|
|
484
|
+
@company.dept_index_for("finance").add("emp_2")
|
|
485
|
+
@company.dept_index_for("finance").size
|
|
486
|
+
#=> 2
|
|
487
|
+
|
|
488
|
+
## Also add some marketing entries (will be orphaned)
|
|
489
|
+
@company.dept_index_for("marketing").add("emp_1")
|
|
490
|
+
@company.dept_index_for("marketing").add("emp_3")
|
|
491
|
+
@company.dept_index_for("marketing").size
|
|
492
|
+
#=> 2
|
|
493
|
+
|
|
494
|
+
## Rebuild via participation collection processes 3 employees
|
|
495
|
+
@company.rebuild_dept_index
|
|
496
|
+
#=> 3
|
|
497
|
+
|
|
498
|
+
## After rebuild: Current engineering dept correctly has both emp1 and emp3
|
|
499
|
+
@company.dept_index_for("engineering").size
|
|
500
|
+
#=> 2
|
|
501
|
+
|
|
502
|
+
## After rebuild: Current sales dept correctly has emp2
|
|
503
|
+
@company.dept_index_for("sales").size
|
|
504
|
+
#=> 1
|
|
505
|
+
|
|
506
|
+
## SCAN cleanup removes orphaned finance keys
|
|
507
|
+
@company.dept_index_for("finance").size
|
|
508
|
+
#=> 0
|
|
509
|
+
|
|
510
|
+
## SCAN cleanup removes orphaned marketing keys
|
|
511
|
+
@company.dept_index_for("marketing").size
|
|
512
|
+
#=> 0
|
|
513
|
+
|
|
514
|
+
# =============================================
|
|
515
|
+
# 10. Scope Filtering (SCAN Strategy)
|
|
516
|
+
# =============================================
|
|
517
|
+
|
|
518
|
+
## Company 1 has 3 employees
|
|
519
|
+
@company.employees.size
|
|
520
|
+
#=> 3
|
|
521
|
+
|
|
522
|
+
## Clear company 1 badge index to force SCAN strategy
|
|
523
|
+
@company.badge_index.clear
|
|
524
|
+
@company.badge_index.size
|
|
525
|
+
#=> 0
|
|
526
|
+
|
|
527
|
+
## Company has 3 employees participating
|
|
528
|
+
@company.employees.size
|
|
529
|
+
#=> 3
|
|
530
|
+
|
|
531
|
+
## Rebuild company 1 index via SCAN - should filter to only company 1's employees
|
|
532
|
+
count = @company.rebuild_badge_index
|
|
533
|
+
count
|
|
534
|
+
#=> 3
|
|
535
|
+
|
|
536
|
+
## Company 1 index only has its own employees (scope filtering verified)
|
|
537
|
+
@company.badge_index.size
|
|
538
|
+
#=> 3
|
|
539
|
+
|
|
540
|
+
## All expected employees found via index
|
|
541
|
+
found1 = @company.find_by_badge_number("BADGE001")
|
|
542
|
+
found2 = @company.find_by_badge_number("BADGE002")
|
|
543
|
+
found3 = @company.find_by_badge_number("BADGE003")
|
|
544
|
+
[found1&.emp_id, found2&.emp_id, found3&.emp_id]
|
|
545
|
+
#=> ["emp_1", "emp_2", "emp_3"]
|
|
546
|
+
|
|
547
|
+
# =============================================
|
|
548
|
+
# 11. Cardinality Guard Protection
|
|
549
|
+
# =============================================
|
|
550
|
+
|
|
551
|
+
## Cardinality guard prevents multi-index corruption
|
|
552
|
+
# Note: This would require manually calling the private method with wrong cardinality
|
|
553
|
+
# The architecture prevents this via factory pattern, but guard provides explicit protection
|
|
554
|
+
begin
|
|
555
|
+
# Simulate calling rebuild_via_participation with multi-index cardinality
|
|
556
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
557
|
+
@company,
|
|
558
|
+
RebuildTestEmployee,
|
|
559
|
+
:department,
|
|
560
|
+
:add_to_rebuild_test_company_dept_index,
|
|
561
|
+
@company.employees,
|
|
562
|
+
:multi, # Wrong cardinality!
|
|
563
|
+
batch_size: 100
|
|
564
|
+
)
|
|
565
|
+
"should have raised"
|
|
566
|
+
rescue ArgumentError => e
|
|
567
|
+
e.message.include?("only supports unique indexes")
|
|
568
|
+
end
|
|
569
|
+
#=> true
|
|
570
|
+
|
|
571
|
+
## Guard accepts correct cardinality (:unique)
|
|
572
|
+
begin
|
|
573
|
+
index_config = RebuildTestEmployee.indexing_relationships.find { |r| r.index_name == :badge_index }
|
|
574
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
575
|
+
@company,
|
|
576
|
+
RebuildTestEmployee,
|
|
577
|
+
:badge_number,
|
|
578
|
+
:add_to_rebuild_test_company_badge_index,
|
|
579
|
+
@company.employees,
|
|
580
|
+
:unique, # Correct cardinality
|
|
581
|
+
batch_size: 100
|
|
582
|
+
)
|
|
583
|
+
"no error"
|
|
584
|
+
rescue ArgumentError
|
|
585
|
+
"should not raise"
|
|
586
|
+
end
|
|
587
|
+
#=> "no error"
|
|
588
|
+
|
|
589
|
+
# Teardown
|
|
590
|
+
RebuildTestUser.email_lookup.clear
|
|
591
|
+
RebuildTestUser.username_lookup.clear
|
|
592
|
+
RebuildTestUser.instances.clear
|
|
593
|
+
@company.badge_index.clear
|
|
594
|
+
@company.employees.clear
|
|
595
|
+
# Clear all department index keys
|
|
596
|
+
["engineering", "sales", "marketing", "finance"].each do |dept|
|
|
597
|
+
@company.dept_index_for(dept).clear rescue nil
|
|
598
|
+
end
|
|
599
|
+
RebuildTestCompany.instances.clear
|
|
600
|
+
RebuildTestEmployee.instances.clear
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# try/features/relationships/indexing_try.rb
|
|
2
2
|
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
3
5
|
# Comprehensive tests for Familia indexing relationships functionality
|
|
4
6
|
# Tests both multi_index (parent-context) and unique_index (class-level) indexing
|
|
5
7
|
#
|
|
@@ -62,7 +64,7 @@ end
|
|
|
62
64
|
@user3.save
|
|
63
65
|
|
|
64
66
|
@company_id = "comp_#{rand(10000000)}"
|
|
65
|
-
@company = TestCompany.create(company_id: @company_id, name: 'Acme Corp')
|
|
67
|
+
@company = TestCompany.create!(company_id: @company_id, name: 'Acme Corp')
|
|
66
68
|
@emp1 = TestEmployee.new(emp_id: 'emp_001', email: 'alice@acme.com', department: 'engineering', manager_id: 'mgr_001', badge_number: 'BADGE001')
|
|
67
69
|
@emp1.save
|
|
68
70
|
@emp2 = TestEmployee.new(emp_id: 'emp_002', email: 'bob@acme.com', department: 'sales', manager_id: 'mgr_002', badge_number: 'BADGE002')
|
|
@@ -86,7 +88,7 @@ sample = @company.sample_from_department(@emp2.department)
|
|
|
86
88
|
|
|
87
89
|
## First indexing relationship has correct configuration
|
|
88
90
|
config = @user1.class.indexing_relationships.first
|
|
89
|
-
[config.field, config.index_name, config.
|
|
91
|
+
[config.field, config.index_name, config.scope_class == TestUser, config.query]
|
|
90
92
|
#=> [:email, :email_lookup, true, true]
|
|
91
93
|
|
|
92
94
|
## Second indexing relationship has query disabled
|
|
@@ -189,7 +191,7 @@ TestUser.respond_to?(:find_by_username)
|
|
|
189
191
|
|
|
190
192
|
## Instance-scoped unique index has correct configuration
|
|
191
193
|
config = @emp1.class.indexing_relationships.find { |r| r.field == :badge_number }
|
|
192
|
-
[config.index_name, config.
|
|
194
|
+
[config.index_name, config.scope_class, config.cardinality]
|
|
193
195
|
#=> [:badge_index, TestCompany, :unique]
|
|
194
196
|
|
|
195
197
|
## Target class gets finder method for unique index
|
|
@@ -251,6 +253,16 @@ found_emps = @company.find_all_by_badge_number('BADGE002')
|
|
|
251
253
|
found_emps.map(&:emp_id)
|
|
252
254
|
#=> ["emp_002"]
|
|
253
255
|
|
|
256
|
+
## Instance-scoped bulk query filters nil inputs
|
|
257
|
+
badges_with_nil = [nil, 'BADGE001', nil]
|
|
258
|
+
found_emps = @company.find_all_by_badge_number(badges_with_nil)
|
|
259
|
+
found_emps.map(&:emp_id)
|
|
260
|
+
#=> ["emp_001"]
|
|
261
|
+
|
|
262
|
+
## Instance-scoped bulk query with only nil returns empty
|
|
263
|
+
@company.find_all_by_badge_number([nil, nil]).length
|
|
264
|
+
#=> 0
|
|
265
|
+
|
|
254
266
|
## Update badge index entry
|
|
255
267
|
old_badge = @emp1.badge_number
|
|
256
268
|
@emp1.badge_number = 'BADGE001_NEW'
|
|
@@ -277,7 +289,7 @@ old_badge = @emp1.badge_number
|
|
|
277
289
|
|
|
278
290
|
## Context-scoped multi_index relationship has correct configuration
|
|
279
291
|
config = @emp1.class.indexing_relationships.find { |r| r.field == :department }
|
|
280
|
-
[config.field, config.index_name, config.
|
|
292
|
+
[config.field, config.index_name, config.scope_class]
|
|
281
293
|
#=> [:department, :dept_index, TestCompany]
|
|
282
294
|
|
|
283
295
|
## Context-scoped methods are generated with collision-free naming
|
|
@@ -420,6 +432,20 @@ found = TestUser.find_all_by_email(emails)
|
|
|
420
432
|
found.map(&:user_id)
|
|
421
433
|
#=> ["user_002"]
|
|
422
434
|
|
|
435
|
+
## Bulk query filters nil inputs before querying
|
|
436
|
+
emails_with_nil = [nil, 'bob@example.com', nil]
|
|
437
|
+
found = TestUser.find_all_by_email(emails_with_nil)
|
|
438
|
+
found.map(&:user_id)
|
|
439
|
+
#=> ["user_002"]
|
|
440
|
+
|
|
441
|
+
## Bulk query with only nil inputs returns empty array
|
|
442
|
+
TestUser.find_all_by_email([nil, nil]).length
|
|
443
|
+
#=> 0
|
|
444
|
+
|
|
445
|
+
## Bulk query with nil as single value returns empty array
|
|
446
|
+
TestUser.find_all_by_email(nil).length
|
|
447
|
+
#=> 0
|
|
448
|
+
|
|
423
449
|
## Adding to index with nil field value does nothing
|
|
424
450
|
@user_nil = TestUser.new(user_id: 'user_nil', email: nil)
|
|
425
451
|
@user_nil.add_to_class_email_lookup
|