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
@@ -1,4 +1,4 @@
1
- # lib/middleware/database_middleware.rb
1
+ # lib/middleware/database_logger.rb
2
2
 
3
3
  require 'concurrent-ruby'
4
4
 
@@ -29,21 +29,40 @@ require 'concurrent-ruby'
29
29
  # often outweigh the slight performance cost when enabled.
30
30
  module DatabaseLogger
31
31
  @logger = nil
32
- @commands = []
32
+ @commands = Concurrent::Array.new
33
+ @max_commands = 10_000
34
+ @process_start = Time.now.to_f.freeze
35
+
36
+ CommandMessage = Data.define(:command, :μs, :timeline) do
37
+ alias_method :to_a, :deconstruct
38
+ def inspect
39
+ cmd, duration, timeline = to_a
40
+ format('%.6f %4dμs > %s', timeline, duration, cmd)
41
+ end
42
+ end
33
43
 
34
44
  class << self
35
45
  # Gets/sets the logger instance used by DatabaseLogger.
36
46
  # @return [Logger, nil] The current logger instance or nil if not set.
37
47
  attr_accessor :logger
38
48
 
49
+ # Gets/sets the maximum number of commands to capture.
50
+ # @return [Integer] The maximum number of commands to capture.
51
+ attr_accessor :max_commands
52
+
39
53
  # Gets the captured commands for testing purposes.
40
- # @return [Array] Array of command hashes with :command, :duration, :timestamp
54
+ # @return [Array] Array of command hashes with :command, :duration, :timeline
41
55
  attr_reader :commands
42
56
 
57
+ # Gets the timestamp when DatabaseLogger was loaded.
58
+ # @return [Float] The timestamp when DatabaseLogger was loaded.
59
+ attr_reader :process_start
60
+
43
61
  # Clears the captured commands array.
44
62
  # @return [Array] Empty array
45
63
  def clear_commands
46
- @commands = []
64
+ @commands.clear
65
+ nil
47
66
  end
48
67
 
49
68
  # Captures commands in a block and returns them.
@@ -62,8 +81,34 @@ module DatabaseLogger
62
81
  def capture_commands
63
82
  clear_commands
64
83
  yield
65
- @commands.dup
84
+ @commands.to_a
85
+ end
86
+
87
+ # Gets the current count of Database commands executed.
88
+ # @return [Integer] The number of Database commands executed.
89
+ def index
90
+ @commands.size
91
+ end
92
+
93
+ # Thread-safe append with bounded size
94
+ #
95
+ # @param message [String] The message to append.
96
+ # @return [Array] The updated array of commands.
97
+ def append_command(message)
98
+ @commands.shift if @commands.size >= @max_commands
99
+ @commands << message
100
+ end
101
+
102
+ # Returns the current time in microseconds.
103
+ # This is used to measure the duration of Database commands.
104
+ #
105
+ # Alias: now_in_microseconds
106
+ #
107
+ # @return [Integer] The current time in microseconds.
108
+ def now_in_μs
109
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
66
110
  end
111
+ alias now_in_microseconds now_in_μs
67
112
  end
68
113
 
69
114
  # Logs the Database command and its execution time.
@@ -79,19 +124,65 @@ module DatabaseLogger
79
124
  # @note Commands are always captured with minimal overhead for testing purposes.
80
125
  # Logging only occurs when DatabaseLogger.logger is set.
81
126
  def call(command, _config)
82
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
127
+ block_start = DatabaseLogger.now_in_μs
83
128
  result = yield
84
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
129
+ block_duration = DatabaseLogger.now_in_μs - block_start
130
+
131
+ # We intentionally use two different codepaths for getting the
132
+ # time, although they will almost always be so similar that the
133
+ # difference is negligible.
134
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
85
135
 
86
- # Always capture commands for testing purposes
87
- DatabaseLogger.instance_variable_get(:@commands) << {
88
- command: command.dup,
89
- duration: duration,
90
- timestamp: Time.now,
91
- }
136
+ msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
137
+ DatabaseLogger.append_command(msgpack)
92
138
 
