familia 2.0.0.pre14 → 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 (276) 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 +66 -6
  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 +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  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 +623 -19
  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 +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -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/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,131 @@
1
+ # Transaction Mode: Warn Tryouts
2
+ #
3
+ # Tests warn transaction mode behavior where operations log a warning
4
+ # and execute commands individually when transactions are unavailable.
5
+ #
6
+ # Warn mode: Logs warning and uses IndividualCommandProxy for fallback
7
+
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Test class for warn mode testing
11
+ class WarnModeTestCustomer < Familia::Horreum
12
+ identifier_field :custid
13
+ field :custid
14
+ field :name
15
+ field :email
16
+ end
17
+
18
+ ## Warn mode can be configured
19
+ Familia.configure { |config| config.transaction_mode = :warn }
20
+ Familia.transaction_mode
21
+ #=> :warn
22
+
23
+ ## Warn mode executes individual commands with CachedConnectionHandler
24
+ begin
25
+ # Force CachedConnectionHandler
26
+ WarnModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
27
+
28
+ customer = WarnModeTestCustomer.new(custid: 'warn_test')
29
+ result = customer.transaction do |conn|
30
+ # Should be IndividualCommandProxy
31
+ conn.class == Familia::Connection::IndividualCommandProxy &&
32
+ conn.hset(customer.dbkey, 'name', 'Warn Mode Works') &&
33
+ conn.hget(customer.dbkey, 'name')
34
+ end
35
+
36
+ result.is_a?(MultiResult) && result.results.last == 'Warn Mode Works'
37
+ ensure
38
+ WarnModeTestCustomer.remove_instance_variable(:@dbclient)
39
+ end
40
+ #=> true
41
+
42
+ ## Warn mode executes individual commands 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 = WarnModeTestCustomer.new(custid: 'fiber_warn_test')
48
+
49
+ result = customer.transaction do |conn|
50
+ conn.class == Familia::Connection::IndividualCommandProxy &&
51
+ conn.hset(customer.dbkey, 'source', 'fiber_warn') &&
52
+ conn.hget(customer.dbkey, 'source')
53
+ end
54
+
55
+ result.is_a?(MultiResult) && result.results.last == 'fiber_warn'
56
+ ensure
57
+ Fiber[:familia_connection] = nil
58
+ Fiber[:familia_connection_handler_class] = nil
59
+ end
60
+ #=> true
61
+
62
+ ## Warn mode still uses normal transactions with CreateConnectionHandler
63
+ begin
64
+ customer = WarnModeTestCustomer.new(custid: 'normal_warn_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 warn mode') &&
69
+ conn.hget(customer.dbkey, 'type')
70
+ end
71
+ result.is_a?(MultiResult) && result.results.last == 'normal in warn mode'
72
+ end
73
+ #=> true
74
+
75
+ ## IndividualCommandProxy collects results correctly in warn mode
76
+ begin
77
+ WarnModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
78
+
79
+ customer = WarnModeTestCustomer.new(custid: 'proxy_warn_test')
80
+ result = customer.transaction do |conn|
81
+ conn.hset(customer.dbkey, 'field1', 'value1')
82
+ conn.hset(customer.dbkey, 'field2', 'value2')
83
+ conn.hget(customer.dbkey, 'field1')
84
+ conn.hget(customer.dbkey, 'field2')
85
+ end
86
+
87
+ # Check that results are collected properly
88
+ result.is_a?(MultiResult) &&
89
+ result.results.size == 4 &&
90
+ result.results.include?('value1') &&
91
+ result.results.include?('value2')
92
+ ensure
93
+ WarnModeTestCustomer.remove_instance_variable(:@dbclient)
94
+ end
95
+ #=> true
96
+
97
+ ## Save operations work in warn mode with fallback
98
+ begin
99
+ WarnModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
100
+
101
+ customer = WarnModeTestCustomer.new(
102
+ custid: 'save_warn_test',
103
+ name: 'Save Test User',
104
+ email: 'save@example.com'
105
+ )
106
+
107
+ # Save should work using individual commands
108
+ save_result = customer.save
109
+ save_result && customer.exists?
110
+ ensure
111
+ WarnModeTestCustomer.remove_instance_variable(:@dbclient)
112
+ end
113
+ #=> true
114
+
115
+ ## Model transactions respect warn mode with cached connections
116
+ begin
117
+ # Test that cached connections on models respect warn mode
118
+ WarnModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
119
+
120
+ customer = WarnModeTestCustomer.new(custid: 'model_warn_test')
121
+ result = customer.transaction do |conn|
122
+ conn.class == Familia::Connection::IndividualCommandProxy &&
123
+ conn.hset(customer.dbkey, 'mode', 'warn_fallback') &&
124
+ conn.hget(customer.dbkey, 'mode')
125
+ end
126
+
127
+ result.is_a?(MultiResult) && result.results.last == 'warn_fallback'
128
+ ensure
129
+ WarnModeTestCustomer.remove_instance_variable(:@dbclient)
130
+ end
131
+ #=> true
@@ -0,0 +1,249 @@
1
+ # Transaction Modes Tryouts
2
+ #
3
+ # Tests the configurable transaction mode system that provides graceful fallback
4
+ # when connection handlers don't support transactions. Three modes available:
5
+ #
6
+ # - :strict (default): Raise OperationModeError when transaction unavailable
7
+ # - :warn: Log warning and execute commands individually with IndividualCommandProxy
8
+ # - :permissive: Silently execute commands individually
9
+ #
10
+ # The IndividualCommandProxy executes Redis commands immediately instead of queuing
11
+ # them in a transaction, maintaining the same MultiResult interface for consistency.
12
+
13
+ require_relative '../helpers/test_helpers'
14
+
15
+ # Setup - ensure clean state
16
+ @original_transaction_mode = Familia.transaction_mode
17
+ @test_customer_class = nil
18
+
19
+ # Create a test customer class for isolation
20
+ class TransactionModeTestCustomer < Familia::Horreum
21
+ identifier_field :custid
22
+ field :custid
23
+ field :name
24
+ field :email
25
+ end
26
+
27
+ ## Default transaction mode is warn (user-friendly)
28
+ Familia.transaction_mode
29
+ #=> :warn
30
+
31
+ ## Transaction mode can be configured to warn
32
+ Familia.configure do |config|
33
+ config.transaction_mode = :warn
34
+ end
35
+ Familia.transaction_mode
36
+ #=> :warn
37
+
38
+ ## Transaction mode can be configured to permissive
39
+ Familia.configure do |config|
40
+ config.transaction_mode = :permissive
41
+ end
42
+ Familia.transaction_mode
43
+ #=> :permissive
44
+
45
+ ## Reset to strict mode for remaining tests
46
+ Familia.configure { |config| config.transaction_mode = :strict }
47
+
48
+ ## Strict mode raises error with CachedConnectionHandler
49
+ begin
50
+ # Ensure we're in strict mode first
51
+ Familia.configure { |config| config.transaction_mode = :strict }
52
+
53
+ # Force CachedConnectionHandler by setting @dbclient
54
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
55
+
56
+ customer = TransactionModeTestCustomer.new(custid: 'strict_test')
57
+ customer.transaction do |conn|
58
+ conn.hset(customer.dbkey, 'name', 'Should Not Work')
59
+ end
60
+ false # Should not reach here
61
+ rescue Familia::OperationModeError => e
62
+ e.message.include?('Cannot start transaction with') && e.message.include?('CachedConnectionHandler')
63
+ ensure
64
+ # Clean up cached connection
65
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
66
+ end
67
+ #=> true
68
+
69
+ ## Warn mode logs warning and executes individual commands
70
+ begin
71
+ Familia.configure { |config| config.transaction_mode = :warn }
72
+
73
+ # Force CachedConnectionHandler
74
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
75
+
76
+ customer = TransactionModeTestCustomer.new(custid: 'warn_test')
77
+
78
+ # Capture log output would be ideal, but test the core functionality
79
+ result = customer.transaction do |conn|
80
+ # This should be an IndividualCommandProxy
81
+ conn.class == Familia::Connection::IndividualCommandProxy &&
82
+ conn.hset(customer.dbkey, 'name', 'Warn Mode Works') &&
83
+ conn.hget(customer.dbkey, 'name')
84
+ end
85
+
86
+ # Should return MultiResult with individual command results
87
+ result.is_a?(MultiResult) && result.results.last == 'Warn Mode Works'
88
+ ensure
89
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
90
+ Familia.configure { |config| config.transaction_mode = :strict }
91
+ end
92
+ #=> true
93
+
94
+ ## Permissive mode silently executes individual commands
95
+ begin
96
+ Familia.configure { |config| config.transaction_mode = :permissive }
97
+
98
+ # Force CachedConnectionHandler
99
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
100
+
101
+ customer = TransactionModeTestCustomer.new(custid: 'permissive_test')
102
+
103
+ result = customer.transaction do |conn|
104
+ # Should be IndividualCommandProxy
105
+ conn.class == Familia::Connection::IndividualCommandProxy &&
106
+ conn.hset(customer.dbkey, 'email', 'permissive@example.com') &&
107
+ conn.hget(customer.dbkey, 'email')
108
+ end
109
+
110
+ # Should return MultiResult
111
+ result.is_a?(MultiResult) && result.results.last == 'permissive@example.com'
112
+ ensure
113
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
114
+ Familia.configure { |config| config.transaction_mode = :strict }
115
+ end
116
+ #=> true
117
+
118
+ ## Normal transactions still work with CreateConnectionHandler
119
+ begin
120
+ customer = TransactionModeTestCustomer.new(custid: 'normal_test')
121
+
122
+ result = customer.transaction do |conn|
123
+ # Should be Redis::MultiConnection for normal transactions
124
+ conn.class == Redis::MultiConnection &&
125
+ conn.hset(customer.dbkey, 'type', 'normal transaction') &&
126
+ conn.hget(customer.dbkey, 'type')
127
+ end
128
+
129
+ result.is_a?(MultiResult) && result.results.last == 'normal transaction'
130
+ end
131
+ #=> true
132
+
133
+ ## IndividualCommandProxy collects results correctly
134
+ begin
135
+ Familia.configure { |config| config.transaction_mode = :permissive }
136
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
137
+
138
+ customer = TransactionModeTestCustomer.new(custid: 'proxy_test')
139
+
140
+ result = customer.transaction do |conn|
141
+ conn.hset(customer.dbkey, 'field1', 'value1')
142
+ conn.hget(customer.dbkey, 'field1')
143
+ end
144
+
145
+ # Check that results are collected and it's a MultiResult
146
+ result.is_a?(MultiResult) && result.results.size >= 2
147
+ ensure
148
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
149
+ Familia.configure { |config| config.transaction_mode = :strict }
150
+ end
151
+ #=> true
152
+
153
+ ## MultiResult success detection works with individual commands
154
+ begin
155
+ Familia.configure { |config| config.transaction_mode = :permissive }
156
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
157
+
158
+ customer = TransactionModeTestCustomer.new(custid: 'success_test')
159
+
160
+ result = customer.transaction do |conn|
161
+ conn.hset(customer.dbkey, 'status', 'active') # Returns 1
162
+ end
163
+
164
+ # Should be successful since 1 is considered success
165
+ result.successful?
166
+ ensure
167
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
168
+ Familia.configure { |config| config.transaction_mode = :strict }
169
+ end
170
+ #=> true
171
+
172
+ ## Global transaction methods also respect transaction modes
173
+ begin
174
+ Familia.configure { |config| config.transaction_mode = :permissive }
175
+
176
+ # Force a handler that doesn't support transactions
177
+ original_provider = Familia.connection_provider
178
+ test_connection = Familia.create_dbclient
179
+ Familia.connection_provider = ->(_uri) { test_connection }
180
+
181
+ # Global transaction should also fallback
182
+ result = Familia.transaction do |conn|
183
+ conn.set('global_test_key', 'global_test_value')
184
+ conn.get('global_test_key')
185
+ end
186
+
187
+ result.is_a?(MultiResult) && result.results.last == 'global_test_value'
188
+ ensure
189
+ Familia.connection_provider = original_provider
190
+ Familia.configure { |config| config.transaction_mode = :strict }
191
+ end
192
+ #=> true
193
+
194
+ ## Transaction fallback preserves connection context
195
+ begin
196
+ Familia.configure { |config| config.transaction_mode = :permissive }
197
+
198
+ # Test with logical database setting
199
+ class DatabaseTestCustomer < Familia::Horreum
200
+ logical_database 5
201
+ identifier_field :custid
202
+ field :custid
203
+ end
204
+
205
+ DatabaseTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
206
+ customer = DatabaseTestCustomer.new(custid: 'db_test')
207
+
208
+ result = customer.transaction do |conn|
209
+ # Commands should execute on the correct database
210
+ conn.set('db_test_key', 'db_test_value')
211
+ conn.get('db_test_key')
212
+ end
213
+
214
+ result.results.last == 'db_test_value'
215
+ ensure
216
+ DatabaseTestCustomer.remove_instance_variable(:@dbclient) if DatabaseTestCustomer.instance_variable_defined?(:@dbclient)
217
+ Familia.configure { |config| config.transaction_mode = :strict }
218
+ end
219
+ #=> true
220
+
221
+ ## Transaction modes work with nested calls
222
+ begin
223
+ Familia.configure { |config| config.transaction_mode = :permissive }
224
+ TransactionModeTestCustomer.instance_variable_set(:@dbclient, Familia.create_dbclient)
225
+
226
+ customer = TransactionModeTestCustomer.new(custid: 'nested_test')
227
+
228
+ # Test that nested transactions work
229
+ outer_result = customer.transaction do |outer_conn|
230
+ outer_conn.hset(customer.dbkey, 'outer', 'value')
231
+
232
+ inner_result = customer.transaction do |inner_conn|
233
+ inner_conn.hset(customer.dbkey, 'inner', 'nested')
234
+ end
235
+
236
+ # Inner transaction should return MultiResult
237
+ inner_result.is_a?(MultiResult)
238
+ end
239
+
240
+ # Outer transaction should also return MultiResult
241
+ outer_result.is_a?(MultiResult)
242
+ ensure
243
+ TransactionModeTestCustomer.remove_instance_variable(:@dbclient)
244
+ Familia.configure { |config| config.transaction_mode = :strict }
245
+ end
246
+ #=> true
247
+
248
+ # Cleanup - restore original transaction mode
249
+ Familia.configure { |config| config.transaction_mode = @original_transaction_mode }
@@ -1,10 +1,30 @@
1
1
  # try/core/autoloader_try.rb
