familia 2.0.0.pre17 → 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/CHANGELOG.rst +60 -0
- data/CLAUDE.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- 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.rb +2 -2
- 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/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/horreum/database_commands.rb +1 -1
- data/lib/familia/horreum/definition.rb +6 -37
- data/lib/familia/horreum/management.rb +17 -12
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum/serialization.rb +91 -73
- data/lib/familia/horreum.rb +10 -6
- 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/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 +6 -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 +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 +1 -1
- 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 +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 +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 +1 -1
- 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 +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 +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 +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/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- 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/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
- 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 +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 +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 +1 -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
- metadata +134 -125
- 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
@@ -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}]")
|
@@ -29,8 +29,10 @@ module Familia
|
|
29
29
|
# Already concealed, store as-is
|
30
30
|
instance_variable_set(:"@#{field_name}", value)
|
31
31
|
elsif field_type.encrypted_json?(value)
|
32
|
-
# Already encrypted JSON from database - wrap in ConcealedString without re-encrypting
|
33
|
-
|
32
|
+
# Already encrypted (JSON string or Hash from database) - wrap in ConcealedString without re-encrypting
|
33
|
+
# Convert Hash back to JSON string if needed (v2.0 deserialization returns Hash)
|
34
|
+
encrypted_string = value.is_a?(Hash) ? Familia::JsonSerializer.dump(value) : value
|
35
|
+
concealed = ConcealedString.new(encrypted_string, self, field_type)
|
34
36
|
instance_variable_set(:"@#{field_name}", concealed)
|
35
37
|
else
|
36
38
|
# Encrypt plaintext and wrap in ConcealedString
|
@@ -138,7 +140,13 @@ module Familia
|
|
138
140
|
|
139
141
|
# Check if a string looks like encrypted JSON data
|
140
142
|
def encrypted_json?(data)
|
141
|
-
|
143
|
+
# Support both JSON strings (legacy) and Hashes (v2.0 deserialization)
|
144
|
+
if data.is_a?(Hash)
|
145
|
+
required_keys = %w[algorithm nonce ciphertext auth_tag key_version]
|
146
|
+
required_keys.all? { |key| data.key?(key) || data.key?(key.to_sym) }
|
147
|
+
else
|
148
|
+
Familia::Encryption::EncryptedData.valid?(data)
|
149
|
+
end
|
142
150
|
end
|
143
151
|
|
144
152
|
private
|
@@ -75,7 +75,7 @@ module Familia
|
|
75
75
|
actual_target_class.class_eval do
|
76
76
|
# Helper method to get index set for a specific field value
|
77
77
|
# This acts as a factory for field-value-specific DataTypes
|
78
|
-
define_method("#{index_name}_for") do |field_value|
|
78
|
+
define_method(:"#{index_name}_for") do |field_value|
|
79
79
|
# Return properly managed DataType instance with parameterized key
|
80
80
|
index_key = "#{index_name}:#{field_value}"
|
81
81
|
Familia::UnsortedSet.new(index_key, parent: self)
|
@@ -99,26 +99,26 @@ module Familia
|
|
99
99
|
# Generate instance sampling method (e.g., company.sample_from_department)
|
100
100
|
actual_target_class.class_eval do
|
101
101
|
|
102
|
-
define_method("sample_from_#{field}") do |field_value, count = 1|
|
102
|
+
define_method(:"sample_from_#{field}") do |field_value, count = 1|
|
103
103
|
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
104
104
|
|
105
105
|
# Get random members efficiently (O(1) via SRANDMEMBER with count)
|
106
106
|
# Returns array even for count=1 for consistent API
|
107
107
|
index_set.sample(count).map do |id|
|
108
|
-
indexed_class.
|
108
|
+
indexed_class.find_by_identifier(id)
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
112
|
# Generate bulk query method (e.g., company.find_all_by_department)
|
113
|
-
define_method("find_all_by_#{field}") do |field_value|
|
113
|
+
define_method(:"find_all_by_#{field}") do |field_value|
|
114
114
|
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
115
115
|
|
116
116
|
# Get all members from set
|
117
|
-
index_set.members.map { |id| indexed_class.
|
117
|
+
index_set.members.map { |id| indexed_class.find_by_identifier(id) }
|
118
118
|
end
|
119
119
|
|
120
120
|
# Generate method to rebuild the index for this parent instance
|
121
|
-
define_method("rebuild_#{index_name}") do
|
121
|
+
define_method(:"rebuild_#{index_name}") do
|
122
122
|
# This would need to be implemented based on how you track which
|
123
123
|
# objects belong to this parent instance
|
124
124
|
# For now, just a placeholder
|
@@ -138,7 +138,7 @@ module Familia
|
|
138
138
|
def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
139
139
|
target_class_config = target_class.config_name
|
140
140
|
indexed_class.class_eval do
|
141
|
-
method_name = "add_to_#{target_class_config}_#{index_name}"
|
141
|
+
method_name = :"add_to_#{target_class_config}_#{index_name}"
|
142
142
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
143
143
|
|
144
144
|
define_method(method_name) do |target_instance|
|
@@ -154,7 +154,7 @@ module Familia
|
|
154
154
|
index_set.add(identifier)
|
155
155
|
end
|
156
156
|
|
157
|
-
method_name = "remove_from_#{target_class_config}_#{index_name}"
|
157
|
+
method_name = :"remove_from_#{target_class_config}_#{index_name}"
|
158
158
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
159
159
|
|
160
160
|
define_method(method_name) do |target_instance|
|
@@ -170,7 +170,7 @@ module Familia
|
|
170
170
|
index_set.remove(identifier)
|
171
171
|
end
|
172
172
|
|
173
|
-
method_name = "update_in_#{target_class_config}_#{index_name}"
|
173
|
+
method_name = :"update_in_#{target_class_config}_#{index_name}"
|
174
174
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
175
175
|
|
176
176
|
define_method(method_name) do |target_instance, old_field_value = nil|
|
@@ -104,36 +104,42 @@ module Familia
|
|
104
104
|
|
105
105
|
# Generate instance query method (e.g., company.find_by_badge_number)
|
106
106
|
actual_target_class.class_eval do
|
107
|
-
define_method("find_by_#{field}") do |
|
107
|
+
define_method(:"find_by_#{field}") do |provided_value|
|
108
108
|
# Use declared field accessor instead of manual instantiation
|
109
109
|
index_hash = send(index_name)
|
110
110
|
|
111
|
-
# Get the identifier from the hash
|
112
|
-
|
113
|
-
|
111
|
+
# Get the identifier from the hash using .get method.
|
112
|
+
# We use .get instead of [] because it's part of the standard interface
|
113
|
+
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
114
|
+
# While unique indexes always use HashKey, using .get maintains consistency
|
115
|
+
# with the broader DataType API patterns used throughout Familia.
|
116
|
+
record_id = index_hash.get(provided_value)
|
117
|
+
return nil unless record_id
|
114
118
|
|
115
|
-
indexed_class.
|
119
|
+
indexed_class.find_by_identifier(record_id)
|
116
120
|
end
|
117
121
|
|
118
122
|
# Generate bulk query method (e.g., company.find_all_by_badge_number)
|
119
|
-
define_method("find_all_by_#{field}") do |
|
120
|
-
|
121
|
-
return [] if
|
123
|
+
define_method(:"find_all_by_#{field}") do |provided_ids|
|
124
|
+
provided_ids = Array(provided_ids)
|
125
|
+
return [] if provided_ids.empty?
|
122
126
|
|
123
127
|
# Use declared field accessor instead of manual instantiation
|
124
128
|
index_hash = send(index_name)
|
125
129
|
|
126
130
|
# Get all identifiers from the hash
|
127
|
-
|
131
|
+
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
128
132
|
# Filter out nil values and instantiate objects
|
129
|
-
|
133
|
+
record_ids.compact.map { |record_id|
|
134
|
+
indexed_class.find_by_identifier(record_id)
|
135
|
+
}
|
130
136
|
end
|
131
137
|
|
132
138
|
# Accessor method already created by ensure_index_field above
|
133
139
|
# No need to manually define it here
|
134
140
|
|
135
141
|
# Generate method to rebuild the unique index for this parent instance
|
136
|
-
define_method("rebuild_#{index_name}") do
|
142
|
+
define_method(:"rebuild_#{index_name}") do
|
137
143
|
# Use declared field accessor instead of manual instantiation
|
138
144
|
index_hash = send(index_name)
|
139
145
|
|
@@ -160,7 +166,7 @@ module Familia
|
|
160
166
|
def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
161
167
|
target_class_config = target_class.config_name
|
162
168
|
indexed_class.class_eval do
|
163
|
-
method_name = "add_to_#{target_class_config}_#{index_name}"
|
169
|
+
method_name = :"add_to_#{target_class_config}_#{index_name}"
|
164
170
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
165
171
|
|
166
172
|
define_method(method_name) do |target_instance|
|
@@ -176,7 +182,7 @@ module Familia
|
|
176
182
|
index_hash[field_value.to_s] = identifier
|
177
183
|
end
|
178
184
|
|
179
|
-
method_name = "remove_from_#{target_class_config}_#{index_name}"
|
185
|
+
method_name = :"remove_from_#{target_class_config}_#{index_name}"
|
180
186
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
181
187
|
|
182
188
|
define_method(method_name) do |target_instance|
|
@@ -192,7 +198,7 @@ module Familia
|
|
192
198
|
index_hash.remove(field_value.to_s)
|
193
199
|
end
|
194
200
|
|
195
|
-
method_name = "update_in_#{target_class_config}_#{index_name}"
|
201
|
+
method_name = :"update_in_#{target_class_config}_#{index_name}"
|
196
202
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
197
203
|
|
198
204
|
define_method(method_name) do |target_instance, old_field_value = nil|
|
@@ -222,31 +228,39 @@ module Familia
|
|
222
228
|
# - Employee.email_index
|
223
229
|
# - Employee.rebuild_email_index
|
224
230
|
def generate_query_methods_class(field, index_name, indexed_class)
|
225
|
-
indexed_class.define_singleton_method("find_by_#{field}") do |
|
231
|
+
indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
|
226
232
|
index_hash = send(index_name) # Access the class-level hashkey DataType
|
227
|
-
object_id = index_hash[field_value.to_s]
|
228
233
|
|
229
|
-
|
234
|
+
# Get the identifier from the hash using .get method.
|
235
|
+
# We use .get instead of [] because it's part of the standard interface
|
236
|
+
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
237
|
+
# While unique indexes always use HashKey, using .get maintains consistency
|
238
|
+
# with the broader DataType API patterns used throughout Familia.
|
239
|
+
record_id = index_hash.get(provided_id)
|
230
240
|
|
231
|
-
|
241
|
+
return nil unless record_id
|
242
|
+
|
243
|
+
indexed_class.find_by_identifier(record_id)
|
232
244
|
end
|
233
245
|
|
234
246
|
# Generate class-level bulk query method
|
235
|
-
indexed_class.define_singleton_method("find_all_by_#{field}") do |
|
236
|
-
|
237
|
-
return [] if
|
247
|
+
indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
|
248
|
+
provided_ids = Array(provided_ids)
|
249
|
+
return [] if provided_ids.empty?
|
238
250
|
|
239
251
|
index_hash = send(index_name) # Access the class-level hashkey DataType
|
240
|
-
|
252
|
+
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
241
253
|
# Filter out nil values and instantiate objects
|
242
|
-
|
254
|
+
record_ids.compact.map { |record_id|
|
255
|
+
indexed_class.find_by_identifier(record_id)
|
256
|
+
}
|
243
257
|
end
|
244
258
|
|
245
259
|
# The index accessor method is already created by the class_hashkey declaration
|
246
260
|
# No need to manually create it - Horreum handles this automatically
|
247
261
|
|
248
262
|
# Generate method to rebuild the class-level index
|
249
|
-
indexed_class.define_singleton_method("rebuild_#{index_name}") do
|
263
|
+
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
|
250
264
|
index_hash = send(index_name) # Access the class-level hashkey DataType
|
251
265
|
|
252
266
|
# Clear existing index using DataType method
|
@@ -265,7 +279,7 @@ module Familia
|
|
265
279
|
# - employee.update_in_class_email_index(old_email)
|
266
280
|
def generate_mutation_methods_class(field, index_name, indexed_class)
|
267
281
|
indexed_class.class_eval do
|
268
|
-
define_method("add_to_class_#{index_name}") do
|
282
|
+
define_method(:"add_to_class_#{index_name}") do
|
269
283
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
270
284
|
field_value = send(field)
|
271
285
|
|
@@ -274,7 +288,7 @@ module Familia
|
|
274
288
|
index_hash[field_value.to_s] = identifier
|
275
289
|
end
|
276
290
|
|
277
|
-
define_method("remove_from_class_#{index_name}") do
|
291
|
+
define_method(:"remove_from_class_#{index_name}") do
|
278
292
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
279
293
|
field_value = send(field)
|
280
294
|
|
@@ -283,7 +297,7 @@ module Familia
|
|
283
297
|
index_hash.remove(field_value.to_s)
|
284
298
|
end
|
285
299
|
|
286
|
-
define_method("update_in_class_#{index_name}") do |old_field_value = nil|
|
300
|
+
define_method(:"update_in_class_#{index_name}") do |old_field_value = nil|
|
287
301
|
new_field_value = send(field)
|
288
302
|
|
289
303
|
# Use class-level transaction for atomicity with DataType abstraction
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# lib/familia/features/safe_dump.rb
|
2
2
|
|
3
3
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# at runtime, so thread safety is not a concern for this feature.
|
4
|
+
# Class instance variables are used here for configuration. These are set
|
5
|
+
# once at loadtime and not mutated, so thread safety is not an issue here.
|
7
6
|
#
|
8
7
|
module Familia
|
9
8
|
module Features
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require_relative 'settings'
|
4
4
|
|
5
|
+
require_relative '../field_type'
|
6
|
+
|
5
7
|
module Familia
|
6
8
|
VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
|
7
9
|
|
@@ -27,10 +29,6 @@ module Familia
|
|
27
29
|
@related_fields = nil
|
28
30
|
@default_expiration = nil
|
29
31
|
|
30
|
-
# Serialization settings
|
31
|
-
@dump_method = nil
|
32
|
-
@load_method = nil
|
33
|
-
|
34
32
|
# Field groups
|
35
33
|
@field_groups = nil
|
36
34
|
@current_field_group = nil
|
@@ -171,30 +169,9 @@ module Familia
|
|
171
169
|
# - :skip - skip definition if method exists
|
172
170
|
# - :warn - warn but proceed (may overwrite)
|
173
171
|
# - :ignore - proceed silently (may overwrite)
|
174
|
-
#
|
175
|
-
|
176
|
-
|
177
|
-
# - :transient - field is not persisted
|
178
|
-
# - Others, depending on features available
|
179
|
-
#
|
180
|
-
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
|
181
|
-
# Use field type system for consistency
|
182
|
-
require_relative '../field_type'
|
183
|
-
|
184
|
-
# Create appropriate field type based on category
|
185
|
-
field_type = if category == :transient
|
186
|
-
require_relative '../features/transient_fields/transient_field_type'
|
187
|
-
TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
|
188
|
-
else
|
189
|
-
# For regular fields and other categories, create custom field type with category override
|
190
|
-
custom_field_type = Class.new(FieldType) do
|
191
|
-
define_method :category do
|
192
|
-
category || :field
|
193
|
-
end
|
194
|
-
end
|
195
|
-
custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
|
196
|
-
end
|
197
|
-
|
172
|
+
#
|
173
|
+
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise)
|
174
|
+
field_type = FieldType.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
|
198
175
|
register_field_type(field_type)
|
199
176
|
end
|
200
177
|
|
@@ -256,14 +233,6 @@ module Familia
|
|
256
233
|
@has_related_fields ||= false
|
257
234
|
end
|
258
235
|
|
259
|
-
def dump_method
|
260
|
-
@dump_method || :to_json # Familia.dump_method
|
261
|
-
end
|
262
|
-
|
263
|
-
def load_method
|
264
|
-
@load_method || :from_json # Familia.load_method
|
265
|
-
end
|
266
|
-
|
267
236
|
# Storage for field type instances
|
268
237
|
def field_types
|
269
238
|
@field_types ||= {}
|
@@ -506,7 +475,7 @@ module Familia
|
|
506
475
|
|
507
476
|
# Convert the provided value to a format suitable for Database storage.
|
508
477
|
prepared = serialize_value(val)
|
509
|
-
Familia.ld "[
|
478
|
+
Familia.ld "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
|
510
479
|
|
511
480
|
# Use the existing accessor method to set the attribute value.
|
512
481
|
send :"#{method_name}=", val
|
@@ -125,15 +125,15 @@ module Familia
|
|
125
125
|
# User.find_by_key("user:123") # Returns a User instance if it exists,
|
126
126
|
# nil otherwise
|
127
127
|
#
|
128
|
-
def
|
128
|
+
def find_by_dbkey(objkey)
|
129
129
|
raise ArgumentError, 'Empty key' if objkey.to_s.empty?
|
130
130
|
|
131
131
|
# We use a lower-level method here b/c we're working with the
|
132
132
|
# full key and not just the identifier.
|
133
133
|
does_exist = dbclient.exists(objkey).positive?
|
134
134
|
|
135
|
-
Familia.ld "[
|
136
|
-
Familia.trace :
|
135
|
+
Familia.ld "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
136
|
+
Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
|
137
137
|
|
138
138
|
# This is the reason for calling exists first. We want to definitively
|
139
139
|
# and without any ambiguity know if the object exists in the database. If it
|
@@ -143,11 +143,16 @@ module Familia
|
|
143
143
|
return unless does_exist
|
144
144
|
|
145
145
|
obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
|
146
|
-
Familia.trace :
|
147
|
-
|
148
|
-
|
146
|
+
Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
|
147
|
+
|
148
|
+
# Create instance and deserialize fields using existing helper method
|
149
|
+
# This avoids duplicating deserialization logic and keeps field-by-field processing
|
150
|
+
instance = allocate
|
151
|
+
instance.send(:initialize_relatives)
|
152
|
+
instance.send(:initialize_with_keyword_args_deserialize_value, **obj)
|
153
|
+
instance
|
149
154
|
end
|
150
|
-
alias
|
155
|
+
alias find_by_key find_by_dbkey
|
151
156
|
|
152
157
|
# Retrieves and instantiates an object from Database using its identifier.
|
153
158
|
#
|
@@ -168,19 +173,19 @@ module Familia
|
|
168
173
|
# @example
|
169
174
|
# User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
|
170
175
|
#
|
171
|
-
def
|
176
|
+
def find_by_identifier(identifier, suffix = nil)
|
172
177
|
suffix ||= self.suffix
|
173
178
|
return nil if identifier.to_s.empty?
|
174
179
|
|
175
180
|
objkey = dbkey(identifier, suffix)
|
176
181
|
|
177
|
-
Familia.ld "[
|
182
|
+
Familia.ld "[find_by_id] #{self} from key #{objkey})"
|
178
183
|
Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
|
179
|
-
|
184
|
+
find_by_dbkey objkey
|
180
185
|
end
|
186
|
+
alias find_by_id find_by_identifier
|
181
187
|
alias find find_by_id
|
182
|
-
alias load find_by_id
|
183
|
-
alias from_identifier find_by_id # deprecated
|
188
|
+
alias load find_by_id
|
184
189
|
|
185
190
|
# Checks if an object with the given identifier exists in the database.
|
186
191
|
#
|
@@ -354,7 +354,7 @@ module Familia
|
|
354
354
|
# their uninitialized state during refresh operations
|
355
355
|
reset_transient_fields!
|
356
356
|
|
357
|
-
|
357
|
+
naive_refresh(**fields)
|
358
358
|
end
|
359
359
|
|
360
360
|
# Refreshes object state from the DB and returns self for method chaining.
|