familia 2.0.0.pre15 → 2.0.0.pre17
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-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,5 +1,5 @@
|
|
1
|
-
# lib/familia/horreum/
|
2
|
-
|
1
|
+
# lib/familia/horreum/persistence.rb
|
2
|
+
|
3
3
|
module Familia
|
4
4
|
# Familia::Horreum
|
5
5
|
#
|
@@ -21,7 +21,7 @@ module Familia
|
|
21
21
|
# - nil - Valid response for certain operations
|
22
22
|
#
|
23
23
|
# @example Validating a command response
|
24
|
-
# response =
|
24
|
+
# response = dbclient.set("key", "value")
|
25
25
|
# valid = @valid_command_return_values.include?(response)
|
26
26
|
# # => true if response is "OK"
|
27
27
|
#
|
@@ -31,10 +31,10 @@ module Familia
|
|
31
31
|
attr_reader :valid_command_return_values
|
32
32
|
end
|
33
33
|
|
34
|
-
# Serialization
|
34
|
+
# Serialization - Instance-level methods for object persistence and retrieval
|
35
35
|
# Handles conversion between Ruby objects and Valkey hash storage
|
36
36
|
#
|
37
|
-
module
|
37
|
+
module Persistence
|
38
38
|
# Persists the object to Valkey storage with automatic timestamping.
|
39
39
|
#
|
40
40
|
# Saves the current object state to Valkey storage, automatically setting
|
@@ -61,16 +61,26 @@ module Familia
|
|
61
61
|
# @see #commit_fields The underlying method that performs the field persistence
|
62
62
|
#
|
63
63
|
def save(update_expiration: true)
|
64
|
-
Familia.trace :SAVE,
|
64
|
+
Familia.trace :SAVE, nil, uri if Familia.debug?
|
65
65
|
|
66
66
|
# No longer need to sync computed identifier with a cache field
|
67
67
|
self.created ||= Familia.now.to_i if respond_to?(:created)
|
68
68
|
self.updated = Familia.now.to_i if respond_to?(:updated)
|
69
69
|
|
70
70
|
# Commit our tale to the Database chronicles
|
71
|
-
#
|
71
|
+
# Wrap in transaction for atomicity between save and indexing
|
72
72
|
ret = commit_fields(update_expiration: update_expiration)
|
73
73
|
|
74
|
+
# Auto-index for class-level indexes after successful save
|
75
|
+
# Use transaction to ensure atomicity with the save operation
|
76
|
+
if ret
|
77
|
+
transaction do |conn|
|
78
|
+
auto_update_class_indexes
|
79
|
+
# Add to class-level instances collection after successful save
|
80
|
+
self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
74
84
|
Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
|
75
85
|
|
76
86
|
# Did Database accept our offering?
|
@@ -123,9 +133,9 @@ module Familia
|
|
123
133
|
identifier_field = self.class.identifier_field
|
124
134
|
|
125
135
|
Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
|
126
|
-
Familia.trace :SAVE_IF_NOT_EXISTS,
|
136
|
+
Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
|
127
137
|
|
128
|
-
dbclient.watch(dbkey) do
|
138
|
+
success = dbclient.watch(dbkey) do
|
129
139
|
if dbclient.exists(dbkey).positive?
|
130
140
|
dbclient.unwatch
|
131
141
|
raise Familia::RecordExistsError, dbkey
|
@@ -137,6 +147,16 @@ module Familia
|
|
137
147
|
|
138
148
|
result.is_a?(Array) # transaction succeeded
|
139
149
|
end
|
150
|
+
|
151
|
+
# Auto-index for class-level indexes after successful save
|
152
|
+
# Use transaction to ensure atomicity with the save operation
|
153
|
+
if success
|
154
|
+
transaction do |conn|
|
155
|
+
auto_update_class_indexes
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
success
|
140
160
|
end
|
141
161
|
|
142
162
|
# Commits object fields to the DB storage.
|
@@ -180,7 +200,7 @@ module Familia
|
|
180
200
|
|
181
201
|
# Updates multiple fields atomically in a Database transaction.
|
182
202
|
#
|
183
|
-
# @param
|
203
|
+
# @param kwargs [Hash] Field names and values to update. Special key :update_expiration
|
184
204
|
# controls whether to update key expiration (default: true)
|
185
205
|
# @return [MultiResult] Transaction result
|
186
206
|
#
|
@@ -194,9 +214,9 @@ module Familia
|
|
194
214
|
update_expiration = kwargs.delete(:update_expiration) { true }
|
195
215
|
fields = kwargs
|
196
216
|
|
197
|
-
Familia.trace :BATCH_UPDATE,
|
217
|
+
Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug?
|
198
218
|
|
199
|
-
|
219
|
+
transaction_result = transaction do |conn|
|
200
220
|
fields.each do |field, value|
|
201
221
|
prepared_value = serialize_value(value)
|
202
222
|
conn.hset dbkey, field, prepared_value
|
@@ -208,9 +228,8 @@ module Familia
|
|
208
228
|
# Update expiration if requested and supported
|
209
229
|
self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
|
210
230
|
|
211
|
-
# Return
|
212
|
-
|
213
|
-
MultiResult.new(summary_boolean, command_return_values)
|
231
|
+
# Return the MultiResult directly (transaction already returns MultiResult)
|
232
|
+
transaction_result
|
214
233
|
end
|
215
234
|
|
216
235
|
# Updates the object by applying multiple field values.
|
@@ -235,11 +254,12 @@ module Familia
|
|
235
254
|
self
|
236
255
|
end
|
237
256
|
|
238
|
-
# Permanently removes this object from the DB
|
257
|
+
# Permanently removes this object and its related fields from the DB.
|
239
258
|
#
|
240
|
-
# Deletes the object's
|
259
|
+
# Deletes the object's database key and all associated data. This operation
|
241
260
|
# is irreversible and will permanently destroy all stored information
|
242
|
-
# for this object instance
|
261
|
+
# for this object instance and the additional list, set, hash, string
|
262
|
+
# etc fields defined for this class.
|
243
263
|
#
|
244
264
|
# @return [void]
|
245
265
|
#
|
@@ -259,8 +279,25 @@ module Familia
|
|
259
279
|
# @see #delete! The underlying method that performs the key deletion
|
260
280
|
#
|
261
281
|
def destroy!
|
262
|
-
Familia.trace :DESTROY,
|
263
|
-
|
282
|
+
Familia.trace :DESTROY, dbkey, uri
|
283
|
+
|
284
|
+
# Execute all deletion operations within a transaction
|
285
|
+
transaction do |conn|
|
286
|
+
# Delete the main object key
|
287
|
+
conn.del(dbkey)
|
288
|
+
|
289
|
+
# Delete all related fields if present
|
290
|
+
if self.class.relations?
|
291
|
+
Familia.trace :DELETE_RELATED_FIELDS!, nil,
|
292
|
+
"#{self.class} has relations: #{self.class.related_fields.keys}"
|
293
|
+
|
294
|
+
self.class.related_fields.each do |name, _definition|
|
295
|
+
obj = send(name)
|
296
|
+
Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})"
|
297
|
+
conn.del(obj.dbkey)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
264
301
|
end
|
265
302
|
|
266
303
|
# Clears all fields by setting them to nil.
|
@@ -306,7 +343,7 @@ module Familia
|
|
306
343
|
# no authoritative source in Valkey storage.
|
307
344
|
#
|
308
345
|
def refresh!
|
309
|
-
Familia.trace :REFRESH,
|
346
|
+
Familia.trace :REFRESH, nil, uri if Familia.debug?
|
310
347
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
311
348
|
|
312
349
|
fields = hgetall
|
@@ -341,169 +378,6 @@ module Familia
|
|
341
378
|
self
|
342
379
|
end
|
343
380
|
|
344
|
-
# Converts the object's persistent fields to a hash for external use.
|
345
|
-
#
|
346
|
-
# Serializes persistent field values for external consumption (APIs, logs),
|
347
|
-
# excluding non-loggable fields like encrypted fields for security.
|
348
|
-
# Only non-nil values are included in the resulting hash.
|
349
|
-
#
|
350
|
-
# @return [Hash] Hash with field names as keys and serialized values
|
351
|
-
# safe for external exposure
|
352
|
-
#
|
353
|
-
# @example Converting an object to hash format for API response
|
354
|
-
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
355
|
-
# user.to_h
|
356
|
-
# # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
|
357
|
-
# # encrypted fields are excluded for security
|
358
|
-
#
|
359
|
-
# @note Only loggable fields are included for security
|
360
|
-
# @note Only fields with non-nil values are included
|
361
|
-
#
|
362
|
-
def to_h
|
363
|
-
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
364
|
-
field_type = self.class.field_types[field]
|
365
|
-
|
366
|
-
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
367
|
-
next unless field_type.loggable
|
368
|
-
|
369
|
-
method_name = field_type.method_name
|
370
|
-
val = send(method_name)
|
371
|
-
prepared = serialize_value(val)
|
372
|
-
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
373
|
-
|
374
|
-
# Only include non-nil values in the hash for Valkey
|
375
|
-
# Use string key for database compatibility
|
376
|
-
hsh[field.to_s] = prepared unless prepared.nil?
|
377
|
-
end
|
378
|
-
end
|
379
|
-
|
380
|
-
# Converts the object's persistent fields to a hash for database storage.
|
381
|
-
#
|
382
|
-
# Serializes ALL persistent field values for database storage, including
|
383
|
-
# encrypted fields. This is used internally by commit_fields and other
|
384
|
-
# persistence operations.
|
385
|
-
#
|
386
|
-
# @return [Hash] Hash with field names as keys and serialized values
|
387
|
-
# ready for database storage
|
388
|
-
#
|
389
|
-
# @note Includes ALL persistent fields, including encrypted fields
|
390
|
-
# @note Only fields with non-nil values are included for storage efficiency
|
391
|
-
#
|
392
|
-
def to_h_for_storage
|
393
|
-
self.class.persistent_fields.each_with_object({}) do |field, hsh|
|
394
|
-
field_type = self.class.field_types[field]
|
395
|
-
method_name = field_type.method_name
|
396
|
-
val = send(method_name)
|
397
|
-
prepared = serialize_value(val)
|
398
|
-
Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
399
|
-
|
400
|
-
# Only include non-nil values in the hash for Valkey
|
401
|
-
# Use string key for database compatibility
|
402
|
-
hsh[field.to_s] = prepared unless prepared.nil?
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
# Converts the object's persistent fields to an array.
|
407
|
-
#
|
408
|
-
# Serializes all persistent field values in field definition order,
|
409
|
-
# preparing them for Valkey storage. Each value is processed through
|
410
|
-
# the serialization pipeline to ensure Valkey compatibility.
|
411
|
-
#
|
412
|
-
# @return [Array] Array of serialized field values in field order
|
413
|
-
#
|
414
|
-
# @example Converting an object to array format
|
415
|
-
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
416
|
-
# user.to_a
|
417
|
-
# # => ["John", "john@example.com", "30"]
|
418
|
-
#
|
419
|
-
# @note Values are serialized using the same process as other persistence
|
420
|
-
# methods to maintain data consistency across operations.
|
421
|
-
#
|
422
|
-
def to_a
|
423
|
-
self.class.persistent_fields.filter_map do |field|
|
424
|
-
field_type = self.class.field_types[field]
|
425
|
-
|
426
|
-
# Security: Skip non-loggable fields (e.g., encrypted fields)
|
427
|
-
next unless field_type.loggable
|
428
|
-
|
429
|
-
method_name = field_type.method_name
|
430
|
-
val = send(method_name)
|
431
|
-
prepared = serialize_value(val)
|
432
|
-
Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
|
433
|
-
prepared
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
# Serializes a Ruby object for Valkey storage.
|
438
|
-
#
|
439
|
-
# Converts Ruby objects into the DB-compatible string representations using
|
440
|
-
# the Familia distinguisher for type coercion. Falls back to JSON serialization
|
441
|
-
# for complex types (Hash, Array) when the primary distinguisher returns nil.
|
442
|
-
#
|
443
|
-
# The serialization process:
|
444
|
-
# 1. Attempts conversion using Familia.distinguisher with relaxed type checking
|
445
|
-
# 2. For Hash/Array types that return nil, tries custom dump_method or JSON.dump
|
446
|
-
# 3. Logs warnings when serialization fails completely
|
447
|
-
#
|
448
|
-
# @param val [Object] The Ruby object to serialize for Valkey storage
|
449
|
-
#
|
450
|
-
# @return [String, nil] The serialized value ready for Valkey storage, or nil
|
451
|
-
# if serialization failed
|
452
|
-
#
|
453
|
-
# @example Serializing different data types
|
454
|
-
# serialize_value("hello") # => "hello"
|
455
|
-
# serialize_value(42) # => "42"
|
456
|
-
# serialize_value({name: "John"}) # => '{"name":"John"}'
|
457
|
-
# serialize_value([1, 2, 3]) # => "[1,2,3]"
|
458
|
-
#
|
459
|
-
# @note This method integrates with Familia's type system and supports
|
460
|
-
# custom serialization methods when available on the object
|
461
|
-
#
|
462
|
-
# @see Familia.distinguisher The primary serialization mechanism
|
463
|
-
#
|
464
|
-
def serialize_value(val)
|
465
|
-
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
466
|
-
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
467
|
-
|
468
|
-
prepared = Familia.distinguisher(val, strict_values: false)
|
469
|
-
|
470
|
-
# If the distinguisher returns nil, try using the dump_method but only
|
471
|
-
# use JSON serialization for complex types that need it.
|
472
|
-
if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
|
473
|
-
prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
|
474
|
-
end
|
475
|
-
|
476
|
-
# If both the distinguisher and dump_method return nil, log an error
|
477
|
-
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
|
478
|
-
|
479
|
-
prepared
|
480
|
-
end
|
481
|
-
|
482
|
-
# Converts a Database string value back to its original Ruby type
|
483
|
-
#
|
484
|
-
# This method attempts to deserialize JSON strings back to their original
|
485
|
-
# Hash or Array types. Simple string values are returned as-is.
|
486
|
-
#
|
487
|
-
# @param val [String] The string value from Database to deserialize
|
488
|
-
# @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
|
489
|
-
# @return [Object] The deserialized value (Hash, Array, or original string)
|
490
|
-
#
|
491
|
-
def deserialize_value(val, symbolize: true)
|
492
|
-
return val if val.nil? || val == ''
|
493
|
-
|
494
|
-
# Try to parse as JSON first for complex types
|
495
|
-
begin
|
496
|
-
parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
|
497
|
-
# Only return parsed value if it's a complex type (Hash/Array)
|
498
|
-
# Simple values should remain as strings
|
499
|
-
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
500
|
-
rescue Familia::SerializerError
|
501
|
-
# Not valid JSON, return as-is
|
502
|
-
end
|
503
|
-
|
504
|
-
val
|
505
|
-
end
|
506
|
-
|
507
381
|
private
|
508
382
|
|
509
383
|
# Reset all transient fields to nil
|
@@ -522,11 +396,60 @@ module Familia
|
|
522
396
|
field_type = self.class.field_types[field_name]
|
523
397
|
next unless field_type&.method_name
|
524
398
|
|
525
|
-
#
|
399
|
+
# UnsortedSet the transient field back to nil
|
526
400
|
send("#{field_type.method_name}=", nil)
|
527
401
|
Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
|
528
402
|
end
|
529
403
|
end
|
404
|
+
|
405
|
+
# Automatically update class-level indexes after save
|
406
|
+
#
|
407
|
+
# Iterates through class-level indexing relationships and calls their
|
408
|
+
# corresponding add_to_class_* methods to populate indexes. Only processes
|
409
|
+
# class-level indexes (where target_class == self.class), skipping
|
410
|
+
# instance-scoped indexes which require parent context.
|
411
|
+
#
|
412
|
+
# Uses idempotent Redis commands (HSET for unique_index) so repeated calls
|
413
|
+
# are safe and have negligible performance overhead. Note that multi_index
|
414
|
+
# always requires within: parameter, so only unique_index benefits from this.
|
415
|
+
#
|
416
|
+
# @return [void]
|
417
|
+
#
|
418
|
+
# @example Automatic indexing on save
|
419
|
+
# class Customer < Familia::Horreum
|
420
|
+
# feature :relationships
|
421
|
+
# unique_index :email, :email_lookup
|
422
|
+
# end
|
423
|
+
#
|
424
|
+
# customer = Customer.new(email: 'test@example.com')
|
425
|
+
# customer.save # Automatically calls add_to_class_email_lookup
|
426
|
+
#
|
427
|
+
# @note Only class-level unique_index declarations auto-populate.
|
428
|
+
# Instance-scoped indexes (with within:) require manual population:
|
429
|
+
# employee.add_to_company_badge_index(company)
|
430
|
+
#
|
431
|
+
# @see Familia::Features::Relationships::Indexing For index declaration details
|
432
|
+
#
|
433
|
+
def auto_update_class_indexes
|
434
|
+
return unless self.class.respond_to?(:indexing_relationships)
|
435
|
+
|
436
|
+
self.class.indexing_relationships.each do |rel|
|
437
|
+
# Skip instance-scoped indexes (require parent context)
|
438
|
+
# Instance-scoped indexes must be manually populated because they need
|
439
|
+
# the parent object reference (e.g., employee.add_to_company_badge_index(company))
|
440
|
+
unless rel.target_class == self.class
|
441
|
+
Familia.ld <<~LOG_MESSAGE
|
442
|
+
[auto_update_class_indexes] Skipping #{rel.index_name} (requires parent context)
|
443
|
+
LOG_MESSAGE
|
444
|
+
next
|
445
|
+
end
|
446
|
+
|
447
|
+
# Call the existing add_to_class_* methods
|
448
|
+
add_method = :"add_to_class_#{rel.index_name}"
|
449
|
+
send(add_method) if respond_to?(add_method)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
530
453
|
end
|
531
454
|
end
|
532
455
|
end
|
@@ -1,32 +1,99 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/related_fields.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
+
|
5
|
+
RelatedFieldDefinition = Data.define(:name, :klass, :opts)
|
6
|
+
|
4
7
|
class Horreum
|
8
|
+
|
9
|
+
# Each related field needs some details from the parent (Horreum model)
|
10
|
+
# in order to generate its dbkey. We use a parent proxy pattern to store
|
11
|
+
# only essential parent information instead of full object reference. We
|
12
|
+
# need only the model class and an optional unique identifier to generate
|
13
|
+
# the dbkey; when the identifier is nil, we treat this as a class-level
|
14
|
+
# relation (e.g. model_name:related_field_name); when the identifier
|
15
|
+
# is not nil, we treat this as an instance-level relation
|
16
|
+
# (model_name:identifier:related_field_name).
|
17
|
+
#
|
18
|
+
ParentDefinition = Data.define(:model_klass, :identifier) do
|
19
|
+
# Factory method to create ParentDefinition from a parent instance
|
20
|
+
def self.from_parent(parent_instance)
|
21
|
+
case parent_instance
|
22
|
+
when Class
|
23
|
+
# Handle class-level relationships
|
24
|
+
new(parent_instance, nil)
|
25
|
+
else
|
26
|
+
# Handle instance-level relationships
|
27
|
+
identifier = parent_instance.respond_to?(:identifier) ? parent_instance.identifier : nil
|
28
|
+
new(parent_instance.class, identifier)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Delegation methods for common operations needed by DataTypes
|
33
|
+
def dbclient(uri = nil)
|
34
|
+
model_klass.dbclient(uri)
|
35
|
+
end
|
36
|
+
|
37
|
+
def logical_database
|
38
|
+
model_klass.logical_database
|
39
|
+
end
|
40
|
+
|
41
|
+
def dbkey(keystring = nil)
|
42
|
+
if identifier
|
43
|
+
# Instance-level relation: model_name:identifier:keystring
|
44
|
+
model_klass.dbkey(identifier, keystring)
|
45
|
+
else
|
46
|
+
# Class-level relation: model_name:keystring
|
47
|
+
model_klass.dbkey(keystring, nil)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Allow comparison with the original parent instance
|
52
|
+
def ==(other)
|
53
|
+
case other
|
54
|
+
when ParentDefinition
|
55
|
+
model_klass == other.model_klass && identifier == other.identifier
|
56
|
+
when Class
|
57
|
+
model_klass == other && identifier.nil?
|
58
|
+
else
|
59
|
+
# Compare with instance: check class and identifier match
|
60
|
+
other.is_a?(model_klass) && other.respond_to?(:identifier) && identifier == other.identifier
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias eql? ==
|
64
|
+
end
|
65
|
+
|
66
|
+
# RelatedFieldsManagement - Class-level methods for defining DataType relationships
|
5
67
|
#
|
6
|
-
#
|
68
|
+
# This module uses metaprogramming to dynamically create field definition methods
|
69
|
+
# that generate both class-level and instance-level accessor methods for DataTypes
|
70
|
+
# (e.g., list, set, zset, hashkey, string).
|
7
71
|
#
|
8
|
-
#
|
9
|
-
#
|
72
|
+
# When included in a class via ManagementMethods, it provides class methods like:
|
73
|
+
# * Customer.list :recent_orders # defines class method for class-level list
|
74
|
+
# * customer.recent_orders # creates instance method returning list instance
|
10
75
|
#
|
11
76
|
# Key metaprogramming features:
|
12
|
-
# * Dynamically defines methods for each Database type (e.g., set, list, hashkey)
|
13
|
-
# *
|
77
|
+
# * Dynamically defines DSL methods for each Database type (e.g., set, list, hashkey)
|
78
|
+
# * Each DSL method creates corresponding instance/class accessor methods
|
14
79
|
# * Provides query methods for checking relation types
|
15
80
|
#
|
16
81
|
# Usage:
|
17
82
|
# Include this module in classes that need DataType management
|
18
|
-
# Call
|
83
|
+
# Call setup_related_fields_definition_methods to initialize the feature
|
19
84
|
#
|
20
85
|
module RelatedFieldsManagement
|
21
86
|
# A practical flag to indicate that a Horreum member has relations,
|
22
87
|
# not just theoretically but actually at least one list/haskey/etc.
|
23
|
-
@
|
88
|
+
@has_related_fields = nil
|
24
89
|
|
25
90
|
def self.included(base)
|
26
91
|
base.extend(RelatedFieldsAccessors)
|
27
|
-
base.
|
92
|
+
base.setup_related_fields_definition_methods
|
28
93
|
end
|
29
94
|
|
95
|
+
# RelatedFieldsManagement::RelatedFieldsAccessors
|
96
|
+
#
|
30
97
|
module RelatedFieldsAccessors
|
31
98
|
# Sets up all DataType related methods
|
32
99
|
# This method generates the following for each registered DataType:
|
@@ -36,9 +103,9 @@ module Familia
|
|
36
103
|
# Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
|
37
104
|
# Class methods: class_set(), class_list(), etc.
|
38
105
|
#
|
39
|
-
def
|
106
|
+
def setup_related_fields_definition_methods
|
40
107
|
Familia::DataType.registered_types.each_pair do |kind, klass|
|
41
|
-
Familia.trace :registered_types, kind, klass
|
108
|
+
Familia.trace :registered_types, kind, klass if Familia.debug?
|
42
109
|
|
43
110
|
# Dynamically define instance-level relation methods
|
44
111
|
#
|
@@ -50,7 +117,7 @@ module Familia
|
|
50
117
|
name, opts = *args
|
51
118
|
|
52
119
|
# As log as we have at least one relation, we can set this flag.
|
53
|
-
@
|
120
|
+
@has_related_fields = true
|
54
121
|
|
55
122
|
attach_instance_related_field name, klass, opts
|
56
123
|
end
|
@@ -95,18 +162,35 @@ module Familia
|
|
95
162
|
|
96
163
|
# Creates an instance-level relation
|
97
164
|
def attach_instance_related_field(name, klass, opts)
|
98
|
-
Familia.trace :
|
165
|
+
Familia.trace :attach_instance_related_field, name, klass, opts if Familia.debug?
|
99
166
|
raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
|
100
167
|
|
101
168
|
name = name.to_s.to_sym
|
102
169
|
opts ||= {}
|
103
170
|
|
104
|
-
related_fields[name] =
|
105
|
-
related_fields[name].name = name
|
106
|
-
related_fields[name].klass = klass
|
107
|
-
related_fields[name].opts = opts
|
171
|
+
related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
|
108
172
|
|
109
|
-
|
173
|
+
# Create lazy-initializing accessor that calls initialize_relatives if needed
|
174
|
+
define_method name do
|
175
|
+
ivar = :"@#{name}"
|
176
|
+
value = instance_variable_get(ivar)
|
177
|
+
|
178
|
+
# If nil and we haven't initialized relatives, do it now
|
179
|
+
# Check singleton class to avoid polluting instance variables
|
180
|
+
if value.nil? && !singleton_class.instance_variable_defined?(:"@relatives_initialized")
|
181
|
+
initialize_relatives
|
182
|
+
value = instance_variable_get(ivar)
|
183
|
+
end
|
184
|
+
|
185
|
+
# If still nil after lazy initialization attempt, raise helpful error
|
186
|
+
# Only raise if we tried to initialize but it's still nil
|
187
|
+
if value.nil? && singleton_class.instance_variable_defined?(:"@relatives_initialized")
|
188
|
+
raise "#{self.class}##{name} is nil. Did you override initialize without calling super? " \
|
189
|
+
"(Field is nil after initialization attempt)"
|
190
|
+
end
|
191
|
+
|
192
|
+
value
|
193
|
+
end
|
110
194
|
|
111
195
|
define_method :"#{name}=" do |val|
|
112
196
|
send(name).replace val
|
@@ -120,17 +204,14 @@ module Familia
|
|
120
204
|
|
121
205
|
# Creates a class-level relation
|
122
206
|
def attach_class_related_field(name, klass, opts)
|
123
|
-
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts
|
207
|
+
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts if Familia.debug?
|
124
208
|
raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
|
125
209
|
|
126
210
|
name = name.to_s.to_sym
|
127
211
|
opts = opts.nil? ? {} : opts.clone
|
128
212
|
opts[:parent] = self unless opts.key?(:parent)
|
129
213
|
|
130
|
-
class_related_fields[name] =
|
131
|
-
class_related_fields[name].name = name
|
132
|
-
class_related_fields[name].klass = klass
|
133
|
-
class_related_fields[name].opts = opts
|
214
|
+
class_related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
|
134
215
|
|
135
216
|
# An accessor method created in the metaclass will
|
136
217
|
# access the instance variables for this class.
|