familia 2.0.0.pre16 → 2.0.0.pre18

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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.rst +82 -0
  7. data/CLAUDE.md +47 -2
  8. data/Gemfile.lock +1 -1
  9. data/README.md +13 -0
  10. data/bin/irb +1 -1
  11. data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/migrating/v2.0.0-pre18.md +58 -0
  14. data/docs/overview.md +2 -2
  15. data/docs/qodo-merge-compliance.md +96 -0
  16. data/docs/reference/api-technical.md +1 -1
  17. data/examples/encrypted_fields.rb +1 -1
  18. data/examples/safe_dump.rb +1 -1
  19. data/lib/familia/base.rb +6 -6
  20. data/lib/familia/connection/middleware.rb +58 -4
  21. data/lib/familia/connection.rb +1 -1
  22. data/lib/familia/data_type/class_methods.rb +63 -0
  23. data/lib/familia/data_type/connection.rb +83 -0
  24. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  25. data/lib/familia/data_type/serialization.rb +5 -5
  26. data/lib/familia/data_type/settings.rb +96 -0
  27. data/lib/familia/data_type/types/hashkey.rb +2 -1
  28. data/lib/familia/data_type/types/sorted_set.rb +113 -10
  29. data/lib/familia/data_type/types/stringkey.rb +0 -4
  30. data/lib/familia/data_type.rb +8 -195
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/features/autoloader.rb +3 -1
  34. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  35. data/lib/familia/features/encrypted_fields.rb +5 -2
  36. data/lib/familia/features/external_identifier.rb +49 -8
  37. data/lib/familia/features/object_identifier.rb +84 -12
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
  40. data/lib/familia/features/relationships/indexing.rb +7 -1
  41. data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/features/transient_fields.rb +7 -2
  44. data/lib/familia/features.rb +6 -1
  45. data/lib/familia/field_type.rb +0 -18
  46. data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
  47. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
  48. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
  49. data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
  50. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
  51. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
  52. data/lib/familia/horreum/serialization.rb +190 -0
  53. data/lib/familia/horreum.rb +39 -14
  54. data/lib/familia/identifier_extractor.rb +60 -0
  55. data/lib/familia/logging.rb +271 -112
  56. data/lib/familia/refinements.rb +0 -1
  57. data/lib/familia/version.rb +1 -1
  58. data/lib/familia.rb +2 -2
  59. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  60. data/pr_agent.toml +31 -0
  61. data/pr_compliance_checklist.yaml +45 -0
  62. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  63. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  64. data/try/edge_cases/json_serialization_try.rb +2 -2
  65. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  66. data/try/edge_cases/race_conditions_try.rb +1 -1
  67. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  68. data/try/edge_cases/string_coercion_try.rb +1 -1
  69. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  70. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  71. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  72. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  77. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  79. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  80. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  81. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  82. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  83. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  84. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  85. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  86. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  87. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  88. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  89. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  94. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  95. data/try/features/expiration/expiration_try.rb +1 -1
  96. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  97. data/try/features/feature_dependencies_try.rb +1 -1
  98. data/try/features/feature_improvements_try.rb +1 -1
  99. data/try/features/field_groups_try.rb +244 -0
  100. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  101. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  102. data/try/features/quantization/quantization_try.rb +1 -1
  103. data/try/features/real_feature_integration_try.rb +17 -14
  104. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  105. data/try/features/relationships/indexing_try.rb +16 -1
  106. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  107. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  108. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  109. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  110. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  111. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  112. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  114. data/try/features/relationships/relationships_performance_try.rb +1 -1
  115. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  116. data/try/features/relationships/relationships_try.rb +1 -1
  117. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  118. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  119. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  121. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  122. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  123. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  124. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  125. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  126. data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
  127. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  128. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  129. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  130. data/try/{core → integration/connection}/pools_try.rb +1 -1
  131. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  135. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  136. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  137. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  138. data/try/{core → integration}/create_method_try.rb +1 -1
  139. data/try/integration/cross_component_try.rb +1 -1
  140. data/try/{core → integration}/database_consistency_try.rb +12 -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 +1 -1
  144. data/try/{models → integration/models}/customer_try.rb +6 -6
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  147. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +2 -2
  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 +15 -7
  156. data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
  157. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  158. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  159. data/try/{core → unit/core}/connection_try.rb +5 -5
  160. data/try/{core → unit/core}/errors_try.rb +4 -4
  161. data/try/{core → unit/core}/extensions_try.rb +1 -1
  162. data/try/unit/core/familia_logger_try.rb +110 -0
  163. data/try/{core → unit/core}/familia_try.rb +2 -2
  164. data/try/{core → unit/core}/middleware_try.rb +41 -1
  165. data/try/{core → unit/core}/settings_try.rb +1 -1
  166. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  167. data/try/{core → unit/core}/tools_try.rb +3 -3
  168. data/try/{core → unit/core}/utils_try.rb +17 -14
  169. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  176. data/try/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
  177. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  178. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  179. data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  182. data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
  183. data/try/unit/horreum/defensive_initialization_try.rb +86 -0
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -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 +2 -2
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  193. data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
  194. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  195. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  196. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  198. data/try/valkey.conf +26 -0
  199. metadata +149 -132
  200. data/lib/familia/distinguisher.rb +0 -85
  201. data/lib/familia/horreum/core.rb +0 -21
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
  205. /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
  206. /data/try/{debugging → support/debugging}/README.md +0 -0
  207. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  227. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  228. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  229. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  230. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  231. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  233. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  234. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  235. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  250. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
