familia 2.0.0.pre16 → 2.0.0.pre18
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/ci.yml +2 -2
- data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.rst +82 -0
- data/CLAUDE.md +47 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/overview.md +2 -2
- data/docs/qodo-merge-compliance.md +96 -0
- data/docs/reference/api-technical.md +1 -1
- data/examples/encrypted_fields.rb +1 -1
- data/examples/safe_dump.rb +1 -1
- data/lib/familia/base.rb +6 -6
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/hashkey.rb +2 -1
- data/lib/familia/data_type/types/sorted_set.rb +113 -10
- data/lib/familia/data_type/types/stringkey.rb +0 -4
- data/lib/familia/data_type.rb +8 -195
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/encrypted_fields.rb +5 -2
- data/lib/familia/features/external_identifier.rb +49 -8
- data/lib/familia/features/object_identifier.rb +84 -12
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
- data/lib/familia/features/relationships/indexing.rb +7 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features/transient_fields.rb +7 -2
- data/lib/familia/features.rb +6 -1
- data/lib/familia/field_type.rb +0 -18
- data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
- data/lib/familia/horreum/serialization.rb +190 -0
- data/lib/familia/horreum.rb +39 -14
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -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 +1 -1
- 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 +1 -1
- 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/field_groups_try.rb +244 -0
- 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 +16 -1
- 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 +1 -1
- 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 +3 -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 +1 -1
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- 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 +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- data/try/{core → integration}/database_consistency_try.rb +12 -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 +1 -1
- data/try/{models → integration/models}/customer_try.rb +6 -6
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +2 -2
- 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 +15 -7
- data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
- 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 +5 -5
- data/try/{core → unit/core}/errors_try.rb +4 -4
- 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 +2 -2
- 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 +3 -3
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
- 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/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/{data_types → unit/data_types}/string_try.rb +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
- data/try/unit/horreum/defensive_initialization_try.rb +86 -0
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -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 +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
- 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
- data/try/valkey.conf +26 -0
- metadata +149 -132
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
- /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
- /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_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
data/lib/familia/base.rb
CHANGED
@@ -17,8 +17,6 @@ module Familia
|
|
17
17
|
|
18
18
|
@features_available = nil
|
19
19
|
@feature_definitions = nil
|
20
|
-
@dump_method = :to_json
|
21
|
-
@load_method = :from_json
|
22
20
|
|
23
21
|
def self.included(base)
|
24
22
|
# Ensure the including class gets its own feature registry
|
@@ -31,14 +29,15 @@ module Familia
|
|
31
29
|
attr_reader :features_available, :feature_definitions
|
32
30
|
attr_accessor :dump_method, :load_method
|
33
31
|
|
34
|
-
def add_feature(klass, feature_name, depends_on: [])
|
32
|
+
def add_feature(klass, feature_name, depends_on: [], field_group: nil)
|
35
33
|
@features_available ||= {}
|
36
34
|
Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
|
37
35
|
|
38
36
|
# Create field definition object
|
39
37
|
feature_def = FeatureDefinition.new(
|
40
38
|
name: feature_name,
|
41
|
-
depends_on: depends_on
|
39
|
+
depends_on: depends_on,
|
40
|
+
field_group: field_group
|
42
41
|
)
|
43
42
|
|
44
43
|
# Track field definitions after defining field methods
|
@@ -112,14 +111,15 @@ module Familia
|
|
112
111
|
attr_reader :features_available, :feature_definitions
|
113
112
|
attr_accessor :dump_method, :load_method
|
114
113
|
|
115
|
-
def add_feature(klass, feature_name, depends_on: [])
|
114
|
+
def add_feature(klass, feature_name, depends_on: [], field_group: nil)
|
116
115
|
@features_available ||= {}
|
117
116
|
Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
|
118
117
|
|
119
118
|
# Create field definition object
|
120
119
|
feature_def = FeatureDefinition.new(
|
121
120
|
name: feature_name,
|
122
|
-
depends_on: depends_on
|
121
|
+
depends_on: depends_on,
|
122
|
+
field_group: field_group
|
123
123
|
)
|
124
124
|
|
125
125
|
# Track field definitions after defining field methods
|
@@ -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
|
data/lib/familia/connection.rb
CHANGED
@@ -42,7 +42,7 @@ module Familia
|
|
42
42
|
@connection_chain = nil # Force rebuild of chain
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
# Sets the default URI for Database connections.
|
46
46
|
#
|
47
47
|
# NOTE: uri is not a property of the Settings module b/c it's not
|
48
48
|
# configured in class defintions like default_expiration or logical DB index.
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# lib/familia/data_type/definition.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class DataType
|
5
|
+
# ClassMethods - Class-level DSL methods for defining DataType behavior
|
6
|
+
#
|
7
|
+
# This module is extended into classes that inherit from Familia::DataType,
|
8
|
+
# providing class methods for type registration, configuration, and inheritance.
|
9
|
+
#
|
10
|
+
# Key features:
|
11
|
+
# * Type registration system for creating DataType subclasses
|
12
|
+
# * Database and connection configuration
|
13
|
+
# * Inheritance hooks for propagating settings
|
14
|
+
# * Option validation and filtering
|
15
|
+
#
|
16
|
+
module ClassMethods
|
17
|
+
attr_accessor :parent, :suffix, :prefix, :uri
|
18
|
+
attr_writer :logical_database
|
19
|
+
|
20
|
+
# To be called inside every class that inherits DataType
|
21
|
+
# +methname+ is the term used for the class and instance methods
|
22
|
+
# that are created for the given +klass+ (e.g. set, list, etc)
|
23
|
+
def register(klass, methname)
|
24
|
+
Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}" if Familia.debug?
|
25
|
+
|
26
|
+
@registered_types[methname] = klass
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the registered type class from a given method name
|
30
|
+
# +methname+ is the method name used to register the class (e.g. :set, :list, etc)
|
31
|
+
# Returns the registered class or nil if not found
|
32
|
+
def registered_type(methname)
|
33
|
+
@registered_types[methname]
|
34
|
+
end
|
35
|
+
|
36
|
+
def logical_database(val = nil)
|
37
|
+
@logical_database = val unless val.nil?
|
38
|
+
@logical_database || parent&.logical_database
|
39
|
+
end
|
40
|
+
|
41
|
+
def uri(val = nil)
|
42
|
+
@uri = val unless val.nil?
|
43
|
+
@uri || (parent ? parent.uri : Familia.uri)
|
44
|
+
end
|
45
|
+
|
46
|
+
def inherited(obj)
|
47
|
+
Familia.trace :DATATYPE, nil, "#{obj} is my kinda type" if Familia.debug?
|
48
|
+
obj.logical_database = logical_database
|
49
|
+
obj.default_expiration = default_expiration # method added via Features::Expiration
|
50
|
+
obj.uri = uri
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid_keys_only(opts)
|
55
|
+
opts.slice(*DataType.valid_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def relations?
|
59
|
+
@has_related_fields ||= false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# lib/familia/data_type/connection.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class DataType
|
5
|
+
# Connection - Instance-level connection and key generation methods
|
6
|
+
#
|
7
|
+
# This module provides instance methods for database connection resolution
|
8
|
+
# and Redis key generation for DataType objects.
|
9
|
+
#
|
10
|
+
# Key features:
|
11
|
+
# * Database connection resolution with Chain of Responsibility pattern
|
12
|
+
# * Redis key generation based on parent context
|
13
|
+
# * Direct database access for advanced operations
|
14
|
+
#
|
15
|
+
module Connection
|
16
|
+
# TODO: Replace with Chain of Responsibility pattern
|
17
|
+
def dbclient
|
18
|
+
return Fiber[:familia_transaction] if Fiber[:familia_transaction]
|
19
|
+
return @dbclient if @dbclient
|
20
|
+
|
21
|
+
# Delegate to parent if present, otherwise fall back to Familia
|
22
|
+
parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Produces the full dbkey for this object.
|
26
|
+
#
|
27
|
+
# @return [String] The full dbkey.
|
28
|
+
#
|
29
|
+
# This method determines the appropriate dbkey based on the context of the DataType object:
|
30
|
+
#
|
31
|
+
# 1. If a hardcoded key is set in the options, it returns that key.
|
32
|
+
# 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
|
33
|
+
# 3. For class-level DataType objects, it uses the parent class's dbkey method.
|
34
|
+
# 4. For standalone DataType objects, it uses the keystring as the full dbkey.
|
35
|
+
#
|
36
|
+
# For class-level DataType objects (parent_class? == true):
|
37
|
+
# - The suffix is optional and used to differentiate between different types of objects.
|
38
|
+
# - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
|
39
|
+
# - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
|
40
|
+
# - Passing nil as the suffix is how class-level DataType objects are created without
|
41
|
+
# the global default 'object' suffix.
|
42
|
+
#
|
43
|
+
# @example Instance-level DataType
|
44
|
+
# user_instance.some_datatype.dbkey # => "user:123:some_datatype"
|
45
|
+
#
|
46
|
+
# @example Class-level DataType
|
47
|
+
# User.some_datatype.dbkey # => "user:some_datatype"
|
48
|
+
#
|
49
|
+
# @example Standalone DataType
|
50
|
+
# DataType.new("mykey").dbkey # => "mykey"
|
51
|
+
#
|
52
|
+
# @example Class-level DataType with explicit nil suffix
|
53
|
+
# User.dbkey("123", nil) # => "user:123"
|
54
|
+
#
|
55
|
+
def dbkey
|
56
|
+
# Return the hardcoded key if it's set. This is useful for
|
57
|
+
# support legacy keys that aren't derived in the same way.
|
58
|
+
return opts[:dbkey] if opts[:dbkey]
|
59
|
+
|
60
|
+
if parent_instance?
|
61
|
+
# This is an instance-level datatype object so the parent instance's
|
62
|
+
# dbkey method is defined in Familia::Horreum::InstanceMethods.
|
63
|
+
parent.dbkey(keystring)
|
64
|
+
elsif parent_class?
|
65
|
+
# This is a class-level datatype object so the parent class' dbkey
|
66
|
+
# method is defined in Familia::Horreum::DefinitionMethods.
|
67
|
+
parent.dbkey(keystring, nil)
|
68
|
+
else
|
69
|
+
# This is a standalone DataType object where it's keystring
|
70
|
+
# is the full database key (dbkey).
|
71
|
+
keystring
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Provides a structured way to "gear down" to run db commands that are
|
76
|
+
# not implemented in our DataType classes since we intentionally don't
|
77
|
+
# have a method_missing method.
|
78
|
+
def direct_access
|
79
|
+
yield(dbclient, dbkey)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
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
|
@@ -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
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# lib/familia/data_type/settings.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
class DataType
|
5
|
+
# Settings - Instance-level configuration and introspection methods
|
6
|
+
#
|
7
|
+
# This module provides instance methods for accessing and managing
|
8
|
+
# DataType object configuration, parent relationships, and serialization.
|
9
|
+
#
|
10
|
+
# Key features:
|
11
|
+
# * Parent object relationship management
|
12
|
+
# * URI and database configuration
|
13
|
+
# * Serialization method delegation
|
14
|
+
# * Type introspection
|
15
|
+
#
|
16
|
+
module Settings
|
17
|
+
attr_reader :keystring, :opts, :logical_database
|
18
|
+
attr_reader :uri
|
19
|
+
|
20
|
+
alias url uri
|
21
|
+
|
22
|
+
def class?
|
23
|
+
!@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent_instance?
|
27
|
+
parent&.is_a?(Horreum::ParentDefinition)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parent_class?
|
31
|
+
parent.is_a?(Class) && parent.ancestors.include?(Familia::Horreum)
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent?
|
35
|
+
parent_class? || parent_instance?
|
36
|
+
end
|
37
|
+
|
38
|
+
def parent
|
39
|
+
# Return cached ParentDefinition if available
|
40
|
+
return @parent if @parent
|
41
|
+
|
42
|
+
# Return class-level parent if no instance parent
|
43
|
+
return self.class.parent unless @parent_ref
|
44
|
+
|
45
|
+
# Create ParentDefinition dynamically from stored reference.
|
46
|
+
# This ensures we get the current identifier value (available after initialization)
|
47
|
+
# rather than a stale nil value from initialization time. Cannot cache due to frozen object.
|
48
|
+
Horreum::ParentDefinition.from_parent(@parent_ref)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parent=(value)
|
52
|
+
case value
|
53
|
+
when Horreum::ParentDefinition
|
54
|
+
@parent = value
|
55
|
+
when nil
|
56
|
+
@parent = nil
|
57
|
+
@parent_ref = nil
|
58
|
+
else
|
59
|
+
# Store parent instance reference for lazy ParentDefinition creation.
|
60
|
+
# During initialization, the parent's identifier may not be available yet,
|
61
|
+
# so we defer ParentDefinition creation until first access for memory efficiency.
|
62
|
+
# Note: @parent_ref is not cleared after use because DataType objects are frozen.
|
63
|
+
@parent_ref = value
|
64
|
+
@parent = nil # Will be created dynamically in parent method
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def uri
|
69
|
+
# Return explicit instance URI if set
|
70
|
+
return @uri if @uri
|
71
|
+
|
72
|
+
# If we have a parent with logical_database, build URI with that database
|
73
|
+
if parent && parent.respond_to?(:logical_database) && parent.logical_database
|
74
|
+
new_uri = (self.class.uri || Familia.uri).dup
|
75
|
+
new_uri.db = parent.logical_database
|
76
|
+
new_uri
|
77
|
+
else
|
78
|
+
# Fall back to class-level URI or global Familia.uri
|
79
|
+
self.class.uri || Familia.uri
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def uri=(value)
|
84
|
+
@uri = value
|
85
|
+
end
|
86
|
+
|
87
|
+
def dump_method
|
88
|
+
self.class.dump_method
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_method
|
92
|
+
self.class.load_method
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -95,7 +95,8 @@ module Familia
|
|
95
95
|
def remove_field(field)
|
96
96
|
dbclient.hdel dbkey, field.to_s
|
97
97
|
end
|
98
|
-
alias remove remove_field
|
98
|
+
alias remove remove_field
|
99
|
+
alias remove_element remove_field
|
99
100
|
|
100
101
|
def increment(field, by = 1)
|
101
102
|
dbclient.hincrby(dbkey, field.to_s, by).to_i
|
@@ -46,17 +46,81 @@ module Familia
|
|
46
46
|
add val, score
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
49
|
+
# Adds an element to the sorted set with an optional score and ZADD options.
|
50
|
+
#
|
51
|
+
# This method supports Redis ZADD options for conditional adds and updates:
|
52
|
+
# - **NX**: Only add new elements (don't update existing)
|
53
|
+
# - **XX**: Only update existing elements (don't add new)
|
54
|
+
# - **GT**: Only update if new score > current score
|
55
|
+
# - **LT**: Only update if new score < current score
|
56
|
+
# - **CH**: Return changed count (new + updated) instead of just new count
|
57
|
+
#
|
58
|
+
# @param val [Object] The value to add to the sorted set
|
59
|
+
# @param score [Numeric, nil] The score for ranking (defaults to current timestamp)
|
60
|
+
# @param nx [Boolean] Only add new elements, don't update existing (default: false)
|
61
|
+
# @param xx [Boolean] Only update existing elements, don't add new (default: false)
|
62
|
+
# @param gt [Boolean] Only update if new score > current score (default: false)
|
63
|
+
# @param lt [Boolean] Only update if new score < current score (default: false)
|
64
|
+
# @param ch [Boolean] Return changed count instead of added count (default: false)
|
65
|
+
#
|
66
|
+
# @return [Boolean] Returns the return value from the redis gem's ZADD
|
67
|
+
# command. Returns true if element was added or changed (with CH option),
|
68
|
+
# false if element score was updated without change tracking or no
|
69
|
+
# operation occurred due to option constraints (NX, XX, GT, LT).
|
70
|
+
#
|
71
|
+
# @raise [ArgumentError] If mutually exclusive options are specified together
|
72
|
+
# (NX+XX, GT+LT, NX+GT, NX+LT)
|
73
|
+
#
|
74
|
+
# @example Add new element with timestamp
|
75
|
+
# metrics.add('pageview', Time.now.to_f) #=> true
|
76
|
+
#
|
77
|
+
# @example Preserve original timestamp on subsequent saves
|
78
|
+
# index.add(email, Time.now.to_f, nx: true) #=> true
|
79
|
+
# index.add(email, Time.now.to_f, nx: true) #=> false (unchanged)
|
80
|
+
#
|
81
|
+
# @example Update timestamp only for existing entries
|
82
|
+
# index.add(email, Time.now.to_f, xx: true) #=> false (if doesn't exist)
|
83
|
+
#
|
84
|
+
# @example Only update if new score is higher (leaderboard)
|
85
|
+
# scores.add(player, 1000, gt: true) #=> true (new entry)
|
86
|
+
# scores.add(player, 1500, gt: true) #=> false (updated)
|
87
|
+
# scores.add(player, 1200, gt: true) #=> false (not updated, score lower)
|
88
|
+
#
|
89
|
+
# @example Track total changes for analytics
|
90
|
+
# changed = metrics.add(user, score, ch: true) #=> true (new or updated)
|
91
|
+
#
|
92
|
+
# @example Combined options: only update existing, only if score increases
|
93
|
+
# index.add(key, new_score, xx: true, gt: true)
|
94
|
+
#
|
95
|
+
# @note GT and LT options do NOT prevent adding new elements, they only
|
96
|
+
# affect update behavior for existing elements.
|
97
|
+
#
|
98
|
+
# @note Default behavior (no options) adds new elements and updates existing
|
99
|
+
# ones unconditionally, matching standard Redis ZADD semantics.
|
100
|
+
#
|
101
|
+
# @note INCR option is not supported. Use the increment method for ZINCRBY operations.
|
102
|
+
#
|
103
|
+
def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
|
58
104
|
score ||= Familia.now
|
59
|
-
|
105
|
+
|
106
|
+
# Validate mutual exclusivity
|
107
|
+
validate_zadd_options!(nx: nx, xx: xx, gt: gt, lt: lt)
|
108
|
+
|
109
|
+
# Build options hash for redis gem
|
110
|
+
opts = {}
|
111
|
+
opts[:nx] = true if nx
|
112
|
+
opts[:xx] = true if xx
|
113
|
+
opts[:gt] = true if gt
|
114
|
+
opts[:lt] = true if lt
|
115
|
+
opts[:ch] = true if ch
|
116
|
+
|
117
|
+
# Pass options to ZADD
|
118
|
+
ret = if opts.empty?
|
119
|
+
dbclient.zadd(dbkey, score, serialize_value(val))
|
120
|
+
else
|
121
|
+
dbclient.zadd(dbkey, score, serialize_value(val), **opts)
|
122
|
+
end
|
123
|
+
|
60
124
|
update_expiration
|
61
125
|
ret
|
62
126
|
end
|
@@ -242,6 +306,45 @@ module Familia
|
|
242
306
|
at(-1)
|
243
307
|
end
|
244
308
|
|
309
|
+
|
310
|
+
private
|
311
|
+
|
312
|
+
# Validates that mutually exclusive ZADD options are not specified together.
|
313
|
+
#
|
314
|
+
# @param nx [Boolean] NX option flag
|
315
|
+
# @param xx [Boolean] XX option flag
|
316
|
+
# @param gt [Boolean] GT option flag
|
317
|
+
# @param lt [Boolean] LT option flag
|
318
|
+
#
|
319
|
+
# @raise [ArgumentError] If mutually exclusive options are specified
|
320
|
+
#
|
321
|
+
# @note Valid combinations: XX+GT, XX+LT
|
322
|
+
# @note Invalid combinations: NX+XX, GT+LT, NX+GT, NX+LT
|
323
|
+
#
|
324
|
+
def validate_zadd_options!(nx:, xx:, gt:, lt:)
|
325
|
+
# NX and XX are mutually exclusive
|
326
|
+
if nx && xx
|
327
|
+
raise ArgumentError, "ZADD options NX and XX are mutually exclusive"
|
328
|
+
end
|
329
|
+
|
330
|
+
# GT and LT are mutually exclusive
|
331
|
+
if gt && lt
|
332
|
+
raise ArgumentError, "ZADD options GT and LT are mutually exclusive"
|
333
|
+
end
|
334
|
+
|
335
|
+
# NX is mutually exclusive with GT
|
336
|
+
if nx && gt
|
337
|
+
raise ArgumentError, "ZADD options NX and GT are mutually exclusive"
|
338
|
+
end
|
339
|
+
|
340
|
+
# NX is mutually exclusive with LT
|
341
|
+
if nx && lt
|
342
|
+
raise ArgumentError, "ZADD options NX and LT are mutually exclusive"
|
343
|
+
end
|
344
|
+
|
345
|
+
# Note: XX + GT and XX + LT are valid combinations
|
346
|
+
end
|
347
|
+
|
245
348
|
Familia::DataType.register self, :sorted_set
|
246
349
|
Familia::DataType.register self, :zset
|
247
350
|
end
|