familia 2.0.0.pre19 → 2.0.0.pre22
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/.talismanrc +5 -1
- data/CHANGELOG.rst +220 -112
- data/CLAUDE.md +28 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -17
- data/bin/try +16 -0
- data/bin/tryouts +16 -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 +161 -117
- 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-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 +4 -3
- 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 +2 -0
- data/lib/familia/connection/handlers.rb +2 -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 -2
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/pipelined_core.rb +3 -3
- data/lib/familia/connection/transaction_core.rb +69 -2
- data/lib/familia/connection.rb +18 -3
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +2 -0
- data/lib/familia/data_type/database_commands.rb +2 -0
- data/lib/familia/data_type/serialization.rb +79 -52
- 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 +7 -5
- 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 +7 -10
- data/lib/familia/data_type/types/stringkey.rb +24 -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 +2 -0
- 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 +3 -1
- data/lib/familia/features/expiration.rb +12 -4
- data/lib/familia/features/external_identifier.rb +62 -7
- data/lib/familia/features/object_identifier.rb +49 -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 +138 -9
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +97 -21
- data/lib/familia/features/relationships/indexing.rb +3 -0
- data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
- 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 +3 -1
- data/lib/familia/horreum/connection.rb +17 -1
- data/lib/familia/horreum/database_commands.rb +8 -1
- data/lib/familia/horreum/definition.rb +16 -6
- data/lib/familia/horreum/management.rb +353 -52
- data/lib/familia/horreum/persistence.rb +179 -108
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +23 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -0
- data/lib/familia/horreum.rb +15 -1
- data/lib/familia/identifier_extractor.rb +3 -1
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +92 -32
- 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 +2 -0
- 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 +295 -170
- data/lib/multi_result.rb +61 -31
- 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 +2 -0
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/count_any_edge_cases_try.rb +486 -0
- data/try/features/count_any_methods_try.rb +197 -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 +4 -0
- data/try/features/external_identifier/external_identifier_try.rb +305 -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 +140 -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 +606 -0
- data/try/features/relationships/indexing_try.rb +2 -0
- 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 +2 -0
- 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 +4 -0
- 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 +4 -0
- data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
- 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 +4 -0
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +9 -3
- data/try/integration/data_types/datatype_transactions_try.rb +17 -7
- 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 +4 -0
- data/try/integration/models/customer_try.rb +7 -3
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +4 -0
- data/try/integration/persistence_operations_try.rb +4 -0
- 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 +39 -22
- 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 +6 -2
- 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/serialization_try.rb +386 -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 +4 -0
- 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 +5 -1
- data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
- data/try/unit/horreum/base_try.rb +4 -0
- data/try/unit/horreum/class_methods_try.rb +4 -0
- 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 +6 -1
- 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 +4 -0
- 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 +4 -0
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +4 -0
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -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 +69 -17
- data/.github/workflows/code-quality.yml +0 -138
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
- data/changelog.d/20251011_203905_delano_next.rst +0 -30
- data/changelog.d/20251011_212633_delano_next.rst +0 -13
- data/changelog.d/20251011_221253_delano_next.rst +0 -26
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/features/external_identifier.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
module Features
|
|
@@ -12,8 +14,8 @@ module Familia
|
|
|
12
14
|
base.extend ModelClassMethods
|
|
13
15
|
base.include ModelInstanceMethods
|
|
14
16
|
|
|
15
|
-
# Ensure default
|
|
16
|
-
base.add_feature_options(:external_identifier,
|
|
17
|
+
# Ensure default format is set in feature options
|
|
18
|
+
base.add_feature_options(:external_identifier, format: 'ext_%{id}')
|
|
17
19
|
|
|
18
20
|
# Add class-level mapping for extid -> id lookups
|
|
19
21
|
base.class_hashkey :extid_lookup
|
|
@@ -35,10 +37,10 @@ module Familia
|
|
|
35
37
|
# - Deterministic generation from objid ensures consistency
|
|
36
38
|
# - Shorter than objid (128-bit vs 256-bit) for external use
|
|
37
39
|
# - Base-36 encoding for URL-safe identifiers
|
|
38
|
-
# - 'ext_' prefix
|
|
40
|
+
# - Customizable format template (default: 'ext_' prefix)
|
|
39
41
|
# - Lazy generation preserves values from initialization
|
|
40
42
|
#
|
|
41
|
-
# @example Using external identifier fields
|
|
43
|
+
# @example Using external identifier fields with default format
|
|
42
44
|
# class User < Familia::Horreum
|
|
43
45
|
# feature :object_identifier
|
|
44
46
|
# feature :external_identifier
|
|
@@ -51,6 +53,30 @@ module Familia
|
|
|
51
53
|
# user2 = User.new(objid: user.objid, email: 'user@example.com')
|
|
52
54
|
# user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
|
|
53
55
|
#
|
|
56
|
+
# @example Using custom format template with hyphen separator
|
|
57
|
+
# class APIKey < Familia::Horreum
|
|
58
|
+
# feature :object_identifier
|
|
59
|
+
# feature :external_identifier, format: 'api-%{id}'
|
|
60
|
+
# end
|
|
61
|
+
# key = APIKey.new
|
|
62
|
+
# key.extid # => "api-abc123def456ghi789"
|
|
63
|
+
#
|
|
64
|
+
# @example Using custom format template with custom prefix
|
|
65
|
+
# class Customer < Familia::Horreum
|
|
66
|
+
# feature :object_identifier
|
|
67
|
+
# feature :external_identifier, format: 'cust_%{id}'
|
|
68
|
+
# end
|
|
69
|
+
# customer = Customer.new
|
|
70
|
+
# customer.extid # => "cust_abc123def456ghi789"
|
|
71
|
+
#
|
|
72
|
+
# @example Using format template without prefix
|
|
73
|
+
# class Resource < Familia::Horreum
|
|
74
|
+
# feature :object_identifier
|
|
75
|
+
# feature :external_identifier, format: 'v2/%{id}'
|
|
76
|
+
# end
|
|
77
|
+
# resource = Resource.new
|
|
78
|
+
# resource.extid # => "v2/abc123def456ghi789"
|
|
79
|
+
#
|
|
54
80
|
class ExternalIdentifierFieldType < Familia::FieldType
|
|
55
81
|
# Override getter to provide lazy generation from objid
|
|
56
82
|
#
|
|
@@ -150,6 +176,35 @@ module Familia
|
|
|
150
176
|
# extid_lookup.remove_field(extid)
|
|
151
177
|
nil
|
|
152
178
|
end
|
|
179
|
+
|
|
180
|
+
# Check if a string matches the extid format for the Horreum class. The specific
|
|
181
|
+
# class is important b/c each one can have its own custom prefix, like `ext_`.
|
|
182
|
+
#
|
|
183
|
+
# The validator accepts a reasonable range of ID lengths (20-32 characters) to
|
|
184
|
+
# accommodate potential changes to the entropy or encoding while maintaining
|
|
185
|
+
# security. The current implementation generates exactly 25 base36 characters
|
|
186
|
+
# from 16 bytes (128 bits), but this flexibility allows for future adjustments
|
|
187
|
+
# without breaking validation.
|
|
188
|
+
#
|
|
189
|
+
# @param guess [String] The string to check
|
|
190
|
+
# @return [Boolean] true if the guess matches the extid format, false otherwise
|
|
191
|
+
def extid?(guess)
|
|
192
|
+
return false if guess.to_s.empty?
|
|
193
|
+
|
|
194
|
+
options = feature_options(:external_identifier)
|
|
195
|
+
format = options[:format] || 'ext_%{id}'
|
|
196
|
+
|
|
197
|
+
# Extract prefix and suffix from format
|
|
198
|
+
return false unless format.include?('%{id}')
|
|
199
|
+
prefix, suffix = format.split('%{id}', 2)
|
|
200
|
+
|
|
201
|
+
# Build regex pattern to match the extid format
|
|
202
|
+
# Accept 20-32 base36 characters to allow for entropy/encoding variations
|
|
203
|
+
# Current generation: 16 bytes -> base36 -> 25 chars (rjust with '0')
|
|
204
|
+
pattern = /\A#{Regexp.escape(prefix)}[0-9a-z]{20,32}#{Regexp.escape(suffix)}\z/i
|
|
205
|
+
|
|
206
|
+
!!(guess =~ pattern)
|
|
207
|
+
end
|
|
153
208
|
end
|
|
154
209
|
|
|
155
210
|
# Instance methods for external identifier management
|
|
@@ -252,11 +307,11 @@ module Familia
|
|
|
252
307
|
# 128 bits is approximately 25 characters in base36.
|
|
253
308
|
external_part = random_bytes.unpack1('H*').to_i(16).to_s(36).rjust(25, '0')
|
|
254
309
|
|
|
255
|
-
# Get
|
|
310
|
+
# Get format from feature options and interpolate the ID
|
|
256
311
|
options = self.class.feature_options(:external_identifier)
|
|
257
|
-
|
|
312
|
+
format = options[:format] || 'ext_%{id}'
|
|
258
313
|
|
|
259
|
-
|
|
314
|
+
format % { id: external_part }
|
|
260
315
|
end
|
|
261
316
|
|
|
262
317
|
# Full-length alias for extid for clarity when needed
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/features/object_identifier.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
module Features
|
|
@@ -277,6 +279,53 @@ module Familia
|
|
|
277
279
|
# objid_lookup.remove_field(objid)
|
|
278
280
|
nil
|
|
279
281
|
end
|
|
282
|
+
|
|
283
|
+
# Check if a string matches the objid format for the Horreum class. The specific
|
|
284
|
+
# class is important b/c each one can have its own type of objid generator.
|
|
285
|
+
#
|
|
286
|
+
# @param guess [String] The string to check
|
|
287
|
+
# @return [Boolean] true if the guess matches the objid format, false otherwise
|
|
288
|
+
def objid?(guess)
|
|
289
|
+
return false if guess.to_s.empty?
|
|
290
|
+
|
|
291
|
+
options = feature_options(:object_identifier)
|
|
292
|
+
generator = options[:generator] || DEFAULT_GENERATOR
|
|
293
|
+
|
|
294
|
+
case generator
|
|
295
|
+
when :uuid_v7, :uuid_v4
|
|
296
|
+
# UUID format: xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx (36 chars with hyphens)
|
|
297
|
+
# Validate structure and that all characters are valid hex digits
|
|
298
|
+
guess_str = guess.to_s
|
|
299
|
+
return false unless guess_str.length == 36
|
|
300
|
+
return false unless guess_str[8] == '-' && guess_str[13] == '-' && guess_str[18] == '-' && guess_str[23] == '-'
|
|
301
|
+
|
|
302
|
+
# Extract segments and validate each is valid hex
|
|
303
|
+
segments = guess_str.split('-')
|
|
304
|
+
return false unless segments.length == 5
|
|
305
|
+
return false unless segments[0] =~ /\A[0-9a-fA-F]{8}\z/ # 8 hex chars
|
|
306
|
+
return false unless segments[1] =~ /\A[0-9a-fA-F]{4}\z/ # 4 hex chars
|
|
307
|
+
return false unless segments[2] =~ /\A[0-9a-fA-F]{4}\z/ # 4 hex chars (includes version)
|
|
308
|
+
return false unless segments[3] =~ /\A[0-9a-fA-F]{4}\z/ # 4 hex chars
|
|
309
|
+
return false unless segments[4] =~ /\A[0-9a-fA-F]{12}\z/ # 12 hex chars
|
|
310
|
+
|
|
311
|
+
# Validate version character
|
|
312
|
+
version_char = guess_str[14]
|
|
313
|
+
if generator == :uuid_v7
|
|
314
|
+
version_char == '7'
|
|
315
|
+
else # generator == :uuid_v4
|
|
316
|
+
version_char == '4'
|
|
317
|
+
end
|
|
318
|
+
when :hex
|
|
319
|
+
# Hex format: pure hexadecimal without hyphens
|
|
320
|
+
!!(guess =~ /\A[0-9a-fA-F]+\z/)
|
|
321
|
+
when Proc
|
|
322
|
+
# Cannot determine format for custom Proc generators
|
|
323
|
+
Familia.warn "[objid?] Validation not supported for custom Proc generators on #{name}" if Familia.debug?
|
|
324
|
+
false
|
|
325
|
+
else
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
end
|
|
280
329
|
end
|
|
281
330
|
|
|
282
331
|
# Instance methods for object identifier management
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/features/quantization.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
module Features
|
|
@@ -389,7 +391,7 @@ module Familia
|
|
|
389
391
|
# user.quantized_identifier(1.hour) # => "123:1672531200"
|
|
390
392
|
# user.quantized_identifier(1.hour, pattern: '%Y%m%d%H') # => "123:2023010114"
|
|
391
393
|
#
|
|
392
|
-
def quantized_identifier(quantum, pattern: nil, separator:
|
|
394
|
+
def quantized_identifier(quantum, pattern: nil, separator: Familia.delim)
|
|
393
395
|
timestamp = qstamp(quantum, pattern: pattern)
|
|
394
396
|
base_id = respond_to?(:identifier) ? identifier : object_id
|
|
395
397
|
"#{base_id}#{separator}#{timestamp}"
|
|
@@ -18,6 +18,7 @@ participates_in Organization, :members, score: :joined_at, bidirectional: true
|
|
|
18
18
|
|
|
19
19
|
**unique_index** - Fast unique lookups ("find object by unique field value")
|
|
20
20
|
```ruby
|
|
21
|
+
unique_index :email, :email_index # Class-level: User.find_by_email()
|
|
21
22
|
unique_index :email, :email_index, within: Organization # Scoped: org.find_by_email()
|
|
22
23
|
```
|
|
23
24
|
|
|
@@ -89,7 +90,8 @@ class Customer < Familia::Horreum
|
|
|
89
90
|
feature :relationships
|
|
90
91
|
|
|
91
92
|
participates_in Organization, :members # Customer belongs to org
|
|
92
|
-
unique_index :email, :email_index
|
|
93
|
+
unique_index :email, :email_index # Class-level: Customer.find_by_email()
|
|
94
|
+
multi_index :status, :status_index, within: Organization # Scoped: org.find_all_by_status()
|
|
93
95
|
end
|
|
94
96
|
```
|
|
95
97
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# lib/familia/features/relationships/indexing/multi_index_generators.rb
|
|
2
|
+
#
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
@@ -78,7 +80,7 @@ module Familia
|
|
|
78
80
|
# This acts as a factory for field-value-specific DataTypes
|
|
79
81
|
define_method(:"#{index_name}_for") do |field_value|
|
|
80
82
|
# Return properly managed DataType instance with parameterized key
|
|
81
|
-
index_key =
|
|
83
|
+
index_key = Familia.join(index_name, field_value)
|
|
82
84
|
Familia::UnsortedSet.new(index_key, parent: self)
|
|
83
85
|
end
|
|
84
86
|
end
|
|
@@ -97,6 +99,9 @@ module Familia
|
|
|
97
99
|
# Resolve scope class using Familia pattern
|
|
98
100
|
actual_scope_class = Familia.resolve_class(scope_class)
|
|
99
101
|
|
|
102
|
+
# Get scope_class_config for method naming (needed for rebuild methods)
|
|
103
|
+
scope_class_config = actual_scope_class.config_name
|
|
104
|
+
|
|
100
105
|
# Generate instance sampling method (e.g., company.sample_from_department)
|
|
101
106
|
actual_scope_class.class_eval do
|
|
102
107
|
|
|
@@ -118,11 +123,135 @@ module Familia
|
|
|
118
123
|
index_set.members.map { |id| indexed_class.find_by_identifier(id) }
|
|
119
124
|
end
|
|
120
125
|
|
|
121
|
-
# Generate method to rebuild the index for this parent instance
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
# Generate method to rebuild the multi-value index for this parent instance
|
|
127
|
+
#
|
|
128
|
+
# Multi-indexes create separate sets for each field value, requiring a three-phase approach:
|
|
129
|
+
# 1. Loading: Load all objects once and cache them (discovers field values simultaneously)
|
|
130
|
+
# 2. Clearing: Remove all existing index sets using SCAN
|
|
131
|
+
# 3. Rebuilding: Rebuild index from cached objects (no reload needed)
|
|
132
|
+
#
|
|
133
|
+
# @param batch_size [Integer] Number of identifiers to process per batch
|
|
134
|
+
# @yield [progress] Optional block called with progress updates
|
|
135
|
+
# @yieldparam progress [Hash] Progress information with keys:
|
|
136
|
+
# - :phase [Symbol] Current phase (:loading, :clearing, :rebuilding)
|
|
137
|
+
# - :current [Integer] Current item count
|
|
138
|
+
# - :total [Integer] Total items (when known)
|
|
139
|
+
# - :field_value [String] Current field value being processed
|
|
140
|
+
#
|
|
141
|
+
# @example Basic rebuild
|
|
142
|
+
# company.rebuild_dept_index
|
|
143
|
+
#
|
|
144
|
+
# @example With progress monitoring
|
|
145
|
+
# company.rebuild_dept_index do |progress|
|
|
146
|
+
# puts "#{progress[:phase]}: #{progress[:current]}/#{progress[:total]}"
|
|
147
|
+
# end
|
|
148
|
+
#
|
|
149
|
+
# @example Memory-conscious rebuild for large collections
|
|
150
|
+
# # Process in smaller batches to reduce memory footprint
|
|
151
|
+
# company.rebuild_dept_index(batch_size: 50)
|
|
152
|
+
#
|
|
153
|
+
# @note Memory Considerations:
|
|
154
|
+
# This method caches all objects in memory during rebuild to avoid duplicate
|
|
155
|
+
# database loads. For very large collections (>100k objects), monitor memory usage
|
|
156
|
+
# and consider processing in chunks or using a streaming approach if memory
|
|
157
|
+
# constraints are encountered. The batch_size parameter controls Redis I/O
|
|
158
|
+
# batching but does not affect memory usage since all objects are cached.
|
|
159
|
+
#
|
|
160
|
+
define_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
|
|
161
|
+
# PHASE 1: Find the collection containing the indexed objects
|
|
162
|
+
# Look for a participation relationship where indexed_class participates in this scope_class
|
|
163
|
+
collection_name = nil
|
|
164
|
+
|
|
165
|
+
# Check if indexed_class has participation to this scope_class
|
|
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
|
+
collection_name = participation&.collection_name if participation
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get the collection DataType if we found a participation relationship
|
|
174
|
+
collection = collection_name ? send(collection_name) : nil
|
|
175
|
+
|
|
176
|
+
if collection
|
|
177
|
+
# PHASE 2: Load objects once and cache them for both discovery and rebuilding
|
|
178
|
+
# This avoids duplicate load_multi calls (previous approach loaded twice)
|
|
179
|
+
progress_block&.call(phase: :loading, current: 0, total: collection.size)
|
|
180
|
+
|
|
181
|
+
field_values = Set.new
|
|
182
|
+
cached_objects = []
|
|
183
|
+
processed = 0
|
|
184
|
+
|
|
185
|
+
collection.members.each_slice(batch_size) do |identifiers|
|
|
186
|
+
# Load objects in batches - SINGLE LOAD for both phases
|
|
187
|
+
objects = indexed_class.load_multi(identifiers).compact
|
|
188
|
+
cached_objects.concat(objects)
|
|
189
|
+
|
|
190
|
+
objects.each do |obj|
|
|
191
|
+
value = obj.send(field)
|
|
192
|
+
# Only track non-nil, non-empty field values
|
|
193
|
+
field_values << value.to_s if value && !value.to_s.strip.empty?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
processed += identifiers.size
|
|
197
|
+
progress_block&.call(phase: :loading, current: processed, total: collection.size)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# PHASE 3: Clear all existing field-value-specific index sets
|
|
201
|
+
# Use SCAN to find all existing index keys (including orphaned ones from deleted field values)
|
|
202
|
+
progress_block&.call(phase: :clearing, current: 0, total: field_values.size)
|
|
203
|
+
|
|
204
|
+
# Get the base pattern for this index by creating a sample index set
|
|
205
|
+
# The "*" creates a wildcard pattern like "company:123:dept_index:*" for SCAN
|
|
206
|
+
sample_index = send(:"#{index_name}_for", "*")
|
|
207
|
+
index_pattern = sample_index.dbkey
|
|
208
|
+
|
|
209
|
+
# Find all existing index keys using SCAN
|
|
210
|
+
cleared_count = 0
|
|
211
|
+
dbclient.scan_each(match: index_pattern) do |key|
|
|
212
|
+
dbclient.del(key)
|
|
213
|
+
cleared_count += 1
|
|
214
|
+
progress_block&.call(phase: :clearing, current: cleared_count, total: field_values.size, key: key)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# PHASE 4: Rebuild index from cached objects (no reload needed)
|
|
218
|
+
progress_block&.call(phase: :rebuilding, current: 0, total: cached_objects.size)
|
|
219
|
+
|
|
220
|
+
processed = 0
|
|
221
|
+
cached_objects.each_slice(batch_size) do |objects|
|
|
222
|
+
transaction do |_tx|
|
|
223
|
+
objects.each do |obj|
|
|
224
|
+
# Use the generated add_to method to maintain consistency
|
|
225
|
+
# This ensures the same logic is used as during normal operation
|
|
226
|
+
obj.send(:"add_to_#{scope_class_config}_#{index_name}", self)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
processed += objects.size
|
|
231
|
+
progress_block&.call(phase: :rebuilding, current: processed, total: cached_objects.size)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
Familia.info "[Rebuild] Multi-index #{index_name} rebuilt: #{field_values.size} field values, #{processed} objects"
|
|
235
|
+
|
|
236
|
+
processed # Return count of processed objects
|
|
237
|
+
|
|
238
|
+
else
|
|
239
|
+
# No participation relationship found - warn and suggest alternative
|
|
240
|
+
Familia.warn <<~WARNING
|
|
241
|
+
[Rebuild] Cannot rebuild multi-index #{index_name}: no participation relationship found
|
|
242
|
+
|
|
243
|
+
Multi-index rebuild requires a participation relationship to find objects.
|
|
244
|
+
Add a participation relationship to #{indexed_class.name}:
|
|
245
|
+
|
|
246
|
+
class #{indexed_class.name} < Familia::Horreum
|
|
247
|
+
participates_in #{self.class.name}, :collection_name, score: :field
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
Then access the collection via: #{self.class.config_name}.collection_name
|
|
251
|
+
WARNING
|
|
252
|
+
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
126
255
|
end
|
|
127
256
|
end
|
|
128
257
|
end
|
|
@@ -140,7 +269,7 @@ module Familia
|
|
|
140
269
|
scope_class_config = scope_class.config_name
|
|
141
270
|
indexed_class.class_eval do
|
|
142
271
|
method_name = :"add_to_#{scope_class_config}_#{index_name}"
|
|
143
|
-
Familia.
|
|
272
|
+
Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
|
|
144
273
|
|
|
145
274
|
define_method(method_name) do |scope_instance|
|
|
146
275
|
return unless scope_instance
|
|
@@ -156,7 +285,7 @@ module Familia
|
|
|
156
285
|
end
|
|
157
286
|
|
|
158
287
|
method_name = :"remove_from_#{scope_class_config}_#{index_name}"
|
|
159
|
-
Familia.
|
|
288
|
+
Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
|
|
160
289
|
|
|
161
290
|
define_method(method_name) do |scope_instance|
|
|
162
291
|
return unless scope_instance
|
|
@@ -172,7 +301,7 @@ module Familia
|
|
|
172
301
|
end
|
|
173
302
|
|
|
174
303
|
method_name = :"update_in_#{scope_class_config}_#{index_name}"
|
|
175
|
-
Familia.
|
|
304
|
+
Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
|
|
176
305
|
|
|
177
306
|
define_method(method_name) do |scope_instance, old_field_value = nil|
|
|
178
307
|
return unless scope_instance
|