familia 2.0.0.pre14 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +66 -6
- data/CLAUDE.md +1 -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 +41 -41
- 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 +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- 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 +623 -19
- 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 +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -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/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- 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 +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- 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/{autoloader.rb → features/autoloader.rb} +49 -23
- 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 +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- 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 +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -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 +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- 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 +5 -5
- data/lib/familia/features.rb +21 -21
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- 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 -15
- 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 +1 -1
- 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 +129 -11
- data/try/core/connection_try.rb +7 -7
- 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 +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- 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 +1 -1
- 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/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/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 +433 -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 +1 -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 +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -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/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- 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
- metadata +77 -45
- 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 -228
- 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/autoloadable.rb +0 -113
- 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/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/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- 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
@@ -32,22 +32,22 @@ module Familia
|
|
32
32
|
@dump_method = nil
|
33
33
|
@load_method = nil
|
34
34
|
|
35
|
-
# DefinitionMethods
|
35
|
+
# DefinitionMethods - Class-level DSL methods for defining Horreum model structure
|
36
36
|
#
|
37
37
|
# This module is extended into classes that include Familia::Horreum,
|
38
|
-
# providing methods for
|
38
|
+
# providing class methods for defining model structure and configuration
|
39
|
+
# (e.g., Customer.field :name, Customer.identifier_field :custid).
|
39
40
|
#
|
40
41
|
# Key features:
|
41
|
-
# *
|
42
|
-
# *
|
43
|
-
# * Provides
|
42
|
+
# * Defines DSL methods for field definitions (field, identifier_field)
|
43
|
+
# * Includes RelatedFieldsManagement for DataType field DSL (list, set, zset, etc.)
|
44
|
+
# * Provides class-level configuration (prefix, suffix, logical_database)
|
45
|
+
# * Manages field metadata and inheritance
|
44
46
|
#
|
45
47
|
module DefinitionMethods
|
46
48
|
include Familia::Settings
|
47
49
|
include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
|
48
50
|
|
49
|
-
using Familia::Refinements::SnakeCase
|
50
|
-
|
51
51
|
# Sets or retrieves the unique identifier field for the class.
|
52
52
|
#
|
53
53
|
# This method defines or returns the field or method that contains the unique
|
@@ -77,13 +77,13 @@ module Familia
|
|
77
77
|
#
|
78
78
|
# This method defines a new field for the class, creating getter and setter
|
79
79
|
# instance methods similar to `attr_accessor`. It also generates a fast
|
80
|
-
# writer method for immediate persistence to
|
80
|
+
# writer method for immediate persistence to the database.
|
81
81
|
#
|
82
82
|
# @param name [Symbol, String] the name of the field to define. If a method
|
83
83
|
# with the same name already exists, an error is raised.
|
84
84
|
# @param as [Symbol, String, false, nil] as the name to use for the accessor method (defaults to name).
|
85
85
|
# If false or nil, no accessor methods are created.
|
86
|
-
# @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to
|
86
|
+
# @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :`"#{name}!"`).
|
87
87
|
# If false or nil, no fast writer method is created.
|
88
88
|
# @param on_conflict [Symbol] conflict resolution strategy when method already exists:
|
89
89
|
# - :raise - raise error if method exists (default)
|
@@ -117,7 +117,7 @@ module Familia
|
|
117
117
|
register_field_type(field_type)
|
118
118
|
end
|
119
119
|
|
120
|
-
# Sets or retrieves the suffix for generating Redis keys.
|
120
|
+
# Sets or retrieves the suffix for generating Valkey/Redis keys.
|
121
121
|
#
|
122
122
|
# @param a [String, Symbol, nil] the suffix to set (optional).
|
123
123
|
# @param blk [Proc] a block that returns the suffix (optional).
|
@@ -128,7 +128,7 @@ module Familia
|
|
128
128
|
@suffix || Familia.default_suffix
|
129
129
|
end
|
130
130
|
|
131
|
-
# Sets or retrieves the prefix for generating Redis keys.
|
131
|
+
# Sets or retrieves the prefix for generating Valkey/Redis keys.
|
132
132
|
#
|
133
133
|
# @param a [String, Symbol, nil] the prefix to set (optional).
|
134
134
|
# @return [String, Symbol] the current prefix.
|
@@ -144,12 +144,12 @@ module Familia
|
|
144
144
|
raise Problem, 'Cannot generate prefix for anonymous class. ' \
|
145
145
|
'Use `prefix` method to set explicitly.'
|
146
146
|
end
|
147
|
-
|
147
|
+
config_name.to_sym
|
148
148
|
end
|
149
149
|
end
|
150
150
|
|
151
151
|
def logical_database(v = nil)
|
152
|
-
Familia.trace :
|
152
|
+
Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", v if Familia.debug?
|
153
153
|
@logical_database = v unless v.nil?
|
154
154
|
@logical_database || parent&.logical_database
|
155
155
|
end
|
@@ -172,20 +172,7 @@ module Familia
|
|
172
172
|
end
|
173
173
|
|
174
174
|
def relations?
|
175
|
-
@
|
176
|
-
end
|
177
|
-
|
178
|
-
# Converts the class name into a string that can be used to look up
|
179
|
-
# configuration values. This is particularly useful when mapping
|
180
|
-
# familia models with specific database numbers in the configuration.
|
181
|
-
#
|
182
|
-
# Familia::Horreum::DefinitionMethods#config_name
|
183
|
-
#
|
184
|
-
# @example V2::Session.config_name => 'session'
|
185
|
-
#
|
186
|
-
# @return [String] The underscored class name as a string
|
187
|
-
def config_name
|
188
|
-
name.snake_case
|
175
|
+
@has_related_fields ||= false
|
189
176
|
end
|
190
177
|
|
191
178
|
def dump_method
|
@@ -234,6 +221,8 @@ module Familia
|
|
234
221
|
# Complete the registration after installation. If we do this beforehand
|
235
222
|
# we can run into issues where it looks like it's already installed.
|
236
223
|
field_types[field_type.name] = field_type
|
224
|
+
# Freeze the field_type to ensure immutability (maintains Data class heritage)
|
225
|
+
field_type.freeze
|
237
226
|
end
|
238
227
|
|
239
228
|
# Retrieves feature options for the current class.
|
@@ -303,21 +292,20 @@ module Familia
|
|
303
292
|
# end
|
304
293
|
#
|
305
294
|
def add_feature_options(feature_name, **options)
|
306
|
-
|
307
|
-
|
295
|
+
@feature_options ||= {}
|
296
|
+
@feature_options[feature_name.to_sym] ||= {}
|
308
297
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
298
|
+
# Only set defaults for options that don't already exist
|
299
|
+
options.each do |key, value|
|
300
|
+
@feature_options[feature_name.to_sym][key] ||= value
|
301
|
+
end
|
313
302
|
|
314
|
-
|
315
|
-
end
|
303
|
+
@feature_options[feature_name.to_sym]
|
304
|
+
end
|
316
305
|
|
317
306
|
# Create and register a transient field type
|
318
307
|
#
|
319
308
|
# @param name [Symbol] The field name
|
320
|
-
# @param options [Hash] Field options
|
321
309
|
#
|
322
310
|
def transient_field(name, **)
|
323
311
|
require_relative '../../features/transient_fields/transient_field_type'
|
@@ -379,8 +367,8 @@ end
|
|
379
367
|
# @raise [ArgumentError] if fast_method_name doesn't end with '!'
|
380
368
|
#
|
381
369
|
# @note Generated method behavior:
|
382
|
-
# - Without args: Retrieves current value from Redis
|
383
|
-
# - With value: Sets and immediately persists to Redis
|
370
|
+
# - Without args: Retrieves current value from Valkey/Redis
|
371
|
+
# - With value: Sets and immediately persists to Valkey/Redis
|
384
372
|
# - Returns boolean indicating success for writes
|
385
373
|
# - Bypasses object-level caching and expiration updates
|
386
374
|
#
|
@@ -394,20 +382,20 @@ end
|
|
394
382
|
handle_method_conflict(fast_method_name, on_conflict) do
|
395
383
|
# Fast attribute accessor method for the '#{field_name}' attribute.
|
396
384
|
# This method provides immediate read and write access to the attribute
|
397
|
-
# in
|
385
|
+
# in the database.
|
398
386
|
#
|
399
387
|
# When called without arguments, it retrieves the current value of the
|
400
|
-
# attribute from
|
388
|
+
# attribute from the database.
|
401
389
|
# When called with an argument, it immediately persists the new value to
|
402
|
-
#
|
390
|
+
# the database.
|
403
391
|
#
|
404
392
|
# @overload #{method_name}
|
405
|
-
# Retrieves the current value of the attribute from
|
393
|
+
# Retrieves the current value of the attribute from the database.
|
406
394
|
# @return [Object] the current value of the attribute.
|
407
395
|
#
|
408
396
|
# @overload #{method_name}(value)
|
409
397
|
# Sets and immediately persists the new value of the attribute to
|
410
|
-
#
|
398
|
+
# the database.
|
411
399
|
# @param value [Object] the new value to set for the attribute.
|
412
400
|
# @return [Object] the newly set value.
|
413
401
|
#
|
@@ -416,7 +404,7 @@ end
|
|
416
404
|
# the method.
|
417
405
|
#
|
418
406
|
# @note This method bypasses any object-level caching and interacts
|
419
|
-
# directly with
|
407
|
+
# directly with the database. It does not trigger updates to other attributes
|
420
408
|
# or the object's expiration time.
|
421
409
|
#
|
422
410
|
# @example
|
@@ -437,7 +425,7 @@ end
|
|
437
425
|
|
438
426
|
begin
|
439
427
|
# Trace the operation if debugging is enabled.
|
440
|
-
Familia.trace :FAST_WRITER,
|
428
|
+
Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
|
441
429
|
|
442
430
|
# Convert the provided value to a format suitable for Database storage.
|
443
431
|
prepared = serialize_value(val)
|
@@ -4,11 +4,11 @@ require_relative 'related_fields_management'
|
|
4
4
|
|
5
5
|
module Familia
|
6
6
|
class Horreum
|
7
|
-
# ManagementMethods
|
8
|
-
# records.
|
7
|
+
# ManagementMethods - Class-level methods for Horreum model management
|
9
8
|
#
|
10
9
|
# This module is extended into classes that include Familia::Horreum,
|
11
|
-
# providing methods for
|
10
|
+
# providing class methods for database operations and object management
|
11
|
+
# (e.g., Customer.create, Customer.find_by_id)
|
12
12
|
#
|
13
13
|
# # Key features:
|
14
14
|
# * Includes RelatedFieldsManagement for DataType field handling
|
@@ -17,11 +17,13 @@ module Familia
|
|
17
17
|
module ManagementMethods
|
18
18
|
include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
|
19
19
|
|
20
|
+
using Familia::Refinements::StylizeWords
|
21
|
+
|
20
22
|
# Creates and persists a new instance of the class.
|
21
23
|
#
|
22
|
-
# @param
|
24
|
+
# @param args [Array] Variable number of positional arguments to be passed
|
23
25
|
# to the constructor.
|
24
|
-
# @param
|
26
|
+
# @param kwargs [Hash] Keyword arguments to be passed to the constructor.
|
25
27
|
# @return [Object] The newly created and persisted instance.
|
26
28
|
# @raise [Familia::Problem] If an instance with the same identifier already
|
27
29
|
# exists.
|
@@ -68,10 +70,35 @@ module Familia
|
|
68
70
|
ids.collect! { |objid| dbkey(objid) }
|
69
71
|
return [] if ids.compact.empty?
|
70
72
|
|
71
|
-
Familia.trace :MULTIGET,
|
73
|
+
Familia.trace :MULTIGET, nil, "#{ids.size}: #{ids}" if Familia.debug?
|
72
74
|
dbclient.mget(*ids)
|
73
75
|
end
|
74
76
|
|
77
|
+
# Converts the class name into a string that can be used to look up
|
78
|
+
# configuration values. This is particularly useful when mapping
|
79
|
+
# familia models with specific database numbers in the configuration.
|
80
|
+
#
|
81
|
+
# Familia::Horreum::DefinitionMethods#config_name
|
82
|
+
#
|
83
|
+
# @example V2::Session.config_name => 'session'
|
84
|
+
#
|
85
|
+
# @return [String] The underscored class name as a string
|
86
|
+
def config_name
|
87
|
+
return nil if name.nil?
|
88
|
+
|
89
|
+
name.demodularize.snake_case
|
90
|
+
end
|
91
|
+
|
92
|
+
# Familia::Horreum::DefinitionMethods#familia_name
|
93
|
+
#
|
94
|
+
# @example V2::Session.config_name => 'Session'
|
95
|
+
#
|
96
|
+
def familia_name
|
97
|
+
return nil if name.nil?
|
98
|
+
|
99
|
+
name.demodularize
|
100
|
+
end
|
101
|
+
|
75
102
|
# Retrieves and instantiates an object from Database using the full object
|
76
103
|
# key.
|
77
104
|
#
|
@@ -83,7 +110,7 @@ module Familia
|
|
83
110
|
# This method performs a two-step process to safely retrieve and
|
84
111
|
# instantiate objects:
|
85
112
|
#
|
86
|
-
# 1. It first checks if the key exists in
|
113
|
+
# 1. It first checks if the key exists in the database. This is crucial because:
|
87
114
|
# - It provides a definitive answer about the object's existence.
|
88
115
|
# - It prevents ambiguity that could arise from `hgetall` returning an
|
89
116
|
# empty hash for non-existent keys, which could lead to the creation
|
@@ -93,7 +120,7 @@ module Familia
|
|
93
120
|
# it.
|
94
121
|
#
|
95
122
|
# This approach ensures that we only attempt to instantiate objects that
|
96
|
-
# actually exist in Redis, improving reliability and simplifying
|
123
|
+
# actually exist in Valkey/Redis, improving reliability and simplifying
|
97
124
|
# debugging.
|
98
125
|
#
|
99
126
|
# @example
|
@@ -108,17 +135,17 @@ module Familia
|
|
108
135
|
does_exist = dbclient.exists(objkey).positive?
|
109
136
|
|
110
137
|
Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
111
|
-
Familia.trace :FROM_KEY,
|
138
|
+
Familia.trace :FROM_KEY, nil, objkey if Familia.debug?
|
112
139
|
|
113
140
|
# This is the reason for calling exists first. We want to definitively
|
114
|
-
# and without any ambiguity know if the object exists in
|
141
|
+
# and without any ambiguity know if the object exists in the database. If it
|
115
142
|
# doesn't, we return nil. If it does, we proceed to load the object.
|
116
143
|
# Otherwise, hgetall will return an empty hash, which will be passed to
|
117
144
|
# the constructor, which will then be annoying to debug.
|
118
145
|
return unless does_exist
|
119
146
|
|
120
147
|
obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
|
121
|
-
Familia.trace :FROM_KEY2,
|
148
|
+
Familia.trace :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
|
122
149
|
|
123
150
|
new(**obj)
|
124
151
|
end
|
@@ -150,33 +177,34 @@ module Familia
|
|
150
177
|
objkey = dbkey(identifier, suffix)
|
151
178
|
|
152
179
|
Familia.ld "[.find_by_id] #{self} from key #{objkey})"
|
153
|
-
Familia.trace :FIND_BY_ID,
|
180
|
+
Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
|
154
181
|
find_by_key objkey
|
155
182
|
end
|
156
183
|
alias find find_by_id
|
157
184
|
alias load find_by_id # deprecated
|
158
185
|
alias from_identifier find_by_id # deprecated
|
159
186
|
|
160
|
-
# Checks if an object with the given identifier exists in
|
187
|
+
# Checks if an object with the given identifier exists in the database.
|
161
188
|
#
|
162
189
|
# @param identifier [String, Integer] The unique identifier for the object.
|
163
190
|
# @param suffix [Symbol, nil] The suffix to use in the dbkey (default: class suffix).
|
164
191
|
# @return [Boolean] true if the object exists, false otherwise.
|
165
192
|
#
|
166
193
|
# This method constructs the full dbkey using the provided identifier and suffix,
|
167
|
-
# then checks if the key exists in
|
194
|
+
# then checks if the key exists in the database.
|
168
195
|
#
|
169
196
|
# @example
|
170
|
-
# User.exists?(123) # Returns true if user:123:object exists in Redis
|
197
|
+
# User.exists?(123) # Returns true if user:123:object exists in Valkey/Redis
|
171
198
|
#
|
172
199
|
def exists?(identifier, suffix = nil)
|
173
|
-
raise NoIdentifier,
|
200
|
+
raise NoIdentifier, 'Empty identifier' if identifier.to_s.empty?
|
201
|
+
|
174
202
|
suffix ||= self.suffix
|
175
203
|
|
176
204
|
objkey = dbkey identifier, suffix
|
177
205
|
|
178
206
|
ret = dbclient.exists objkey
|
179
|
-
Familia.trace :EXISTS,
|
207
|
+
Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
|
180
208
|
|
181
209
|
ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
|
182
210
|
end
|
@@ -193,17 +221,36 @@ module Familia
|
|
193
221
|
# `delete!` when working directly with dbkeys.
|
194
222
|
#
|
195
223
|
# @example
|
196
|
-
# User.destroy!(123) # Removes user:123:object from Redis
|
224
|
+
# User.destroy!(123) # Removes user:123:object from Valkey/Redis
|
197
225
|
#
|
198
226
|
def destroy!(identifier, suffix = nil)
|
199
227
|
suffix ||= self.suffix
|
200
|
-
return false if identifier.to_s.empty?
|
228
|
+
return MultiResult.new(false, []) if identifier.to_s.empty?
|
201
229
|
|
202
230
|
objkey = dbkey identifier, suffix
|
203
231
|
|
204
|
-
|
205
|
-
|
206
|
-
|
232
|
+
# Execute all deletion operations within a transaction
|
233
|
+
transaction do |conn|
|
234
|
+
# Clean up related fields first to avoid orphaned keys
|
235
|
+
if relations?
|
236
|
+
Familia.trace :DESTROY_RELATIONS!, nil, "#{self} has relations: #{related_fields.keys}" if Familia.debug?
|
237
|
+
|
238
|
+
# Create a temporary instance to access related fields.
|
239
|
+
# Pass identifier in constructor so init() sees it and can set dependent fields.
|
240
|
+
identifier_field_name = self.identifier_field
|
241
|
+
temp_instance = identifier_field_name ? new(identifier_field_name => identifier.to_s) : new
|
242
|
+
|
243
|
+
related_fields.each do |name, _definition|
|
244
|
+
obj = temp_instance.send(name)
|
245
|
+
Familia.trace :DESTROY_RELATION!, name, "Deleting related field #{name} (#{obj.dbkey})" if Familia.debug?
|
246
|
+
conn.del(obj.dbkey)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Delete the main object key
|
251
|
+
ret = conn.del(objkey)
|
252
|
+
Familia.trace :DESTROY!, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
|
253
|
+
end
|
207
254
|
end
|
208
255
|
|
209
256
|
# Finds all keys in Database matching the given suffix pattern.
|
@@ -249,7 +296,7 @@ module Familia
|
|
249
296
|
end
|
250
297
|
|
251
298
|
def any?(filter = '*')
|
252
|
-
matching_keys_count(filter)
|
299
|
+
matching_keys_count(filter).positive?
|
253
300
|
end
|
254
301
|
|
255
302
|
# Returns the number of dbkeys matching the given filter pattern
|
@@ -259,7 +306,8 @@ module Familia
|
|
259
306
|
def matching_keys_count(filter = '*')
|
260
307
|
dbclient.keys(dbkey(filter)).compact.size
|
261
308
|
end
|
262
|
-
alias size matching_keys_count
|
309
|
+
alias size matching_keys_count
|
310
|
+
alias length matching_keys_count
|
263
311
|
end
|
264
312
|
end
|
265
313
|
end
|
@@ -1,32 +1,99 @@
|
|
1
1
|
# lib/familia/horreum/related_fields_management.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,16 +162,13 @@ 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
|
attr_reader name
|
110
174
|
|
@@ -120,17 +184,14 @@ module Familia
|
|
120
184
|
|
121
185
|
# Creates a class-level relation
|
122
186
|
def attach_class_related_field(name, klass, opts)
|
123
|
-
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts
|
187
|
+
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts if Familia.debug?
|
124
188
|
raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
|
125
189
|
|
126
190
|
name = name.to_s.to_sym
|
127
191
|
opts = opts.nil? ? {} : opts.clone
|
128
192
|
opts[:parent] = self unless opts.key?(:parent)
|
129
193
|
|
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
|
194
|
+
class_related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
|
134
195
|
|
135
196
|
# An accessor method created in the metaclass will
|
136
197
|
# access the instance variables for this class.
|