familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/single_connection_transaction_confusions.rb
3
+
4
+ # Redis Single Connection Mode Confusions
5
+ #
6
+ # This file demonstrates why mixing Redis operation modes on a single connection
7
+ # causes subtle but critical failures in production applications.
8
+ #
9
+ # Key Concepts:
10
+ # - Normal mode: Commands execute immediately, return actual values
11
+ # - MULTI mode: Commands return "QUEUED", execute atomically on EXEC
12
+ # - Pipeline mode: Commands return Futures, execute in batch
13
+ #
14
+ # Production Impact:
15
+ # - Conditional logic breaks when expecting values but getting "QUEUED"
16
+ # - Business rules fail silently when code assumes immediate execution
17
+ # - Nested operations cause Redis protocol errors
18
+ # - Race conditions emerge from incorrect mode assumptions
19
+ #
20
+ # Key Insight: Connection source should determine which operations are
21
+ # allowed. This is how we prevent bugs like expecting values but getting "QUEUED".
22
+ #
23
+ # Summary of Behaviors:
24
+ #
25
+ # | Handler | Transaction | Pipeline | Ad-hoc Commands |
26
+ # |---------|------------|----------|-----------------|
27
+ # | **FiberTransaction** | Reentrant (same conn) | Error | Use transaction conn |
28
+ # | **FiberConnection** | Error | Error | ✓ Allowed |
29
+ # | **Provider** | ✓ New checkout | ✓ New checkout | ✓ New checkout |
30
+ # | **Default** | ✓ With guards | ✓ With guards | ✓ Check mode |
31
+ # | **Create** | ✓ Fresh conn | ✓ Fresh conn | ✓ Fresh conn |
32
+ #
33
+ #
34
+ # Usage:
35
+ #
36
+ # $ irb
37
+ # load './single_connection_transaction_confusions.rb'
38
+ # demo1_multi_queues_commands
39
+ # demo2_nested_multi_fails
40
+ # ...
41
+
42
+ require 'redis'
43
+
44
+ # Global Redis connection for demonstrations
45
+ $redis = Redis.new(host: 'localhost', port: 6379)
46
+
47
+ # Service class for demonstration 5
48
+ class OrderService
49
+ def initialize(redis_conn)
50
+ @redis = redis_conn
51
+ end
52
+
53
+ # This method assumes immediate command execution
54
+ def process_order(order_id)
55
+ # Step 1: Set processing status
56
+ @redis.set("order:#{order_id}:status", 'processing')
57
+
58
+ # Step 2: Verify status before proceeding (CRITICAL CHECK)
59
+ status = @redis.get("order:#{order_id}:status")
60
+
61
+ # Step 3: Conditional business logic
62
+ if status == 'processing'
63
+ @redis.set("order:#{order_id}:confirmed", 'true')
64
+ puts "✅ Order #{order_id}: Status confirmed, order processed"
65
+ else
66
+ puts "❌ Order #{order_id}: Status check failed - got #{status.inspect}"
67
+ # In production: would trigger error handling, alerts, etc.
68
+ end
69
+ end
70
+ end
71
+
72
+ def setup_redis_demo
73
+ # Sets up clean Redis state for demonstrations
74
+ $redis.flushdb
75
+ $redis.set('counter', '10')
76
+ $redis.set('name', 'Alice')
77
+ $redis.set('score', '50')
78
+ $redis.set('value1', '100')
79
+
80
+ puts '=== Redis Demo Environment Ready ==='
81
+ puts "counter: #{$redis.get('counter')}"
82
+ puts "name: #{$redis.get('name')}"
83
+ puts "score: #{$redis.get('score')}"
84
+ puts
85
+ end
86
+
87
+ def demo1_multi_queues_commands
88
+ # CRITICAL: Commands inside MULTI don't execute immediately
89
+ #
90
+ # Problem: Business logic expecting immediate results gets "QUEUED" instead
91
+ # Impact: Conditional statements, calculations, and validations all break
92
+
93
+ puts '=== Demo 1: Commands inside MULTI return "QUEUED", not values ==='
94
+
95
+ $redis.multi do |conn|
96
+ puts 'Inside MULTI block - all commands are queued...'
97
+
98
+ # These increment counter but return "QUEUED"
99
+ conn.incr('counter')
100
+ result = conn.get('counter')
101
+
102
+ puts "❌ GET inside MULTI returns: #{result.inspect} (expected: '11')"
103
+ puts "❌ Direct redis.get still shows: #{$redis.get('counter')} (expected: '11')"
104
+
105
+ # This condition will ALWAYS be false during MULTI
106
+ if result == '11'
107
+ puts '✅ Counter validation passed'
108
+ else
109
+ puts "⚠️ Counter validation failed - got #{result.inspect}, not '11'"
110
+ end
111
+
112
+ conn.set('name', 'Bob')
113
+ end
114
+
115
+ puts "\n📊 After MULTI/EXEC completes:"
116
+ puts "counter: #{$redis.get('counter')} (now executed)"
117
+ puts "name: #{$redis.get('name')} (now executed)"
118
+ puts "\n💡 Lesson: Never use command results inside MULTI for logic\n\n"
119
+ end
120
+
121
+ def demo2_nested_multi_fails
122
+ # CRITICAL: Redis protocol forbids nested MULTI commands
123
+ #
124
+ # Problem: Code reuse becomes impossible when methods assume fresh connections
125
+ # Impact: Shared utilities break when called from within transactions
126
+
127
+ puts '=== Demo 2: Nested MULTI causes Redis protocol errors ==='
128
+
129
+ begin
130
+ $redis.multi do |outer_conn|
131
+ puts 'In outer MULTI transaction...'
132
+ outer_conn.incr('counter')
133
+
134
+ # This breaks the Redis protocol - MULTI calls cannot be nested
135
+ $redis.multi do |inner_conn|
136
+ puts 'Attempting inner MULTI...'
137
+ inner_conn.incr('counter')
138
+ end
139
+ end
140
+ rescue Redis::CommandError => e
141
+ puts "❌ Redis Error: #{e.message}"
142
+ puts '💡 This makes shared connection patterns dangerous'
143
+ end
144
+
145
+ puts "📊 Counter after error: #{$redis.get('counter')} (partial execution)\n\n"
146
+ end
147
+
148
+ def demo3_pipeline_multi_confusion
149
+ # COMPLEX: Pipeline and MULTI modes create execution order confusion
150
+ #
151
+ # Problem: Commands execute in unexpected batches with mixed return types
152
+ # Impact: Debugging becomes nearly impossible with interleaved operations
153
+
154
+ puts '=== Demo 3: Pipeline + MULTI creates execution chaos ==='
155
+
156
+ $redis.set('counter', '20')
157
+
158
+ results = $redis.pipelined do |pipeline|
159
+ # Returns Future object, not value
160
+ pipeline.get('counter')
161
+
162
+ # MULTI/EXEC block executes inside pipeline
163
+ pipeline.multi do |multi|
164
+ multi.incr('counter') # Will be 21
165
+ multi.incr('counter') # Will be 22
166
+ end
167
+
168
+ # This sees the final value after MULTI completes
169
+ pipeline.get('counter')
170
+ end
171
+
172
+ puts "📊 Pipeline results: #{results.inspect}"
173
+ puts '🔍 Analysis:'
174
+ puts " - results[0]: Initial value ('20')"
175
+ puts " - results[1]: MULTI/EXEC result (['21', '22'])"
176
+ puts " - results[2]: Final value ('22')"
177
+ puts "\n💡 Mixed return types make result processing error-prone\n\n"
178
+ end
179
+
180
+ def demo4_interrupted_multi_cleanup
181
+ # RECOVERY: Redis auto-DISCARD on exceptions, but application state unclear
182
+ #
183
+ # Problem: Partial transaction state is invisible to application code
184
+ # Impact: Error recovery logic may make incorrect assumptions about data state
185
+
186
+ puts '=== Demo 4: Exception handling in MULTI transactions ==='
187
+
188
+ $redis.set('value1', '100')
189
+ original_value = $redis.get('value1')
190
+
191
+ begin
192
+ $redis.multi do |conn|
193
+ conn.incr('value1') # Queued but not executed
194
+
195
+ # Simulate business logic error
196
+ raise StandardError, 'Payment validation failed'
197
+
198
+ conn.incr('value1') # Never reached
199
+ end
200
+ rescue StandardError => e
201
+ puts "🔥 Exception caught: #{e.message}"
202
+
203
+ # Redis automatically DISCARD on exception
204
+ current_value = $redis.get('value1')
205
+ puts "📊 Value after exception: #{current_value} (#{current_value == original_value ? 'unchanged ✅' : 'modified ❌'})"
206
+ end
207
+
208
+ puts "💡 Redis auto-DISCARD protects consistency but masks partial state\n\n"
209
+ end
210
+
211
+ def demo5_shared_connection_logic_failure
212
+ # PRODUCTION BUG: Business logic fails silently with shared connections
213
+ #
214
+ # Problem: Service classes can't detect when they're called inside transactions
215
+ # Impact: Critical business rules execute incorrectly without visible errors
216
+
217
+ puts '=== Demo 5: Shared connection breaks business logic ==='
218
+
219
+ service = OrderService.new($redis)
220
+
221
+ puts '📈 Normal operation:'
222
+ service.process_order(1)
223
+ puts " Result: #{$redis.get('order:1:confirmed')}"
224
+
225
+ puts "\n🔄 Inside MULTI transaction:"
226
+ $redis.multi do |conn|
227
+ # Same service class, but now connection is in MULTI mode
228
+ transaction_service = OrderService.new(conn)
229
+ transaction_service.process_order(2) # Logic fails silently
230
+ end
231
+ puts " Result: #{$redis.get('order:2:confirmed')} (business logic bypassed!)"
232
+
233
+ puts "\n💡 Service classes need connection mode awareness for safety\n\n"
234
+ end
235
+
236
+ def demo6_pipeline_future_confusion
237
+ # TIMING: Pipeline returns Future objects that don't behave like values
238
+ #
239
+ # Problem: Conditional logic using Future objects produces unexpected results
240
+ # Impact: Business rules execute incorrectly based on Future truthiness
241
+
242
+ puts '=== Demo 6: Pipeline Futures break conditional logic ==='
243
+
244
+ $redis.set('score', '30')
245
+
246
+ puts '❌ Broken approach - using Future in conditional:'
247
+ begin
248
+ $redis.pipelined do |pipeline|
249
+ current_score = pipeline.get('score') # Returns Redis::Future
250
+
251
+ puts " Future object: #{current_score.class}"
252
+ puts " Future value (immediate): #{current_score.inspect}"
253
+
254
+ # This will cause an error - Future doesn't have to_i method
255
+ if current_score.to_i > 40
256
+ pipeline.set('achievement', 'high_score')
257
+ puts ' ❌ Achievement awarded (incorrect - score is 30!)'
258
+ end
259
+ end
260
+ rescue NoMethodError => e
261
+ puts " ❌ Error: #{e.message}"
262
+ puts " 💡 Future objects don't behave like values!"
263
+ end
264
+
265
+ puts "\n✅ Correct approach - wait for pipeline completion:"
266
+ results = $redis.pipelined do |pipeline|
267
+ pipeline.get('score')
268
+ pipeline.exists('achievement')
269
+ end
270
+
271
+ current_score = results[0].to_i
272
+ has_achievement = results[1]
273
+
274
+ puts " Actual score: #{current_score}"
275
+ puts " Has achievement: #{has_achievement} (incorrect from above)"
276
+
277
+ puts "\n💡 Always extract values from pipeline results before logic\n\n"
278
+ end
279
+
280
+ def demo7_mode_switching_catastrophe
281
+ # CATASTROPHIC: Methods that switch connection modes mid-operation
282
+ #
283
+ # Problem: Utility functions work in normal mode but break in MULTI mode
284
+ # Impact: Subtle bugs that only appear when code paths intersect
285
+
286
+ puts '=== Demo 7: Mode switching breaks utility functions ==='
287
+
288
+ # Utility function that assumes immediate execution
289
+ def update_stats(redis_conn, key, increment = 10)
290
+ # Read current value
291
+ current = redis_conn.get(key).to_i
292
+ puts " 📖 Read current value: #{current}"
293
+
294
+ # Business logic
295
+ new_value = current + increment
296
+ puts " 🧮 Calculated new value: #{new_value}"
297
+
298
+ # Write new value
299
+ redis_conn.set(key, new_value)
300
+
301
+ # Verification read (common pattern for critical updates)
302
+ verified = redis_conn.get(key)
303
+ puts " ✅ Verification read: #{verified}"
304
+
305
+ verified
306
+ rescue NoMethodError => e
307
+ puts " ❌ Error: #{e.message}"
308
+ puts ' 💡 Function expects immediate values but got Future objects!'
309
+ "ERROR: #{e.message}"
310
+ end
311
+
312
+ $redis.set('stats', '5')
313
+
314
+ puts '📈 Normal mode - utility function works:'
315
+ result = update_stats($redis, 'stats')
316
+ puts " Final result: #{result} ✅"
317
+
318
+ puts "\n🔄 MULTI mode - same function breaks:"
319
+ $redis.set('stats', '5') # Reset
320
+
321
+ $redis.multi do |conn|
322
+ result = update_stats(conn, 'stats')
323
+ puts " Final result: #{result.inspect} ❌ (expected: '15')"
324
+ end
325
+
326
+ # Check what actually happened
327
+ actual_value = $redis.get('stats')
328
+ puts " Actual final value: #{actual_value}"
329
+
330
+ puts "\n💡 Utility functions need explicit mode handling or connection isolation\n\n"
331
+ end
332
+
333
+ def summary_and_solutions
334
+ # Summary of problems and production-ready solutions
335
+
336
+ puts '=== Summary: Redis Connection Mode Problems ==='
337
+ puts
338
+ puts '🔴 PROBLEMS:'
339
+ puts ' 1. MULTI mode returns "QUEUED" instead of values'
340
+ puts ' 2. Pipeline mode returns Futures instead of values'
341
+ puts ' 3. Nested MULTI operations cause protocol errors'
342
+ puts ' 4. Business logic fails silently with wrong assumptions'
343
+ puts ' 5. Conditional statements break with mode confusion'
344
+ puts ' 6. Error recovery becomes unpredictable'
345
+ puts
346
+ puts '🟢 SOLUTIONS:'
347
+ puts ' 1. Connection pooling - fresh connection per operation type'
348
+ puts ' 2. Mode-aware service classes with explicit connection requirements'
349
+ puts ' 3. Never share connections between normal and transactional code'
350
+ puts ' 4. Use connection decorators that enforce mode contracts'
351
+ puts ' 5. Implement connection mode detection and validation'
352
+ puts ' 6. Design APIs that make mode requirements explicit'
353
+ puts
354
+ puts '📚 PRODUCTION PATTERNS:'
355
+ puts ' - Repository pattern with mode-specific connections'
356
+ puts ' - Command/Query separation with dedicated connection pools'
357
+ puts ' - Transaction boundaries clearly defined at service layer'
358
+ puts ' - Connection mode validation in development/testing'
359
+ puts
360
+ end
361
+
362
+ # Main execution - run all demonstrations
363
+ if __FILE__ == $PROGRAM_NAME
364
+ setup_redis_demo
365
+ demo1_multi_queues_commands
366
+ demo2_nested_multi_fails
367
+ demo3_pipeline_multi_confusion
368
+ demo4_interrupted_multi_cleanup
369
+ demo5_shared_connection_logic_failure
370
+ demo6_pipeline_future_confusion
371
+ demo7_mode_switching_catastrophe
372
+ summary_and_solutions
373
+
374
+ puts '🎯 Demo complete! Load in IRB to run individual methods:'
375
+ puts " load './single_connection_transaction_confusions.rb'"
376
+ puts ' demo1_multi_queues_commands'
377
+ puts ' demo2_nested_multi_fails'
378
+ puts ' # ... etc'
379
+ end
data/lib/familia/base.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # lib/familia/base.rb
2
2
 