data/lib/familia/base.rb CHANGED
@@ -17,8 +17,6 @@ module Familia
17
17
 
18
18
  @features_available = nil
19
19
  @feature_definitions = nil
20
- @dump_method = :to_json
21
- @load_method = :from_json
22
20
 
23
21
  def self.included(base)
24
22
  # Ensure the including class gets its own feature registry
@@ -31,14 +29,15 @@ module Familia
31
29
  attr_reader :features_available, :feature_definitions
32
30
  attr_accessor :dump_method, :load_method
33
31
 
34
- def add_feature(klass, feature_name, depends_on: [])
32
+ def add_feature(klass, feature_name, depends_on: [], field_group: nil)
35
33
  @features_available ||= {}
36
34
  Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
37
35
 
38
36
  # Create field definition object
39
37
  feature_def = FeatureDefinition.new(
40
38
  name: feature_name,
41
- depends_on: depends_on
39
+ depends_on: depends_on,
40
+ field_group: field_group
42
41
  )
43
42
 
44
43
  # Track field definitions after defining field methods
@@ -112,14 +111,15 @@ module Familia
112
111
  attr_reader :features_available, :feature_definitions
113
112
  attr_accessor :dump_method, :load_method
114
113
 
115
- def add_feature(klass, feature_name, depends_on: [])
114
+ def add_feature(klass, feature_name, depends_on: [], field_group: nil)
116
115
  @features_available ||= {}
117
116
  Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
118
117
 
119
118
  # Create field definition object
120
119
  feature_def = FeatureDefinition.new(
121
120
  name: feature_name,
122
- depends_on: depends_on
121
+ depends_on: depends_on,
122
+ field_group: field_group
123
123
  )
124
124
 
125
125
  # Track field definitions after defining field methods
@@ -1,6 +1,6 @@
1
1
  # lib/familia/connection/middleware.rb
2
2
 
3
- require_relative '../../middleware/database_middleware'
3
+ require_relative '../../middleware/database_logger'
4
4
 
5
5
  module Familia
6
6
  module Connection
@@ -19,13 +19,13 @@ module Familia
19
19
  # Increments the middleware version, invalidating all cached connections
20
20
  def increment_middleware_version!
21
21
  @middleware_version += 1
22
- Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}" if Familia.debug?
22
+ Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
23
23
  end
24
24
 
25
25
  # Sets a versioned fiber-local connection
26
- def set_fiber_connection(connection)
26
+ def fiber_connection=(connection)
27
27
  Fiber[:familia_connection] = [connection, middleware_version]
28
- Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}" if Familia.debug?
28
+ Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
29
29
  end
30
30
 
31
31
  # Clears the fiber-local connection
@@ -50,24 +50,78 @@ module Familia
50
50
  increment_middleware_version! if value
