familia 2.0.0.pre17 → 2.0.0.pre19
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/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
# lib/familia/connection/middleware.rb
|
2
2
|
|
3
|
-
require_relative '../../middleware/
|
3
|
+
require_relative '../../middleware/database_logger'
|
4
4
|
|
5
5
|
module Familia
|
6
6
|
module Connection
|
@@ -19,13 +19,13 @@ module Familia
|
|
19
19
|
# Increments the middleware version, invalidating all cached connections
|
20
20
|
def increment_middleware_version!
|
21
21
|
@middleware_version += 1
|
22
|
-
Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
|
22
|
+
Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
|
23
23
|
end
|
24
24
|
|
25
25
|
# Sets a versioned fiber-local connection
|
26
|
-
def
|
26
|
+
def fiber_connection=(connection)
|
27
27
|
Fiber[:familia_connection] = [connection, middleware_version]
|
28
|
-
Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
|
28
|
+
Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
|
29
29
|
end
|
30
30
|
|
31
31
|
# Clears the fiber-local connection
|
@@ -50,24 +50,78 @@ module Familia
|
|
50
50
|
increment_middleware_version! if value
|
51
51
|
end
|
52
52
|
|
53
|
+
# Reconnects with fresh middleware registration
|
54
|
+
#
|
55
|
+
# This method is useful when middleware needs to be applied to connection pools
|
56
|
+
# that were created before middleware was enabled. It:
|
57
|
+
#
|
58
|
+
# 1. Clears the middleware registration flag to allow re-registration
|
59
|
+
# 2. Re-runs the middleware registration logic
|
60
|
+
# 3. Clears connection chain to force rebuild
|
61
|
+
# 4. Increments middleware version to invalidate cached connections
|
62
|
+
# 5. Clears fiber-local connections
|
63
|
+
#
|
64
|
+
# The next connection request will use the updated middleware configuration.
|
65
|
+
# Existing connection pools will naturally create new connections with middleware
|
66
|
+
# as old connections are cycled out.
|
67
|
+
#
|
68
|
+
# @note If no middleware is enabled, this method safely clears connection state
|
69
|
+
# but won't register any middleware until it's enabled.
|
70
|
+
#
|
71
|
+
# @example Enable middleware and reconnect
|
72
|
+
# Familia.enable_database_logging = true
|
73
|
+
# Familia.reconnect!
|
74
|
+
#
|
75
|
+
# @example In test suites
|
76
|
+
# # Test file A creates pools
|
77
|
+
# Familia.connection_provider = ->(uri) { pool.with { |c| c } }
|
78
|
+
#
|
79
|
+
# # Test file B enables middleware
|
80
|
+
# Familia.enable_database_logging = true
|
81
|
+
# Familia.reconnect! # Force new connections with middleware
|
82
|
+
#
|
83
|
+
def reconnect!
|
84
|
+
# Allow middleware to be re-registered
|
85
|
+
@middleware_registered = false
|
86
|
+
register_middleware_once
|
87
|
+
|
88
|
+
# Clear connection chain to force rebuild
|
89
|
+
@connection_chain = nil
|
90
|
+
|
91
|
+
# Increment version to invalidate all cached connections
|
92
|
+
increment_middleware_version!
|
93
|
+
|
94
|
+
# Clear fiber-local connections
|
95
|
+
clear_fiber_connection!
|
96
|
+
|
97
|
+
Familia.trace :RECONNECT, nil, 'Connection chain cleared, will rebuild with current middleware on next use'
|
98
|
+
end
|
99
|
+
|
53
100
|
private
|
54
101
|
|
55
102
|
# Registers middleware once globally, regardless of when clients are created.
|
56
103
|
# This prevents duplicate middleware registration and ensures all clients get middleware.
|
57
104
|
def register_middleware_once
|
105
|
+
# Skip if already registered
|
58
106
|
return if @middleware_registered
|
59
107
|
|
108
|
+
# Check if any middleware is enabled
|
109
|
+
return unless Familia.enable_database_logging || Familia.enable_database_counter
|
110
|
+
|
60
111
|
if Familia.enable_database_logging
|
61
112
|
DatabaseLogger.logger = Familia.logger
|
62
113
|
RedisClient.register(DatabaseLogger)
|
114
|
+
Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseLogger'
|
63
115
|
end
|
64
116
|
|
65
117
|
if Familia.enable_database_counter
|
66
118
|
# NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
|
67
119
|
# less contentious than Mutex-based counters. Safe for production.
|
68
120
|
RedisClient.register(DatabaseCommandCounter)
|
121
|
+
Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseCommandCounter'
|
69
122
|
end
|
70
123
|
|
124
|
+
# Set flag after successful registration
|
71
125
|
@middleware_registered = true
|
72
126
|
end
|
73
127
|
end
|
@@ -16,7 +16,7 @@ module Familia
|
|
16
16
|
#
|
17
17
|
# Handles pipeline execution based on connection handler capabilities.
|
18
18
|
# When handler doesn't support pipelines, fallback behavior is controlled
|
19
|
-
# by Familia.
|
19
|
+
# by Familia.pipelined_mode setting.
|
20
20
|
#
|
21
21
|
# @param dbclient_proc [Proc] Lambda that returns the Redis connection
|
22
22
|
# @param block [Proc] Block containing Redis commands to execute
|
@@ -32,7 +32,7 @@ module Familia
|
|
32
32
|
# result.results # => ["OK", 1]
|
33
33
|
#
|
34
34
|
# @example With fallback modes
|
35
|
-
# Familia.configure { |c| c.
|
35
|
+
# Familia.configure { |c| c.pipelined_mode = :permissive }
|
36
36
|
# result = PipelineCore.execute_pipeline(-> { cached_conn }) do |conn|
|
37
37
|
# conn.set('key', 'value') # Executes individually, no error
|
38
38
|
# end
|
@@ -34,27 +34,25 @@ module Familia
|
|
34
34
|
# result.successful? # => true/false
|
35
35
|
# result.results # => ["OK", 1]
|
36
36
|
#
|
37
|
-
def self.execute_transaction(dbclient_proc, &
|
37
|
+
def self.execute_transaction(dbclient_proc, &)
|
38
38
|
# First, get the connection to populate the handler class
|
39
|
-
|
39
|
+
dbclient_proc.call
|
40
40
|
handler_class = Fiber[:familia_connection_handler_class]
|
41
41
|
|
42
42
|
# Check transaction capability
|
43
43
|
transaction_capability = handler_class&.allows_transaction
|
44
44
|
|
45
45
|
if transaction_capability == false
|
46
|
-
handle_transaction_fallback(dbclient_proc, handler_class, &
|
46
|
+
handle_transaction_fallback(dbclient_proc, handler_class, &)
|
47
47
|
elsif transaction_capability == :reentrant
|
48
48
|
# Already in transaction, just yield the connection
|
49
49
|
yield(Fiber[:familia_transaction])
|
50
50
|
else
|
51
51
|
# Normal transaction flow (includes nil, true, and other values)
|
52
|
-
execute_normal_transaction(dbclient_proc, &
|
52
|
+
execute_normal_transaction(dbclient_proc, &)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
private
|
57
|
-
|
58
56
|
# Handles transaction fallback based on configured transaction mode
|
59
57
|
#
|
60
58
|
# Delegates to OperationCore.handle_fallback for consistent behavior
|
@@ -65,8 +63,8 @@ module Familia
|
|
65
63
|
# @param block [Proc] Block containing Redis commands to execute
|
66
64
|
# @return [MultiResult] Result from individual command execution or raises error
|
67
65
|
#
|
68
|
-
def self.handle_transaction_fallback(dbclient_proc, handler_class, &
|
69
|
-
OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &
|
66
|
+
def self.handle_transaction_fallback(dbclient_proc, handler_class, &)
|
67
|
+
OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &)
|
70
68
|
end
|
71
69
|
|
72
70
|
# Executes a normal Redis transaction using MULTI/EXEC
|
@@ -78,7 +76,7 @@ module Familia
|
|
78
76
|
# @param block [Proc] Block containing Redis commands to execute
|
79
77
|
# @return [MultiResult] Result object with transaction command results
|
80
78
|
#
|
81
|
-
def self.execute_normal_transaction(dbclient_proc
|
79
|
+
def self.execute_normal_transaction(dbclient_proc)
|
82
80
|
# Check for existing transaction context
|
83
81
|
return yield(Fiber[:familia_transaction]) if Fiber[:familia_transaction]
|
84
82
|
|
data/lib/familia/connection.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# lib/familia/connection.rb
|
2
2
|
|
3
|
+
require_relative 'connection/behavior'
|
3
4
|
require_relative 'connection/handlers'
|
4
5
|
require_relative 'connection/middleware'
|
5
6
|
require_relative 'connection/operations'
|
6
7
|
require_relative 'connection/individual_command_proxy'
|
7
8
|
require_relative 'connection/operation_core'
|
8
9
|
require_relative 'connection/transaction_core'
|
9
|
-
require_relative 'connection/
|
10
|
+
require_relative 'connection/pipelined_core'
|
10
11
|
|
11
12
|
# Familia
|
12
13
|
#
|
@@ -5,21 +5,104 @@ module Familia
|
|
5
5
|
# Connection - Instance-level connection and key generation methods
|
6
6
|
#
|
7
7
|
# This module provides instance methods for database connection resolution
|
8
|
-
# and Redis key generation for DataType objects.
|
8
|
+
# and Redis key generation for DataType objects. It includes shared connection
|
9
|
+
# behavior from Familia::Connection::Behavior, enabling transaction and pipeline
|
10
|
+
# support for both parent-owned and standalone DataType objects.
|
9
11
|
#
|
10
12
|
# Key features:
|
11
13
|
# * Database connection resolution with Chain of Responsibility pattern
|
12
14
|
# * Redis key generation based on parent context
|
13
15
|
# * Direct database access for advanced operations
|
16
|
+
# * Transaction support (MULTI/EXEC) for atomic operations
|
17
|
+
# * Pipeline support for batched command execution
|
18
|
+
# * Parent delegation for owned DataType objects
|
19
|
+
# * Standalone connection management for independent DataType objects
|
20
|
+
#
|
21
|
+
# Connection Chain Priority:
|
22
|
+
# 1. FiberTransactionHandler - Active transaction context
|
23
|
+
# 2. FiberConnectionHandler - Fiber-local connections
|
24
|
+
# 3. ProviderConnectionHandler - User-defined connection provider
|
25
|
+
# 4. ParentDelegationHandler - Delegate to parent object (primary for owned DataTypes)
|
26
|
+
# 5. StandaloneConnectionHandler - Independent DataType connection
|
27
|
+
#
|
28
|
+
# @example Parent-owned DataType (automatic delegation)
|
29
|
+
# class User < Familia::Horreum
|
30
|
+
# logical_database 2
|
31
|
+
# zset :scores
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# user = User.new(userid: 'user_123')
|
35
|
+
# user.scores.transaction do |conn|
|
36
|
+
# conn.zadd(user.scores.dbkey, 100, 'level1')
|
37
|
+
# conn.zadd(user.scores.dbkey, 200, 'level2')
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Standalone DataType with transaction
|
41
|
+
# leaderboard = Familia::SortedSet.new('game:leaderboard')
|
42
|
+
# leaderboard.transaction do |conn|
|
43
|
+
# conn.zadd(leaderboard.dbkey, 500, 'player1')
|
44
|
+
# conn.zadd(leaderboard.dbkey, 600, 'player2')
|
45
|
+
# end
|
14
46
|
#
|
15
47
|
module Connection
|
16
|
-
|
17
|
-
|
48
|
+
include Familia::Connection::Behavior
|
49
|
+
|
50
|
+
# Returns the effective URI this DataType will use for connections
|
51
|
+
#
|
52
|
+
# For parent-owned DataTypes, delegates to parent's URI.
|
53
|
+
# For standalone DataTypes with logical_database option, constructs URI with that database.
|
54
|
+
# For standalone DataTypes without options, returns global Familia.uri.
|
55
|
+
# Explicit @uri assignment (via uri=) takes precedence.
|
56
|
+
#
|
57
|
+
# @return [URI, nil] The URI for database connections
|
58
|
+
#
|
59
|
+
def uri
|
60
|
+
return @uri if defined?(@uri) && @uri
|
61
|
+
return parent.uri if parent && parent.respond_to?(:uri)
|
62
|
+
|
63
|
+
# Check opts[:logical_database] first, then parent's logical_database
|
64
|
+
db_num = opts[:logical_database]
|
65
|
+
db_num ||= parent.logical_database if parent && parent.respond_to?(:logical_database)
|
66
|
+
|
67
|
+
if db_num
|
68
|
+
# Create a new URI with the database number but without custom port
|
69
|
+
# This ensures consistent URI representation (e.g., redis://host/db not redis://host:port/db)
|
70
|
+
base_uri = Familia.uri
|
71
|
+
URI.parse("redis://#{base_uri.host}/#{db_num}")
|
72
|
+
else
|
73
|
+
Familia.uri
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Retrieves a Database connection using the Chain of Responsibility pattern
|
78
|
+
#
|
79
|
+
# Implements connection resolution optimized for DataType usage patterns:
|
80
|
+
# - Fast path check for active transaction context
|
81
|
+
# - Full connection chain for comprehensive resolution
|
82
|
+
# - Parent delegation as primary behavior for owned DataTypes
|
83
|
+
# - Standalone connection handling for independent DataTypes
|
84
|
+
#
|
85
|
+
# Note: We don't cache the connection chain in an instance variable because
|
86
|
+
# DataType objects are frozen for thread safety. Building the chain is cheap
|
87
|
+
# (just creating handler objects), and the actual connection resolution work
|
88
|
+
# is done by the handlers themselves.
|
89
|
+
#
|
90
|
+
# @param uri [String, URI, Integer, nil] Optional URI for database selection
|
91
|
+
# @return [Redis] The Database client for the specified URI
|
92
|
+
#
|
93
|
+
# @example Getting connection from parent-owned DataType
|
94
|
+
# user.tags.dbclient # Delegates to user.dbclient
|
95
|
+
#
|
96
|
+
# @example Getting connection from standalone DataType
|
97
|
+
# cache = Familia::HashKey.new('app:cache', logical_database: 2)
|
98
|
+
# cache.dbclient # Uses standalone handler with db 2
|
99
|
+
#
|
100
|
+
def dbclient(uri = nil)
|
101
|
+
# Fast path for transaction context (highest priority)
|
18
102
|
return Fiber[:familia_transaction] if Fiber[:familia_transaction]
|
19
|
-
return @dbclient if @dbclient
|
20
103
|
|
21
|
-
#
|
22
|
-
|
104
|
+
# Build connection chain (not cached due to frozen objects)
|
105
|
+
build_connection_chain.handle(uri)
|
23
106
|
end
|
24
107
|
|
25
108
|
# Produces the full dbkey for this object.
|
@@ -75,8 +158,69 @@ module Familia
|
|
75
158
|
# Provides a structured way to "gear down" to run db commands that are
|
76
159
|
# not implemented in our DataType classes since we intentionally don't
|
77
160
|
# have a method_missing method.
|
161
|
+
#
|
162
|
+
# Enhanced to work seamlessly with transactions and pipelines. When called
|
163
|
+
# within a transaction or pipeline context, uses that connection automatically.
|
164
|
+
#
|
165
|
+
# @yield [Redis, String] Yields the connection and dbkey to the block
|
166
|
+
# @return The return value of the block
|
167
|
+
#
|
168
|
+
# @example Basic usage
|
169
|
+
# datatype.direct_access do |conn, key|
|
170
|
+
# conn.zadd(key, 100, 'member')
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# @example Within transaction (automatic context detection)
|
174
|
+
# datatype.transaction do |trans_conn|
|
175
|
+
# datatype.direct_access do |conn, key|
|
176
|
+
# # conn is the same as trans_conn
|
177
|
+
# conn.zadd(key, 200, 'member')
|
178
|
+
# end
|
179
|
+
# end
|
180
|
+
#
|
78
181
|
def direct_access
|
79
|
-
|
182
|
+
if Fiber[:familia_transaction]
|
183
|
+
# Already in transaction, use that connection
|
184
|
+
yield(Fiber[:familia_transaction], dbkey)
|
185
|
+
elsif Fiber[:familia_pipeline]
|
186
|
+
# Already in pipeline, use that connection
|
187
|
+
yield(Fiber[:familia_pipeline], dbkey)
|
188
|
+
else
|
189
|
+
yield(dbclient, dbkey)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# Builds the connection chain with handlers in priority order
|
196
|
+
#
|
197
|
+
# Creates the Chain of Responsibility for connection resolution with
|
198
|
+
# DataType-specific handlers. Handlers are checked in order:
|
199
|
+
#
|
200
|
+
# 1. FiberTransactionHandler - Return active transaction connection
|
201
|
+
# 2. FiberConnectionHandler - Use fiber-local connection
|
202
|
+
# 3. ProviderConnectionHandler - Delegate to connection provider
|
203
|
+
# 4. ParentDelegationHandler - Delegate to parent's connection (primary for owned DataTypes)
|
204
|
+
# 5. StandaloneConnectionHandler - Handle standalone DataTypes
|
205
|
+
#
|
206
|
+
# @return [ResponsibilityChain] Configured connection chain
|
207
|
+
#
|
208
|
+
def build_connection_chain
|
209
|
+
# Create fresh handler instances each time since DataType objects are frozen
|
210
|
+
# The chain itself is cached in @connection_chain, so this only runs once
|
211
|
+
fiber_connection_handler = Familia::Connection::FiberConnectionHandler.new
|
212
|
+
provider_connection_handler = Familia::Connection::ProviderConnectionHandler.new
|
213
|
+
|
214
|
+
# DataType-specific handlers for parent delegation and standalone usage
|
215
|
+
parent_delegation_handler = Familia::Connection::ParentDelegationHandler.new(self)
|
216
|
+
standalone_connection_handler = Familia::Connection::StandaloneConnectionHandler.new(self)
|
217
|
+
|
218
|
+
Familia::Connection::ResponsibilityChain.new
|
219
|
+
.add_handler(Familia::Connection::FiberTransactionHandler.instance)
|
220
|
+
.add_handler(fiber_connection_handler)
|
221
|
+
.add_handler(provider_connection_handler)
|
222
|
+
.add_handler(parent_delegation_handler)
|
223
|
+
.add_handler(standalone_connection_handler)
|
80
224
|
end
|
81
225
|
end
|
82
226
|
end
|
@@ -1,10 +1,10 @@
|
|
1
|
-
# lib/familia/data_type/
|
1
|
+
# lib/familia/data_type/database_commands.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class DataType
|
5
5
|
# Must be included in all DataType classes to provide Valkey/Redis
|
6
6
|
# commands. The class must have a dbkey method.
|
7
|
-
module
|
7
|
+
module DatabaseCommands
|
8
8
|
def move(logical_database)
|
9
9
|
dbclient.move dbkey, logical_database
|
10
10
|
end
|
@@ -22,11 +22,14 @@ module Familia
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# Deletes the entire dbkey
|
25
|
-
#
|
25
|
+
#
|
26
|
+
# We return the dbclient.del command's return value instead of a friendly
|
27
|
+
# boolean b/c that logic doesn't work inside of a transaction. The return
|
28
|
+
# value in that case is a Redis::Future which based on the name indicates
|
29
|
+
# that the commend hasn't even run yet.
|
26
30
|
def delete!
|
27
|
-
Familia.trace :DELETE!, nil, uri if Familia.debug?
|
28
|
-
|
29
|
-
ret.positive?
|
31
|
+
Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
|
32
|
+
dbclient.del dbkey
|
30
33
|
end
|
31
34
|
alias clear delete!
|
32
35
|
|
@@ -11,9 +11,9 @@ module Familia
|
|
11
11
|
# @return [String, nil] The serialized representation of the value, or nil
|
12
12
|
# if serialization fails.
|
13
13
|
#
|
14
|
-
# @note When a class option is specified, it uses
|
15
|
-
#
|
16
|
-
#
|
14
|
+
# @note When a class option is specified, it uses Familia.identifier_extractor
|
15
|
+
# to extract the identifier from objects. Otherwise, it extracts identifiers
|
16
|
+
# from Familia::Base instances or class names.
|
17
17
|
#
|
18
18
|
# @example With a class option
|
19
19
|
# serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
|
@@ -31,13 +31,13 @@ module Familia
|
|
31
31
|
Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
|
32
32
|
|
33
33
|
if opts[:class]
|
34
|
-
prepared = Familia.
|
34
|
+
prepared = Familia.identifier_extractor(opts[:class])
|
35
35
|
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
|
36
36
|
end
|
37
37
|
|
38
38
|
if prepared.nil?
|
39
39
|
# Enforce strict values when no class option is specified
|
40
|
-
prepared = Familia.
|
40
|
+
prepared = Familia.identifier_extractor(val)
|
41
41
|
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
42
42
|
end
|
43
43
|
|
@@ -117,7 +117,11 @@ module Familia
|
|
117
117
|
# for serialization since everything becomes a string in Valkey.
|
118
118
|
#
|
119
119
|
def deserialize_value(val)
|
120
|
+
# Handle Redis::Future objects during transactions first
|
121
|
+
return val if val.is_a?(Redis::Future)
|
122
|
+
|
120
123
|
return @opts[:default] if val.nil?
|
124
|
+
|
121
125
|
return val unless @opts[:class]
|
122
126
|
|
123
127
|
ret = deserialize_values val
|
@@ -152,7 +152,7 @@ module Familia
|
|
152
152
|
# puts "Oops! Our hash seems to have vanished into the Database void!"
|
153
153
|
# end
|
154
154
|
def refresh!
|
155
|
-
Familia.trace :REFRESH, nil, uri if Familia.debug?
|
155
|
+
Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
|
156
156
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
157
157
|
|
158
158
|
fields = hgetall
|
data/lib/familia/data_type.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require_relative 'data_type/class_methods'
|
4
4
|
require_relative 'data_type/settings'
|
5
5
|
require_relative 'data_type/connection'
|
6
|
-
require_relative 'data_type/
|
6
|
+
require_relative 'data_type/database_commands'
|
7
7
|
require_relative 'data_type/serialization'
|
8
8
|
|
9
9
|
# Familia
|
@@ -77,7 +77,7 @@ module Familia
|
|
77
77
|
|
78
78
|
include Settings
|
79
79
|
include Connection
|
80
|
-
include
|
80
|
+
include DatabaseCommands
|
81
81
|
include Serialization
|
82
82
|
end
|
83
83
|
|
@@ -44,8 +44,18 @@ module Familia
|
|
44
44
|
new(**parsed)
|
45
45
|
end
|
46
46
|
|
47
|
-
def self.from_json(
|
48
|
-
|
47
|
+
def self.from_json(json_string_or_hash)
|
48
|
+
# Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
|
49
|
+
if json_string_or_hash.is_a?(Hash)
|
50
|
+
# Already parsed - use directly
|
51
|
+
parsed = json_string_or_hash
|
52
|
+
# Symbolize keys if they're strings
|
53
|
+
parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
|
54
|
+
new(**parsed)
|
55
|
+
else
|
56
|
+
# JSON string - validate and parse
|
57
|
+
validate!(json_string_or_hash)
|
58
|
+
end
|
49
59
|
end
|
50
60
|
|
51
61
|
# Instance methods for decryptability validation
|
@@ -32,15 +32,22 @@ module Familia
|
|
32
32
|
Familia::Encryption.secure_wipe(key) if key
|
33
33
|
end
|
34
34
|
|
35
|
-
def decrypt(
|
36
|
-
return nil if
|
35
|
+
def decrypt(encrypted_json_or_hash, context:, additional_data: nil)
|
36
|
+
return nil if encrypted_json_or_hash.nil? || (encrypted_json_or_hash.respond_to?(:empty?) && encrypted_json_or_hash.empty?)
|
37
37
|
|
38
38
|
# Increment counter immediately to track all decryption attempts, even failed ones
|
39
39
|
Familia::Encryption.derivation_count.increment
|
40
40
|
|
41
41
|
begin
|
42
|
-
|
43
|
-
|
42
|
+
# Delegate parsing and instantiation to EncryptedData.from_json
|
43
|
+
# Wrap validation errors for security (don't expose internal structure details)
|
44
|
+
begin
|
45
|
+
data = Familia::Encryption::EncryptedData.from_json(encrypted_json_or_hash)
|
46
|
+
raise EncryptionError, 'Failed to parse encrypted data' unless data
|
47
|
+
rescue EncryptionError => e
|
48
|
+
# Re-wrap validation errors with generic message for security
|
49
|
+
raise EncryptionError, "Decryption failed: #{e.message}"
|
50
|
+
end
|
44
51
|
|
45
52
|
# Validate algorithm support
|
46
53
|
provider = Registry.get(data.algorithm)
|
data/lib/familia/errors.rb
CHANGED
@@ -1,19 +1,52 @@
|
|
1
1
|
# lib/familia/errors.rb
|
2
2
|
#
|
3
3
|
module Familia
|
4
|
+
# Base exception class for all Familia errors
|
4
5
|
class Problem < RuntimeError; end
|
5
|
-
class NoIdentifier < Problem; end
|
6
|
-
class NonUniqueKey < Problem; end
|
7
6
|
|
8
|
-
class
|
9
|
-
class
|
7
|
+
# Base exception class for Redis/persistence-related errors
|
8
|
+
class PersistenceError < Problem; end
|
10
9
|
|
11
|
-
class
|
10
|
+
# Base exception class for Horreum models
|
11
|
+
class HorreumError < Problem; end
|
12
12
|
|
13
|
-
# Raised when
|
14
|
-
|
13
|
+
# Raised when an object creation fails (e.g. the identifier
|
14
|
+
# is already in use)
|
15
|
+
class CreationError < HorreumError; end
|
15
16
|
|
16
|
-
|
17
|
+
# Raised when an object lacks a required identifier
|
18
|
+
class NoIdentifier < HorreumError; end
|
19
|
+
|
20
|
+
# Raised when a key is expected to be unique but isn't
|
21
|
+
class NonUniqueKey < PersistenceError; end
|
22
|
+
|
23
|
+
# Raised when watch failed (e.g. key was modified), typically
|
24
|
+
# retry
|
25
|
+
class OptimisticLockError < PersistenceError; end
|
26
|
+
|
27
|
+
# Raised when a field type is invalid or unexpected
|
28
|
+
class FieldTypeError < HorreumError; end
|
29
|
+
|
30
|
+
# Raised when autoloading fails
|
31
|
+
class AutoloadError < HorreumError; end
|
32
|
+
|
33
|
+
# Raised when serialization or deserialization fails
|
34
|
+
class SerializerError < HorreumError; end
|
35
|
+
|
36
|
+
# Raised when attempting to start transactions or pipelines on
|
37
|
+
# connection types that don't support them
|
38
|
+
class OperationModeError < PersistenceError; end
|
39
|
+
|
40
|
+
# Raised when attempting to call a major method like save when
|
41
|
+
# inside a transaction or pipeline
|
42
|
+
class NestedTransactionError < OperationModeError; end
|
43
|
+
|
44
|
+
# Raised when attempting to reference a field that doesn't exist
|
45
|
+
class UnknownFieldError < HorreumError; end
|
46
|
+
|
47
|
+
# Raised when a value cannot be converted to a distinguishable
|
48
|
+
# string representation
|
49
|
+
class NotDistinguishableError < HorreumError
|
17
50
|
attr_reader :value
|
18
51
|
|
19
52
|
def initialize(value)
|
@@ -26,7 +59,8 @@ module Familia
|
|
26
59
|
end
|
27
60
|
end
|
28
61
|
|
29
|
-
|
62
|
+
# Raised when no connection is available for a given URI
|
63
|
+
class NotConnected < PersistenceError
|
30
64
|
attr_reader :uri
|
31
65
|
|
32
66
|
def initialize(uri)
|
@@ -39,13 +73,15 @@ module Familia
|
|
39
73
|
end
|
40
74
|
end
|
41
75
|
|
42
|
-
# UnsortedSet Familia.connection_provider or use middleware
|
43
|
-
|
76
|
+
# UnsortedSet Familia.connection_provider or use middleware
|
77
|
+
# to provide connections.
|
78
|
+
class NoConnectionAvailable < PersistenceError; end
|
44
79
|
|
45
80
|
# Raised when a load method fails to find the requested object
|
46
|
-
class NotFound <
|
81
|
+
class NotFound < PersistenceError; end
|
47
82
|
|
48
|
-
# Raised when attempting to refresh an object whose key
|
83
|
+
# Raised when attempting to refresh an object whose key
|
84
|
+
# doesn't exist in the database
|
49
85
|
class KeyNotFoundError < NonUniqueKey
|
50
86
|
attr_reader :key
|
51
87
|
|
@@ -59,7 +95,8 @@ module Familia
|
|
59
95
|
end
|
60
96
|
end
|
61
97
|
|
62
|
-
# Raised when attempting to create an object that already
|
98
|
+
# Raised when attempting to create an object that already
|
99
|
+
# exists in the database
|
63
100
|
class RecordExistsError < NonUniqueKey
|
64
101
|
attr_reader :key
|
65
102
|
|
@@ -28,7 +28,9 @@ module Familia::Features
|
|
28
28
|
]
|
29
29
|
|
30
30
|
# Ensure the Features module exists within the base module
|
31
|
-
|
31
|
+
unless base.const_defined?(:Features) || config_name.eql?('features')
|
32
|
+
base.const_set(:Features, Module.new)
|
33
|
+
end
|
32
34
|
|
33
35
|
# Use the shared autoload_files method
|
34
36
|
autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
|