familia 2.0.0.pre17 → 2.0.0.pre19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -6,40 +6,16 @@ module Familia
|
|
6
6
|
# Provides connection handling, transactions, and URI normalization for both
|
7
7
|
# class-level operations (e.g., Customer.dbclient) and instance-level operations
|
8
8
|
# (e.g., customer.dbclient)
|
9
|
+
#
|
10
|
+
# Includes shared connection behavior from Familia::Connection::Behavior, providing:
|
11
|
+
# - URI normalization (normalize_uri)
|
12
|
+
# - Connection creation (create_dbclient)
|
13
|
+
# - Transaction method signatures
|
14
|
+
# - Pipeline method signatures
|
9
15
|
module Connection
|
10
|
-
|
11
|
-
|
12
|
-
# Normalizes various URI formats to a consistent URI object
|
13
|
-
# Considers the class/instance logical_database when uri is nil or Integer
|
14
|
-
def normalize_uri(uri)
|
15
|
-
case uri
|
16
|
-
when Integer
|
17
|
-
new_uri = Familia.uri.dup
|
18
|
-
new_uri.db = uri
|
19
|
-
new_uri
|
20
|
-
when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
|
21
|
-
URI.parse(uri)
|
22
|
-
when URI
|
23
|
-
uri
|
24
|
-
when nil
|
25
|
-
# Use logical_database if available, otherwise fall back to Familia.uri
|
26
|
-
if respond_to?(:logical_database) && logical_database
|
27
|
-
new_uri = Familia.uri.dup
|
28
|
-
new_uri.db = logical_database
|
29
|
-
new_uri
|
30
|
-
else
|
31
|
-
Familia.uri
|
32
|
-
end
|
33
|
-
else
|
34
|
-
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
35
|
-
end
|
36
|
-
end
|
16
|
+
include Familia::Connection::Behavior
|
37
17
|
|
38
|
-
|
39
|
-
def create_dbclient(uri = nil)
|
40
|
-
parsed_uri = normalize_uri(uri)
|
41
|
-
Familia.create_dbclient(parsed_uri)
|
42
|
-
end
|
18
|
+
attr_reader :uri
|
43
19
|
|
44
20
|
# Returns the Database connection for the class using Chain of Responsibility pattern.
|
45
21
|
#
|
@@ -277,9 +253,9 @@ module Familia
|
|
277
253
|
@provider_connection_handler ||= Familia::Connection::ProviderConnectionHandler.new
|
278
254
|
|
279
255
|
# Determine the appropriate class context
|
280
|
-
# When called from instance: self is instance,
|
281
|
-
# When called from class:
|
282
|
-
klass =
|
256
|
+
# When called from instance: self is instance, use the model class connection
|
257
|
+
# When called from class: we'll use our own connection
|
258
|
+
klass = is_a?(Class) ? self : self.class
|
283
259
|
|
284
260
|
# Always check class first for @dbclient since instance-level connections were removed
|
285
261
|
@cached_connection_handler ||= Familia::Connection::CachedConnectionHandler.new(klass)
|
@@ -16,6 +16,10 @@ module Familia
|
|
16
16
|
# just load the object again.
|
17
17
|
#
|
18
18
|
module DatabaseCommands
|
19
|
+
# Moves the object's key to a different logical database.
|
20
|
+
#
|
21
|
+
# @param logical_database [Integer] The target database number
|
22
|
+
# @return [Boolean] true if the key was moved successfully
|
19
23
|
def move(logical_database)
|
20
24
|
dbclient.move dbkey, logical_database
|
21
25
|
end
|
@@ -40,7 +44,18 @@ module Familia
|
|
40
44
|
key_exists = self.class.exists?(identifier)
|
41
45
|
return key_exists unless check_size
|
42
46
|
|
43
|
-
|
47
|
+
# Handle Redis::Future in transactions - skip size check
|
48
|
+
if key_exists.is_a?(Redis::Future)
|
49
|
+
return key_exists
|
50
|
+
end
|
51
|
+
|
52
|
+
current_size = size
|
53
|
+
# Handle Redis::Future from size call too
|
54
|
+
if current_size.is_a?(Redis::Future)
|
55
|
+
return current_size
|
56
|
+
end
|
57
|
+
|
58
|
+
key_exists && !current_size.zero?
|
44
59
|
end
|
45
60
|
|
46
61
|
# Returns the number of fields in the main object hash
|
@@ -55,6 +70,8 @@ module Familia
|
|
55
70
|
# automatically be deleted. Returns 1 if the timeout was set, 0 if key
|
56
71
|
# does not exist or the timeout could not be set.
|
57
72
|
#
|
73
|
+
# @param default_expiration [Integer] TTL in seconds (uses class default if nil)
|
74
|
+
# @return [Integer] 1 if timeout was set, 0 otherwise
|
58
75
|
def expire(default_expiration = nil)
|
59
76
|
default_expiration ||= self.class.default_expiration
|
60
77
|
Familia.trace :EXPIRE, nil, default_expiration if Familia.debug?
|
@@ -69,7 +86,7 @@ module Familia
|
|
69
86
|
# @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
|
70
87
|
# or has no associated expire time.
|
71
88
|
def current_expiration
|
72
|
-
Familia.trace :CURRENT_EXPIRATION, nil, uri if Familia.debug?
|
89
|
+
Familia.trace :CURRENT_EXPIRATION, nil, self.class.uri if Familia.debug?
|
73
90
|
dbclient.ttl dbkey
|
74
91
|
end
|
75
92
|
|
@@ -83,24 +100,38 @@ module Familia
|
|
83
100
|
end
|
84
101
|
alias remove remove_field # deprecated
|
85
102
|
|
103
|
+
# Returns the Redis data type of the key.
|
104
|
+
#
|
105
|
+
# @return [String] The data type (e.g., 'hash', 'string', 'list')
|
86
106
|
def data_type
|
87
|
-
Familia.trace :DATATYPE, nil, uri if Familia.debug?
|
107
|
+
Familia.trace :DATATYPE, nil, self.class.uri if Familia.debug?
|
88
108
|
dbclient.type dbkey(suffix)
|
89
109
|
end
|
90
110
|
|
91
|
-
#
|
111
|
+
# Returns all fields and values in the hash.
|
112
|
+
#
|
113
|
+
# @return [Hash] All field-value pairs in the hash
|
114
|
+
# @note For parity with DataType#hgetall
|
92
115
|
def hgetall
|
93
|
-
Familia.trace :HGETALL, nil, uri if Familia.debug?
|
116
|
+
Familia.trace :HGETALL, nil, self.class.uri if Familia.debug?
|
94
117
|
dbclient.hgetall dbkey(suffix)
|
95
118
|
end
|
96
119
|
alias all hgetall
|
97
120
|
|
121
|
+
# Gets the value of a hash field.
|
122
|
+
#
|
123
|
+
# @param field [String] The field name
|
124
|
+
# @return [String, nil] The value of the field, or nil if field doesn't exist
|
98
125
|
def hget(field)
|
99
126
|
Familia.trace :HGET, nil, field if Familia.debug?
|
100
127
|
dbclient.hget dbkey(suffix), field
|
101
128
|
end
|
102
129
|
|
103
|
-
#
|
130
|
+
# Sets the value of a hash field.
|
131
|
+
#
|
132
|
+
# @param field [String] The field name
|
133
|
+
# @param value [String] The value to set
|
134
|
+
# @return [Integer] The number of fields that were added to the hash. If the
|
104
135
|
# field already exists, this will return 0.
|
105
136
|
def hset(field, value)
|
106
137
|
Familia.trace :HSET, nil, field if Familia.debug?
|
@@ -120,51 +151,92 @@ module Familia
|
|
120
151
|
dbclient.hsetnx dbkey, field, value
|
121
152
|
end
|
122
153
|
|
154
|
+
# Sets multiple hash fields to multiple values.
|
155
|
+
#
|
156
|
+
# @param hsh [Hash] Hash of field-value pairs to set
|
157
|
+
# @return [String] 'OK' on success
|
123
158
|
def hmset(hsh = {})
|
124
|
-
hsh ||=
|
159
|
+
hsh ||= to_h_for_storage
|
125
160
|
Familia.trace :HMSET, nil, hsh if Familia.debug?
|
126
161
|
dbclient.hmset dbkey(suffix), hsh
|
127
162
|
end
|
128
163
|
|
164
|
+
# Returns all field names in the hash.
|
165
|
+
#
|
166
|
+
# @return [Array<String>] Array of field names
|
129
167
|
def hkeys
|
130
|
-
Familia.trace :HKEYS, nil,
|
168
|
+
Familia.trace :HKEYS, nil, self.class.uri if Familia.debug?
|
131
169
|
dbclient.hkeys dbkey(suffix)
|
132
170
|
end
|
133
171
|
|
172
|
+
# Returns all values in the hash.
|
173
|
+
#
|
174
|
+
# @return [Array<String>] Array of values
|
134
175
|
def hvals
|
135
176
|
dbclient.hvals dbkey(suffix)
|
136
177
|
end
|
137
178
|
|
179
|
+
# Increments the integer value of a hash field by 1.
|
180
|
+
#
|
181
|
+
# @param field [String] The field name
|
182
|
+
# @return [Integer] The value after incrementing
|
138
183
|
def incr(field)
|
139
184
|
dbclient.hincrby dbkey(suffix), field, 1
|
140
185
|
end
|
141
186
|
alias increment incr
|
142
187
|
|
188
|
+
# Increments the integer value of a hash field by the given amount.
|
189
|
+
#
|
190
|
+
# @param field [String] The field name
|
191
|
+
# @param increment [Integer] The increment value
|
192
|
+
# @return [Integer] The value after incrementing
|
143
193
|
def incrby(field, increment)
|
144
194
|
dbclient.hincrby dbkey(suffix), field, increment
|
145
195
|
end
|
146
196
|
alias incrementby incrby
|
147
197
|
|
198
|
+
# Increments the float value of a hash field by the given amount.
|
199
|
+
#
|
200
|
+
# @param field [String] The field name
|
201
|
+
# @param increment [Float] The increment value
|
202
|
+
# @return [Float] The value after incrementing
|
148
203
|
def incrbyfloat(field, increment)
|
149
204
|
dbclient.hincrbyfloat dbkey(suffix), field, increment
|
150
205
|
end
|
151
206
|
alias incrementbyfloat incrbyfloat
|
152
207
|
|
208
|
+
# Decrements the integer value of a hash field by the given amount.
|
209
|
+
#
|
210
|
+
# @param field [String] The field name
|
211
|
+
# @param decrement [Integer] The decrement value
|
212
|
+
# @return [Integer] The value after decrementing
|
153
213
|
def decrby(field, decrement)
|
154
214
|
dbclient.decrby dbkey(suffix), field, decrement
|
155
215
|
end
|
156
216
|
alias decrementby decrby
|
157
217
|
|
218
|
+
# Decrements the integer value of a hash field by 1.
|
219
|
+
#
|
220
|
+
# @param field [String] The field name
|
221
|
+
# @return [Integer] The value after decrementing
|
158
222
|
def decr(field)
|
159
223
|
dbclient.hdecr field
|
160
224
|
end
|
161
225
|
alias decrement decr
|
162
226
|
|
227
|
+
# Returns the string length of the value associated with field in the hash.
|
228
|
+
#
|
229
|
+
# @param field [String] The field name
|
230
|
+
# @return [Integer] The string length of the field value, or 0 if field doesn't exist
|
163
231
|
def hstrlen(field)
|
164
232
|
dbclient.hstrlen dbkey(suffix), field
|
165
233
|
end
|
166
234
|
alias hstrlength hstrlen
|
167
235
|
|
236
|
+
# Determines if a hash field exists.
|
237
|
+
#
|
238
|
+
# @param field [String] The field name
|
239
|
+
# @return [Boolean] true if the field exists, false otherwise
|
168
240
|
def key?(field)
|
169
241
|
dbclient.hexists dbkey(suffix), field
|
170
242
|
end
|
@@ -176,14 +248,61 @@ module Familia
|
|
176
248
|
#
|
177
249
|
# @return [Boolean] true if the key was deleted, false otherwise
|
178
250
|
def delete!
|
179
|
-
Familia.trace :DELETE!, nil, uri if Familia.debug?
|
251
|
+
Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
|
180
252
|
|
181
253
|
# Delete the main object key
|
182
|
-
|
183
|
-
ret.positive?
|
254
|
+
dbclient.del dbkey
|
184
255
|
end
|
185
256
|
alias clear delete!
|
186
257
|
|
258
|
+
# Watches the key for changes during a MULTI/EXEC transaction.
|
259
|
+
#
|
260
|
+
# Decision Matrix:
|
261
|
+
#
|
262
|
+
# | Scenario | Use | Why |
|
263
|
+
# |----------|-----|-----|
|
264
|
+
# | Check if exists, then create | WATCH | Must prevent duplicate creation |
|
265
|
+
# | Read value, update conditionally | WATCH | Decision depends on current state |
|
266
|
+
# | Compare-and-swap operations | WATCH | Need optimistic locking |
|
267
|
+
# | Version-based updates | WATCH | Must detect concurrent changes |
|
268
|
+
# | Batch field updates | MULTI only | No conditional logic |
|
269
|
+
# | Increment + timestamp together | MULTI only | Concurrent increments OK |
|
270
|
+
# | Save object atomically | MULTI only | Just need atomicity |
|
271
|
+
# | Update indexes with save | MULTI only | No state checking needed |
|
272
|
+
#
|
273
|
+
# @param suffix_override [String, nil] Optional suffix override
|
274
|
+
# @return [String] 'OK' on success
|
275
|
+
def watch(...)
|
276
|
+
raise ArgumentError, 'Block required' unless block_given?
|
277
|
+
|
278
|
+
# Forward all arguments including the block to the watch command
|
279
|
+
dbclient.watch(dbkey, ...)
|
280
|
+
|
281
|
+
rescue Redis::BaseError => e
|
282
|
+
raise OptimisticLockError, "Redis error: #{e.message}"
|
283
|
+
end
|
284
|
+
|
285
|
+
# Flushes all the previously watched keys for a transaction.
|
286
|
+
#
|
287
|
+
# If a transaction completes successfully or discard is called, there's
|
288
|
+
# no need to manually call unwatch.
|
289
|
+
#
|
290
|
+
# NOTE: This command operates on the connection itself; not a specific key
|
291
|
+
#
|
292
|
+
# @return [String] 'OK' always, regardless of whether the key was watched or not
|
293
|
+
def unwatch(...) = dbclient.unwatch(...)
|
294
|
+
|
295
|
+
# Flushes all previously queued commands in a transaction and all watched keys
|
296
|
+
#
|
297
|
+
# NOTE: This command operates on the connection itself; not a specific key
|
298
|
+
#
|
299
|
+
# @return [String] 'OK' always
|
300
|
+
def discard(...) = dbclient.discard(...)
|
301
|
+
|
302
|
+
# Echoes a message through the Redis connection.
|
303
|
+
#
|
304
|
+
# @param args [Array] Arguments to join and echo
|
305
|
+
# @return [String] The echoed message
|
187
306
|
def echo(*args)
|
188
307
|
dbclient.echo "[#{self.class}] #{args.join(' ')}"
|
189
308
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require_relative 'settings'
|
4
4
|
|
5
|
+
require_relative '../field_type'
|
6
|
+
|
5
7
|
module Familia
|
6
8
|
VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
|
7
9
|
|
@@ -27,10 +29,6 @@ module Familia
|
|
27
29
|
@related_fields = nil
|
28
30
|
@default_expiration = nil
|
29
31
|
|
30
|
-
# Serialization settings
|
31
|
-
@dump_method = nil
|
32
|
-
@load_method = nil
|
33
|
-
|
34
32
|
# Field groups
|
35
33
|
@field_groups = nil
|
36
34
|
@current_field_group = nil
|
@@ -171,30 +169,9 @@ module Familia
|
|
171
169
|
# - :skip - skip definition if method exists
|
172
170
|
# - :warn - warn but proceed (may overwrite)
|
173
171
|
# - :ignore - proceed silently (may overwrite)
|
174
|
-
#
|
175
|
-
|
176
|
-
|
177
|
-
# - :transient - field is not persisted
|
178
|
-
# - Others, depending on features available
|
179
|
-
#
|
180
|
-
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
|
181
|
-
# Use field type system for consistency
|
182
|
-
require_relative '../field_type'
|
183
|
-
|
184
|
-
# Create appropriate field type based on category
|
185
|
-
field_type = if category == :transient
|
186
|
-
require_relative '../features/transient_fields/transient_field_type'
|
187
|
-
TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
|
188
|
-
else
|
189
|
-
# For regular fields and other categories, create custom field type with category override
|
190
|
-
custom_field_type = Class.new(FieldType) do
|
191
|
-
define_method :category do
|
192
|
-
category || :field
|
193
|
-
end
|
194
|
-
end
|
195
|
-
custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
|
196
|
-
end
|
197
|
-
|
172
|
+
#
|
173
|
+
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise)
|
174
|
+
field_type = FieldType.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
|
198
175
|
register_field_type(field_type)
|
199
176
|
end
|
200
177
|
|
@@ -256,14 +233,6 @@ module Familia
|
|
256
233
|
@has_related_fields ||= false
|
257
234
|
end
|
258
235
|
|
259
|
-
def dump_method
|
260
|
-
@dump_method || :to_json # Familia.dump_method
|
261
|
-
end
|
262
|
-
|
263
|
-
def load_method
|
264
|
-
@load_method || :from_json # Familia.load_method
|
265
|
-
end
|
266
|
-
|
267
236
|
# Storage for field type instances
|
268
237
|
def field_types
|
269
238
|
@field_types ||= {}
|
@@ -498,7 +467,8 @@ module Familia
|
|
498
467
|
|
499
468
|
# If no value is provided to this fast attribute method, make a call
|
500
469
|
# to the db to return the current stored value of the hash field.
|
501
|
-
|
470
|
+
# Handle Redis::Future objects during transactions
|
471
|
+
return hget field_name if val.nil? || val.is_a?(Redis::Future)
|
502
472
|
|
503
473
|
begin
|
504
474
|
# Trace the operation if debugging is enabled.
|
@@ -506,7 +476,7 @@ module Familia
|
|
506
476
|
|
507
477
|
# Convert the provided value to a format suitable for Database storage.
|
508
478
|
prepared = serialize_value(val)
|
509
|
-
Familia.ld "[
|
479
|
+
Familia.ld "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
|
510
480
|
|
511
481
|
# Use the existing accessor method to set the attribute value.
|
512
482
|
send :"#{method_name}=", val
|
@@ -23,7 +23,7 @@ module Familia
|
|
23
23
|
# to the constructor.
|
24
24
|
# @param kwargs [Hash] Keyword arguments to be passed to the constructor.
|
25
25
|
# @return [Object] The newly created and persisted instance.
|
26
|
-
# @raise [Familia::
|
26
|
+
# @raise [Familia::RecordExistsError] If an instance with the same identifier already
|
27
27
|
# exists.
|
28
28
|
#
|
29
29
|
# This method serves as a factory method for creating and persisting new
|
@@ -35,7 +35,7 @@ module Familia
|
|
35
35
|
# - Keyword arguments (**kwargs) are passed as a hash to the constructor.
|
36
36
|
#
|
37
37
|
# After instantiation, the method checks if an object with the same
|
38
|
-
# identifier already exists. If it does, a Familia::
|
38
|
+
# identifier already exists. If it does, a Familia::RecordExistsError exception is
|
39
39
|
# raised to prevent overwriting existing data.
|
40
40
|
#
|
41
41
|
# Finally, the method saves the new instance returns it.
|
@@ -52,24 +52,27 @@ module Familia
|
|
52
52
|
# @see #new
|
53
53
|
# @see #exists?
|
54
54
|
# @see #save
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
def create!(...)
|
56
|
+
hobj = new(...)
|
57
|
+
hobj.save_if_not_exists!
|
58
|
+
|
59
|
+
# If a block is given, yield the created object
|
60
|
+
# This allows for additional operations on successful creation
|
61
|
+
yield hobj if block_given?
|
62
|
+
|
63
|
+
hobj
|
60
64
|
end
|
61
65
|
|
62
|
-
def multiget(
|
63
|
-
|
64
|
-
ids.filter_map { |json| from_json(json) }
|
66
|
+
def multiget(...)
|
67
|
+
rawmultiget(...).filter_map { |json| Familia::JsonSerializer.parse(json) }
|
65
68
|
end
|
66
69
|
|
67
|
-
def rawmultiget(*
|
68
|
-
|
69
|
-
return [] if
|
70
|
+
def rawmultiget(*hids)
|
71
|
+
hids.collect! { |hobjid| dbkey(hobjid) }
|
72
|
+
return [] if hids.compact.empty?
|
70
73
|
|
71
|
-
Familia.trace :MULTIGET, nil, "#{
|
72
|
-
dbclient.mget(*
|
74
|
+
Familia.trace :MULTIGET, nil, "#{hids.size}: #{hids}" if Familia.debug?
|
75
|
+
dbclient.mget(*hids)
|
73
76
|
end
|
74
77
|
|
75
78
|
# Converts the class name into a string that can be used to look up
|
@@ -125,15 +128,15 @@ module Familia
|
|
125
128
|
# User.find_by_key("user:123") # Returns a User instance if it exists,
|
126
129
|
# nil otherwise
|
127
130
|
#
|
128
|
-
def
|
131
|
+
def find_by_dbkey(objkey)
|
129
132
|
raise ArgumentError, 'Empty key' if objkey.to_s.empty?
|
130
133
|
|
131
134
|
# We use a lower-level method here b/c we're working with the
|
132
135
|
# full key and not just the identifier.
|
133
136
|
does_exist = dbclient.exists(objkey).positive?
|
134
137
|
|
135
|
-
Familia.ld "[
|
136
|
-
Familia.trace :
|
138
|
+
Familia.ld "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
139
|
+
Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
|
137
140
|
|
138
141
|
# This is the reason for calling exists first. We want to definitively
|
139
142
|
# and without any ambiguity know if the object exists in the database. If it
|
@@ -143,11 +146,16 @@ module Familia
|
|
143
146
|
return unless does_exist
|
144
147
|
|
145
148
|
obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
|
146
|
-
Familia.trace :
|
147
|
-
|
148
|
-
|
149
|
+
Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
|
150
|
+
|
151
|
+
# Create instance and deserialize fields using existing helper method
|
152
|
+
# This avoids duplicating deserialization logic and keeps field-by-field processing
|
153
|
+
instance = allocate
|
154
|
+
instance.send(:initialize_relatives)
|
155
|
+
instance.send(:initialize_with_keyword_args_deserialize_value, **obj)
|
156
|
+
instance
|
149
157
|
end
|
150
|
-
alias
|
158
|
+
alias find_by_key find_by_dbkey
|
151
159
|
|
152
160
|
# Retrieves and instantiates an object from Database using its identifier.
|
153
161
|
#
|
@@ -168,19 +176,19 @@ module Familia
|
|
168
176
|
# @example
|
169
177
|
# User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
|
170
178
|
#
|
171
|
-
def
|
179
|
+
def find_by_identifier(identifier, suffix = nil)
|
172
180
|
suffix ||= self.suffix
|
173
181
|
return nil if identifier.to_s.empty?
|
174
182
|
|
175
183
|
objkey = dbkey(identifier, suffix)
|
176
184
|
|
177
|
-
Familia.ld "[
|
185
|
+
Familia.ld "[find_by_id] #{self} from key #{objkey})"
|
178
186
|
Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
|
179
|
-
|
187
|
+
find_by_dbkey objkey
|
180
188
|
end
|
189
|
+
alias find_by_id find_by_identifier
|
181
190
|
alias find find_by_id
|
182
|
-
alias load find_by_id
|
183
|
-
alias from_identifier find_by_id # deprecated
|
191
|
+
alias load find_by_id
|
184
192
|
|
185
193
|
# Checks if an object with the given identifier exists in the database.
|
186
194
|
#
|
@@ -204,6 +212,9 @@ module Familia
|
|
204
212
|
ret = dbclient.exists objkey
|
205
213
|
Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
|
206
214
|
|
215
|
+
# Handle Redis::Future objects during transactions
|
216
|
+
return ret if ret.is_a?(Redis::Future)
|
217
|
+
|
207
218
|
ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
|
208
219
|
end
|
209
220
|
|