93
139
  # Log if logger is set
94
- DatabaseLogger.logger&.debug("Redis: #{command.inspect} (#{duration}µs)")
140
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
141
+ DatabaseLogger.logger&.trace(message)
142
+
143
+ result
144
+ end
145
+
146
+ # Handle pipelined commands (including MULTI/EXEC transactions)
147
+ #
148
+ # Captures MULTI/EXEC and shows you the full transaction. The WATCH
149
+ # and EXISTS appear separately because they're executed as individual
150
+ # commands before the transaction starts.
151
+ def call_pipelined(commands, _config)
152
+ block_start = DatabaseLogger.now_in_μs
153
+ results = yield
154
+ block_duration = DatabaseLogger.now_in_μs - block_start
155
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
156
+
157
+ # Log the entire pipeline as a single operation
158
+ cmd_string = commands.map { |cmd| cmd.join(' ') }.join(' | ')
159
+ msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
160
+ DatabaseLogger.append_command(msgpack)
161
+
162
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
163
+ DatabaseLogger.logger&.trace(message)
164
+
165
+ results
166
+ end
167
+
168
+ # call_once is used for commands that need dedicated connection handling:
169
+ #
170
+ # * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
171
+ # * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
172
+ # * Commands requiring connection affinity
173
+ # * Explicit non-pooled command execution
174
+ #
175
+ def call_once(command, _config)
176
+ block_start = DatabaseLogger.now_in_μs
177
+ result = yield
178
+ block_duration = DatabaseLogger.now_in_μs - block_start
179
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
180
+
181
+ msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
182
+ DatabaseLogger.append_command(msgpack)
183
+
184
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
185
+ DatabaseLogger.logger&.trace(message)
95
186
 
96
187
  result
97
188
  end
@@ -188,5 +279,18 @@ module DatabaseCommandCounter
188
279
  klass.increment unless klass.skip_command?(command)
189
280
  yield
190
281
  end
282
+
283
+ def call_pipelined(commands, _config)
284
+ # Count all commands in the pipeline (except skipped ones)
285
+ commands.each do |command|
286
+ klass.increment unless klass.skip_command?(command)
287
+ end
288
+ yield
289
+ end
290
+
291
+ def call_once(command, _config)
292
+ klass.increment unless klass.skip_command?(command)
293
+ yield
294
+ end
191
295
  end
192
296
  # rubocop:enable ThreadSafety/ClassInstanceVariable
