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,128 @@
1
+ #
2
+ # Tests pipeline fallback modes when connection handlers don't support pipelines.
3
+ # Validates that pipeline_mode configuration works correctly with cached connections
4
+ # and that the fallback behavior matches transaction fallback patterns.
5
+ #
6
+
7
+ require_relative '../helpers/test_helpers'
8
+
9
+ # Store original values
10
+ $original_pipeline_mode = Familia.pipeline_mode
11
+ $original_transaction_mode = Familia.transaction_mode
12
+
13
+ # Test model for pipeline fallback scenarios
14
+ class PipelineFallbackTest < Familia::Horreum
15
+ identifier_field :testid
16
+ field :testid
17
+ end
18
+
19
+ ## Test 1: Strict mode raises error with cached connection
20
+ Familia.configure { |c| c.pipeline_mode = :strict }
21
+
22
+ # Cache connection at class level (uses DefaultConnectionHandler which doesn't support pipelines)
23
+ PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
24
+
25
+ customer = PipelineFallbackTest.new(testid: 'strict_test')
26
+ customer.pipelined { |c| c.set('key', 'value') }
27
+ #=:> Familia::OperationModeError
28
+ #=~> /Cannot start pipeline with.*CachedConnectionHandler/
29
+
30
+ ## Test 2: Warn mode falls back successfully with cached connection
31
+ Familia.configure { |c| c.pipeline_mode = :warn }
32
+
33
+ # Cache connection at class level
34
+ PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
35
+
36
+ customer2 = PipelineFallbackTest.new(testid: 'warn_test')
37
+ $warn_result = customer2.pipelined do |conn|
38
+ conn.set(customer2.dbkey('field1'), 'value1')
39
+ conn.set(customer2.dbkey('field2'), 'value2')
40
+ conn.get(customer2.dbkey('field1'))
41
+ end
42
+ $warn_result.successful?
43
+ #=> true
44
+
45
+ ## Test 2b: Warn mode result contains all command results
46
+ $warn_result.results.size
47
+ #=> 3
48
+
49
+ ## Test 2c: Warn mode result contains correct value
50
+ $warn_result.results[2]
51
+ #=> 'value1'
52
+
53
+ ## Test 3: Fresh connections still support real pipelines in strict mode
54
+ Familia.configure { |c| c.pipeline_mode = :strict }
55
+
56
+ # Clear cached class-level connection to force CreateConnectionHandler
57
+ PipelineFallbackTest.remove_instance_variable(:@dbclient) if PipelineFallbackTest.instance_variable_defined?(:@dbclient)
58
+
59
+ customer3 = PipelineFallbackTest.new(testid: 'fresh_test')
60
+ $fresh_result = customer3.pipelined do |conn|
61
+ conn.set('pipeline_key1', 'val1')
62
+ conn.set('pipeline_key2', 'val2')
63
+ conn.get('pipeline_key1')
64
+ end
65
+ $fresh_result.successful?
66
+ #=> true
67
+
68
+ ## Test 3b: Real pipeline returns correct result count
69
+ $fresh_result.results.size
70
+ #=> 3
71
+
72
+ ## Test 4: MultiResult format is correct for fallback
73
+ Familia.configure { |c| c.pipeline_mode = :permissive }
74
+
75
+ # Cache connection at class level
76
+ PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
77
+
78
+ customer4 = PipelineFallbackTest.new(testid: 'multiresult_test')
79
+ $multi_result = customer4.pipelined do |conn|
80
+ conn.set(customer4.dbkey('test'), 'value')
81
+ conn.get(customer4.dbkey('test'))
82
+ end
83
+ $multi_result.class.name
84
+ #=> 'MultiResult'
85
+
86
+ ## Test 4b: MultiResult successful? returns correct value
87
+ $multi_result.successful?
88
+ #=> true
89
+
90
+ ## Test 4c: MultiResult results is an Array
91
+ $multi_result.results.class
92
+ #=> Array
93
+
94
+ ## Test 5: Permissive mode silently falls back
95
+ Familia.configure { |c| c.pipeline_mode = :permissive }
96
+
97
+ # Cache connection at class level
98
+ PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
99
+
100
+ customer5 = PipelineFallbackTest.new(testid: 'permissive_test')
101
+ $permissive_result = customer5.pipelined do |conn|
102
+ conn.set(customer5.dbkey('counter'), '0')
103
+ conn.incr(customer5.dbkey('counter'))
104
+ conn.get(customer5.dbkey('counter'))
105
+ end
106
+ $permissive_result.successful?
107
+ #=> true
108
+
109
+ ## Test 5b: Permissive mode result contains correct values
110
+ $permissive_result.results
111
+ #=> ['OK', 1, '1']
112
+
113
+ ## Test 6: Pipeline mode configuration validation
114
+ Familia.configure { |c| c.pipeline_mode = :invalid }
115
+ #=:> ArgumentError
116
+ #=~> /Pipeline mode must be :strict, :warn, or :permissive/
117
+
118
+ ## Test 7: Default pipeline_mode is :warn
119
+ Familia.instance_variable_set(:@pipeline_mode, nil)
120
+ Familia.pipeline_mode
121
+ #=> :warn
122
+
123
+ ## Cleanup: Restore original values
124
+ Familia.configure do |c|
125
+ c.pipeline_mode = $original_pipeline_mode
126
+ c.transaction_mode = $original_transaction_mode
127
+ end
128
+ PipelineFallbackTest.remove_instance_variable(:@dbclient) if PipelineFallbackTest.instance_variable_defined?(:@dbclient)
@@ -0,0 +1,72 @@
1
+ # ResponsibilityChain Handler Tracking Tryouts
2
+ #
3
+ # Tests that the ResponsibilityChain correctly tracks which handler provided
4
+ # each connection by setting Fiber[:familia_connection_handler_class]. This enables
5
+ # operation mode guards to enforce constraints based on connection source.
6
+ #
7
+ # The chain should:
8
+ # - Try handlers in order until one returns a connection
9
+ # - Set Fiber[:familia_connection_handler_class] to the successful handler's class
10
+ # - Return the connection from the successful handler
11
+ # - Return nil if no handler provides a connection
12
+
13
+ require_relative '../helpers/test_helpers'
14
+
15
+ # Setup - clear any existing fiber state
16
+ Fiber[:familia_connection_handler_class] = nil
17
+ Fiber[:familia_connection] = nil
18
+
19
+ ## Chain tracks CreateConnectionHandler for normal connections
20
+ customer = Customer.new
21
+ customer.custid = 'tracking_test'
22
+ connection = customer.dbclient
23
+ Fiber[:familia_connection_handler_class]
24
+ #=> Familia::Connection::CreateConnectionHandler
25
+
26
+ ## Chain tracks FiberConnectionHandler for middleware connections
27
+ begin
28
+ test_conn = Customer.create_dbclient
29
+ Fiber[:familia_connection] = [test_conn, Familia.middleware_version]
30
+
31
+ customer = Customer.new
32
+ customer.custid = 'fiber_test'
33
+ connection = customer.dbclient
34
+ Fiber[:familia_connection_handler_class]
35
+ ensure
36
+ Fiber[:familia_connection] = nil
37
+ Fiber[:familia_connection_handler_class] = nil
38
+ end
39
+ #=> Familia::Connection::FiberConnectionHandler
40
+
41
+ ## Chain tracks ProviderConnectionHandler for connection providers
42
+ original_provider = Familia.connection_provider
43
+ begin
44
+ Familia.connection_provider = ->(uri) { Redis.new(url: uri) }
45
+
46
+ customer = Customer.new
47
+ customer.custid = 'provider_test'
48
+ connection = customer.dbclient
49
+ Fiber[:familia_connection_handler_class]
50
+ ensure
51
+ Familia.connection_provider = original_provider
52
+ Fiber[:familia_connection_handler_class] = nil
53
+ end
54
+ #=> Familia::Connection::ProviderConnectionHandler
55
+
56
+ ## Chain returns nil when no handler provides connection
57
+ # Create a mock chain with handlers that return nil
58
+ chain = Familia::Connection::ResponsibilityChain.new
59
+ chain.add_handler(Familia::Connection::FiberTransactionHandler.new)
60
+ chain.add_handler(Familia::Connection::FiberConnectionHandler.new)
61
+
62
+ result = chain.handle('test_uri')
63
+ result.nil?
64
+ #=> true
65
+
66
+ ## Chain sets handler class even when tracking connections
67
+ customer = Customer.new
68
+ customer.custid = 'final_test'
69
+ conn = customer.dbclient
70
+ handler_class = Fiber[:familia_connection_handler_class]
71
+ handler_class == Familia::Connection::CreateConnectionHandler && !conn.nil?
72
+ #=> true
@@ -0,0 +1,288 @@
1
+ # Transaction Fallback Integration Tryouts
2
+ #
3
+ # Tests real-world scenarios where transaction fallback is needed, particularly
4
+ # focusing on save operations, relationship updates, and other high-level
5
+ # operations that internally use transactions.
6
+ #
7
+ # These tests verify that the transaction mode configuration works seamlessly
8
+ # with Familia's existing features when connection handlers don't support
9
+ # transactions (e.g., cached connections, middleware connections).
10
+
11
+ require_relative '../helpers/test_helpers'
12
+
13
+ # Setup - store original values
14
+ $original_transaction_mode = Familia.transaction_mode
15
+ $original_provider = Familia.connection_provider
16
+
17
+ # Create test classes for integration testing
18
+ class IntegrationTestUser < Familia::Horreum
19
+ identifier_field :user_id
20
+ field :user_id
21
+ field :name
22
+ field :email
23
+ field :status
24
+ list :activity_log
25
+ set :tags
26
+ zset :scores
27
+ end
28
+
29
+ class IntegrationTestSession < Familia::Horreum
30
+ identifier_field :session_id
31
+ field :session_id
32
+ field :user_id
33
+ field :created_at
34
+ field :expires_at
35
+ end
36
+
37
+ ## Save operation works with transaction fallback in warn mode
38
+ begin
39
+ Familia.configure { |config| config.transaction_mode = :warn }
40
+
41
+ # Force CachedConnectionHandler
42
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
43
+
44
+ user = IntegrationTestUser.new(
45
+ user_id: 'save_test_001',
46
+ name: 'Test User',
47
+ email: 'test@example.com',
48
+ status: 'active'
49
+ )
50
+
51
+ # Save internally uses transactions for atomicity
52
+ result = user.save
53
+
54
+ # Should complete successfully using individual commands
55
+ result && user.exists?
56
+ ensure
57
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
58
+ Familia.configure { |config| config.transaction_mode = :strict }
59
+ end
60
+ #=> true
61
+
62
+ ## Save operation works with transaction fallback in permissive mode
63
+ begin
64
+ Familia.configure { |config| config.transaction_mode = :permissive }
65
+
66
+ # Force CachedConnectionHandler
67
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
68
+
69
+ user = IntegrationTestUser.new(
70
+ user_id: 'save_test_002',
71
+ name: 'Permissive User',
72
+ email: 'permissive@example.com'
73
+ )
74
+
75
+ # Should save silently using individual commands
76
+ result = user.save
77
+
78
+ result && user.exists?
79
+ ensure
80
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
81
+ Familia.configure { |config| config.transaction_mode = :strict }
82
+ end
83
+ #=> true
84
+
85
+ ## Multiple field updates work with fallback
86
+ begin
87
+ Familia.configure { |config| config.transaction_mode = :permissive }
88
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
89
+
90
+ user = IntegrationTestUser.new(user_id: 'update_test_001')
91
+ user.save
92
+
93
+ # Update multiple fields - should use individual commands
94
+ user.name = 'Updated Name'
95
+ user.email = 'updated@example.com'
96
+ user.status = 'updated'
97
+
98
+ result = user.save
99
+
100
+ # Verify all fields were updated
101
+ reloaded_user = IntegrationTestUser.load('update_test_001')
102
+ result &&
103
+ reloaded_user.name == 'Updated Name' &&
104
+ reloaded_user.email == 'updated@example.com' &&
105
+ reloaded_user.status == 'updated'
106
+ ensure
107
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
108
+ Familia.configure { |config| config.transaction_mode = :strict }
109
+ end
110
+ #=> true
111
+
112
+ ## Data type operations work with transaction fallback
113
+
114
+ Familia.configure { |config| config.transaction_mode = :permissive }
115
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
116
+
117
+ user = IntegrationTestUser.new(user_id: 'datatype_test_001')
118
+ user.transaction { |conn| conn.set('test', 'value') }
119
+
120
+ # Test list operations
121
+ user.activity_log.add('user_created')
122
+ user.activity_log.add('profile_updated')
123
+
124
+ # Test set operations
125
+ user.tags.add('premium')
126
+ user.tags.add('verified')
127
+
128
+ # Test sorted set operations
129
+ user.scores.add('game_score', 100)
130
+ user.scores.add('quiz_score', 85)
131
+
132
+ # Verify all operations worked
133
+ results = [
134
+ user.activity_log.size,
135
+ user.tags.include?('premium'),
136
+ user.scores.score('game_score')
137
+ ]
138
+
139
+ user.destroy!
140
+
141
+ # Clean up test data
142
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
143
+ Familia.configure { |config| config.transaction_mode = :strict }
144
+
145
+ results
146
+ #=> [2, true, 100.0]
147
+
148
+ ## Connection provider integration with transaction modes
149
+ begin
150
+ Familia.configure { |config| config.transaction_mode = :warn }
151
+
152
+ # Set up connection provider that doesn't support transactions
153
+ test_connections = {}
154
+ Familia.connection_provider = lambda do |uri|
155
+ test_connections[uri] ||= Familia.create_dbclient(uri)
156
+ end
157
+
158
+ user = IntegrationTestUser.new(
159
+ user_id: 'provider_test_001',
160
+ name: 'Provider Test User'
161
+ )
162
+
163
+ # Should work with connection provider
164
+ result = user.save
165
+
166
+ result && user.exists?
167
+ ensure
168
+ Familia.connection_provider = $original_provider
169
+ Familia.configure { |config| config.transaction_mode = :strict }
170
+ end
171
+ #=> true
172
+
173
+ ## Mixed connection handlers in same operation
174
+ begin
175
+ Familia.configure { |config| config.transaction_mode = :permissive }
176
+
177
+ # User class with cached connection (fallback mode)
178
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
179
+
180
+ # Session class without cached connection (normal mode)
181
+ user = IntegrationTestUser.new(user_id: 'mixed_test_001')
182
+ session = IntegrationTestSession.new(
183
+ session_id: 'sess_mixed_001',
184
+ user_id: 'mixed_test_001',
185
+ created_at: Time.now.to_i
186
+ )
187
+
188
+ # Both should save successfully despite different connection handlers
189
+ user_saved = user.save
190
+ session_saved = session.save
191
+
192
+ user_saved && session_saved && user.exists? && session.exists?
193
+ ensure
194
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
195
+ Familia.configure { |config| config.transaction_mode = :strict }
196
+ end
197
+ #=> true
198
+
199
+ ## Transaction mode changes during runtime
200
+ begin
201
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
202
+
203
+ user = IntegrationTestUser.new(user_id: 'runtime_test_001')
204
+
205
+ # Start in strict mode - should fail
206
+ Familia.configure { |config| config.transaction_mode = :strict }
207
+ strict_failed = false
208
+ begin
209
+ user.transaction { |conn| conn.set('test_key', 'test_value') }
210
+ rescue Familia::OperationModeError
211
+ strict_failed = true
212
+ end
213
+
214
+ # Switch to permissive mode - should work
215
+ Familia.configure { |config| config.transaction_mode = :permissive }
216
+ result = user.transaction { |conn| conn.set('test_key', 'test_value') }
217
+ permissive_worked = result.successful?
218
+
219
+ strict_failed && permissive_worked
220
+ ensure
221
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
222
+ Familia.configure { |config| config.transaction_mode = :strict }
223
+ end
224
+ #=> true
225
+
226
+ ## Large batch operations with transaction fallback
227
+ begin
228
+ Familia.configure { |config| config.transaction_mode = :permissive }
229
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
230
+
231
+ user = IntegrationTestUser.new(user_id: 'batch_test_001')
232
+
233
+ # Simulate a large batch operation that would normally use transactions
234
+ result = user.transaction do |conn|
235
+ # Add multiple activity log entries
236
+ (1..10).each do |i|
237
+ conn.rpush(user.activity_log.dbkey, "activity_#{i}")
238
+ end
239
+
240
+ # Add multiple tags
241
+ %w[tag1 tag2 tag3 tag4 tag5].each do |tag|
242
+ conn.sadd(user.tags.dbkey, tag)
243
+ end
244
+
245
+ # Add user fields
246
+ conn.hset(user.dbkey, 'batch_processed', 'true')
247
+ end
248
+
249
+ # Should complete successfully and return MultiResult
250
+ result.is_a?(MultiResult) && result.successful?
251
+ ensure
252
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
253
+ Familia.configure { |config| config.transaction_mode = :strict }
254
+ end
255
+ #=> true
256
+
257
+ ## Error handling in individual command mode
258
+ begin
259
+ Familia.configure { |config| config.transaction_mode = :permissive }
260
+ IntegrationTestUser.instance_variable_set(:@dbclient, Familia.create_dbclient)
261
+
262
+ user = IntegrationTestUser.new(user_id: 'error_test_001')
263
+
264
+ # Execute commands where some might fail
265
+ result = user.transaction do |conn|
266
+ conn.hset(user.dbkey, 'field1', 'value1') # Should succeed
267
+
268
+ # This might fail but shouldn't stop other commands
269
+ begin
270
+ conn.incr('non_numeric_key') # Might fail if key exists as string
271
+ rescue => e
272
+ # Individual commands can handle their own errors
273
+ end
274
+
275
+ conn.hset(user.dbkey, 'field2', 'value2') # Should succeed
276
+ end
277
+
278
+ # Should return MultiResult even with mixed success/failure
279
+ result.is_a?(MultiResult)
280
+ ensure
281
+ IntegrationTestUser.remove_instance_variable(:@dbclient)
282
+ Familia.configure { |config| config.transaction_mode = :strict }
283
+ end
284
+ #=> true
285
+
286
+ # Cleanup - restore original settings
287
+ Familia.configure { |config| config.transaction_mode = $original_transaction_mode }
288
+ Familia.connection_provider = $original_provider
@@ -0,0 +1,153 @@
1
+ # Transaction Mode: Permissive Tryouts
2
+ #
3
+ # Tests permissive transaction mode behavior where operations silently
4
+ # execute commands individually when transactions are unavailable.
5
+ #
6
+ # Permissive mode: Silently uses IndividualCommandProxy for fallback
7
+
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Test class for permissive mode testing
11
+ class PermissiveModeTestCustomer < Familia::Horreum
12
+ identifier_field :custid
13
+ field :custid
14
+ field :name
15
+ field :status
16
+ end
17
+
18
+ ## Permissive mode can be configured
19
+ Familia.configure { |config| config.transaction_mode = :permissive }
20
+ Familia.transaction_mode
21
+ #=> :permissive
22
+
23
+ ## Permissive mode silently executes individual commands with CachedConnectionHandler
24
+ begin
25
+ # Force CachedConnectionHandler
26
+ PermissiveModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
27
+
28
+ customer = PermissiveModeTestCustomer.new(custid: 'permissive_test')
29
+ result = customer.transaction do |conn|
30
+ # Should be IndividualCommandProxy
31
+ conn.class == Familia::Connection::IndividualCommandProxy &&
32
+ conn.hset(customer.dbkey, 'name', 'Permissive Mode Works') &&
33
+ conn.hget(customer.dbkey, 'name')
34
+ end
35
+
36
+ result.is_a?(MultiResult) && result.results.last == 'Permissive Mode Works'
37
+ ensure
38
+ PermissiveModeTestCustomer.remove_instance_variable(:@dbclient)
39
+ end
40
+ #=> true
41
+
42
+ ## Permissive mode works silently with FiberConnectionHandler
43
+ begin
44
+ # Simulate middleware connection
45
+ Fiber[:familia_connection] = [Customer.create_dbclient, Familia.middleware_version]
46
+ Fiber[:familia_connection_handler_class] = Familia::Connection::FiberConnectionHandler
47
+ customer = PermissiveModeTestCustomer.new(custid: 'fiber_permissive_test')
48
+
49
+ result = customer.transaction do |conn|
50
+ conn.class == Familia::Connection::IndividualCommandProxy &&
51
+ conn.hset(customer.dbkey, 'source', 'fiber_permissive') &&
52
+ conn.hget(customer.dbkey, 'source')
53
+ end
54
+
55
+ result.is_a?(MultiResult) && result.results.last == 'fiber_permissive'
56
+ ensure
57
+ Fiber[:familia_connection] = nil
58
+ Fiber[:familia_connection_handler_class] = nil
59
+ end
60
+ #=> true
61
+
62
+ ## Permissive mode still uses normal transactions when available
63
+ begin
64
+ customer = PermissiveModeTestCustomer.new(custid: 'normal_permissive_test')
65
+ result = customer.transaction do |conn|
66
+ # Should be Redis::MultiConnection for normal transactions
67
+ conn.class == Redis::MultiConnection &&
68
+ conn.hset(customer.dbkey, 'type', 'normal in permissive mode') &&
69
+ conn.hget(customer.dbkey, 'type')
70
+ end
71
+ result.is_a?(MultiResult) && result.results.last == 'normal in permissive mode'
72
+ end
73
+ #=> true
74
+
75
+ ## Permissive mode handles complex operations silently
76
+ begin
77
+ PermissiveModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
78
+
79
+ customer = PermissiveModeTestCustomer.new(custid: 'complex_permissive_test')
80
+ result = customer.transaction do |conn|
81
+ # Multiple operations that would normally be atomic
82
+ conn.hset(customer.dbkey, 'status', 'processing')
83
+ conn.hset(customer.dbkey, 'updated_at', Time.now.to_i)
84
+ conn.hset(customer.dbkey, 'version', '1.0')
85
+ conn.hget(customer.dbkey, 'status')
86
+ end
87
+
88
+ result.is_a?(MultiResult) && result.results.last == 'processing'
89
+ ensure
90
+ PermissiveModeTestCustomer.remove_instance_variable(:@dbclient)
91
+ end
92
+ #=> true
93
+
94
+ ## Permissive mode works with nested calls
95
+ begin
96
+ PermissiveModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
97
+
98
+ customer = PermissiveModeTestCustomer.new(custid: 'nested_permissive_test')
99
+
100
+ outer_result = customer.transaction do |outer_conn|
101
+ outer_conn.hset(customer.dbkey, 'outer', 'value')
102
+
103
+ inner_result = customer.transaction do |inner_conn|
104
+ inner_conn.hset(customer.dbkey, 'inner', 'nested_value')
105
+ end
106
+
107
+ # Both should return MultiResult
108
+ inner_result.is_a?(MultiResult)
109
+ end
110
+
111
+ outer_result.is_a?(MultiResult)
112
+ ensure
113
+ PermissiveModeTestCustomer.remove_instance_variable(:@dbclient)
114
+ end
115
+ #=> true
116
+
117
+ ## Batch operations work silently in permissive mode
118
+ begin
119
+ PermissiveModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
120
+
121
+ customer = PermissiveModeTestCustomer.new(custid: 'batch_permissive_test')
122
+
123
+ # Large batch that would normally be atomic
124
+ result = customer.transaction do |conn|
125
+ 10.times do |i|
126
+ conn.hset(customer.dbkey, "field_#{i}", "value_#{i}")
127
+ end
128
+ conn.hget(customer.dbkey, 'field_9')
129
+ end
130
+
131
+ result.is_a?(MultiResult) && result.results.last == 'value_9'
132
+ ensure
133
+ PermissiveModeTestCustomer.remove_instance_variable(:@dbclient)
134
+ end
135
+ #=> true
136
+
137
+ ## Save operations work in permissive mode
138
+ begin
139
+ PermissiveModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
140
+
141
+ customer = PermissiveModeTestCustomer.new(
142
+ custid: 'save_permissive_test',
143
+ name: 'Permissive Save User',
144
+ status: 'active'
145
+ )
146
+
147
+ # Should save successfully using individual commands
148
+ save_result = customer.save
149
+ save_result && customer.exists?
150
+ ensure
151
+ PermissiveModeTestCustomer.remove_instance_variable(:@dbclient)
152
+ end
153
+ #=> true