51
51
  end
52
52
 
53
+ # Reconnects with fresh middleware registration
54
+ #
55
+ # This method is useful when middleware needs to be applied to connection pools
56
+ # that were created before middleware was enabled. It:
57
+ #
58
+ # 1. Clears the middleware registration flag to allow re-registration
59
+ # 2. Re-runs the middleware registration logic
60
+ # 3. Clears connection chain to force rebuild
61
+ # 4. Increments middleware version to invalidate cached connections
62
+ # 5. Clears fiber-local connections
63
+ #
64
+ # The next connection request will use the updated middleware configuration.
65
+ # Existing connection pools will naturally create new connections with middleware
66
+ # as old connections are cycled out.
67
+ #
68
+ # @note If no middleware is enabled, this method safely clears connection state
69
+ # but won't register any middleware until it's enabled.
70
+ #
71
+ # @example Enable middleware and reconnect
72
+ # Familia.enable_database_logging = true
73
+ # Familia.reconnect!
74
+ #
75
+ # @example In test suites
76
+ # # Test file A creates pools
77
+ # Familia.connection_provider = ->(uri) { pool.with { |c| c } }
78
+ #
79
+ # # Test file B enables middleware
80
+ # Familia.enable_database_logging = true
81
+ # Familia.reconnect! # Force new connections with middleware
82
+ #
83
+ def reconnect!
84
+ # Allow middleware to be re-registered
85
+ @middleware_registered = false
86
+ register_middleware_once
87
+
88
+ # Clear connection chain to force rebuild
89
+ @connection_chain = nil
90
+
91
+ # Increment version to invalidate all cached connections
92
+ increment_middleware_version!
93
+
94
+ # Clear fiber-local connections
95
+ clear_fiber_connection!
96
+
97
+ Familia.trace :RECONNECT, nil, 'Connection chain cleared, will rebuild with current middleware on next use'
98
+ end
99
+
53
100
  private
54
101
 
55
102
  # Registers middleware once globally, regardless of when clients are created.
56
103
  # This prevents duplicate middleware registration and ensures all clients get middleware.
57
104
  def register_middleware_once
105
+ # Skip if already registered
58
106
  return if @middleware_registered
59
107
 
108
+ # Check if any middleware is enabled
109
+ return unless Familia.enable_database_logging || Familia.enable_database_counter
110
+
60
111
  if Familia.enable_database_logging
61
112
  DatabaseLogger.logger = Familia.logger
62
113
  RedisClient.register(DatabaseLogger)
114
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseLogger'
63
115
  end
64
116
 
65
117
  if Familia.enable_database_counter
66
118
  # NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
67
119
  # less contentious than Mutex-based counters. Safe for production.
68
120
  RedisClient.register(DatabaseCommandCounter)
121
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseCommandCounter'
69
122
  end
70
123
 
124
+ # Set flag after successful registration
71
125
  @middleware_registered = true
72
126
  end
73
127
  end
@@ -42,7 +42,7 @@ module Familia
42
42
  @connection_chain = nil # Force rebuild of chain
43
43
  end
44
44
 
45
- # Sets the default URI for Database connections.
45
+ # Sets the default URI for Database connections.
46
46
  #
47
47
  # NOTE: uri is not a property of the Settings module b/c it's not
48
48
  # configured in class defintions like default_expiration or logical DB index.
