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,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')
@@ -52,6 +72,7 @@ $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')
@@ -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')
@@ -102,11 +124,107 @@ $pattern1_loaded && $pattern2_loaded
102
124
  include Familia::Features::Autoloader
103
125
  end
104
126
 
105
- # The Features module already includes Autoloader, so test indirectly
127
+ ## The Features module already includes Autoloader, so test indirectly
106
128
  Familia::Features.ancestors.include?(Familia::Features::Autoloader)
107
129
  #=> true
108
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
224
+ #=> true
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
 
@@ -0,0 +1,130 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Define test classes in global namespace
4
+ class ::TestVehicle < Familia::Horreum
5
+ identifier_field :vin
6
+ field :vin
7
+ field :make
8
+ field :model
9
+ field :year
10
+ feature :expiration
11
+ list :maintenance_log
12
+ set :tags
13
+ end
14
+
15
+ class ::TestCar < ::TestVehicle
16
+ field :doors
17
+ field :fuel_type
18
+ end
19
+
20
+ class ::TestElectricCar < ::TestCar
21
+ field :battery_capacity
22
+ field :range_miles
23
+ end
24
+
25
+ class ::TestMotorcycle < ::TestVehicle
26
+ field :engine_cc
27
+ field :has_sidecar
28
+ end
29
+
30
+ class ::TestBaseModel < Familia::Horreum
31
+ end
32
+
33
+ class ::TestConcreteModel < ::TestBaseModel
34
+ identifier_field :id
35
+ field :name
36
+ end
37
+
38
+ ## Creates a parent class with various configurations
39
+ @vehicle = TestVehicle.new(vin: 'ABC123', make: 'Toyota', model: 'Camry', year: 2020)
40
+
41
+ ## Parent class has expected configuration
42
+ TestVehicle.identifier_field
43
+ #=> :vin
44
+
45
+ ## Parent class has fields defined
46
+ TestVehicle.fields
47
+ #=> [:vin, :make, :model, :year]
48
+
49
+ ## Parent class has features enabled
50
+ TestVehicle.features_enabled
51
+ #=> [:expiration]
52
+
53
+ ## Child class inherits parent identifier_field
54
+ TestCar.identifier_field
55
+ #=> :vin
56
+
57
+ ## Child class inherits parent fields and adds its own
58
+ TestCar.fields
59
+ #=> [:vin, :make, :model, :year, :doors, :fuel_type]
60
+
61
+ ## Child class inherits parent features
62
+ TestCar.features_enabled
63
+ #=> [:expiration]
64
+
65
+ ## Child instance works with inherited configuration
66
+ @car = TestCar.new(vin: 'DEF456', make: 'Honda', model: 'Civic', year: 2021, doors: 4, fuel_type: 'gasoline')
67
+ @car.identifier
68
+ #=> "DEF456"
69
+
70
+ ## Child instance can access inherited fields
71
+ @car.make
72
+ #=> "Honda"
73
+
74
+ ## Child instance can access new fields
75
+ @car.doors
76
+ #=> 4
77
+
78
+ ## Child instance inherits DataType relationships
79
+ @car.maintenance_log.class
80
+ #=> Familia::ListKey
81
+
82
+ ## Child instance can use inherited DataType relationships
83
+ @car.tags << 'reliable'
84
+ @car.tags.members
85
+ #=> ["reliable"]
86
+
87
+ ## Grandchild inherits all ancestor configuration
88
+ TestElectricCar.identifier_field
89
+ #=> :vin
90
+
91
+ ## Grandchild inherits all ancestor fields
92
+ TestElectricCar.fields
93
+ #=> [:vin, :make, :model, :year, :doors, :fuel_type, :battery_capacity, :range_miles]
94
+
95
+ ## Grandchild inherits all ancestor features
96
+ TestElectricCar.features_enabled
97
+ #=> [:expiration]
98
+
99
+ ## Grandchild instance works correctly
100
+ @electric = TestElectricCar.new(vin: 'TESLA123', make: 'Tesla', model: 'Model 3', doors: 4, battery_capacity: 75)
101
+ @electric.identifier
102
+ #=> "TESLA123"
103
+
104
+ ## Parent and child classes remain independent after inheritance
105
+ TestVehicle.fields.size
106
+ #=> 4
107
+
108
+ ## Child class has correct field count
109
+ TestCar.fields.size
110
+ #=> 6
111
+
112
+ ## Grandchild class has correct field count
113
+ TestElectricCar.fields.size
114
+ #=> 8
115
+
116
+ ## Parent field count unchanged after adding new child
117
+ TestVehicle.fields.size
118
+ #=> 4
119
+
120
+ ## New child class has correct field count
121
+ TestMotorcycle.fields.size
122
+ #=> 6
123
+
124
+ ## Child of empty parent inherits correctly
125
+ TestConcreteModel.identifier_field
126
+ #=> :id
127
+
128
+ ## Child of empty parent has correct fields
129
+ TestConcreteModel.fields
130
+ #=> [:name]