2
2
 
3
+ # Tests for Familia::Features::Autoloader
4
+ #
5
+ # TESTING STRATEGY:
6
+ # Autoloading is inherently tricky to test because:
7
+ # 1. Files are loaded once and cached by Ruby's require system
8
+ # 2. We need to simulate different directory structures
9
+ # 3. We need to verify that files are actually loaded (not just found)
10
+ #
11
+ # SOLUTION:
12
+ # Use temporary directories with Dir.mktmpdir to create isolated test
13
+ # environments and write test files that set global variables when
14
+ # loaded ($test_feature_loaded = true). Globals are reset before each
15
+ # test and FileUtils.rm_rf to clean up temp directories after each test.
16
+ #
17
+ # This approach allows us to:
18
+ # - Test actual file loading behavior (not just glob patterns) that verify
19
+ # the directory patterns that autoloader.included generates and also the
20
+ # exclusion logic works correctly.
21
+
3
22
  require_relative '../../lib/familia'
4
23
  require 'fileutils'
5
24
  require 'tmpdir'
6
25
 
7
- # Create test directory structure for Autoloader testing
26
+ # SETUP: Create test directory structure for basic autoloader testing
27
+ # This simulates the lib/familia/features/ directory structure
8
28
  @test_dir = Dir.mktmpdir('familia_autoloader_test')