data/pr_agent.toml ADDED
@@ -0,0 +1,31 @@
1
+ # Qodo Merge Configuration
2
+ # Documentation: https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/
3
+
4
+ [config]
5
+ # Ensure consistent review language across all PRs
6
+ response_language = "en"
7
+
8
+ [rag_arguments]
9
+ # Enable RAG context enrichment for codebase duplication compliance checks
10
+ enable_rag = true
11
+ # Include related repositories for comprehensive context
12
+ rag_repo_list = ['delano/familia', 'delano/tryouts', 'delano/otto']
13
+
14
+ [compliance]
15
+ # Reference custom compliance checklist for project-specific rules
16
+ custom_compliance_path = "pr_compliance_checklist.yaml"
17
+
18
+ [ignore]
19
+ # Reduce noise by excluding generated files and build artifacts
20
+ glob = [
21
+ "*.lock", # Lock files (Gemfile.lock, etc.)
22
+ "*.gem", # Built gem files
23
+ "vendor/**", # Vendored dependencies
24
+ "tmp/**", # Temporary files
25
+ "log/**", # Log files
26
+ "data/**", # Data directories
27
+ "public/**", # Public assets
28
+ ".yardoc/**", # YARD documentation cache
29
+ "dump.rdb", # Redis database dumps
30
+ "appendonlydir/**", # Redis append-only file directory
31
+ ]
@@ -0,0 +1,45 @@
1
+ # Custom Compliance Checklist for Familia
2
+ # Documentation: https://qodo-merge-docs.qodo.ai/tools/compliance/
3
+
4
+ pr_compliances:
5
+ - title: "ErrorHandling"
6
+ compliance_label: true
7
+ objective: "All external API calls and database operations must have proper error handling"
8
+ success_criteria: "Try-catch blocks around external calls with appropriate logging or error handling mechanisms"
9
+ failure_criteria: "External API calls, database operations, or network requests without error handling"
10
+
11
+ - title: "TestCoverage"
12
+ compliance_label: true
13
+ objective: "New features must include corresponding tests using the Tryouts framework"
14
+ success_criteria: "Test files present in try/ directory for new functionality following *_try.rb or *.try.rb naming convention"
15
+ failure_criteria: "New code without test coverage or tests not following Tryouts framework conventions"
16
+
17
+ - title: "ChangelogFragment"
18
+ compliance_label: true
19
+ objective: "User-facing changes must include a changelog"
20
+ success_criteria: "New fragment file in changelog.d/ directory following the naming convention and RST format, or updates to CHANGELOG.rst in root directory, or explicit justification for omission"
21
+ failure_criteria: "User-facing changes without changelog fragment, CHANGELOG.rst updates, or documentation updates"
22
+
23
+ - title: "DocumentationUpdates"
24
+ compliance_label: true
25
+ objective: "API changes must be reflected in documentation"
26
+ success_criteria: "YARD documentation comments for new public methods, or updates to docs/ for significant changes"
27
+ failure_criteria: "New public APIs or significant behavior changes without documentation updates"
28
+
29
+ - title: "BackwardCompatibility"
30
+ compliance_label: true
31
+ objective: "Changes must maintain backward compatibility or document breaking changes"
32
+ success_criteria: "No breaking changes to public APIs, or breaking changes clearly documented in migration guides"
33
+ failure_criteria: "Breaking changes without migration documentation or deprecation warnings"
34
+
35
+ - title: "ThreadSafety"
36
+ compliance_label: true
37
+ objective: "Code handling shared state must be thread-safe"
38
+ success_criteria: "Proper synchronization for shared mutable state, or clear documentation of thread-safety assumptions"
39
+ failure_criteria: "Shared mutable state accessed without synchronization in concurrent contexts"
40
+
41
+ - title: "DatabaseKeyNaming"
42
+ compliance_label: true
43
+ objective: "Database key generation must follow Familia conventions"
44
+ success_criteria: "Keys use delim separator, avoid reserved keywords (ttl, db, valkey, redis), and handle empty identifiers"
45
+ failure_criteria: "Keys using reserved keywords, empty identifiers, or non-standard separators"
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test empty identifier edge cases
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../support/helpers/test_helpers'
6
6
 
7
7
 
8
8
  ## empty string identifier handling
@@ -4,7 +4,7 @@
4
4
  # bug in Tryouts 3.1 that prevents the setup instance vars from
5
5
  # being available to the testcases.
6
6
 
7
- require_relative '../helpers/test_helpers'
7
+ require_relative '../support/helpers/test_helpers'
8
8
 
9
9
  Familia.debug = false
10
10
 
@@ -25,22 +25,31 @@ end
25
25
  @test_hash.keys
26
26
  #=> ["name", "age", "nested"]
27
27
 
28
- ## After save and refresh, default behavior uses symbol keys
28
+ ## After save and refresh, default behavior uses string keys
29
29
  @test_obj.refresh!
30
30
  @test_obj.config.keys
31
- #=> [:name, :age, :nested]
31
+ #=> ["name", "age", "nested"]
32
32
 
33
- ## Nested hash also has symbol keys
34
- @test_obj.config[:nested].keys
35
- #=> [:theme]
33
+ ## Nested hash also has string keys
34
+ @test_obj.config["nested"].keys
35
+ #=> ["theme"]
36
36
 
37
37
  ## Get raw JSON from Valkey/Redis
38
38
  @raw_json = @test_obj.hget('config')
39
39
  @raw_json.class
40
40
  #=> String
41
41
 
