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.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +118 -6
  3. data/CLAUDE.md +43 -11
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +52 -0
  7. data/bin/irb +1 -1
  8. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  9. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  10. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  11. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/guides/feature-expiration.md +18 -18
  14. data/docs/migrating/v2.0.0-pre18.md +58 -0
  15. data/docs/migrating/v2.0.0-pre19.md +197 -0
  16. data/docs/qodo-merge-compliance.md +96 -0
  17. data/examples/datatype_standalone.rb +281 -0
  18. data/lib/familia/base.rb +0 -2
  19. data/lib/familia/connection/behavior.rb +252 -0
  20. data/lib/familia/connection/handlers.rb +95 -0
  21. data/lib/familia/connection/middleware.rb +58 -4
  22. data/lib/familia/connection/operation_core.rb +1 -1
  23. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  24. data/lib/familia/connection/transaction_core.rb +7 -9
  25. data/lib/familia/connection.rb +2 -1
  26. data/lib/familia/data_type/connection.rb +151 -7
  27. data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
  28. data/lib/familia/data_type/serialization.rb +9 -5
  29. data/lib/familia/data_type/types/hashkey.rb +1 -1
  30. data/lib/familia/data_type.rb +2 -2
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/errors.rb +51 -14
  34. data/lib/familia/features/autoloader.rb +3 -1
  35. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  36. data/lib/familia/features/expiration/extensions.rb +8 -10
  37. data/lib/familia/features/expiration.rb +19 -19
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
  40. data/lib/familia/features/relationships/indexing.rb +37 -42
  41. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/field_type.rb +2 -1
  44. data/lib/familia/horreum/connection.rb +11 -35
  45. data/lib/familia/horreum/database_commands.rb +130 -11
  46. data/lib/familia/horreum/definition.rb +8 -38
  47. data/lib/familia/horreum/management.rb +38 -27
  48. data/lib/familia/horreum/persistence.rb +191 -67
  49. data/lib/familia/horreum/serialization.rb +94 -73
  50. data/lib/familia/horreum/utils.rb +0 -8
  51. data/lib/familia/horreum.rb +41 -18
  52. data/lib/familia/identifier_extractor.rb +60 -0
  53. data/lib/familia/logging.rb +268 -112
  54. data/lib/familia/refinements.rb +0 -1
  55. data/lib/familia/settings.rb +7 -7
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -2
  58. data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
  59. data/pr_agent.toml +31 -0
  60. data/pr_compliance_checklist.yaml +45 -0
  61. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  62. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  63. data/try/edge_cases/json_serialization_try.rb +2 -2
  64. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  65. data/try/edge_cases/race_conditions_try.rb +1 -1
  66. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  67. data/try/edge_cases/string_coercion_try.rb +5 -5
  68. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  69. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  70. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  71. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  72. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  77. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  79. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  80. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  81. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  82. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  83. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  84. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  85. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  86. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  87. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  88. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  89. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  94. data/try/features/expiration/expiration_try.rb +2 -2
  95. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  96. data/try/features/feature_dependencies_try.rb +1 -1
  97. data/try/features/feature_improvements_try.rb +1 -1
  98. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  99. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  100. data/try/features/quantization/quantization_try.rb +1 -1
  101. data/try/features/real_feature_integration_try.rb +17 -14
  102. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  103. data/try/features/relationships/indexing_try.rb +34 -5
  104. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  105. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  106. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  107. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  108. data/try/features/relationships/relationships_api_changes_try.rb +5 -5
  109. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  110. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  111. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  112. data/try/features/relationships/relationships_performance_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  114. data/try/features/relationships/relationships_try.rb +1 -1
  115. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  116. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  117. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  118. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  119. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  121. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  122. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
  123. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  124. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  125. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  126. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
  127. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
  128. data/try/{core → integration/connection}/pools_try.rb +1 -1
  129. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  130. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  131. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  135. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  136. data/try/{core → integration}/create_method_try.rb +23 -23
  137. data/try/integration/cross_component_try.rb +1 -1
  138. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  139. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  140. data/try/{core → integration}/database_consistency_try.rb +11 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
  144. data/try/{models → integration/models}/customer_try.rb +1 -1
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +2 -2
  147. data/try/{core → integration}/persistence_operations_try.rb +163 -11
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  156. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  157. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  158. data/try/{core → unit/core}/connection_try.rb +1 -1
  159. data/try/{core → unit/core}/errors_try.rb +1 -1
  160. data/try/{core → unit/core}/extensions_try.rb +1 -1
  161. data/try/unit/core/familia_logger_try.rb +110 -0
  162. data/try/{core → unit/core}/familia_try.rb +1 -1
  163. data/try/{core → unit/core}/middleware_try.rb +41 -1
  164. data/try/{core → unit/core}/settings_try.rb +1 -1
  165. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  166. data/try/{core → unit/core}/tools_try.rb +1 -1
  167. data/try/{core → unit/core}/utils_try.rb +17 -14
  168. data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
  169. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  176. data/try/{data_types → unit/data_types}/string_try.rb +2 -2
  177. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  178. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
  179. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +4 -4
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
  182. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  183. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
  193. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  194. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  195. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  196. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  198. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  199. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  200. metadata +147 -126
  201. data/lib/familia/distinguisher.rb +0 -85
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/try/{debugging → support/debugging}/README.md +0 -0
  205. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  206. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  207. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  227. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  228. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  229. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  230. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  231. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  233. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  234. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  235. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  249. /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
- attr_reader :uri
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
- # Creates a new Database connection instance using the class/instance configuration
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, self.class is the model class
281
- # When called from class: self is the model class
282
- klass = self.is_a?(Class) ? self : self.class
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
- key_exists && !size.zero?
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
- # For parity with DataType#hgetall
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
- # @return The number of fields that were added to the hash. If the
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 ||= to_h
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, 'uri' if Familia.debug?
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
- ret = dbclient.del dbkey
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
- # @param category [Symbol, nil] field category for special handling:
175
- # - nil - regular field (default)
176
- # - :encrypted - field contains encrypted data
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
- return hget field_name if val.nil?
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 "[.define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
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::Problem] If an instance with the same identifier already
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::Problem exception is
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
- def create(*, **)
57
- fobj = new(*, **)
58
- fobj.save_if_not_exists
59
- fobj
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(*ids)
63
- ids = rawmultiget(*ids)
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(*ids)
68
- ids.collect! { |objid| dbkey(objid) }
69
- return [] if ids.compact.empty?
70
+ def rawmultiget(*hids)
71
+ hids.collect! { |hobjid| dbkey(hobjid) }
72
+ return [] if hids.compact.empty?
70
73
 
71
- Familia.trace :MULTIGET, nil, "#{ids.size}: #{ids}" if Familia.debug?
72
- dbclient.mget(*ids)
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 find_by_key(objkey)
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 "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
136
- Familia.trace :FROM_KEY, nil, objkey if Familia.debug?
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 :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
147
-
148
- new(**obj)
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 from_dbkey find_by_key # deprecated
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 find_by_id(identifier, suffix = nil)
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 "[.find_by_id] #{self} from key #{objkey})"
185
+ Familia.ld "[find_by_id] #{self} from key #{objkey})"
178
186
  Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
179
- find_by_key objkey
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 # deprecated
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