9
29
  @features_dir = File.join(@test_dir, 'features')
10
30
  @test_file1 = File.join(@features_dir, 'test_feature1.rb')
@@ -30,16 +50,16 @@ File.write(@excluded_file, <<~RUBY)
30
50
  $autoloader_file_loaded = true
31
51
  RUBY
32
52
 
33
- ## Test that Familia::Autoloader exists and is a module
34
- Familia::Autoloader.is_a?(Module)
53
+ ## Test that Familia::Features::Autoloader exists and is a module
54
+ Familia::Features::Autoloader.is_a?(Module)
35
55
  #=> true
36
56
 
37
57
  ## Test that autoload_files class method exists
38
- Familia::Autoloader.respond_to?(:autoload_files)
58
+ Familia::Features::Autoloader.respond_to?(:autoload_files)
39
59
  #=> true
40
60
 
41
61
  ## Test that included class method exists
42
- Familia::Autoloader.respond_to?(:included)
62
+ Familia::Features::Autoloader.respond_to?(:included)
43
63
  #=> true
44
64
 
45
65
  ## Test autoload_files with single pattern
@@ -47,11 +67,12 @@ $test_feature1_loaded = false
47
67
  $test_feature2_loaded = false
48
68
  $autoloader_file_loaded = false
49
69
 
