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
|
@@ -12,424 +12,555 @@
|
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
14
14
|
|
|
15
|
-
The Object Identifier feature provides automatic generation of unique identifiers for Familia objects. Instead of manually creating identifiers, you can configure different generation strategies that suit your application's needs - from globally unique UUIDs to
|
|
15
|
+
The Object Identifier feature provides automatic generation of unique identifiers for Familia objects. Instead of manually creating identifiers, you can configure different generation strategies that suit your application's needs - from globally unique UUIDs to high-entropy hexadecimal strings.
|
|
16
16
|
|
|
17
17
|
## Why Use Object Identifiers?
|
|
18
18
|
|
|
19
|
-
**
|
|
19
|
+
**Automatic Generation**: No manual ID management - identifiers are generated lazily when first accessed.
|
|
20
20
|
|
|
21
|
-
**
|
|
21
|
+
**Configurable Strategies**: Choose from UUID v7 (timestamped), UUID v4 (random), hex (high-entropy), or custom generators.
|
|
22
22
|
|
|
23
|
-
**
|
|
23
|
+
**Provenance Tracking**: System tracks which generator created each ID for security and debugging.
|
|
24
24
|
|
|
25
|
-
**
|
|
25
|
+
**Data Integrity**: Preserves existing IDs during initialization - never overwrites loaded data.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
**Lookup Support**: Automatic bidirectional mapping enables finding objects by their objid.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
## Generation Strategies
|
|
30
|
+
|
|
31
|
+
### UUID v7 (Default)
|
|
32
|
+
|
|
33
|
+
UUID version 7 with embedded timestamp for natural sorting:
|
|
30
34
|
|
|
31
35
|
```ruby
|
|
32
36
|
class User < Familia::Horreum
|
|
33
|
-
feature :object_identifier
|
|
37
|
+
feature :object_identifier # Uses :uuid_v7 by default
|
|
34
38
|
|
|
35
|
-
field :
|
|
39
|
+
field :email, :name
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
user
|
|
40
|
-
puts user.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
|
42
|
+
user = User.new(email: 'alice@example.com')
|
|
43
|
+
user.objid # => "01234567-89ab-7def-8000-123456789abc"
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
**Characteristics:**
|
|
47
|
+
- 128-bit identifier with embedded timestamp
|
|
48
|
+
- Naturally sortable by creation time
|
|
49
|
+
- Globally unique across distributed systems
|
|
50
|
+
- May leak timing information (consider for security-sensitive apps)
|
|
51
|
+
|
|
52
|
+
### UUID v4 (Random)
|
|
53
|
+
|
|
54
|
+
UUID version 4 for legacy compatibility and maximum randomness:
|
|
44
55
|
|
|
45
56
|
```ruby
|
|
46
|
-
class
|
|
47
|
-
feature :object_identifier, generator: :
|
|
57
|
+
class LegacyUser < Familia::Horreum
|
|
58
|
+
feature :object_identifier, generator: :uuid_v4
|
|
48
59
|
|
|
49
|
-
field :
|
|
60
|
+
field :email, :username
|
|
50
61
|
end
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
user = LegacyUser.new(email: 'bob@example.com')
|
|
64
|
+
user.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
|
54
65
|
```
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
**Characteristics:**
|
|
68
|
+
- 122-bit random identifier (6 bits for version/variant)
|
|
69
|
+
- No timing correlation for enhanced security
|
|
70
|
+
- Widely supported format
|
|
71
|
+
- Compatible with existing UUID systems
|
|
57
72
|
|
|
58
|
-
###
|
|
73
|
+
### High-Entropy Hex
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
256-bit hexadecimal for security-critical applications:
|
|
61
76
|
|
|
62
77
|
```ruby
|
|
63
|
-
class
|
|
64
|
-
feature :object_identifier, generator: :
|
|
65
|
-
|
|
78
|
+
class SecureDocument < Familia::Horreum
|
|
79
|
+
feature :object_identifier, generator: :hex
|
|
80
|
+
|
|
81
|
+
field :title, :classification
|
|
66
82
|
end
|
|
67
83
|
|
|
68
|
-
doc =
|
|
69
|
-
doc.objid # => "
|
|
84
|
+
doc = SecureDocument.new(title: 'Classified Report')
|
|
85
|
+
doc.objid # => "a1b2c3d4e5f6789012345678901234567890abcdef..." (64 chars)
|
|
70
86
|
```
|
|
71
87
|
|
|
72
88
|
**Characteristics:**
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
> **💡 Best Practice**
|
|
79
|
-
>
|
|
80
|
-
> Use UUID v4 for objects that will be exposed externally or across service boundaries.
|
|
89
|
+
- Maximum entropy (256 bits of randomness)
|
|
90
|
+
- No structure or timing information
|
|
91
|
+
- Compact representation without hyphens
|
|
92
|
+
- Ideal for security-sensitive applications
|
|
81
93
|
|
|
82
|
-
###
|
|
94
|
+
### Custom Generator
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
Provide your own generation logic:
|
|
85
97
|
|
|
86
98
|
```ruby
|
|
87
|
-
class
|
|
88
|
-
feature :object_identifier,
|
|
89
|
-
|
|
99
|
+
class TimestampedItem < Familia::Horreum
|
|
100
|
+
feature :object_identifier,
|
|
101
|
+
generator: -> { "item_#{Familia.now.to_i}_#{SecureRandom.hex(4)}" }
|
|
102
|
+
|
|
103
|
+
field :data, :category
|
|
90
104
|
end
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
item = TimestampedItem.new(data: 'test')
|
|
107
|
+
item.objid # => "item_1693857600_a1b2c3d4"
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
**
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
110
|
+
**Custom Generator Requirements:**
|
|
111
|
+
- Must be callable (Proc, lambda, or respond to `call`)
|
|
112
|
+
- Should return unique strings
|
|
113
|
+
- Avoid collision-prone patterns
|
|
114
|
+
|
|
115
|
+
## Basic Usage
|
|
116
|
+
|
|
117
|
+
### Lazy Generation
|
|
118
|
+
|
|
119
|
+
Object identifiers are only generated when first accessed:
|
|
100
120
|
|
|
101
121
|
```ruby
|
|
102
|
-
|
|
103
|
-
feature :object_identifier,
|
|
104
|
-
generator: :hex,
|
|
105
|
-
length: 16,
|
|
106
|
-
prefix: "tk_"
|
|
122
|
+
user = User.new(email: 'test@example.com')
|
|
107
123
|
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
# No objid generated yet
|
|
125
|
+
user.instance_variable_get(:@objid) # => nil
|
|
126
|
+
|
|
127
|
+
# First access triggers generation
|
|
128
|
+
user.objid # => "01234567-89ab-7def-8000-123456789abc"
|
|
110
129
|
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
# Subsequent access returns cached value
|
|
131
|
+
user.objid # => "01234567-89ab-7def-8000-123456789abc" (same)
|
|
113
132
|
```
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
- **Format**: Configurable length hexadecimal string
|
|
117
|
-
- **Performance**: Very fast generation
|
|
118
|
-
- **Storage**: Compact representation
|
|
119
|
-
- **Use Cases**: Internal IDs, tokens, session identifiers
|
|
134
|
+
### Preserved During Initialization
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
>
|
|
123
|
-
> Hex generators provide good uniqueness but aren't globally unique like UUIDs. Use appropriate length for your collision tolerance.
|
|
136
|
+
Existing IDs are never overwritten:
|
|
124
137
|
|
|
125
|
-
|
|
138
|
+
```ruby
|
|
139
|
+
# Loading existing object from database
|
|
140
|
+
existing_user = User.new(
|
|
141
|
+
objid: '01234567-89ab-7def-8000-existing123',
|
|
142
|
+
email: 'existing@example.com'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Existing ID is preserved, not regenerated
|
|
146
|
+
existing_user.objid # => "01234567-89ab-7def-8000-existing123"
|
|
147
|
+
```
|
|
126
148
|
|
|
127
|
-
|
|
149
|
+
### Finding by Object ID
|
|
128
150
|
|
|
129
151
|
```ruby
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
# Save user first
|
|
153
|
+
user = User.new(email: 'findme@example.com')
|
|
154
|
+
user.save
|
|
155
|
+
puts user.objid # => "01234567-89ab-7def-8000-123456789abc"
|
|
132
156
|
|
|
133
|
-
|
|
157
|
+
# Find by object identifier
|
|
158
|
+
found = User.find_by_objid('01234567-89ab-7def-8000-123456789abc')
|
|
159
|
+
found.email # => "findme@example.com"
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
end
|
|
141
|
-
end
|
|
161
|
+
# Returns nil if not found
|
|
162
|
+
missing = User.find_by_objid('nonexistent') # => nil
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Long-form Methods
|
|
142
166
|
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
```ruby
|
|
168
|
+
user.object_identifier # Same as user.objid
|
|
169
|
+
user.object_identifier = 'new' # Same as user.objid = 'new'
|
|
145
170
|
```
|
|
146
171
|
|
|
147
|
-
|
|
148
|
-
- Must define `self.generate_identifier` class method
|
|
149
|
-
- Should return a string identifier
|
|
150
|
-
- Must handle uniqueness and collision scenarios
|
|
151
|
-
- Consider thread safety for concurrent access
|
|
172
|
+
## Provenance Tracking
|
|
152
173
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
The system tracks which generator created each objid:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Generated by the system
|
|
178
|
+
user = User.new
|
|
179
|
+
user.objid # Triggers generation
|
|
180
|
+
user.objid_generator_used # => :uuid_v7
|
|
181
|
+
|
|
182
|
+
# Loaded from database (provenance inferred from format)
|
|
183
|
+
loaded = User.new(objid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479')
|
|
184
|
+
loaded.objid_generator_used # => :uuid_v4 (inferred from format)
|
|
185
|
+
|
|
186
|
+
# Unknown format
|
|
187
|
+
custom = User.new(objid: 'custom-format-id')
|
|
188
|
+
custom.objid_generator_used # => nil (unknown provenance)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Why Provenance Matters:**
|
|
192
|
+
- Security features like ExternalIdentifier require known provenance
|
|
193
|
+
- Debugging and auditing benefit from generator tracking
|
|
194
|
+
- Format validation can be performed based on expected generator
|
|
156
195
|
|
|
157
|
-
##
|
|
196
|
+
## Lookup Management
|
|
158
197
|
|
|
159
|
-
###
|
|
198
|
+
### Automatic Mapping
|
|
160
199
|
|
|
161
|
-
|
|
200
|
+
The feature maintains lookup tables for objid-to-primary-key mapping:
|
|
162
201
|
|
|
163
202
|
```ruby
|
|
164
203
|
class Product < Familia::Horreum
|
|
165
|
-
feature :object_identifier
|
|
166
|
-
|
|
167
|
-
collision_check: true,
|
|
168
|
-
max_retries: 5
|
|
204
|
+
feature :object_identifier
|
|
205
|
+
identifier_field :product_code # Different from objid
|
|
169
206
|
|
|
170
|
-
field :
|
|
207
|
+
field :product_code, :name, :price
|
|
171
208
|
end
|
|
209
|
+
|
|
210
|
+
product = Product.new(product_code: 'PROD123', name: 'Widget')
|
|
211
|
+
product.save
|
|
212
|
+
|
|
213
|
+
# Lookup table maps objid to primary key
|
|
214
|
+
Product.objid_lookup.class # => Familia::DataType::HashKey
|
|
215
|
+
Product.objid_lookup[product.objid] # => "PROD123"
|
|
172
216
|
```
|
|
173
217
|
|
|
174
|
-
|
|
175
|
-
- `collision_check`: Enable/disable collision detection (default: true)
|
|
176
|
-
- `max_retries`: Maximum retry attempts on collision (default: 3)
|
|
177
|
-
- `retry_delay`: Delay between retries in seconds (default: 0.001)
|
|
218
|
+
### Cleanup on Destroy
|
|
178
219
|
|
|
179
|
-
|
|
220
|
+
```ruby
|
|
221
|
+
product.destroy!
|
|
222
|
+
|
|
223
|
+
# Lookup entry is automatically cleaned up
|
|
224
|
+
Product.objid_lookup[product.objid] # => nil
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Integration Patterns
|
|
180
228
|
|
|
181
|
-
|
|
229
|
+
### Using objid as Primary Key
|
|
182
230
|
|
|
183
231
|
```ruby
|
|
184
|
-
class
|
|
185
|
-
feature :object_identifier
|
|
186
|
-
|
|
187
|
-
def self.generate_identifier
|
|
188
|
-
loop do
|
|
189
|
-
candidate = SecureRandom.alphanumeric(32)
|
|
190
|
-
# Ensure no ambiguous characters
|
|
191
|
-
next if candidate.match?(/[0O1lI]/)
|
|
192
|
-
return "st_#{candidate.downcase}"
|
|
193
|
-
end
|
|
194
|
-
end
|
|
232
|
+
class SimpleModel < Familia::Horreum
|
|
233
|
+
feature :object_identifier
|
|
234
|
+
identifier_field :objid # Use objid as the primary key
|
|
195
235
|
|
|
196
|
-
|
|
197
|
-
id.match?(/^st_[a-z0-9]{32}$/) && !id.match?(/[0O1lI]/)
|
|
198
|
-
end
|
|
236
|
+
field :data, :status
|
|
199
237
|
end
|
|
200
|
-
```
|
|
201
238
|
|
|
202
|
-
|
|
239
|
+
model = SimpleModel.new(data: 'test')
|
|
240
|
+
model.save
|
|
203
241
|
|
|
204
|
-
|
|
242
|
+
# No separate lookup needed - objid is the primary key
|
|
243
|
+
SimpleModel.find_by_objid(model.objid) == SimpleModel.find(model.objid) # => true
|
|
244
|
+
```
|
|
205
245
|
|
|
206
|
-
|
|
246
|
+
### Combining with External Identifiers
|
|
207
247
|
|
|
208
248
|
```ruby
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
x.report("Hex 12:") { 10_000.times { SecureRandom.hex(6) } }
|
|
215
|
-
x.report("Hex 24:") { 10_000.times { SecureRandom.hex(12) } }
|
|
216
|
-
x.report("Custom:") { 10_000.times { MyClass.generate_identifier } }
|
|
249
|
+
class User < Familia::Horreum
|
|
250
|
+
feature :object_identifier # Provides internal objid
|
|
251
|
+
feature :external_identifier # Derives public extid from objid
|
|
252
|
+
|
|
253
|
+
field :email, :name
|
|
217
254
|
end
|
|
255
|
+
|
|
256
|
+
user = User.new(email: 'test@example.com')
|
|
257
|
+
user.save
|
|
258
|
+
|
|
259
|
+
user.objid # => "01234567-89ab-7def-8000-123456789abc" (internal)
|
|
260
|
+
user.extid # => "ext_abc123def456ghi789jkl" (public-facing)
|
|
218
261
|
```
|
|
219
262
|
|
|
220
|
-
###
|
|
263
|
+
### API Design Patterns
|
|
221
264
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
265
|
+
```ruby
|
|
266
|
+
# Internal operations use objid
|
|
267
|
+
def sync_user_data(objid)
|
|
268
|
+
user = User.find_by_objid(objid)
|
|
269
|
+
# ... sync logic
|
|
270
|
+
end
|
|
225
271
|
|
|
226
|
-
|
|
272
|
+
# Public APIs use external identifiers
|
|
273
|
+
class UsersController
|
|
274
|
+
def show
|
|
275
|
+
@user = User.find_by_extid(params[:id]) # Public ID
|
|
276
|
+
render json: {
|
|
277
|
+
id: @user.extid, # Hide internal objid
|
|
278
|
+
email: @user.email,
|
|
279
|
+
name: @user.name
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Error Handling
|
|
227
286
|
|
|
228
|
-
|
|
287
|
+
### Invalid Generator Configuration
|
|
229
288
|
|
|
230
289
|
```ruby
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
1 - Math.exp(-(count * (count - 1)) / (2.0 * total_space))
|
|
290
|
+
# ❌ Invalid generator type
|
|
291
|
+
class BadModel < Familia::Horreum
|
|
292
|
+
feature :object_identifier, generator: :invalid_type
|
|
235
293
|
end
|
|
236
294
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
295
|
+
model = BadModel.new
|
|
296
|
+
model.objid
|
|
297
|
+
# => Familia::Problem: Invalid object identifier generator: :invalid_type
|
|
240
298
|
```
|
|
241
299
|
|
|
242
|
-
|
|
243
|
-
>
|
|
244
|
-
> - **8 hex chars**: Good for < 10K objects
|
|
245
|
-
> - **12 hex chars**: Good for < 1M objects
|
|
246
|
-
> - **16 hex chars**: Good for < 100M objects
|
|
247
|
-
> - **UUID v4**: Suitable for any scale
|
|
300
|
+
### Custom Generator Errors
|
|
248
301
|
|
|
249
|
-
|
|
302
|
+
```ruby
|
|
303
|
+
# ❌ Non-callable generator
|
|
304
|
+
class BadCustomModel < Familia::Horreum
|
|
305
|
+
feature :object_identifier, generator: "not_callable"
|
|
306
|
+
end
|
|
250
307
|
|
|
251
|
-
|
|
308
|
+
model = BadCustomModel.new
|
|
309
|
+
model.objid
|
|
310
|
+
# => Familia::Problem: Invalid object identifier generator: "not_callable"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Testing Strategies
|
|
314
|
+
|
|
315
|
+
### Basic Generation Testing
|
|
252
316
|
|
|
253
317
|
```ruby
|
|
254
|
-
class
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
318
|
+
class ObjectIdentifierTest < Minitest::Test
|
|
319
|
+
def test_lazy_generation
|
|
320
|
+
user = User.new(email: 'test@example.com')
|
|
321
|
+
|
|
322
|
+
# No objid until first access
|
|
323
|
+
assert_nil user.instance_variable_get(:@objid)
|
|
324
|
+
|
|
325
|
+
# First access generates ID
|
|
326
|
+
objid = user.objid
|
|
327
|
+
assert_match UUID_V7_PATTERN, objid
|
|
328
|
+
|
|
329
|
+
# Subsequent access returns same ID
|
|
330
|
+
assert_equal objid, user.objid
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_preserves_existing_objid
|
|
334
|
+
existing_id = '01234567-89ab-7def-8000-existing123'
|
|
335
|
+
user = User.new(objid: existing_id, email: 'test@example.com')
|
|
336
|
+
|
|
337
|
+
assert_equal existing_id, user.objid
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_find_by_objid
|
|
341
|
+
user = User.new(email: 'findme@example.com')
|
|
342
|
+
user.save
|
|
343
|
+
|
|
344
|
+
found = User.find_by_objid(user.objid)
|
|
345
|
+
assert_equal user.email, found.email
|
|
268
346
|
end
|
|
269
347
|
end
|
|
270
348
|
```
|
|
271
349
|
|
|
272
|
-
###
|
|
350
|
+
### Generator Strategy Testing
|
|
273
351
|
|
|
274
352
|
```ruby
|
|
275
|
-
class
|
|
276
|
-
|
|
277
|
-
|
|
353
|
+
class GeneratorTest < Minitest::Test
|
|
354
|
+
def test_uuid_v7_format
|
|
355
|
+
user = User.new # Uses default :uuid_v7
|
|
356
|
+
objid = user.objid
|
|
357
|
+
|
|
358
|
+
assert_match(/\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/, objid)
|
|
359
|
+
assert_equal :uuid_v7, user.objid_generator_used
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_uuid_v4_format
|
|
363
|
+
user = LegacyUser.new # Uses :uuid_v4
|
|
364
|
+
objid = user.objid
|
|
278
365
|
|
|
279
|
-
|
|
280
|
-
|
|
366
|
+
assert_match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/, objid)
|
|
367
|
+
assert_equal :uuid_v4, user.objid_generator_used
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def test_hex_format
|
|
371
|
+
doc = SecureDocument.new # Uses :hex
|
|
372
|
+
objid = doc.objid
|
|
373
|
+
|
|
374
|
+
assert_match(/\A[0-9a-f]{64}\z/, objid) # 256 bits = 64 hex chars
|
|
375
|
+
assert_equal :hex, doc.objid_generator_used
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_custom_generator
|
|
379
|
+
item = TimestampedItem.new
|
|
380
|
+
objid = item.objid
|
|
381
|
+
|
|
382
|
+
assert_match(/\Aitem_\d+_[0-9a-f]{8}\z/, objid)
|
|
281
383
|
end
|
|
282
384
|
end
|
|
385
|
+
```
|
|
283
386
|
|
|
284
|
-
|
|
285
|
-
feature :object_identifier, generator: :hex
|
|
286
|
-
field :order_id, :product_id, :quantity
|
|
387
|
+
### Provenance Testing
|
|
287
388
|
|
|
288
|
-
|
|
289
|
-
|
|
389
|
+
```ruby
|
|
390
|
+
class ProvenanceTest < Minitest::Test
|
|
391
|
+
def test_provenance_inference
|
|
392
|
+
# UUID v7 format
|
|
393
|
+
user = User.new(objid: '01234567-89ab-7def-8000-123456789abc')
|
|
394
|
+
assert_equal :uuid_v7, user.objid_generator_used
|
|
395
|
+
|
|
396
|
+
# UUID v4 format
|
|
397
|
+
user = User.new(objid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479')
|
|
398
|
+
assert_equal :uuid_v4, user.objid_generator_used
|
|
399
|
+
|
|
400
|
+
# Hex format
|
|
401
|
+
user = User.new(objid: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890123456789012')
|
|
402
|
+
assert_equal :hex, user.objid_generator_used
|
|
403
|
+
|
|
404
|
+
# Unknown format
|
|
405
|
+
user = User.new(objid: 'custom-format-id')
|
|
406
|
+
assert_nil user.objid_generator_used
|
|
290
407
|
end
|
|
291
408
|
end
|
|
409
|
+
```
|
|
292
410
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
411
|
+
## Performance Considerations
|
|
412
|
+
|
|
413
|
+
### Lookup Table Growth
|
|
414
|
+
|
|
415
|
+
Each class maintains its own lookup table:
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
# Monitor table sizes
|
|
419
|
+
puts User.objid_lookup.length # Number of objid mappings
|
|
420
|
+
puts Product.objid_lookup.length # Separate table per class
|
|
421
|
+
|
|
422
|
+
# Consider cleanup for large datasets
|
|
423
|
+
User.objid_lookup.clear if rebuilding_data
|
|
300
424
|
```
|
|
301
425
|
|
|
302
|
-
###
|
|
426
|
+
### Lazy Generation Benefits
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
# ✅ Efficient - objid only generated if needed
|
|
430
|
+
users = 1000.times.map { User.new(email: "user#{_1}@example.com") }
|
|
431
|
+
# No objids generated yet
|
|
303
432
|
|
|
304
|
-
|
|
433
|
+
# objids generated only on access
|
|
434
|
+
users.each { |user| puts user.objid } # Now generated
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Memory Usage
|
|
305
438
|
|
|
306
439
|
```ruby
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"Session #{objid}: User #{user_id} performed #{action}",
|
|
314
|
-
session_id: objid,
|
|
315
|
-
user_id: user_id,
|
|
316
|
-
action: action,
|
|
317
|
-
timestamp: Familia.now.to_i
|
|
318
|
-
)
|
|
319
|
-
end
|
|
320
|
-
end
|
|
440
|
+
# Each objid uses approximately:
|
|
441
|
+
# - UUID: 36 bytes (with hyphens)
|
|
442
|
+
# - Hex: 64 bytes (256-bit)
|
|
443
|
+
# - Custom: varies by format
|
|
444
|
+
|
|
445
|
+
# Plus lookup table overhead per class
|
|
321
446
|
```
|
|
322
447
|
|
|
323
|
-
##
|
|
448
|
+
## Best Practices
|
|
324
449
|
|
|
325
|
-
###
|
|
450
|
+
### Choose Appropriate Generator
|
|
326
451
|
|
|
327
452
|
```ruby
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def test_uuid_generation
|
|
333
|
-
user = User.create(name: "Test User")
|
|
334
|
-
|
|
335
|
-
# Verify UUID format
|
|
336
|
-
assert_match(
|
|
337
|
-
/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i,
|
|
338
|
-
user.objid
|
|
339
|
-
)
|
|
340
|
-
end
|
|
453
|
+
# ✅ UUID v7 for distributed systems needing sortability
|
|
454
|
+
class DistributedEvent < Familia::Horreum
|
|
455
|
+
feature :object_identifier # Default :uuid_v7
|
|
456
|
+
end
|
|
341
457
|
|
|
342
|
-
|
|
343
|
-
|
|
458
|
+
# ✅ UUID v4 for legacy system compatibility
|
|
459
|
+
class LegacyRecord < Familia::Horreum
|
|
460
|
+
feature :object_identifier, generator: :uuid_v4
|
|
461
|
+
end
|
|
344
462
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
463
|
+
# ✅ Hex for maximum security
|
|
464
|
+
class CryptographicKey < Familia::Horreum
|
|
465
|
+
feature :object_identifier, generator: :hex
|
|
466
|
+
end
|
|
467
|
+
```
|
|
349
468
|
|
|
350
|
-
|
|
351
|
-
order = OrderNumber.create(customer_id: "cust123")
|
|
469
|
+
### Consistent Strategy Per Application
|
|
352
470
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
471
|
+
```ruby
|
|
472
|
+
# Define a base class with consistent strategy
|
|
473
|
+
class ApplicationRecord < Familia::Horreum
|
|
474
|
+
feature :object_identifier, generator: :uuid_v7
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
class User < ApplicationRecord
|
|
478
|
+
field :email, :name
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
class Order < ApplicationRecord
|
|
482
|
+
field :total, :status
|
|
356
483
|
end
|
|
357
484
|
```
|
|
358
485
|
|
|
359
|
-
###
|
|
486
|
+
### Use objid for Internal Operations
|
|
360
487
|
|
|
361
488
|
```ruby
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
end
|
|
489
|
+
# ✅ Internal APIs use objid
|
|
490
|
+
def process_user(user_objid)
|
|
491
|
+
user = User.find_by_objid(user_objid)
|
|
492
|
+
# ... processing logic
|
|
367
493
|
end
|
|
368
494
|
|
|
369
|
-
#
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def self.generate_identifier
|
|
374
|
-
if Rails.env.test?
|
|
375
|
-
TestIdentifierGenerator.generate_test_uuid
|
|
376
|
-
else
|
|
377
|
-
SecureRandom.uuid
|
|
378
|
-
end
|
|
379
|
-
end
|
|
495
|
+
# ✅ Public APIs use external identifiers
|
|
496
|
+
def public_user_profile(user_extid)
|
|
497
|
+
user = User.find_by_extid(user_extid)
|
|
498
|
+
# ... public profile logic
|
|
380
499
|
end
|
|
381
500
|
```
|
|
382
501
|
|
|
383
502
|
## Troubleshooting
|
|
384
503
|
|
|
385
|
-
###
|
|
504
|
+
### objid Returns Nil
|
|
505
|
+
|
|
506
|
+
Check that object has been properly initialized:
|
|
386
507
|
|
|
387
|
-
**Identifier Not Generated**
|
|
388
508
|
```ruby
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
509
|
+
user = User.new
|
|
510
|
+
# Missing feature configuration?
|
|
511
|
+
user.class.features_enabled.include?(:object_identifier) # => Should be true
|
|
512
|
+
|
|
513
|
+
# Access objid to trigger generation
|
|
514
|
+
user.objid # Should generate and return ID
|
|
394
515
|
```
|
|
395
516
|
|
|
396
|
-
|
|
517
|
+
### Lookup Not Working
|
|
518
|
+
|
|
519
|
+
Ensure object was saved to populate lookup table:
|
|
520
|
+
|
|
397
521
|
```ruby
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
522
|
+
user = User.new(email: 'test@example.com')
|
|
523
|
+
objid = user.objid # Generates objid but doesn't save lookup
|
|
524
|
+
|
|
525
|
+
User.find_by_objid(objid) # => nil (lookup not saved)
|
|
526
|
+
|
|
527
|
+
user.save # Saves lookup mapping
|
|
528
|
+
User.find_by_objid(objid) # => user (now works)
|
|
402
529
|
```
|
|
403
530
|
|
|
404
|
-
|
|
531
|
+
### Generator Not Applied
|
|
532
|
+
|
|
533
|
+
Check feature options syntax:
|
|
534
|
+
|
|
405
535
|
```ruby
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
536
|
+
# ❌ Wrong - hash instead of keyword arguments
|
|
537
|
+
class User < Familia::Horreum
|
|
538
|
+
feature :object_identifier, { generator: :uuid_v4 }
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# ✅ Correct - keyword arguments
|
|
542
|
+
class User < Familia::Horreum
|
|
543
|
+
feature :object_identifier, generator: :uuid_v4
|
|
411
544
|
end
|
|
412
545
|
```
|
|
413
546
|
|
|
414
|
-
###
|
|
547
|
+
### Custom Generator Errors
|
|
415
548
|
|
|
416
549
|
```ruby
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
#=> {generator: :uuid_v4, collision_check: true, max_retries: 3}
|
|
550
|
+
# ✅ Valid custom generator
|
|
551
|
+
class Model < Familia::Horreum
|
|
552
|
+
feature :object_identifier,
|
|
553
|
+
generator: -> { "prefix_#{SecureRandom.hex(8)}" }
|
|
554
|
+
end
|
|
423
555
|
|
|
424
|
-
#
|
|
425
|
-
|
|
556
|
+
# ❌ Invalid - not callable
|
|
557
|
+
class Model < Familia::Horreum
|
|
558
|
+
feature :object_identifier, generator: "not_a_proc"
|
|
559
|
+
end
|
|
426
560
|
```
|
|
427
561
|
|
|
428
|
-
---
|
|
429
|
-
|
|
430
562
|
## See Also
|
|
431
563
|
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
-
|
|
435
|
-
- **[Implementation Guide](implementation.md)** - Advanced configuration patterns
|
|
564
|
+
- [External Identifiers](feature-external-identifiers.md) - Public-facing IDs derived from objid
|
|
565
|
+
- [Feature System](feature-system.md) - Understanding Familia's feature architecture
|
|
566
|
+
- [Relationships](feature-relationships.md) - Using object identifiers in relationships
|