@@ -0,0 +1,63 @@
1
+ # lib/familia/data_type/definition.rb
2
+
3
+ module Familia
4
+ class DataType
5
+ # ClassMethods - Class-level DSL methods for defining DataType behavior
6
+ #
7
+ # This module is extended into classes that inherit from Familia::DataType,
8
+ # providing class methods for type registration, configuration, and inheritance.
9
+ #
10
+ # Key features:
11
+ # * Type registration system for creating DataType subclasses
12
+ # * Database and connection configuration
13
+ # * Inheritance hooks for propagating settings
14
+ # * Option validation and filtering
15
+ #
16
+ module ClassMethods
17
+ attr_accessor :parent, :suffix, :prefix, :uri
18
+ attr_writer :logical_database
19
+
20
+ # To be called inside every class that inherits DataType
21
+ # +methname+ is the term used for the class and instance methods
22
+ # that are created for the given +klass+ (e.g. set, list, etc)
23
+ def register(klass, methname)
24
+ Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}" if Familia.debug?
25
+
26
+ @registered_types[methname] = klass
27
+ end
28
+
29
+ # Get the registered type class from a given method name
30
+ # +methname+ is the method name used to register the class (e.g. :set, :list, etc)
31
+ # Returns the registered class or nil if not found
32
+ def registered_type(methname)
33
+ @registered_types[methname]
34
+ end
35
+
36
+ def logical_database(val = nil)
37
+ @logical_database = val unless val.nil?
38
+ @logical_database || parent&.logical_database
39
+ end
40
+
41
+ def uri(val = nil)
42
+ @uri = val unless val.nil?
43
+ @uri || (parent ? parent.uri : Familia.uri)
44
+ end
45
+
46
+ def inherited(obj)
47
+ Familia.trace :DATATYPE, nil, "#{obj} is my kinda type" if Familia.debug?
48
+ obj.logical_database = logical_database
49
+ obj.default_expiration = default_expiration # method added via Features::Expiration
50
+ obj.uri = uri
51
+ super
52
+ end
53
+
54
+ def valid_keys_only(opts)
55
+ opts.slice(*DataType.valid_options)
56
+ end
57
+
58
+ def relations?
59
+ @has_related_fields ||= false
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,83 @@
1
+ # lib/familia/data_type/connection.rb
2
+
3
+ module Familia
4
+ class DataType
5
+ # Connection - Instance-level connection and key generation methods
6
+ #
7
+ # This module provides instance methods for database connection resolution
8
+ # and Redis key generation for DataType objects.
9
+ #
10
+ # Key features:
11
+ # * Database connection resolution with Chain of Responsibility pattern
12
+ # * Redis key generation based on parent context
13
+ # * Direct database access for advanced operations
14
+ #
15
+ module Connection
16
+ # TODO: Replace with Chain of Responsibility pattern
17
+ def dbclient
18
+ return Fiber[:familia_transaction] if Fiber[:familia_transaction]
19
+ return @dbclient if @dbclient
20
+
21
+ # Delegate to parent if present, otherwise fall back to Familia
22
+ parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
23
+ end
24
+
25
+ # Produces the full dbkey for this object.
26
+ #
27
+ # @return [String] The full dbkey.
28
+ #
29
+ # This method determines the appropriate dbkey based on the context of the DataType object:
30
+ #
31
+ # 1. If a hardcoded key is set in the options, it returns that key.
32
+ # 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
33
+ # 3. For class-level DataType objects, it uses the parent class's dbkey method.
34
+ # 4. For standalone DataType objects, it uses the keystring as the full dbkey.
35
+ #
36
+ # For class-level DataType objects (parent_class? == true):
37
+ # - The suffix is optional and used to differentiate between different types of objects.
38
+ # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
39
+ # - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
40
+ # - Passing nil as the suffix is how class-level DataType objects are created without
41
+ # the global default 'object' suffix.
42
+ #
43
+ # @example Instance-level DataType
44
+ # user_instance.some_datatype.dbkey # => "user:123:some_datatype"
45
+ #
46
+ # @example Class-level DataType
47
+ # User.some_datatype.dbkey # => "user:some_datatype"
48
+ #
49
+ # @example Standalone DataType
50
+ # DataType.new("mykey").dbkey # => "mykey"
51
+ #
52
+ # @example Class-level DataType with explicit nil suffix
53
+ # User.dbkey("123", nil) # => "user:123"
54
+ #
55
+ def dbkey
56
+ # Return the hardcoded key if it's set. This is useful for
57
+ # support legacy keys that aren't derived in the same way.
58
+ return opts[:dbkey] if opts[:dbkey]
59
+
60
+ if parent_instance?
61
+ # This is an instance-level datatype object so the parent instance's
62
+ # dbkey method is defined in Familia::Horreum::InstanceMethods.
63
+ parent.dbkey(keystring)
64
+ elsif parent_class?
65
+ # This is a class-level datatype object so the parent class' dbkey
66
+ # method is defined in Familia::Horreum::DefinitionMethods.
67
+ parent.dbkey(keystring, nil)
68
+ else
69
+ # This is a standalone DataType object where it's keystring
70
+ # is the full database key (dbkey).
71
+ keystring
72
+ end
73
+ end
74
+
75
+ # Provides a structured way to "gear down" to run db commands that are
76
+ # not implemented in our DataType classes since we intentionally don't
77
+ # have a method_missing method.
78
+ def direct_access
79
+ yield(dbclient, dbkey)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,10 +1,10 @@
1
- # lib/familia/data_type/commands.rb
1
+ # lib/familia/data_type/database_commands.rb
2
2
 