42
- ## deserialize_value with default symbolize: true returns symbol keys
43
- @symbol_result = @test_obj.deserialize_value(@raw_json)
42
+ ## deserialize_value with default symbolize: false returns string keys
43
+ @string_result_default = @test_obj.deserialize_value(@raw_json)
44
+ @string_result_default.keys
45
+ #=> ["name", "age", "nested"]
46
+
47
+ ## Nested hash in default result also has string keys
48
+ @string_result_default["nested"].keys
49
+ #=> ["theme"]
50
+
51
+ ## deserialize_value with symbolize: true returns symbol keys
52
+ @symbol_result = @test_obj.deserialize_value(@raw_json, symbolize: true)
44
53
  @symbol_result.keys
45
54
  #=> [:name, :age, :nested]
46
55
 
@@ -48,21 +57,12 @@ end
48
57
  @symbol_result[:nested].keys
49
58
  #=> [:theme]
50
59
 
51
- ## deserialize_value with symbolize: false returns string keys
52
- @string_result = @test_obj.deserialize_value(@raw_json, symbolize: false)
53
- @string_result.keys
54
- #=> ["name", "age", "nested"]
55
-
56
- ## Nested hash in string result also has string keys
57
- @string_result['nested'].keys
58
- #=> ["theme"]
59
-
60
- ## Values are preserved correctly in both cases
60
+ ## Values are preserved correctly with symbol keys
61
61
  @symbol_result[:name]
62
62
  #=> "John"
63
63
 
64
- ## String keys also work correctly
65
- @string_result['name']
64
+ ## Values are preserved correctly with string keys
65
+ @string_result_default['name']
66
66
  #=> "John"
67
67
 
68
68
  ## Arrays are handled correctly too
@@ -71,27 +71,27 @@ end
71
71
  @array_json = @test_obj.hget('config')
72
72
  #=> "[{\"item\":\"value\"},\"string\",123]"
73
73
 
74
+ ## Array with default (symbolize: false) keeps hash keys as strings
75
+ @string_array_default = @test_obj.deserialize_value(@array_json)
76
+ @string_array_default[0].keys
77
+ #=> ["item"]
78
+
74
79
  ## Array with symbolize: true converts hash keys to symbols
75
- @symbol_array = @test_obj.deserialize_value(@array_json)
80
+ @symbol_array = @test_obj.deserialize_value(@array_json, symbolize: true)
76
81
  @symbol_array[0].keys
77
82
  #=> [:item]
78
83
 
79
- ## Array with symbolize: false keeps hash keys as strings
80
- @string_array = @test_obj.deserialize_value(@array_json, symbolize: false)
81
- @string_array[0].keys
82
- #=> ["item"]
83
-
84
- ## Non-hash/array values are returned as-is
84
+ ## JSON-encoded string is parsed correctly
85
85
  @test_obj.deserialize_value('"just a string"')
86
- #=> "\"just a string\""
86
+ #=> "just a string"
87
87
 
88
- ## Non-hash/array values are returned as-is
88
+ ## Non-JSON string returns as-is
89
89
  @test_obj.deserialize_value('just a string')
90
90
  #=> "just a string"
91
91
 
92
- ## A stringified number is still a stringified number
92
+ ## JSON number is parsed to Integer
93
93
  @test_obj.deserialize_value('42')
94
- #=> "42"
94
+ #=> 42
95
95
 
96
96
  ## Invalid JSON returns original string
97
97
  @test_obj.deserialize_value('invalid json')
@@ -1,6 +1,6 @@
1
1
  # try/edge_cases/json_serialization_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -44,7 +44,7 @@ test_obj.simple = 'just a string'
44
44
  test_obj.tags = %w[ruby valkey json familia]
45
45
  test_obj.save
46
46
  test_obj.hgetall
47
- #=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
47
+ #=> {"id"=>"\"json_test_1\"", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"\"just a string\""}
48
48
 
49
49
  ## Test 4: Hash should be deserialized back to Hash
50
50
  test_obj = JsonTest.new 'any_id_will_do'
