familia 2.0.0.pre18 → 2.0.0.pre21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +205 -88
- data/CLAUDE.md +62 -10
- data/Gemfile +3 -3
- data/Gemfile.lock +27 -62
- data/README.md +39 -0
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +177 -133
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +282 -0
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +254 -0
- data/lib/familia/connection/handlers.rb +97 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +3 -1
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
- data/lib/familia/connection/transaction_core.rb +75 -9
- data/lib/familia/connection.rb +21 -5
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +153 -7
- data/lib/familia/data_type/database_commands.rb +9 -4
- data/lib/familia/data_type/serialization.rb +10 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +8 -6
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +53 -14
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +11 -11
- data/lib/familia/features/expiration.rb +29 -21
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
- data/lib/familia/features/relationships/indexing.rb +40 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/connection.rb +28 -36
- data/lib/familia/horreum/database_commands.rb +131 -10
- data/lib/familia/horreum/definition.rb +18 -7
- data/lib/familia/horreum/management.rb +233 -57
- data/lib/familia/horreum/persistence.rb +314 -122
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +26 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -8
- data/lib/familia/horreum.rb +46 -13
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +94 -37
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +9 -7
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +325 -129
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +6 -4
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +5 -1
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +30 -4
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +6 -4
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +5 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +26 -22
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
- data/try/integration/data_types/datatype_transactions_try.rb +251 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +9 -1
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +5 -1
- data/try/integration/persistence_operations_try.rb +166 -10
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +5 -1
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +5 -1
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
- data/try/unit/horreum/base_try.rb +5 -1
- data/try/unit/horreum/class_methods_try.rb +6 -2
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +5 -1
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +8 -4
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +6 -2
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +81 -14
- data/.github/workflows/code-quality.yml +0 -138
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# try/integration/persistence_operations_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
1
5
|
# try/core/persistence_operations_try.rb
|
|
2
6
|
#
|
|
3
7
|
# Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
|
|
@@ -13,6 +17,27 @@ class PersistenceTestModel < Familia::Horreum
|
|
|
13
17
|
field :value
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
# Create model with expiration feature for save_fields testing
|
|
21
|
+
class ExpirationPersistenceTest < Familia::Horreum
|
|
22
|
+
feature :expiration
|
|
23
|
+
identifier_field :id
|
|
24
|
+
field :id
|
|
25
|
+
field :name
|
|
26
|
+
field :email
|
|
27
|
+
field :status
|
|
28
|
+
field :metadata
|
|
29
|
+
|
|
30
|
+
default_expiration 3600 # 1 hour
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Simple model without expiration feature
|
|
34
|
+
class SimpleModel < Familia::Horreum
|
|
35
|
+
identifier_field :id
|
|
36
|
+
field :id
|
|
37
|
+
field :name
|
|
38
|
+
field :value
|
|
39
|
+
end
|
|
40
|
+
|
|
16
41
|
# Clean up any existing test data
|
|
17
42
|
cleanup_keys = []
|
|
18
43
|
begin
|
|
@@ -111,9 +136,9 @@ result = @sine_new.save_if_not_exists
|
|
|
111
136
|
[result, @sine_new.exists?]
|
|
112
137
|
#=> [true, true]
|
|
113
138
|
|
|
114
|
-
## save_if_not_exists raises error for existing object
|
|
139
|
+
## save_if_not_exists! raises error for existing object
|
|
115
140
|
@sine_duplicate = PersistenceTestModel.new(id: @sine_new.identifier, name: 'Duplicate')
|
|
116
|
-
@sine_duplicate.save_if_not_exists
|
|
141
|
+
@sine_duplicate.save_if_not_exists!
|
|
117
142
|
#=!> Familia::RecordExistsError
|
|
118
143
|
|
|
119
144
|
## save_if_not_exists with update_expiration: false
|
|
@@ -128,14 +153,10 @@ original_name = 'Original Name'
|
|
|
128
153
|
@sine_fail_test.save_if_not_exists
|
|
129
154
|
# Now create duplicate and verify state doesn't change on failure
|
|
130
155
|
@sine_fail_duplicate = PersistenceTestModel.new(id: @sine_fail_test.identifier, name: 'Changed Name')
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# State should be unchanged
|
|
136
|
-
@sine_fail_duplicate.name == 'Changed Name'
|
|
137
|
-
end
|
|
138
|
-
#=> true
|
|
156
|
+
result = @sine_fail_duplicate.save_if_not_exists
|
|
157
|
+
# save_if_not_exists returns false on failure, state should be unchanged
|
|
158
|
+
[result == false, @sine_fail_duplicate.name == 'Changed Name']
|
|
159
|
+
#=> [true, true]
|
|
139
160
|
|
|
140
161
|
# =============================================
|
|
141
162
|
# 4. create Method Coverage (MISSING from current tests)
|
|
@@ -291,7 +312,142 @@ actual_key = @key_obj.dbkey
|
|
|
291
312
|
# Cleanup
|
|
292
313
|
# =============================================
|
|
293
314
|
|
|
315
|
+
# =============================================
|
|
316
|
+
# 8. save_fields Method Coverage
|
|
317
|
+
# =============================================
|
|
318
|
+
|
|
319
|
+
## save_fields basic functionality with specified fields
|
|
320
|
+
@save_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Original Name', email: 'test@example.com', status: 'active')
|
|
321
|
+
@save_fields_obj.save
|
|
322
|
+
# Modify fields locally
|
|
323
|
+
@save_fields_obj.name = 'Updated Name'
|
|
324
|
+
@save_fields_obj.status = 'inactive'
|
|
325
|
+
@save_fields_obj.metadata = { updated: true }
|
|
326
|
+
# Save only specific fields
|
|
327
|
+
result = @save_fields_obj.save_fields(:name, :metadata)
|
|
328
|
+
[result.class == ExpirationPersistenceTest, @save_fields_obj.exists?]
|
|
329
|
+
#=> [true, true]
|
|
330
|
+
|
|
331
|
+
## Verify only specified fields were saved
|
|
332
|
+
@save_fields_obj.refresh!
|
|
333
|
+
[@save_fields_obj.name, @save_fields_obj.status, @save_fields_obj.metadata]
|
|
334
|
+
#=> ['Updated Name', 'active', { 'updated' => true }]
|
|
335
|
+
|
|
336
|
+
## save_fields with update_expiration: true (default)
|
|
337
|
+
@exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Expiration Test')
|
|
338
|
+
@exp_obj.save
|
|
339
|
+
original_ttl = @exp_obj.ttl
|
|
340
|
+
# Wait a moment to ensure TTL decreases
|
|
341
|
+
sleep 0.1
|
|
342
|
+
@exp_obj.name = 'Updated with TTL'
|
|
343
|
+
@exp_obj.save_fields(:name) # Should update expiration by default
|
|
344
|
+
new_ttl = @exp_obj.ttl
|
|
345
|
+
# TTL should be refreshed (closer to default_expiration)
|
|
346
|
+
# Allow for small timing variations
|
|
347
|
+
new_ttl >= (ExpirationPersistenceTest.default_expiration - 10)
|
|
348
|
+
#=> true
|
|
349
|
+
|
|
350
|
+
## save_fields with update_expiration: false
|
|
351
|
+
@no_exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'No Exp Update')
|
|
352
|
+
@no_exp_obj.save
|
|
353
|
+
# Wait briefly and get TTL
|
|
354
|
+
sleep 0.1
|
|
355
|
+
original_ttl = @no_exp_obj.ttl
|
|
356
|
+
@no_exp_obj.name = 'Updated without TTL'
|
|
357
|
+
@no_exp_obj.save_fields(:name, update_expiration: false)
|
|
358
|
+
new_ttl = @no_exp_obj.ttl
|
|
359
|
+
# TTL should be approximately the same (slightly less due to time passing)
|
|
360
|
+
(new_ttl - original_ttl).abs < 2
|
|
361
|
+
#=> true
|
|
362
|
+
|
|
363
|
+
## save_fields with multiple fields
|
|
364
|
+
@multi_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Multi', email: 'multi@test.com')
|
|
365
|
+
@multi_fields_obj.save
|
|
366
|
+
@multi_fields_obj.name = 'Multi Updated'
|
|
367
|
+
@multi_fields_obj.email = 'updated@test.com'
|
|
368
|
+
@multi_fields_obj.status = 'new_status'
|
|
369
|
+
result = @multi_fields_obj.save_fields(:name, :email, :status)
|
|
370
|
+
@multi_fields_obj.refresh!
|
|
371
|
+
[@multi_fields_obj.name, @multi_fields_obj.email, @multi_fields_obj.status]
|
|
372
|
+
#=> ['Multi Updated', 'updated@test.com', 'new_status']
|
|
373
|
+
|
|
374
|
+
## save_fields with string field names
|
|
375
|
+
@string_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'String Fields')
|
|
376
|
+
@string_fields_obj.save
|
|
377
|
+
@string_fields_obj.name = 'Updated via String'
|
|
378
|
+
result = @string_fields_obj.save_fields('name') # String instead of symbol
|
|
379
|
+
@string_fields_obj.refresh!
|
|
380
|
+
@string_fields_obj.name
|
|
381
|
+
#=> 'Updated via String'
|
|
382
|
+
|
|
383
|
+
## save_fields error handling - empty fields
|
|
384
|
+
@empty_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Empty Test')
|
|
385
|
+
@empty_fields_obj.save
|
|
386
|
+
@empty_fields_obj.save_fields()
|
|
387
|
+
#=!> ArgumentError
|
|
388
|
+
|
|
389
|
+
## save_fields error handling - unknown field
|
|
390
|
+
@unknown_field_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Unknown Field')
|
|
391
|
+
@unknown_field_obj.save
|
|
392
|
+
@unknown_field_obj.save_fields(:nonexistent_field)
|
|
393
|
+
#=!> ArgumentError
|
|
394
|
+
|
|
395
|
+
## save_fields with nil values
|
|
396
|
+
@nil_values_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Nil Values', status: 'initial')
|
|
397
|
+
@nil_values_obj.save
|
|
398
|
+
@nil_values_obj.status = nil
|
|
399
|
+
@nil_values_obj.save_fields(:status)
|
|
400
|
+
@nil_values_obj.refresh!
|
|
401
|
+
@nil_values_obj.status
|
|
402
|
+
#=> nil
|
|
403
|
+
|
|
404
|
+
## save_fields with complex data types (Hash, Array)
|
|
405
|
+
@complex_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Complex')
|
|
406
|
+
@complex_obj.save
|
|
407
|
+
@complex_obj.metadata = {
|
|
408
|
+
tags: ['ruby', 'redis'],
|
|
409
|
+
config: { timeout: 30, retries: 3 },
|
|
410
|
+
enabled: true
|
|
411
|
+
}
|
|
412
|
+
@complex_obj.save_fields(:metadata)
|
|
413
|
+
@complex_obj.refresh!
|
|
414
|
+
expected_metadata = {
|
|
415
|
+
'tags' => ['ruby', 'redis'],
|
|
416
|
+
'config' => { 'timeout' => 30, 'retries' => 3 },
|
|
417
|
+
'enabled' => true
|
|
418
|
+
}
|
|
419
|
+
@complex_obj.metadata == expected_metadata
|
|
420
|
+
#=> true
|
|
421
|
+
|
|
422
|
+
## save_fields transactional behavior
|
|
423
|
+
@transaction_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Transaction Test')
|
|
424
|
+
@transaction_obj.save
|
|
425
|
+
@transaction_obj.name = 'Updated in Transaction'
|
|
426
|
+
@transaction_obj.email = 'transaction@test.com'
|
|
427
|
+
# All fields should be saved atomically
|
|
428
|
+
@transaction_obj.save_fields(:name, :email)
|
|
429
|
+
@transaction_obj.refresh!
|
|
430
|
+
[@transaction_obj.name, @transaction_obj.email]
|
|
431
|
+
#=> ['Updated in Transaction', 'transaction@test.com']
|
|
432
|
+
|
|
433
|
+
## save_fields performance with model without expiration feature
|
|
434
|
+
|
|
435
|
+
@simple_obj = SimpleModel.new(id: next_test_id, name: 'Simple', value: 'test')
|
|
436
|
+
@simple_obj.save
|
|
437
|
+
@simple_obj.name = 'Simple Updated'
|
|
438
|
+
# Should work without expiration feature (update_expiration param ignored)
|
|
439
|
+
result = @simple_obj.save_fields(:name, update_expiration: true)
|
|
440
|
+
@simple_obj.refresh!
|
|
441
|
+
@simple_obj.name
|
|
442
|
+
#=> 'Simple Updated'
|
|
443
|
+
|
|
444
|
+
# =============================================
|
|
445
|
+
# Cleanup
|
|
446
|
+
# =============================================
|
|
447
|
+
|
|
294
448
|
# Clean up test data
|
|
295
449
|
test_keys = Familia.dbclient.keys('persistencetestmodel:*')
|
|
296
450
|
test_keys.concat(Familia.dbclient.keys('encryptedpersistencetest:*')) if defined?(EncryptedPersistenceTest)
|
|
451
|
+
test_keys.concat(Familia.dbclient.keys('expirationpersistencetest:*'))
|
|
452
|
+
test_keys.concat(Familia.dbclient.keys('simplemodel:*'))
|
|
297
453
|
Familia.dbclient.del(*test_keys) if test_keys.any?
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# try/integration/relationships_persistence_round_trip_try.rb
|
|
2
2
|
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
3
5
|
# CRITICAL PRIORITY: Full Persistence Round-Trip Testing for Relationships
|
|
4
6
|
#
|
|
5
7
|
# PURPOSE:
|
|
@@ -113,22 +115,23 @@ class ::RTPDomain < Familia::Horreum
|
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
# Setup - create test data with known values
|
|
116
|
-
@
|
|
117
|
-
@
|
|
118
|
-
@
|
|
118
|
+
@test_run_id = Familia.now.to_i
|
|
119
|
+
@test_user_id = "rtp_user_#{@test_run_id}"
|
|
120
|
+
@test_email = "roundtrip-#{@test_run_id}@example.com"
|
|
121
|
+
@test_name = "Alice Roundtrip #{@test_run_id}"
|
|
119
122
|
@test_age = 30
|
|
120
123
|
|
|
121
|
-
@test_company_id = "rtp_comp_#{
|
|
124
|
+
@test_company_id = "rtp_comp_#{@test_run_id}"
|
|
122
125
|
@test_company_name = "Acme Corp"
|
|
123
126
|
@test_industry = "Technology"
|
|
124
127
|
|
|
125
|
-
@test_emp_id = "rtp_emp_#{
|
|
126
|
-
@test_emp_email = "employee@acme.com"
|
|
128
|
+
@test_emp_id = "rtp_emp_#{@test_run_id}"
|
|
129
|
+
@test_emp_email = "employee-#{@test_run_id}@acme.com"
|
|
127
130
|
@test_department = "engineering"
|
|
128
|
-
@test_badge = "
|
|
131
|
+
@test_badge = "BADGE_RTP_#{@test_run_id}"
|
|
129
132
|
@test_hire_date = Time.now.to_i
|
|
130
133
|
|
|
131
|
-
@test_domain_id = "rtp_dom_#{
|
|
134
|
+
@test_domain_id = "rtp_dom_#{@test_run_id}"
|
|
132
135
|
@test_domain_name = "example.com"
|
|
133
136
|
@test_domain_created = Familia.now.to_i
|
|
134
137
|
|
|
@@ -291,10 +294,10 @@ RTPEmployee.exists?(@test_emp_id)
|
|
|
291
294
|
|
|
292
295
|
## Multiple employees in same department
|
|
293
296
|
@emp2_id = "rtp_emp2_#{Familia.now.to_i}"
|
|
294
|
-
@emp2_badge = "
|
|
297
|
+
@emp2_badge = "BADGE_RTP_002_#{@test_run_id}"
|
|
295
298
|
@emp2 = RTPEmployee.new(
|
|
296
299
|
emp_id: @emp2_id,
|
|
297
|
-
email: "emp2@acme.com",
|
|
300
|
+
email: "emp2-#{@test_run_id}@acme.com",
|
|
298
301
|
department: @test_department,
|
|
299
302
|
badge_number: @emp2_badge
|
|
300
303
|
)
|
|
@@ -323,7 +326,7 @@ RTPDomain.exists?(@test_domain_id)
|
|
|
323
326
|
#=> true
|
|
324
327
|
|
|
325
328
|
## Add domain to company participation collection
|
|
326
|
-
@company.
|
|
329
|
+
@company.add_domains_instance(@domain)
|
|
327
330
|
@company.domains.size
|
|
328
331
|
#=> 1
|
|
329
332
|
|
|
@@ -377,7 +380,7 @@ RTPDomain.all_domains.size
|
|
|
377
380
|
#=> @new_age
|
|
378
381
|
|
|
379
382
|
## Update email and verify index updates
|
|
380
|
-
@new_email = "newemail@example.com"
|
|
383
|
+
@new_email = "newemail-#{@test_run_id}@example.com"
|
|
381
384
|
@old_email = @user.email
|
|
382
385
|
@user.email = @new_email
|
|
383
386
|
@user.save
|
|
@@ -396,8 +399,8 @@ RTPUser.find_by_email(@old_email)
|
|
|
396
399
|
## User with nil field saves correctly
|
|
397
400
|
@user_nil_age = RTPUser.new(
|
|
398
401
|
user_id: "rtp_nil_#{Familia.now.to_i}",
|
|
399
|
-
email: "nil@example.com",
|
|
400
|
-
name: "Nil Tester",
|
|
402
|
+
email: "nil-#{@test_run_id}@example.com",
|
|
403
|
+
name: "Nil Tester #{@test_run_id}",
|
|
401
404
|
age: nil
|
|
402
405
|
)
|
|
403
406
|
@user_nil_age.save
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# try/integration/save_methods_consistency_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Test coverage for save and save_if_not_exists consistency improvements
|
|
6
|
+
#
|
|
7
|
+
# This test verifies that both save and save_if_not_exists! produce identical
|
|
8
|
+
# results when creating new objects, including:
|
|
9
|
+
# - Timestamp updates (created/updated)
|
|
10
|
+
# - Unique index validation
|
|
11
|
+
# - Class-level index updates
|
|
12
|
+
# - Instance collection tracking
|
|
13
|
+
|
|
14
|
+
require_relative '../support/helpers/test_helpers'
|
|
15
|
+
|
|
16
|
+
# Model with timestamps to verify timestamp handling
|
|
17
|
+
class TimestampedModel < Familia::Horreum
|
|
18
|
+
identifier_field :id
|
|
19
|
+
field :id
|
|
20
|
+
field :name
|
|
21
|
+
field :created
|
|
22
|
+
field :updated
|
|
23
|
+
|
|
24
|
+
zset :instances
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Model with unique indexes to verify validation
|
|
28
|
+
class UniqueIndexModel < Familia::Horreum
|
|
29
|
+
feature :relationships
|
|
30
|
+
include Familia::Features::Relationships::Indexing
|
|
31
|
+
|
|
32
|
+
identifier_field :id
|
|
33
|
+
field :id
|
|
34
|
+
field :email
|
|
35
|
+
|
|
36
|
+
unique_index :email, :email_lookup
|
|
37
|
+
zset :instances
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Clean up any existing test data
|
|
41
|
+
cleanup_keys = Familia.dbclient.keys('timestampedmodel:*') +
|
|
42
|
+
Familia.dbclient.keys('uniqueindexmodel:*') +
|
|
43
|
+
Familia.dbclient.keys('*:email_lookup:*')
|
|
44
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
|
45
|
+
|
|
46
|
+
@test_counter = 0
|
|
47
|
+
def next_id
|
|
48
|
+
@test_counter += 1
|
|
49
|
+
"test-#{Familia.now.to_i}-#{@test_counter}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# =============================================
|
|
53
|
+
# 1. Timestamp Consistency Tests
|
|
54
|
+
# =============================================
|
|
55
|
+
|
|
56
|
+
## save sets created and updated timestamps
|
|
57
|
+
@save_model = TimestampedModel.new(id: next_id, name: 'Save Test')
|
|
58
|
+
@save_model.save
|
|
59
|
+
[@save_model.created.nil?, @save_model.updated.nil?]
|
|
60
|
+
#=> [false, false]
|
|
61
|
+
|
|
62
|
+
## save_if_not_exists! sets created and updated timestamps
|
|
63
|
+
@sine_model = TimestampedModel.new(id: next_id, name: 'SINE Test')
|
|
64
|
+
@sine_model.save_if_not_exists!
|
|
65
|
+
[@sine_model.created.nil?, @sine_model.updated.nil?]
|
|
66
|
+
#=> [false, false]
|
|
67
|
+
|
|
68
|
+
## Both methods set timestamps to the same approximate time
|
|
69
|
+
time_diff = (@save_model.created - @save_model.updated).abs
|
|
70
|
+
time_diff < 1 # Should be within 1 second
|
|
71
|
+
#=> true
|
|
72
|
+
|
|
73
|
+
## save_if_not_exists! timestamps match save behavior
|
|
74
|
+
sine_time_diff = (@sine_model.created - @sine_model.updated).abs
|
|
75
|
+
sine_time_diff < 1
|
|
76
|
+
#=> true
|
|
77
|
+
|
|
78
|
+
# =============================================
|
|
79
|
+
# 2. Instance Collection Consistency
|
|
80
|
+
# =============================================
|
|
81
|
+
|
|
82
|
+
## save adds object to instances collection
|
|
83
|
+
@inst_save = TimestampedModel.new(id: next_id, name: 'Instance Save')
|
|
84
|
+
@inst_save.save
|
|
85
|
+
TimestampedModel.instances.members.include?(@inst_save.identifier)
|
|
86
|
+
#=> true
|
|
87
|
+
|
|
88
|
+
## save_if_not_exists! adds object to instances collection
|
|
89
|
+
@inst_sine = TimestampedModel.new(id: next_id, name: 'Instance SINE')
|
|
90
|
+
@inst_sine.save_if_not_exists!
|
|
91
|
+
TimestampedModel.instances.members.include?(@inst_sine.identifier)
|
|
92
|
+
#=> true
|
|
93
|
+
|
|
94
|
+
## Both methods produce identical instance collection state
|
|
95
|
+
[@inst_save, @inst_sine].all? { |obj| TimestampedModel.instances.members.include?(obj.identifier) }
|
|
96
|
+
#=> true
|
|
97
|
+
|
|
98
|
+
# =============================================
|
|
99
|
+
# 3. Unique Index Validation Consistency
|
|
100
|
+
# =============================================
|
|
101
|
+
|
|
102
|
+
## save validates unique indexes
|
|
103
|
+
@unique_email_1 = "save-#{next_id}@test.com"
|
|
104
|
+
@unique_save = UniqueIndexModel.new(id: next_id, email: @unique_email_1)
|
|
105
|
+
@unique_save.save
|
|
106
|
+
@unique_save.exists?
|
|
107
|
+
#=> true
|
|
108
|
+
|
|
109
|
+
## save raises RecordExistsError for duplicate unique index
|
|
110
|
+
@unique_dup_save = UniqueIndexModel.new(id: next_id, email: @unique_email_1)
|
|
111
|
+
@unique_dup_save.save
|
|
112
|
+
#=!> Familia::RecordExistsError
|
|
113
|
+
|
|
114
|
+
## save_if_not_exists! validates unique indexes
|
|
115
|
+
@unique_email_2 = "sine-#{next_id}@test.com"
|
|
116
|
+
@unique_sine = UniqueIndexModel.new(id: next_id, email: @unique_email_2)
|
|
117
|
+
@unique_sine.save_if_not_exists!
|
|
118
|
+
@unique_sine.exists?
|
|
119
|
+
#=> true
|
|
120
|
+
|
|
121
|
+
## save_if_not_exists! raises RecordExistsError for duplicate unique index
|
|
122
|
+
@unique_dup_sine = UniqueIndexModel.new(id: next_id, email: @unique_email_2)
|
|
123
|
+
@unique_dup_sine.save_if_not_exists!
|
|
124
|
+
#=!> Familia::RecordExistsError
|
|
125
|
+
|
|
126
|
+
# =============================================
|
|
127
|
+
# 4. Return Value Consistency
|
|
128
|
+
# =============================================
|
|
129
|
+
|
|
130
|
+
## save returns true for successful save
|
|
131
|
+
@ret_save = TimestampedModel.new(id: next_id, name: 'Return Test Save')
|
|
132
|
+
result_save = @ret_save.save
|
|
133
|
+
result_save
|
|
134
|
+
#=> true
|
|
135
|
+
|
|
136
|
+
## save_if_not_exists! returns true for successful save
|
|
137
|
+
@ret_sine = TimestampedModel.new(id: next_id, name: 'Return Test SINE')
|
|
138
|
+
result_sine = @ret_sine.save_if_not_exists!
|
|
139
|
+
result_sine
|
|
140
|
+
#=> true
|
|
141
|
+
|
|
142
|
+
## save_if_not_exists returns true for new object
|
|
143
|
+
@ret_sine_safe = TimestampedModel.new(id: next_id, name: 'Return Safe SINE')
|
|
144
|
+
result = @ret_sine_safe.save_if_not_exists
|
|
145
|
+
result
|
|
146
|
+
#=> true
|
|
147
|
+
|
|
148
|
+
## save_if_not_exists returns false for existing object
|
|
149
|
+
@ret_existing = TimestampedModel.new(id: next_id, name: 'Existing')
|
|
150
|
+
@ret_existing.save
|
|
151
|
+
@ret_dup = TimestampedModel.new(id: @ret_existing.identifier, name: 'Duplicate')
|
|
152
|
+
result = @ret_dup.save_if_not_exists
|
|
153
|
+
result
|
|
154
|
+
#=> false
|
|
155
|
+
|
|
156
|
+
# =============================================
|
|
157
|
+
# 5. Data Persistence Consistency
|
|
158
|
+
# =============================================
|
|
159
|
+
|
|
160
|
+
## save persists all fields
|
|
161
|
+
@data_save = TimestampedModel.new(id: next_id, name: 'Data Save Test')
|
|
162
|
+
@data_save.save
|
|
163
|
+
@data_save.refresh
|
|
164
|
+
@data_save.name
|
|
165
|
+
#=> 'Data Save Test'
|
|
166
|
+
|
|
167
|
+
## save_if_not_exists! persists all fields
|
|
168
|
+
@data_sine = TimestampedModel.new(id: next_id, name: 'Data SINE Test')
|
|
169
|
+
@data_sine.save_if_not_exists!
|
|
170
|
+
@data_sine.refresh
|
|
171
|
+
@data_sine.name
|
|
172
|
+
#=> 'Data SINE Test'
|
|
173
|
+
|
|
174
|
+
## Both methods produce identical persistence
|
|
175
|
+
[@data_save.name, @data_sine.name]
|
|
176
|
+
#=> ['Data Save Test', 'Data SINE Test']
|
|
177
|
+
|
|
178
|
+
# =============================================
|
|
179
|
+
# 6. Expiration Handling Consistency
|
|
180
|
+
# =============================================
|
|
181
|
+
|
|
182
|
+
## save with update_expiration: true handles TTL
|
|
183
|
+
@exp_save = TimestampedModel.new(id: next_id, name: 'Exp Save')
|
|
184
|
+
@exp_save.save(update_expiration: true)
|
|
185
|
+
# No default expiration set, so TTL is -1 (no expiration)
|
|
186
|
+
@exp_save.ttl == -1
|
|
187
|
+
#=> true
|
|
188
|
+
|
|
189
|
+
## save_if_not_exists! with update_expiration: true handles TTL
|
|
190
|
+
@exp_sine = TimestampedModel.new(id: next_id, name: 'Exp SINE')
|
|
191
|
+
@exp_sine.save_if_not_exists!(update_expiration: true)
|
|
192
|
+
# No default expiration set, so TTL is -1 (no expiration)
|
|
193
|
+
@exp_sine.ttl == -1
|
|
194
|
+
#=> true
|
|
195
|
+
|
|
196
|
+
# =============================================
|
|
197
|
+
# 7. OptimisticLockError Behavior
|
|
198
|
+
# =============================================
|
|
199
|
+
|
|
200
|
+
## save_if_not_exists allows OptimisticLockError to propagate
|
|
201
|
+
# Note: This is difficult to test reliably without mocking, but we can
|
|
202
|
+
# verify the method signature and rescue clause structure through the API
|
|
203
|
+
|
|
204
|
+
## save_if_not_exists rescues only RecordExistsError
|
|
205
|
+
@opt_test = TimestampedModel.new(id: next_id, name: 'Opt Test')
|
|
206
|
+
@opt_test.save
|
|
207
|
+
@opt_dup = TimestampedModel.new(id: @opt_test.identifier, name: 'Opt Dup')
|
|
208
|
+
# This should return false, not raise
|
|
209
|
+
result = @opt_dup.save_if_not_exists
|
|
210
|
+
result
|
|
211
|
+
#=> false
|
|
212
|
+
|
|
213
|
+
# =============================================
|
|
214
|
+
# 8. Edge Cases
|
|
215
|
+
# =============================================
|
|
216
|
+
|
|
217
|
+
## save works with nil field values
|
|
218
|
+
@nil_save = TimestampedModel.new(id: next_id, name: nil)
|
|
219
|
+
@nil_save.save
|
|
220
|
+
@nil_save.exists?
|
|
221
|
+
#=> true
|
|
222
|
+
|
|
223
|
+
## save_if_not_exists! works with nil field values
|
|
224
|
+
@nil_sine = TimestampedModel.new(id: next_id, name: nil)
|
|
225
|
+
@nil_sine.save_if_not_exists!
|
|
226
|
+
@nil_sine.exists?
|
|
227
|
+
#=> true
|
|
228
|
+
|
|
229
|
+
## Both methods handle empty strings
|
|
230
|
+
@empty_save = TimestampedModel.new(id: next_id, name: '')
|
|
231
|
+
@empty_save.save
|
|
232
|
+
@empty_sine = TimestampedModel.new(id: next_id, name: '')
|
|
233
|
+
@empty_sine.save_if_not_exists!
|
|
234
|
+
[@empty_save.exists?, @empty_sine.exists?]
|
|
235
|
+
#=> [true, true]
|
|
236
|
+
|
|
237
|
+
# Cleanup
|
|
238
|
+
cleanup_keys = Familia.dbclient.keys('timestampedmodel:*') +
|
|
239
|
+
Familia.dbclient.keys('uniqueindexmodel:*') +
|
|
240
|
+
Familia.dbclient.keys('*:email_lookup:*')
|
|
241
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|