familia 2.0.0.pre4 → 2.0.0.pre6
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/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,47 @@
|
|
1
|
+
# lib/familia/features/transient_fields.rb
|
2
|
+
|
3
|
+
require_relative 'transient_fields/redacted_string'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
# Familia::Features::TransientFields
|
8
|
+
#
|
9
|
+
# Provides secure transient fields that wrap sensitive values in RedactedString
|
10
|
+
# objects. These fields are excluded from serialization operations and provide
|
11
|
+
# automatic memory wiping for security.
|
12
|
+
#
|
13
|
+
module TransientFields
|
14
|
+
def self.included(base)
|
15
|
+
Familia.trace :included, base, self, caller(1..1) if Familia.debug?
|
16
|
+
base.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
# ClassMethods
|
20
|
+
#
|
21
|
+
module ClassMethods
|
22
|
+
# Define a transient field that automatically wraps values in RedactedString
|
23
|
+
#
|
24
|
+
# @param name [Symbol] The field name
|
25
|
+
# @param as [Symbol] The method name (defaults to field name)
|
26
|
+
# @param kwargs [Hash] Additional field options
|
27
|
+
#
|
28
|
+
# @example Define a transient API key field
|
29
|
+
# class Service < Familia::Horreum
|
30
|
+
# feature :transient_fields
|
31
|
+
# transient_field :api_key
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
def transient_field(name, as: name, **kwargs)
|
35
|
+
# Use the field type system - much cleaner than alias_method approach!
|
36
|
+
# We can now remove the transient_field method from this feature entirely
|
37
|
+
# since it's built into DefinitionMethods using TransientFieldType
|
38
|
+
require_relative 'transient_fields/transient_field_type'
|
39
|
+
field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
|
40
|
+
register_field_type(field_type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Familia::Base.add_feature self, :transient_fields, depends_on: nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/familia/features.rb
CHANGED
@@ -2,45 +2,61 @@
|
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
|
5
|
+
FeatureDefinition = Data.define(:name, :depends_on)
|
6
|
+
|
7
|
+
# Familia::Features
|
8
|
+
#
|
5
9
|
module Features
|
6
10
|
|
7
11
|
@features_enabled = nil
|
8
12
|
attr_reader :features_enabled
|
9
13
|
|
10
|
-
def feature(
|
14
|
+
def feature(feature_name = nil)
|
11
15
|
@features_enabled ||= []
|
12
16
|
|
13
|
-
|
14
|
-
if val
|
15
|
-
val = val.to_sym
|
16
|
-
raise Familia::Problem, "Unsupported feature: #{val}" unless Familia::Base.features.key?(val)
|
17
|
+
return features_enabled if feature_name.nil?
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
# If there's a value provied check that it's a valid feature
|
20
|
+
feature_name = feature_name.to_sym
|
21
|
+
unless Familia::Base.features_available.key?(feature_name)
|
22
|
+
raise Familia::Problem, "Unsupported feature: #{feature_name}"
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
+
# If the feature is already available, do nothing but log about it
|
26
|
+
if features_enabled.member?(feature_name)
|
27
|
+
Familia.warn "[#{self.class}] feature already available: #{feature_name}"
|
28
|
+
return
|
29
|
+
end
|
25
30
|
|
26
|
-
|
31
|
+
if Familia.debug?
|
32
|
+
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1)
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
35
|
+
# Add it to the list available features_enabled for Familia::Base classes.
|
36
|
+
features_enabled << feature_name
|
30
37
|
|
31
|
-
|
32
|
-
# call safe_dump on relations fields (e.g. list, set, zset, hashkey). Or
|
33
|
-
# maybe that only makes sense for hashk/object relations.
|
34
|
-
#
|
35
|
-
# We'd need to avoid it getting included multiple times (i.e. once for each
|
36
|
-
# Familia::Horreum subclass that includes the feature).
|
38
|
+
klass = Familia::Base.features_available[feature_name]
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
# Validate dependencies
|
41
|
+
feature_def = Familia::Base.feature_definitions[feature_name]
|
42
|
+
if feature_def&.depends_on&.any?
|
43
|
+
missing = feature_def.depends_on - features_enabled
|
44
|
+
raise Familia::Problem, "#{feature_name} requires: #{missing.join(', ')}" if missing.any?
|
41
45
|
end
|
42
46
|
|
43
|
-
|
47
|
+
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
48
|
+
include klass
|
49
|
+
|
50
|
+
# NOTE: Do we want to extend Familia::DataType here? That would make it
|
51
|
+
# possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
|
52
|
+
#
|
53
|
+
# The challenge is that DataType classes (List, Set, etc.) are shared across
|
54
|
+
# all Horreum models. If Customer extends DataType with safe_dump, then
|
55
|
+
# Session's lists would also have it. Not ideal. If that's all we wanted
|
56
|
+
# then we can do that by looping through every DataType class here.
|
57
|
+
#
|
58
|
+
# We'd need to extend the DataType instances for each Horreum subclass. That
|
59
|
+
# avoids it getting included multiple times per DataType
|
44
60
|
end
|
45
61
|
|
46
62
|
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# lib/familia/field_type.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
# Base class for all field types in Familia
|
5
|
+
#
|
6
|
+
# Field types encapsulate the behavior for different kinds of fields,
|
7
|
+
# including how their getter/setter methods are defined and how values
|
8
|
+
# are serialized/deserialized.
|
9
|
+
#
|
10
|
+
# @example Creating a custom field type
|
11
|
+
# class TimestampFieldType < Familia::FieldType
|
12
|
+
# def define_setter(klass)
|
13
|
+
# field_name = @name
|
14
|
+
# klass.define_method :"#{@method_name}=" do |value|
|
15
|
+
# timestamp = value.is_a?(Time) ? value.to_i : value
|
16
|
+
# instance_variable_set(:"@#{field_name}", timestamp)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def define_getter(klass)
|
21
|
+
# field_name = @name
|
22
|
+
# klass.define_method @method_name do
|
23
|
+
# timestamp = instance_variable_get(:"@#{field_name}")
|
24
|
+
# timestamp ? Time.at(timestamp) : nil
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
class FieldType
|
30
|
+
attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict, :loggable
|
31
|
+
|
32
|
+
# Initialize a new field type
|
33
|
+
#
|
34
|
+
# @param name [Symbol] The field name
|
35
|
+
# @param as [Symbol, String, false] The method name (defaults to field name)
|
36
|
+
# If false, no accessor methods are created
|
37
|
+
# @param fast_method [Symbol, String, false] The fast method name
|
38
|
+
# (defaults to "#{name}!"). If false, no fast method is created
|
39
|
+
# @param on_conflict [Symbol] Conflict resolution strategy when method
|
40
|
+
# already exists (:raise, :skip, :warn, :overwrite)
|
41
|
+
# @param loggable [Boolean] Whether this field should be included in
|
42
|
+
# serialization and logging operations (default: true)
|
43
|
+
# @param options [Hash] Additional options for the field type
|
44
|
+
#
|
45
|
+
def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options)
|
46
|
+
@name = name.to_sym
|
47
|
+
@method_name = as == false ? nil : as.to_sym
|
48
|
+
@fast_method_name = fast_method == false ? nil : fast_method&.to_sym
|
49
|
+
|
50
|
+
# Validate fast method name format
|
51
|
+
if @fast_method_name && !@fast_method_name.to_s.end_with?('!')
|
52
|
+
raise ArgumentError, "Fast method name must end with '!' (got: #{@fast_method_name})"
|
53
|
+
end
|
54
|
+
|
55
|
+
@on_conflict = on_conflict
|
56
|
+
@loggable = loggable
|
57
|
+
@options = options
|
58
|
+
end
|
59
|
+
|
60
|
+
# Install this field type on a class
|
61
|
+
#
|
62
|
+
# This method defines all necessary methods on the target class
|
63
|
+
# and registers the field type for later reference.
|
64
|
+
#
|
65
|
+
# @param klass [Class] The class to install this field type on
|
66
|
+
#
|
67
|
+
def install(klass)
|
68
|
+
if @method_name
|
69
|
+
# For skip strategy, check for any method conflicts first
|
70
|
+
if @on_conflict == :skip
|
71
|
+
has_getter_conflict = klass.method_defined?(@method_name) || klass.private_method_defined?(@method_name)
|
72
|
+
has_setter_conflict = klass.method_defined?(:"#{@method_name}=") || klass.private_method_defined?(:"#{@method_name}=")
|
73
|
+
|
74
|
+
# If either getter or setter conflicts, skip the whole field
|
75
|
+
return if has_getter_conflict || has_setter_conflict
|
76
|
+
end
|
77
|
+
|
78
|
+
define_getter(klass)
|
79
|
+
define_setter(klass)
|
80
|
+
end
|
81
|
+
|
82
|
+
define_fast_writer(klass) if @fast_method_name
|
83
|
+
end
|
84
|
+
|
85
|
+
# Define the getter method on the target class
|
86
|
+
#
|
87
|
+
# Subclasses can override this to customize getter behavior.
|
88
|
+
# The default implementation creates a simple attr_reader equivalent.
|
89
|
+
#
|
90
|
+
# @param klass [Class] The class to define the method on
|
91
|
+
#
|
92
|
+
def define_getter(klass)
|
93
|
+
field_name = @name
|
94
|
+
method_name = @method_name
|
95
|
+
|
96
|
+
handle_method_conflict(klass, method_name) do
|
97
|
+
klass.define_method method_name do
|
98
|
+
instance_variable_get(:"@#{field_name}")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Define the setter method on the target class
|
104
|
+
#
|
105
|
+
# Subclasses can override this to customize setter behavior.
|
106
|
+
# The default implementation creates a simple attr_writer equivalent.
|
107
|
+
#
|
108
|
+
# @param klass [Class] The class to define the method on
|
109
|
+
#
|
110
|
+
def define_setter(klass)
|
111
|
+
field_name = @name
|
112
|
+
method_name = @method_name
|
113
|
+
|
114
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
115
|
+
klass.define_method :"#{method_name}=" do |value|
|
116
|
+
instance_variable_set(:"@#{field_name}", value)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Define the fast writer method on the target class
|
122
|
+
#
|
123
|
+
# Fast methods provide direct database access for immediate persistence.
|
124
|
+
# Subclasses can override this to customize fast method behavior.
|
125
|
+
#
|
126
|
+
# @param klass [Class] The class to define the method on
|
127
|
+
#
|
128
|
+
def define_fast_writer(klass)
|
129
|
+
return unless @fast_method_name&.to_s&.end_with?('!')
|
130
|
+
|
131
|
+
field_name = @name
|
132
|
+
method_name = @method_name
|
133
|
+
fast_method_name = @fast_method_name
|
134
|
+
|
135
|
+
handle_method_conflict(klass, fast_method_name) do
|
136
|
+
klass.define_method fast_method_name do |*args|
|
137
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
|
138
|
+
|
139
|
+
val = args.first
|
140
|
+
|
141
|
+
# If no value provided, return current stored value
|
142
|
+
return hget(field_name) if val.nil?
|
143
|
+
|
144
|
+
begin
|
145
|
+
# Trace the operation if debugging is enabled
|
146
|
+
Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
|
147
|
+
|
148
|
+
# Convert value for database storage
|
149
|
+
prepared = serialize_value(val)
|
150
|
+
Familia.ld "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
|
151
|
+
|
152
|
+
# Use the setter method to update instance variable
|
153
|
+
send(:"#{method_name}=", val) if method_name
|
154
|
+
|
155
|
+
# Persist to database immediately
|
156
|
+
ret = hset(field_name, prepared)
|
157
|
+
ret.zero? || ret.positive?
|
158
|
+
rescue Familia::Problem => e
|
159
|
+
raise "#{fast_method_name} method failed: #{e.message}", e.backtrace
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Whether this field should be persisted to the database
|
166
|
+
#
|
167
|
+
# @return [Boolean] true if field should be persisted
|
168
|
+
#
|
169
|
+
def persistent?
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
def transient?
|
174
|
+
!persistent?
|
175
|
+
end
|
176
|
+
|
177
|
+
# The category for this field type (used for filtering)
|
178
|
+
#
|
179
|
+
# @return [Symbol] the field category
|
180
|
+
#
|
181
|
+
def category
|
182
|
+
:field
|
183
|
+
end
|
184
|
+
|
185
|
+
# Serialize a value for database storage
|
186
|
+
#
|
187
|
+
# Subclasses can override this to customize serialization.
|
188
|
+
# The default implementation passes values through unchanged.
|
189
|
+
#
|
190
|
+
# @param value [Object] The value to serialize
|
191
|
+
# @param record [Object] The record instance (for context)
|
192
|
+
# @return [Object] The serialized value
|
193
|
+
#
|
194
|
+
def serialize(value, _record = nil)
|
195
|
+
value
|
196
|
+
end
|
197
|
+
|
198
|
+
# Deserialize a value from database storage
|
199
|
+
#
|
200
|
+
# Subclasses can override this to customize deserialization.
|
201
|
+
# The default implementation passes values through unchanged.
|
202
|
+
#
|
203
|
+
# @param value [Object] The value to deserialize
|
204
|
+
# @param record [Object] The record instance (for context)
|
205
|
+
# @return [Object] The deserialized value
|
206
|
+
#
|
207
|
+
def deserialize(value, _record = nil)
|
208
|
+
value
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns all method names generated for this field (used for conflict detection)
|
212
|
+
#
|
213
|
+
# @return [Array<Symbol>] Array of method names this field type generates
|
214
|
+
#
|
215
|
+
def generated_methods
|
216
|
+
[@method_name, @fast_method_name].compact
|
217
|
+
end
|
218
|
+
|
219
|
+
# Enhanced inspection output for debugging
|
220
|
+
#
|
221
|
+
# @return [String] Human-readable representation
|
222
|
+
#
|
223
|
+
def inspect
|
224
|
+
attributes = [
|
225
|
+
"name=#{@name}",
|
226
|
+
"method_name=#{@method_name}",
|
227
|
+
"fast_method_name=#{@fast_method_name}",
|
228
|
+
"on_conflict=#{@on_conflict}",
|
229
|
+
"category=#{category}"
|
230
|
+
]
|
231
|
+
"#<#{self.class.name} #{attributes.join(' ')}>"
|
232
|
+
end
|
233
|
+
alias to_s inspect
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
# Handle method name conflicts during definition
|
238
|
+
#
|
239
|
+
# @param klass [Class] The target class
|
240
|
+
# @param method_name [Symbol] The method name to define
|
241
|
+
# @yield Block that defines the method
|
242
|
+
#
|
243
|
+
def handle_method_conflict(klass, method_name)
|
244
|
+
case @on_conflict
|
245
|
+
when :skip
|
246
|
+
return if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
|
247
|
+
when :warn
|
248
|
+
if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
|
249
|
+
warn <<~WARNING
|
250
|
+
|
251
|
+
WARNING: Method >>> #{method_name} <<< already exists on #{klass}.
|
252
|
+
Field functionality may be broken. Consider using a different name
|
253
|
+
with field(:#{@name}, as: :other_name)
|
254
|
+
|
255
|
+
Called from:
|
256
|
+
#{Familia.pretty_stack(limit: 3)}
|
257
|
+
|
258
|
+
WARNING
|
259
|
+
end
|
260
|
+
when :raise
|
261
|
+
if klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
|
262
|
+
raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{klass}"
|
263
|
+
end
|
264
|
+
when :overwrite
|
265
|
+
# Proceed silently - allow overwrite
|
266
|
+
else
|
267
|
+
raise ArgumentError, "Unknown conflict resolution strategy: #{@on_conflict}"
|
268
|
+
end
|
269
|
+
|
270
|
+
yield
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -2,9 +2,8 @@
|
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class Horreum
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
5
|
+
# Connection: Valkey connection management for Horreum instances
|
6
|
+
# Provides both instance and class-level connection methods
|
8
7
|
module Connection
|
9
8
|
attr_reader :uri
|
10
9
|
|
@@ -47,36 +46,28 @@ module Familia
|
|
47
46
|
#
|
48
47
|
# @note This method works with the global Familia.transaction context when available
|
49
48
|
#
|
50
|
-
def transaction
|
49
|
+
def transaction(&)
|
51
50
|
# If we're already in a Familia.transaction context, just yield the multi connection
|
52
51
|
if Fiber[:familia_transaction]
|
53
52
|
yield(Fiber[:familia_transaction])
|
54
53
|
else
|
55
54
|
# Otherwise, create a local transaction
|
56
|
-
block_result = dbclient.multi
|
57
|
-
yield(conn)
|
58
|
-
end
|
55
|
+
block_result = dbclient.multi(&)
|
59
56
|
end
|
60
57
|
block_result
|
61
58
|
end
|
62
59
|
alias multi transaction
|
63
60
|
|
64
|
-
def pipeline
|
61
|
+
def pipeline(&)
|
65
62
|
# If we're already in a Familia.pipeline context, just yield the pipeline connection
|
66
63
|
if Fiber[:familia_pipeline]
|
67
64
|
yield(Fiber[:familia_pipeline])
|
68
65
|
else
|
69
66
|
# Otherwise, create a local transaction
|
70
|
-
block_result = dbclient.pipeline
|
71
|
-
yield(conn)
|
72
|
-
end
|
67
|
+
block_result = dbclient.pipeline(&)
|
73
68
|
end
|
74
69
|
block_result
|
75
70
|
end
|
76
|
-
|
77
71
|
end
|
78
|
-
|
79
|
-
# Include Connection module for instance methods after it's loaded
|
80
|
-
include Familia::Horreum::Connection
|
81
72
|
end
|
82
73
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/database_commands.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
# InstanceMethods - Module containing instance-level methods for Familia
|
@@ -7,7 +7,6 @@ module Familia
|
|
7
7
|
# instance-level functionality for Database operations and object management.
|
8
8
|
#
|
9
9
|
class Horreum
|
10
|
-
|
11
10
|
# Methods that call Database commands (InstanceMethods)
|
12
11
|
#
|
13
12
|
# NOTE: There is no hgetall for Horreum. This is because Horreum
|
@@ -16,8 +15,7 @@ module Familia
|
|
16
15
|
# emphasize this, instead of "refreshing" the object with hgetall,
|
17
16
|
# just load the object again.
|
18
17
|
#
|
19
|
-
module
|
20
|
-
|
18
|
+
module DatabaseCommands
|
21
19
|
def move(logical_database)
|
22
20
|
dbclient.move dbkey, logical_database
|
23
21
|
end
|
@@ -39,8 +37,9 @@ module Familia
|
|
39
37
|
# @note The default behavior maintains backward compatibility by treating empty hashes
|
40
38
|
# as non-existent. Use `check_size: false` for pure key existence checking.
|
41
39
|
def exists?(check_size: true)
|
42
|
-
key_exists = self.class.
|
40
|
+
key_exists = self.class.exists?(identifier)
|
43
41
|
return key_exists unless check_size
|
42
|
+
|
44
43
|
key_exists && !size.zero?
|
45
44
|
end
|
46
45
|
|
@@ -83,21 +82,11 @@ module Familia
|
|
83
82
|
end
|
84
83
|
alias remove remove_field # deprecated
|
85
84
|
|
86
|
-
def
|
85
|
+
def data_type
|
87
86
|
Familia.trace :DATATYPE, dbclient, uri, caller(1..1) if Familia.debug?
|
88
87
|
dbclient.type dbkey(suffix)
|
89
88
|
end
|
90
89
|
|
91
|
-
|
92
|
-
# Retrieves the prefix for the current instance by delegating to its class.
|
93
|
-
#
|
94
|
-
# @return [String] The prefix associated with the class of the current instance.
|
95
|
-
# @example
|
96
|
-
# instance.prefix
|
97
|
-
def prefix
|
98
|
-
self.class.prefix
|
99
|
-
end
|
100
|
-
|
101
90
|
# For parity with DataType#hgetall
|
102
91
|
def hgetall
|
103
92
|
Familia.trace :HGETALL, dbclient, uri, caller(1..1) if Familia.debug?
|
@@ -117,8 +106,21 @@ module Familia
|
|
117
106
|
dbclient.hset dbkey, field, value
|
118
107
|
end
|
119
108
|
|
120
|
-
|
121
|
-
|
109
|
+
# Sets field in the hash stored at key to value, only if field does not yet exist.
|
110
|
+
# If key does not exist, a new key holding a hash is created. If field already exists,
|
111
|
+
# this operation has no effect.
|
112
|
+
#
|
113
|
+
# @param field [String] The field to set in the hash
|
114
|
+
# @param value [String] The value to set for the field
|
115
|
+
# @return [Integer] 1 if the field is a new field in the hash and the value was set,
|
116
|
+
# 0 if the field already exists in the hash and no operation was performed
|
117
|
+
def hsetnx(field, value)
|
118
|
+
Familia.trace :HSETNX, dbclient, field, caller(1..1) if Familia.debug?
|
119
|
+
dbclient.hsetnx dbkey, field, value
|
120
|
+
end
|
121
|
+
|
122
|
+
def hmset(hsh = {})
|
123
|
+
hsh ||= to_h
|
122
124
|
Familia.trace :HMSET, dbclient, hsh, caller(1..1) if Familia.debug?
|
123
125
|
dbclient.hmset dbkey(suffix), hsh
|
124
126
|
end
|
@@ -175,9 +177,6 @@ module Familia
|
|
175
177
|
ret.positive?
|
176
178
|
end
|
177
179
|
alias clear delete!
|
178
|
-
|
179
180
|
end
|
180
|
-
|
181
|
-
include Commands # these become Familia::Horreum instance methods
|
182
181
|
end
|
183
182
|
end
|