3
- #
4
3
  module Familia
5
4
  # A common module for Familia::DataType and Familia::Horreum to include.
6
5
  #
@@ -14,7 +13,6 @@ module Familia
14
13
  # @see Familia::DataType
15
14
  #
16
15
  module Base
17
-
18
16
  using Familia::Refinements::TimeLiterals
19
17
 
20
18
  @features_available = nil
@@ -33,14 +31,15 @@ module Familia
33
31
  attr_reader :features_available, :feature_definitions
34
32
  attr_accessor :dump_method, :load_method
35
33
 
36
- def add_feature(klass, feature_name, depends_on: [])
34
+ def add_feature(klass, feature_name, depends_on: [], field_group: nil)
37
35
  @features_available ||= {}
38
- Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
36
+ Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
39
37
 
40
38
  # Create field definition object
41
39
  feature_def = FeatureDefinition.new(
42
40
  name: feature_name,
43
41
  depends_on: depends_on,
42
+ field_group: field_group
44
43
  )
45
44
 
46
45
  # Track field definitions after defining field methods
@@ -66,19 +65,63 @@ module Familia
66
65
  "#<#{self.class}:0x#{object_id.to_s(16)}>"
67
66
  end
68
67
 
68
+ # Prepares the object for JSON serialization by converting it to a hash.
69
+ # This method provides the data preparation step in the standard Ruby JSON
70
+ # pattern: to_json → as_json → JSON serialization.
71
+ #
72
+ # Implementing classes can override this method to customize their JSON
73
+ # representation. For Horreum objects, this delegates to to_h which returns
74
+ # only the public fields. For DataType objects, this returns the raw value.
75
+ #
76
+ # @param options [Hash] Optional parameters for customizing JSON output
77
+ # @return [Hash, Object] JSON-serializable representation of the object
78
+ #
79
+ def as_json(options = nil)
80
+ if respond_to?(:to_h)
81
+ # Horreum objects - return their field hash
82
+ to_h
83
+ elsif respond_to?(:members)
84
+ # DataType objects (List, Set, etc.) - return their members
85
+ members
86
+ elsif respond_to?(:value)
87
+ # String-like objects or simple values
88
+ value
89
+ else
90
+ # Fallback for objects that don't have standard value methods
91
+ # This ensures we don't expose internal state accidentally
92
+ { class: self.class.name, id: respond_to?(:identifier) ? identifier : object_id }
93
+ end
94
+ end
95
+
96
+ # Converts the object to a JSON string using Familia's JsonSerializer.
97
+ # This method completes the standard Ruby JSON pattern by calling as_json
98
+ # to prepare the data, then using JsonSerializer.dump for serialization.
99
+ #
100
+ # This maintains security by ensuring all JSON serialization goes through
101
+ # Familia's controlled JsonSerializer (OJ in strict mode) rather than
102
+ # potentially unsafe serialization methods.
103
+ #
104
+ # @param options [Hash] Optional parameters passed to as_json
105
+ # @return [String] JSON string representation of the object
106
+ #
107
+ def to_json(options = nil)
108
+ Familia::JsonSerializer.dump(as_json(options))
109
+ end
110
+
69
111
  # Module-level methods for Familia::Base itself