50
- Familia::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
70
+ Familia::Features::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
51
71
  $test_feature1_loaded && $test_feature2_loaded
52
72
  #=> true
53
73
 
54
74
  ## Test that autoload_files respects exclusions (using fresh files)
75
+ # Create a separate test environment to avoid conflicts with cached requires
55
76
  @exclude_test_dir = Dir.mktmpdir('familia_autoloader_exclude_test')
56
77
  @exclude_features_dir = File.join(@exclude_test_dir, 'features')
57
78
  @include_file = File.join(@exclude_features_dir, 'include_me.rb')
@@ -64,7 +85,7 @@ File.write(@exclude_file, '$exclude_me_loaded = true')
64
85
  $include_me_loaded = false
65
86
  $exclude_me_loaded = false
66
87
 
67
- Familia::Autoloader.autoload_files(
88
+ Familia::Features::Autoloader.autoload_files(
68
89
  File.join(@exclude_features_dir, '*.rb'),
69
90
  exclude: ['autoloader.rb']
70
91
  )
@@ -74,6 +95,7 @@ $include_me_loaded && !$exclude_me_loaded
74
95
  #=> true
75
96
 
76
97
  ## Test autoload_files with array of patterns (using fresh files)
98
+ # Test that multiple glob patterns can be processed in a single call
77
99
  @pattern_test_dir = Dir.mktmpdir('familia_autoloader_pattern_test')
78
100
  @pattern_dir1 = File.join(@pattern_test_dir, 'dir1')
79
101
  @pattern_dir2 = File.join(@pattern_test_dir, 'dir2')
@@ -88,7 +110,7 @@ File.write(@pattern_file2, '$pattern2_loaded = true')
88
110
  $pattern1_loaded = false
89
111
  $pattern2_loaded = false
90
112
 
91
- Familia::Autoloader.autoload_files([
113
+ Familia::Features::Autoloader.autoload_files([
92
114
  File.join(@pattern_dir1, '*.rb'),
93
115
  File.join(@pattern_dir2, '*.rb')
94
116
  ])
@@ -99,14 +121,110 @@ $pattern1_loaded && $pattern2_loaded
99
121
  ## Test that included method loads features from features directory
100
122
  # Create a mock module that includes Autoloader
101
123
  @mock_features_module = Module.new do
102
- include Familia::Autoloader
124
+ include Familia::Features::Autoloader
103
125
  end
104
126
 
105
- # The Features module already includes Autoloader, so test indirectly
106
- Familia::Features.ancestors.include?(Familia::Autoloader)
127
+ ## The Features module already includes Autoloader, so test indirectly
128
+ Familia::Features.ancestors.include?(Familia::Features::Autoloader)
129
+ #=> true
130
+
131
+ ## Test normalize_to_config_name method exists
132
+ # This method was added to fix issues with namespaced classes after commit d319d9d
133
+ # moved the namespace splitting logic from snake_case to config_name
134
+ Familia::Features::Autoloader.respond_to?(:normalize_to_config_name)
135
+ #=> true
136
+
137
+ ## Test normalize_to_config_name with simple class name
138
+ Familia::Features::Autoloader.normalize_to_config_name('Customer')
139
+ #=> 'customer'
140
+
141
+ ## Test normalize_to_config_name with PascalCase class name
142
+ Familia::Features::Autoloader.normalize_to_config_name('ApiTestUser')
143
+ #=> 'api_test_user'
144
+
145
+ ## Test normalize_to_config_name with namespaced class name (single level)
146
+ Familia::Features::Autoloader.normalize_to_config_name('V2::Customer')
147
+ #=> 'customer'
148
+
149
+ ## Test normalize_to_config_name with deeply namespaced class name
150
+ Familia::Features::Autoloader.normalize_to_config_name('My::Deep::Nested::Module::ApiTestUser')
151
+ #=> 'api_test_user'
152
+
153
+ ## Test normalize_to_config_name with leading double colon
154
+ Familia::Features::Autoloader.normalize_to_config_name('::Customer')
155
+ #=> 'customer'
156
+
157
+ ## Test normalize_to_config_name handles edge case with anonymous class representation
158
+ Familia::Features::Autoloader.normalize_to_config_name('#<Class:0x0001991a8>::ApiTestUser')
159
+ #=> 'api_test_user'
160
+
161
+ ## Test that autoloader directory patterns work with namespaced classes
162
+ # This tests the core fix: ensuring that namespaced classes like TestNamespace::ApiTestModule
163
+ # correctly generate directory patterns using only the demodularized name (api_test_module)
164
+ # rather than the full namespaced name
165
+ # Create a test directory structure that simulates what would happen
166
+ # when a namespaced class includes the autoloader
167
+ @namespace_pattern_test_dir = Dir.mktmpdir('familia_autoloader_namespace_test')
168
+ @base_path = @namespace_pattern_test_dir
169
+ @config_name = 'api_test_module' # This would be the result of normalize_to_config_name
170
+
171
+ # Create directory structure for different patterns
172
+ @features_global_dir = File.join(@base_path, 'features')
173
+ @features_config_dir = File.join(@base_path, @config_name, 'features')
174
+ @features_file = File.join(@base_path, @config_name, 'features.rb')
175
+
176
+ FileUtils.mkdir_p(@features_global_dir)
177
+ FileUtils.mkdir_p(@features_config_dir)
178
+
179
+ # Write test files for each pattern
180
+ @global_feature = File.join(@features_global_dir, 'global_feature.rb')
181
+ @config_feature = File.join(@features_config_dir, 'config_feature.rb')
182
+
183
+ File.write(@global_feature, '$global_feature_loaded = true')
184
+ File.write(@config_feature, '$config_feature_loaded = true')
185
+ File.write(@features_file, '$features_file_loaded = true')
186
+
187
+ # These are the exact patterns that autoloader.included generates:
188
+ # 1. Global features dir: base_path/features/*.rb
189
+ # 2. Config-specific features dir: base_path/config_name/features/*.rb
190
+ # 3. Config-specific features file: base_path/config_name/features.rb
191
+ @dir_patterns = [
192
+ File.join(@base_path, 'features', '*.rb'),
193
+ File.join(@base_path, @config_name, 'features', '*.rb'),
194
+ File.join(@base_path, @config_name, 'features.rb'),
195
+ ]
196
+ # Verify all three patterns are correctly constructed
197
+ @dir_patterns.length
198
+ #=> 3
199
+
200
+ # Reset test flags - critical for testing actual file loading behavior
201
+ $global_feature_loaded = false
202
+ $config_feature_loaded = false
203
+ $features_file_loaded = false
204
+
205
+ ## Test that global features pattern matches correctly
206
+ Dir.glob(@dir_patterns[0]).length
207
+ #=> 1
208
+
209
+ ## Test that config-specific features pattern matches correctly
210
+ Dir.glob(@dir_patterns[1]).length
211
+ #=> 1
212
+
213
+ ## Test that config-specific features.rb file exists
214
+ File.exist?(@dir_patterns[2])
215
+ #=> true
216
+
217
+ ## Test loading all patterns simulates autoloader.included behavior
218
+ # This simulates what happens when a class includes Familia::Features::Autoloader
219
+ # All three file patterns should be processed and their contents loaded
220
+ Familia::Features::Autoloader.autoload_files(@dir_patterns)
221
+
222
+ # Verify all three test files were actually loaded (not just found)
223
+ $global_feature_loaded && $config_feature_loaded && $features_file_loaded
107
224
  #=> true
108
225
 
109
226
  # Cleanup test files and directories
110
227
  FileUtils.rm_rf(@test_dir)
111
228
  FileUtils.rm_rf(@exclude_test_dir)
112
229
  FileUtils.rm_rf(@pattern_test_dir)
230
+ FileUtils.rm_rf(@namespace_pattern_test_dir)
@@ -20,23 +20,23 @@ uri.host
20
20
  #=> "localhost"
21
21
 
22
22
  ## Can establish Database connection
23
+ Familia.create_dbclient
24
+ #=:> Redis
25
+
26
+ ## Can establish Database connection with deprecated method
23
27
  Familia.connect
24
28
  #=:> Redis
25
29
 
26
- ## Can connect to different URI
30
+ ## Can create connection to different URI
27
31
  ## Doesn't confirm the logical DB number, dbclient.options raises an error?
28
32
  test_uri = 'redis://localhost:6379/2'
29
- Familia.connect(test_uri)
33
+ Familia.create_dbclient(test_uri)
30
34
  #=:> Redis
31
35
 
32
36
  ## Database client responds to basic commands
33
37
  Familia.dbclient.ping
34
38
  #=> "PONG"
35
39
 
36
- ## Multiple connections are managed separately
37
- Familia.database_clients.size >= 1
38
- #=> true
39
-
40
40
  ## Can enable Database logging
41
41
  Familia.enable_database_logging = true
42
42
  Familia.enable_database_logging
@@ -48,7 +48,7 @@ Familia.enable_database_counter
48
48
  #=> true
49
49
 
50
50
  ## Middleware gets registered when enabled
51
- dbclient = Familia.connect('redis://localhost:6379/3')
51
+ dbclient = Familia.create_dbclient('redis://localhost:6379/3')
52
52
  dbclient.ping
53
53
  #=> "PONG"
54
54