@@ -0,0 +1,170 @@
1
+ # Edge case tests for deserialize_value with legacy data detection
2
+ #
3
+ # Tests the nuanced deserialization that distinguishes between:
4
+ # - Corrupted JSON (data that looks like JSON but fails to parse)
5
+ # - Legacy plain strings (data that was never JSON)
6
+ # - Valid JSON data
7
+
8
+ require_relative '../../../lib/familia'
9
+ require 'logger'
10
+ require 'stringio'
11
+
12
+ # Capture log output for verification
13
+ @log_output = StringIO.new
14
+ @original_logger = Familia.instance_variable_get(:@logger)
15
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
16
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
17
+
18
+ class TestModel < Familia::Horreum
19
+ identifier_field :test_id
20
+ field :test_id
21
+ field :data
22
+ end
23
+
24
+ @model = TestModel.new(test_id: "test1")
25
+
26
+ ## Valid JSON number deserializes correctly
27
+ @result = @model.deserialize_value("123", field_name: :data)
28
+ @result
29
+ #=> 123
30
+
31
+ ## Valid JSON boolean deserializes correctly
32
+ @result = @model.deserialize_value("true", field_name: :data)
33
+ @result
34
+ #=> true
35
+
36
+ ## Valid JSON string deserializes correctly
37
+ @result = @model.deserialize_value('"hello"', field_name: :data)
38
+ @result
39
+ #=> "hello"
40
+
41
+ ## Valid JSON array deserializes correctly
42
+ @result = @model.deserialize_value('[1,2,3]', field_name: :data)
43
+ @result
44
+ #=> [1, 2, 3]
45
+
46
+ ## Valid JSON object deserializes correctly
47
+ @result = @model.deserialize_value('{"key":"value"}', field_name: :data)
48
+ @result
49
+ #=> {"key"=>"value"}
50
+
51
+ ## Plain string (legacy data) returns as-is
52
+ @log_output = StringIO.new
53
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
54
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
55
+ @result = @model.deserialize_value("plain text", field_name: :data)
56
+ @result
57
+ #=> "plain text"
58
+
59
+ ## Legacy data logs at debug level
60
+ @log_output.rewind
61
+ @log_content = @log_output.read
62
+ puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
63
+ @log_content
64
+ #=~> /Legacy plain string/
65
+
66
+ ## Corrupted JSON starting with { logs error
67
+ @log_output = StringIO.new
68
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
69
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
70
+ @result = @model.deserialize_value("{broken", field_name: :data)
71
+ @result
72
+ #=> "{broken"
73
+
74
+ ## Corrupted JSON logs at error level
75
+ @log_output.rewind
76
+ @log_content = @log_output.read
77
+ puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
78
+ @log_content.match?(/Corrupted JSON/)
79
+ #=> true
80
+
81
+ ## Corrupted JSON starting with [ logs error
82
+ @log_output = StringIO.new
83
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
84
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
85
+ @result = @model.deserialize_value("[1,2,", field_name: :data)
86
+ @result
87
+ #=> "[1,2,"
88
+
89
+ ## Corrupted array logs at error level
90
+ @log_output.rewind
91
+ @log_content = @log_output.read
92
+ @log_content.match?(/Corrupted JSON/)
93
+ #=> true
94
+
95
+ ## Corrupted JSON starting with quote logs error
96
+ @log_output = StringIO.new
97
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
98
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
99
+ @result = @model.deserialize_value('"unterminated', field_name: :data)
100
+ @result
101
+ #=> '"unterminated'
102
+
103
+ ## Unterminated string logs at error level
104
+ @log_output.rewind
105
+ @log_content = @log_output.read
106
+ @log_content.match?(/Corrupted JSON/)
107
+ #=> true
108
+
109
+ ## Corrupted boolean-like value logs error
110
+ @log_output = StringIO.new
111
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
112
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
113
+ @result = @model.deserialize_value("true123", field_name: :data)
114
+ @result
115
+ #=> "true123"
116
+
117
+ ## Plain text starting with 'true' is legacy data
118
+ @log_output.rewind
119
+ @log_content = @log_output.read
120
+ @log_content
121
+ #=~> /Legacy plain string/
122
+
123
+ ## Field name context appears in error messages
124
+ @log_output = StringIO.new
125
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
126
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
127
+ @result = @model.deserialize_value("{broken", field_name: :important_field)
128
+ @log_output.rewind
129
+ @log_content = @log_output.read
130
+ @log_content.match?(/TestModel#important_field/)
131
+ #=> true
132
+
133
+ ## dbkey context appears in error messages when available
134
+ @model.save
135
+ @log_output = StringIO.new
136
+ Familia.instance_variable_set(:@logger, Logger.new(@log_output))
137
+ Familia.instance_variable_get(:@logger).level = Logger::DEBUG
138
+ @result = @model.deserialize_value("{broken", field_name: :data)
139
+ @log_output.rewind
140
+ @log_content = @log_output.read
141
+ @log_content.match?(/#{Regexp.escape(@model.dbkey)}/)
142
+ #=> true
143
+
144
+ ## Empty string returns nil
145
+ @result = @model.deserialize_value("", field_name: :data)
146
+ @result
147
+ #=> nil
148
+
149
+ ## nil returns nil
150
+ @result = @model.deserialize_value(nil, field_name: :data)
151
+ @result
152
+ #=> nil
153
+
154
+ ## JSON null deserializes to nil
155
+ @result = @model.deserialize_value("null", field_name: :data)
156
+ @result
157
+ #=> nil
158
+
159
+ ## Symbolize option works with hash keys
160
+ @result = @model.deserialize_value('{"name":"test"}', symbolize: true, field_name: :data)
161
+ @result.keys.first.class
162
+ #=> Symbol
163
+
164
+ ## Default keeps string keys
165
+ @result = @model.deserialize_value('{"name":"test"}', field_name: :data)
166
+ @result.keys.first.class
167
+ #=> String
168
+
169
+ # Teardown
170
+ Familia.instance_variable_set(:@logger, @original_logger)
@@ -1,6 +1,6 @@
1
1
  # Test connection race conditions
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## concurrent connection access test
6
6
  user_class = Class.new(Familia::Horreum) do
@@ -1,6 +1,6 @@
1
1
  # Test reserved keyword handling
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## attempting to use ttl as field name causes error
6
6
  TestClass = Class.new(Familia::Horreum) do
@@ -1,6 +1,6 @@
1
1
  # try/edge_cases/string_coercion_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -130,15 +130,15 @@ process_identifier(@customer)
130
130
 
131
131
  ## Cleanup after test, 1
132
132
  @metadata.delete!
133
- #=> true
133
+ #=> 1
134
134
 
135
135
  ## Cleanup after test, 2
136
136
  @customer.delete!
137
- #=> true
137
+ #=> 1
138
138
 
139
139
  ## Cleanup after test, 3
140
140
  @session.delete!
141
- #=> true
141
+ #=> 1
142
142
 
143
143
  ## to_s handles identifier errors gracefully
144
144
  badboi = BadIdentifierTest.new
@@ -154,4 +154,4 @@ badboi.to_s # .include?('BadIdentifierTest')
154
154
 
155
155
  ## Delete customer2
156
156
  [@customer2.exists?, @customer2.delete!]
157
- #=> [false, false]
157
+ #=> [false, 0]
@@ -1,6 +1,6 @@
1
1
  # Test TTL side effects
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## field update behavior with TTL
6
6
  begin
@@ -3,7 +3,7 @@
3
3
  require 'concurrent'
4
4
  require 'base64'
5
5
 
6
- require_relative '../../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  test_keys = {
9
9
  v1: Base64.strict_encode64('a' * 32),
@@ -1,6 +1,6 @@
1
1
  # try/features/encryption_fields/concealed_string_core_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
  Familia.debug = false
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  # Setup encryption keys for testing
8
8
  test_keys = {
@@ -1,6 +1,6 @@
1
1
  # try/features/encrypted_fields_core_try.rb
2
2
 
3
- require_relative '../../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
  require 'base64'
5
5
 
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test constants will be redefined in each test since variables don't persist
4
4
 
5
- require_relative '../../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
  require 'base64'
7
7
 
8
8