3
3
  module Familia
4
4
  class DataType
5
5
  # Must be included in all DataType classes to provide Valkey/Redis
6
6
  # commands. The class must have a dbkey method.
7
- module Commands
7
+ module DatabaseCommands
8
8
  def move(logical_database)
9
9
  dbclient.move dbkey, logical_database
10
10
  end
@@ -11,9 +11,9 @@ module Familia
11
11
  # @return [String, nil] The serialized representation of the value, or nil
12
12
  # if serialization fails.
13
13
  #
14
- # @note When a class option is specified, it uses that class's
15
- # serialization method. Otherwise, it relies on Familia.distinguisher for
16
- # serialization.
14
+ # @note When a class option is specified, it uses Familia.identifier_extractor
15
+ # to extract the identifier from objects. Otherwise, it extracts identifiers
16
+ # from Familia::Base instances or class names.
17
17
  #
18
18
  # @example With a class option
19
19
  # serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
@@ -31,13 +31,13 @@ module Familia
31
31
  Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
32
32
 
33
33
  if opts[:class]
34
- prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
34
+ prepared = Familia.identifier_extractor(opts[:class])
35
35
  Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
36
36
  end
37
37
 
38
38
  if prepared.nil?
39
39
  # Enforce strict values when no class option is specified
40
- prepared = Familia.distinguisher(val, strict_values: true)
40
+ prepared = Familia.identifier_extractor(val)
41
41
  Familia.ld " from <#{val.class}> => <#{prepared.class}>"
42
42
  end
43
43
 
@@ -0,0 +1,96 @@
1
+ # lib/familia/data_type/settings.rb
2
+
3
+ module Familia
4
+ class DataType
5
+ # Settings - Instance-level configuration and introspection methods
6
+ #
7
+ # This module provides instance methods for accessing and managing
8
+ # DataType object configuration, parent relationships, and serialization.
9
+ #
10
+ # Key features:
11
+ # * Parent object relationship management
12
+ # * URI and database configuration
13
+ # * Serialization method delegation
14
+ # * Type introspection
15
+ #
16
+ module Settings
17
+ attr_reader :keystring, :opts, :logical_database
18
+ attr_reader :uri
19
+
20
+ alias url uri
21
+
22
+ def class?
23
+ !@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
24
+ end
25
+
26
+ def parent_instance?
27
+ parent&.is_a?(Horreum::ParentDefinition)
28
+ end
29
+
30
+ def parent_class?
31
+ parent.is_a?(Class) && parent.ancestors.include?(Familia::Horreum)
32
+ end
33
+
34
+ def parent?
35
+ parent_class? || parent_instance?
36
+ end
37
+
38
+ def parent
39
+ # Return cached ParentDefinition if available
40
+ return @parent if @parent
41
+
42
+ # Return class-level parent if no instance parent
43
+ return self.class.parent unless @parent_ref
44
+
45
+ # Create ParentDefinition dynamically from stored reference.
46
+ # This ensures we get the current identifier value (available after initialization)
47
+ # rather than a stale nil value from initialization time. Cannot cache due to frozen object.
48
+ Horreum::ParentDefinition.from_parent(@parent_ref)
49
+ end
50
+
51
+ def parent=(value)
52
+ case value
53
+ when Horreum::ParentDefinition
54
+ @parent = value
55
+ when nil
56
+ @parent = nil
57
+ @parent_ref = nil
58
+ else
59
+ # Store parent instance reference for lazy ParentDefinition creation.
60
+ # During initialization, the parent's identifier may not be available yet,
61
+ # so we defer ParentDefinition creation until first access for memory efficiency.
62
+ # Note: @parent_ref is not cleared after use because DataType objects are frozen.
63
+ @parent_ref = value
64
+ @parent = nil # Will be created dynamically in parent method
65
+ end
66
+ end
67
+
68
+ def uri
69
+ # Return explicit instance URI if set
70
+ return @uri if @uri
71
+
72
+ # If we have a parent with logical_database, build URI with that database
73
+ if parent && parent.respond_to?(:logical_database) && parent.logical_database
74
+ new_uri = (self.class.uri || Familia.uri).dup
75
+ new_uri.db = parent.logical_database
76
+ new_uri
77
+ else
78
+ # Fall back to class-level URI or global Familia.uri
79
+ self.class.uri || Familia.uri
80
+ end
81
+ end
82
+
83
+ def uri=(value)
84
+ @uri = value
85
+ end
86
+
87
+ def dump_method
88
+ self.class.dump_method
89
+ end
90
+
91
+ def load_method
92
+ self.class.load_method
93
+ end
94
+ end
95
+ end
96
+ end
@@ -95,7 +95,8 @@ module Familia
95
95
  def remove_field(field)
