familia 2.0.0.pre15 → 2.0.0.pre16

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,277 @@
1
+ # lib/familia/connection/operations.rb
2
+
3
+ # Familia
4
+ #
5
+ # A family warehouse for your keystore data.
6
+ #
7
+ module Familia
8
+ module Connection
9
+ module Operations
10
+ # Executes Database commands atomically within a transaction (MULTI/EXEC).
11
+ #
12
+ # Database transactions queue commands and execute them atomically as a single unit.
13
+ # All commands succeed together or all fail together, ensuring data consistency.
14
+ #
15
+ # @yield [Redis] The Database transaction connection
16
+ # @return [Array] Results of all commands executed in the transaction
17
+ #
18
+ # @example Basic transaction usage
19
+ # Familia.transaction do |trans|
20
+ # trans.set("key1", "value1")
21
+ # trans.incr("counter")
22
+ # trans.lpush("list", "item")
23
+ # end
24
+ # # Returns: ["OK", 2, 1] - results of all commands
25
+ #
26
+ # @note **Comparison of Database batch operations:**
27
+ #
28
+ # | Feature | Multi/Exec | Pipeline |
29
+ # |-----------------|-----------------|-----------------|
30
+ # | Atomicity | Yes | No |
31
+ # | Performance | Good | Better |
32
+ # | Error handling | All-or-nothing | Per-command |
33
+ # | Use case | Data consistency| Bulk operations |
34
+ #
35
+ # Executes a Redis transaction (MULTI/EXEC) with proper connection handling.
36
+ #
37
+ # Provides atomic execution of multiple Redis commands with automatic connection
38
+ # management and operation mode enforcement. Returns a MultiResult object containing
39
+ # both success status and command results.
40
+ #
41
+ # @param [Proc] block The block containing Redis commands to execute atomically
42
+ # @yield [Redis] conn The Redis connection configured for transaction mode
43
+ # @return [MultiResult] Result object with success status and command results
44
+ #
45
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
46
+ # (e.g., FiberConnectionHandler or DefaultConnectionHandler that don't support transactions)
47
+ #
48
+ # @example Basic transaction usage
49
+ # result = Familia.transaction do |conn|
50
+ # conn.set('key1', 'value1')
51
+ # conn.set('key2', 'value2')
52
+ # conn.get('key1')
53
+ # end
54
+ # result.successful? # => true (if all commands succeeded)
55
+ # result.results # => ["OK", "OK", "value1"]
56
+ # result.results.first # => "OK"
57
+ #
58
+ # @example Checking transaction success
59
+ # result = Familia.transaction do |conn|
60
+ # conn.incr('counter')
61
+ # conn.decr('other_counter')
62
+ # end
63
+ #
64
+ # if result.successful?
65
+ # puts "All commands succeeded: #{result.results}"
66
+ # else
67
+ # puts "Some commands failed: #{result.results}"
68
+ # end
69
+ #
70
+ # @example Nested transactions (reentrant behavior)
71
+ # result = Familia.transaction do |outer_conn|
72
+ # outer_conn.set('outer', 'value')
73
+ #
74
+ # # Nested transaction reuses the same connection
75
+ # inner_result = Familia.transaction do |inner_conn|
76
+ # inner_conn.set('inner', 'value')
77
+ # inner_conn.get('inner') # Returns the value directly in nested context
78
+ # end
79
+ #
80
+ # [outer_result, inner_result]
81
+ # end
82
+ #
83
+ # @note Connection Handler Compatibility:
84
+ # - FiberTransactionHandler: Supports reentrant transactions
85
+ # - ProviderConnectionHandler: Full transaction support
86
+ # - CreateConnectionHandler: Full transaction support
87
+ # - FiberConnectionHandler: Blocked (raises OperationModeError)
88
+ # - DefaultConnectionHandler: Blocked (raises OperationModeError)
89
+ #
90
+ # @note Thread Safety:
91
+ # Uses Fiber-local storage to maintain transaction context across nested calls
92
+ # and ensure proper cleanup even when exceptions occur.
93
+ #
94
+ # @see MultiResult For details on the return value structure
95
+ # @see Familia.pipelined For non-atomic command batching
96
+ # @see #batch_update For similar MultiResult pattern in Horreum models
97
+ def transaction(&)
98
+ Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
99
+ end
100
+ alias multi transaction
101
+
102
+ # Executes Database commands in a pipeline for improved performance.
103
+ #
104
+ # Pipelines send multiple commands without waiting for individual responses,
105
+ # reducing network round-trips. Commands execute independently and can
106
+ # succeed or fail without affecting other commands in the pipeline.
107
+ #
108
+ # @yield [Redis] The Database pipeline connection
109
+ # @return [Array] Results of all commands executed in the pipeline
110
+ #
111
+ # @example Basic pipeline usage
112
+ # Familia.pipelined do |pipe|
113
+ # pipe.set("key1", "value1")
114
+ # pipe.incr("counter")
115
+ # pipe.lpush("list", "item")
116
+ # end
117
+ # # Returns: ["OK", 2, 1] - results of all commands
118
+ #
119
+ # @example Error handling - commands succeed/fail independently
120
+ # results = Familia.pipelined do |conn|
121
+ # conn.set("valid_key", "value") # This will succeed
122
+ # conn.incr("string_key") # This will fail (wrong type)
123
+ # conn.set("another_key", "value2") # This will still succeed
124
+ # end
125
+ # # Returns: ["OK", Redis::CommandError, "OK"]
126
+ # # Notice how the error doesn't prevent other commands from executing
127
+ #
128
+ # @example Contrast with transaction behavior
129
+ # results = Familia.transaction do |conn|
130
+ # conn.set("inventory:item1", 100)
131
+ # conn.incr("invalid_key") # Fails, rolls back everything
132
+ # conn.set("inventory:item2", 200) # Won't be applied
133
+ # end
134
+ # # Result: neither item1 nor item2 are set due to the error
135
+ #
136
+ # Executes Redis commands in a pipeline for improved performance.
137
+ #
138
+ # Batches multiple Redis commands together and sends them in a single network
139
+ # round-trip, improving performance for multiple independent operations. Returns
140
+ # a MultiResult object containing both success status and command results.
141
+ #
142
+ # @param [Proc] block The block containing Redis commands to execute in pipeline
143
+ # @yield [Redis] conn The Redis connection configured for pipelined mode
144
+ # @return [MultiResult] Result object with success status and command results
145
+ #
146
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
147
+ # (e.g., FiberConnectionHandler or DefaultConnectionHandler that don't support pipelines)
148
+ #
149
+ # @example Basic pipeline usage
150
+ # result = Familia.pipelined do |conn|
151
+ # conn.set('key1', 'value1')
152
+ # conn.set('key2', 'value2')
153
+ # conn.get('key1')
154
+ # conn.incr('counter')
155
+ # end
156
+ # result.successful? # => true (if all commands succeeded)
157
+ # result.results # => ["OK", "OK", "value1", 1]
158
+ # result.results.length # => 4
159
+ #
160
+ # @example Performance optimization with pipeline
161
+ # # Instead of multiple round-trips:
162
+ # # value1 = redis.get('key1') # Round-trip 1
163
+ # # value2 = redis.get('key2') # Round-trip 2
164
+ # # value3 = redis.get('key3') # Round-trip 3
165
+ #
166
+ # # Use pipeline for single round-trip:
167
+ # result = Familia.pipelined do |conn|
168
+ # conn.get('key1')
169
+ # conn.get('key2')
170
+ # conn.get('key3')
171
+ # end
172
+ # values = result.results # => ["value1", "value2", "value3"]
173
+ #
174
+ # @example Checking pipeline success
175
+ # result = Familia.pipelined do |conn|
176
+ # conn.set('temp_key', 'temp_value')
177
+ # conn.expire('temp_key', 60)
178
+ # conn.get('temp_key')
179
+ # end
180
+ #
181
+ # if result.successful?
182
+ # puts "Pipeline completed: #{result.results}"
183
+ # else
184
+ # puts "Some operations failed: #{result.results}"
185
+ # end
186
+ #
187
+ # @example Nested pipelines (reentrant behavior)
188
+ # result = Familia.pipelined do |outer_conn|
189
+ # outer_conn.set('outer', 'value')
190
+ #
191
+ # # Nested pipeline reuses the same connection
192
+ # inner_result = Familia.pipelined do |inner_conn|
193
+ # inner_conn.get('outer') # Returns Redis::Future in nested context
194
+ # end
195
+ #
196
+ # outer_conn.get('outer')
197
+ # end
198
+ #
199
+ # @note Pipeline vs Transaction Differences:
200
+ # - Pipeline: Commands executed independently, some may succeed while others fail
201
+ # - Transaction: All-or-nothing execution, commands are atomic as a group
202
+ # - Pipeline: Better performance for independent operations
203
+ # - Transaction: Better consistency for related operations
204
+ #
205
+ # @note Connection Handler Compatibility:
206
+ # - ProviderConnectionHandler: Full pipeline support
207
+ # - CreateConnectionHandler: Full pipeline support
208
+ # - FiberTransactionHandler: Blocked (raises OperationModeError)
209
+ # - FiberConnectionHandler: Blocked (raises OperationModeError)
210
+ # - DefaultConnectionHandler: Blocked (raises OperationModeError)
211
+ #
212
+ # @note Thread Safety:
213
+ # Uses Fiber-local storage to maintain pipeline context across nested calls
214
+ # and ensure proper cleanup even when exceptions occur.
215
+ #
216
+ # @see MultiResult For details on the return value structure
217
+ # @see Familia.transaction For atomic command execution
218
+ # @see #batch_update For similar MultiResult pattern in Horreum models
219
+ def pipelined(&block)
220
+ PipelineCore.execute_pipeline(-> { dbclient }, &block)
221
+ end
222
+ alias pipeline pipelined
223
+
224
+ # Provides explicit access to a Database connection.
225
+ #
226
+ # This method is useful when you need direct access to a connection
227
+ # for operations not covered by other methods. The connection is
228
+ # properly managed and returned to the pool (if using connection_provider).
229
+ #
230
+ # @yield [Redis] A Database connection
231
+ # @return The result of the block
232
+ #
233
+ # @example Using with_dbclient for custom operations
234
+ # Familia.with_dbclient do |conn|
235
+ # conn.set("custom_key", "value")
236
+ # conn.expire("custom_key", 3600)
237
+ # end
238
+ #
239
+ def with_dbclient(&)
240
+ yield dbclient
241
+ end
242
+
243
+ # Provides explicit access to an isolated Database connection for temporary operations.
244
+ #
245
+ # This method creates a new connection that won't interfere with the cached
246
+ # connection pool, executes the given block with that connection, and ensures
247
+ # the connection is properly closed afterward.
248
+ #
249
+ # Perfect for database scanning, inspection, or migration operations where
250
+ # you need to access different databases without affecting your models'
251
+ # normal connections.
252
+ #
253
+ # @param uri [String, URI, Integer, nil] The URI or database number to connect to.
254
+ # @yield [Redis] An isolated Database connection
255
+ # @return The result of the block
256
+ #
257
+ # @example Safely scanning for legacy data
258
+ # Familia.with_isolated_dbclient(5) do |conn|
259
+ # conn.keys("session:*")
260
+ # end
261
+ #
262
+ # @example Performing migration tasks
263
+ # Familia.with_isolated_dbclient(1) do |conn|
264
+ # conn.scan_each(match: "user:*") { |key| puts key }
265
+ # end
266
+ #
267
+ def with_isolated_dbclient(uri = nil, &)
268
+ client = isolated_dbclient(uri)
269
+ begin
270
+ yield client
271
+ ensure
272
+ client&.close
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ module Connection
5
+ # Pipeline execution with configurable fallback behavior
6
+ #
7
+ # Handles two pipeline scenarios based on connection handler capabilities:
8
+ # 1. Normal pipeline when handler supports pipelines
9
+ # 2. Individual command execution with configurable error/warn/silent modes
10
+ #
11
+ # @see OperationCore For shared fallback logic
12
+ # @see TransactionCore For similar transaction handling
13
+ #
14
+ module PipelineCore
15
+ # Executes a pipeline with configurable fallback behavior
16
+ #
17
+ # Handles pipeline execution based on connection handler capabilities.
18
+ # When handler doesn't support pipelines, fallback behavior is controlled
19
+ # by Familia.pipeline_mode setting.
20
+ #
21
+ # @param dbclient_proc [Proc] Lambda that returns the Redis connection
22
+ # @param block [Proc] Block containing Redis commands to execute
23
+ # @return [MultiResult] Result object with success status and command results
24
+ # @yield [Redis] Redis connection or proxy for command execution
25
+ #
26
+ # @example Basic usage
27
+ # result = PipelineCore.execute_pipeline(-> { dbclient }) do |conn|
28
+ # conn.set('key1', 'value1')
29
+ # conn.incr('counter')
30
+ # end
31
+ # result.successful? # => true/false
32
+ # result.results # => ["OK", 1]
33
+ #
34
+ # @example With fallback modes
35
+ # Familia.configure { |c| c.pipeline_mode = :permissive }
36
+ # result = PipelineCore.execute_pipeline(-> { cached_conn }) do |conn|
37
+ # conn.set('key', 'value') # Executes individually, no error
38
+ # end
39
+ #
40
+ def self.execute_pipeline(dbclient_proc, &block)
41
+ # First, get the connection to populate the handler class
42
+ connection = dbclient_proc.call
43
+ handler_class = Fiber[:familia_connection_handler_class]
44
+
45
+ # Check pipeline capability
46
+ pipeline_capability = handler_class&.allows_pipelined
47
+
48
+ if pipeline_capability == false
49
+ OperationCore.handle_fallback(:pipeline, dbclient_proc, handler_class, &block)
50
+ else
51
+ # Normal pipeline flow (includes nil, true, and other values)
52
+ execute_normal_pipeline(dbclient_proc, &block)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Executes a normal Redis pipeline
59
+ #
60
+ # Handles proper Fiber-local state management and cleanup in ensure blocks.
61
+ # Manages nested pipeline contexts by checking for existing pipeline state.
62
+ #
63
+ # @param dbclient_proc [Proc] Lambda that returns the Redis connection
64
+ # @param block [Proc] Block containing Redis commands to execute
65
+ # @return [MultiResult] Result object with pipeline command results
66
+ #
67
+ def self.execute_normal_pipeline(dbclient_proc, &block)
68
+ # Check for existing pipeline context
69
+ return yield(Fiber[:familia_pipeline]) if Fiber[:familia_pipeline]
70
+
71
+ command_return_values = dbclient_proc.call.pipelined do |conn|
72
+ Fiber[:familia_pipeline] = conn
73
+ begin
74
+ yield(conn)
75
+ ensure
76
+ Fiber[:familia_pipeline] = nil
77
+ end
78
+ end
79
+
80
+ # Return same MultiResult format as other methods
81
+ # Pipeline success is true if no exceptions occurred (all commands executed)
82
+ summary_boolean = command_return_values.none? { |ret| ret.is_a?(Exception) }
83
+ MultiResult.new(summary_boolean, command_return_values)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,100 @@
1
+ # lib/familia/connection/transaction_core.rb
2
+
3
+ module Familia
4
+ module Connection
5
+ # Core transaction logic shared between global and instance transaction methods
6
+ #
7
+ # This module provides unified transaction handling with configurable fallback
8
+ # behavior when transactions are unavailable due to connection handler constraints.
9
+ # Eliminates code duplication between Operations and Horreum Connection modules.
10
+ #
11
+ # @example Usage in transaction methods
12
+ # def transaction(&block)
13
+ # TransactionCore.execute_transaction(-> { dbclient }, &block)
14
+ # end
15
+ #
16
+ module TransactionCore
17
+ # Executes a transaction with configurable fallback behavior
18
+ #
19
+ # Handles three transaction scenarios based on connection handler capabilities:
20
+ # 1. Normal transaction (MULTI/EXEC) when handler supports transactions
21
+ # 2. Reentrant transaction when already within a transaction context
22
+ # 3. Individual command execution with configurable error/warn/silent modes
23
+ #
24
+ # @param dbclient_proc [Proc] Lambda that returns the Redis connection
25
+ # @param block [Proc] Block containing Redis commands to execute
26
+ # @return [MultiResult] Result object with success status and command results
27
+ # @yield [Redis] Redis connection or proxy for command execution
28
+ #
29
+ # @example Basic usage
30
+ # result = TransactionCore.execute_transaction(-> { dbclient }) do |conn|
31
+ # conn.set('key1', 'value1')
32
+ # conn.incr('counter')
33
+ # end
34
+ # result.successful? # => true/false
35
+ # result.results # => ["OK", 1]
36
+ #
37
+ def self.execute_transaction(dbclient_proc, &block)
38
+ # First, get the connection to populate the handler class
39
+ connection = dbclient_proc.call
40
+ handler_class = Fiber[:familia_connection_handler_class]
41
+
42
+ # Check transaction capability
43
+ transaction_capability = handler_class&.allows_transaction
44
+
45
+ if transaction_capability == false
46
+ handle_transaction_fallback(dbclient_proc, handler_class, &block)
47
+ elsif transaction_capability == :reentrant
48
+ # Already in transaction, just yield the connection
49
+ yield(Fiber[:familia_transaction])
50
+ else
51
+ # Normal transaction flow (includes nil, true, and other values)
52
+ execute_normal_transaction(dbclient_proc, &block)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Handles transaction fallback based on configured transaction mode
59
+ #
60
+ # Delegates to OperationCore.handle_fallback for consistent behavior
61
+ # across transaction and pipeline operations.
62
+ #
63
+ # @param dbclient_proc [Proc] Lambda that returns the Redis connection
64
+ # @param handler_class [Class] The connection handler class that blocked transaction
65
+ # @param block [Proc] Block containing Redis commands to execute
66
+ # @return [MultiResult] Result from individual command execution or raises error
67
+ #
68
+ def self.handle_transaction_fallback(dbclient_proc, handler_class, &block)
69
+ OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &block)
70
+ end
71
+
72
+ # Executes a normal Redis transaction using MULTI/EXEC
73
+ #
74
+ # Handles the standard transaction flow including nested transaction detection,
75
+ # proper Fiber-local state management, and cleanup in ensure blocks.
76
+ #
77
+ # @param dbclient_proc [Proc] Lambda that returns the Redis connection
78
+ # @param block [Proc] Block containing Redis commands to execute
79
+ # @return [MultiResult] Result object with transaction command results
80
+ #
81
+ def self.execute_normal_transaction(dbclient_proc, &block)
82
+ # Check for existing transaction context
83
+ return yield(Fiber[:familia_transaction]) if Fiber[:familia_transaction]
84
+
85
+ command_return_values = dbclient_proc.call.multi do |conn|
86
+ Fiber[:familia_transaction] = conn
87
+ begin
88
+ yield(conn)
89
+ ensure
90
+ Fiber[:familia_transaction] = nil
91
+ end
92
+ end
93
+
94
+ # Return same MultiResult format as other methods
95
+ summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) }
96
+ MultiResult.new(summary_boolean, command_return_values)
97
+ end
98
+ end
99
+ end
100
+ end