70
112
  class << self
71
113
  attr_reader :features_available, :feature_definitions
72
114
  attr_accessor :dump_method, :load_method
73
115
 
74
- def add_feature(klass, feature_name, depends_on: [])
116
+ def add_feature(klass, feature_name, depends_on: [], field_group: nil)
75
117
  @features_available ||= {}
76
- Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
118
+ Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
77
119
 
78
120
  # Create field definition object
79
121
  feature_def = FeatureDefinition.new(
80
122
  name: feature_name,
81
123
  depends_on: depends_on,
124
+ field_group: field_group
82
125
  )
83
126
 
84
127
  # Track field definitions after defining field methods
@@ -99,9 +142,7 @@ module Familia
99
142
  next unless ancestor.respond_to?(:features_available)
100
143
  next unless ancestor.features_available
101
144
 
102
- if ancestor.features_available.key?(feature_name)
103
- return ancestor.features_available[feature_name]
104
- end
145
+ return ancestor.features_available[feature_name] if ancestor.features_available.key?(feature_name)
105
146
  end
106
147
 
107
148
  nil
@@ -109,7 +150,7 @@ module Familia
109
150
  end
110
151
 
111
152
  def generate_id
112
- @identifier ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
153
+ @identifier ||= Familia.generate_id
113
154
  end
114
155
 
115
156
  def uuid