96
96
  dbclient.hdel dbkey, field.to_s
97
97
  end
98
- alias remove remove_field # deprecated
98
+ alias remove remove_field
99
+ alias remove_element remove_field
99
100
 
100
101
  def increment(field, by = 1)
101
102
  dbclient.hincrby(dbkey, field.to_s, by).to_i
@@ -46,17 +46,81 @@ module Familia
46
46
  add val, score
47
47
  end
48
48
 
49
- def add(val, score = nil)
50
- # TODO: Support some or all of the ZADD options.
51
- # XX: Only update existing elements. Don't add new ones.
52
- # NX: Only add new elements. Don't update existing ones.
53
- # LT: Only update if new score < current score. Doesn't prevent adding.
54
- # GT: Only update if new score > current score. Doesn't prevent adding.
55
- # CH: Return total changed elements (new + updated) instead of just new.
56
- # INCR: Acts like ZINCRBY. Only one score-element pair allowed.
57
- # Note: GT, LT and NX options are mutually exclusive.
49
+ # Adds an element to the sorted set with an optional score and ZADD options.
50
+ #
51
+ # This method supports Redis ZADD options for conditional adds and updates:
52
+ # - **NX**: Only add new elements (don't update existing)
53
+ # - **XX**: Only update existing elements (don't add new)
54
+ # - **GT**: Only update if new score > current score
55
+ # - **LT**: Only update if new score < current score
56
+ # - **CH**: Return changed count (new + updated) instead of just new count
57
+ #
58
+ # @param val [Object] The value to add to the sorted set
59
+ # @param score [Numeric, nil] The score for ranking (defaults to current timestamp)
60
+ # @param nx [Boolean] Only add new elements, don't update existing (default: false)
61
+ # @param xx [Boolean] Only update existing elements, don't add new (default: false)
62
+ # @param gt [Boolean] Only update if new score > current score (default: false)
63
+ # @param lt [Boolean] Only update if new score < current score (default: false)
64
+ # @param ch [Boolean] Return changed count instead of added count (default: false)
65
+ #
66
+ # @return [Boolean] Returns the return value from the redis gem's ZADD
67
+ # command. Returns true if element was added or changed (with CH option),
68
+ # false if element score was updated without change tracking or no
69
+ # operation occurred due to option constraints (NX, XX, GT, LT).
70
+ #
71
+ # @raise [ArgumentError] If mutually exclusive options are specified together
72
+ # (NX+XX, GT+LT, NX+GT, NX+LT)
73
+ #
74
+ # @example Add new element with timestamp
75
+ # metrics.add('pageview', Time.now.to_f) #=> true
76
+ #
77
+ # @example Preserve original timestamp on subsequent saves
78
+ # index.add(email, Time.now.to_f, nx: true) #=> true
79
+ # index.add(email, Time.now.to_f, nx: true) #=> false (unchanged)
80
+ #
81
+ # @example Update timestamp only for existing entries
82
+ # index.add(email, Time.now.to_f, xx: true) #=> false (if doesn't exist)
83
+ #
84
+ # @example Only update if new score is higher (leaderboard)
85
+ # scores.add(player, 1000, gt: true) #=> true (new entry)
86
+ # scores.add(player, 1500, gt: true) #=> false (updated)
87
+ # scores.add(player, 1200, gt: true) #=> false (not updated, score lower)
88
+ #
89
+ # @example Track total changes for analytics
90
+ # changed = metrics.add(user, score, ch: true) #=> true (new or updated)
91
+ #
92
+ # @example Combined options: only update existing, only if score increases
93
+ # index.add(key, new_score, xx: true, gt: true)
94
+ #
95
+ # @note GT and LT options do NOT prevent adding new elements, they only
96
+ # affect update behavior for existing elements.
97
+ #
98
+ # @note Default behavior (no options) adds new elements and updates existing
99
+ # ones unconditionally, matching standard Redis ZADD semantics.
100
+ #
101
+ # @note INCR option is not supported. Use the increment method for ZINCRBY operations.
102
+ #
103
+ def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
58
104
  score ||= Familia.now
59
- ret = dbclient.zadd dbkey, score, serialize_value(val)
105
+
106
+ # Validate mutual exclusivity
107
+ validate_zadd_options!(nx: nx, xx: xx, gt: gt, lt: lt)
108
+
109
+ # Build options hash for redis gem
110
+ opts = {}
111
+ opts[:nx] = true if nx
112
+ opts[:xx] = true if xx
113
+ opts[:gt] = true if gt
114
+ opts[:lt] = true if lt
115
+ opts[:ch] = true if ch
116
+
117
+ # Pass options to ZADD
118
+ ret = if opts.empty?
119
+ dbclient.zadd(dbkey, score, serialize_value(val))
120
+ else
121
+ dbclient.zadd(dbkey, score, serialize_value(val), **opts)
122
+ end
123
+
60
124
  update_expiration
61
125
  ret
62
126
  end
@@ -242,6 +306,45 @@ module Familia
242
306
  at(-1)
243
307
  end
244
308
 
309
+
310
+ private
311
+
312
+ # Validates that mutually exclusive ZADD options are not specified together.
313
+ #
314
+ # @param nx [Boolean] NX option flag
315
+ # @param xx [Boolean] XX option flag
316
+ # @param gt [Boolean] GT option flag
317
+ # @param lt [Boolean] LT option flag
318
+ #
319
+ # @raise [ArgumentError] If mutually exclusive options are specified
320
+ #
321
+ # @note Valid combinations: XX+GT, XX+LT
322
+ # @note Invalid combinations: NX+XX, GT+LT, NX+GT, NX+LT
323
+ #
324
+ def validate_zadd_options!(nx:, xx:, gt:, lt:)
325
+ # NX and XX are mutually exclusive
326
+ if nx && xx
327
+ raise ArgumentError, "ZADD options NX and XX are mutually exclusive"
328
+ end
329
+
330
+ # GT and LT are mutually exclusive
331
+ if gt && lt
332
+ raise ArgumentError, "ZADD options GT and LT are mutually exclusive"
333
+ end
334
+
335
+ # NX is mutually exclusive with GT
336
+ if nx && gt
337
+ raise ArgumentError, "ZADD options NX and GT are mutually exclusive"
338
+ end
339
+
340
+ # NX is mutually exclusive with LT
341
+ if nx && lt
342
+ raise ArgumentError, "ZADD options NX and LT are mutually exclusive"
343
+ end
344
+
345
+ # Note: XX + GT and XX + LT are valid combinations
346
+ end
347
+
245
348
  Familia::DataType.register self, :sorted_set
246
349
  Familia::DataType.register self, :zset
247
350
  end
@@ -114,10 +114,6 @@ module Familia
114
114
  ret.positive?
115
115
  end
116
116
 
117
- def nil?
118
- value.nil?
119
- end
120
-
121
117
  Familia::DataType.register self, :string
122
118
  Familia::DataType.register self, :stringkey
123
119
  end