familia 2.0.0.pre17 → 2.0.0.pre19

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +118 -6
  3. data/CLAUDE.md +43 -11
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +52 -0
  7. data/bin/irb +1 -1
  8. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  9. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  10. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  11. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/guides/feature-expiration.md +18 -18
  14. data/docs/migrating/v2.0.0-pre18.md +58 -0
  15. data/docs/migrating/v2.0.0-pre19.md +197 -0
  16. data/docs/qodo-merge-compliance.md +96 -0
  17. data/examples/datatype_standalone.rb +281 -0
  18. data/lib/familia/base.rb +0 -2
  19. data/lib/familia/connection/behavior.rb +252 -0
  20. data/lib/familia/connection/handlers.rb +95 -0
  21. data/lib/familia/connection/middleware.rb +58 -4
  22. data/lib/familia/connection/operation_core.rb +1 -1
  23. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  24. data/lib/familia/connection/transaction_core.rb +7 -9
  25. data/lib/familia/connection.rb +2 -1
  26. data/lib/familia/data_type/connection.rb +151 -7
  27. data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
  28. data/lib/familia/data_type/serialization.rb +9 -5
  29. data/lib/familia/data_type/types/hashkey.rb +1 -1
  30. data/lib/familia/data_type.rb +2 -2
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/errors.rb +51 -14
  34. data/lib/familia/features/autoloader.rb +3 -1
  35. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  36. data/lib/familia/features/expiration/extensions.rb +8 -10
  37. data/lib/familia/features/expiration.rb +19 -19
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
  40. data/lib/familia/features/relationships/indexing.rb +37 -42
  41. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/field_type.rb +2 -1
  44. data/lib/familia/horreum/connection.rb +11 -35
  45. data/lib/familia/horreum/database_commands.rb +130 -11
  46. data/lib/familia/horreum/definition.rb +8 -38
  47. data/lib/familia/horreum/management.rb +38 -27
  48. data/lib/familia/horreum/persistence.rb +191 -67
  49. data/lib/familia/horreum/serialization.rb +94 -73
  50. data/lib/familia/horreum/utils.rb +0 -8
  51. data/lib/familia/horreum.rb +41 -18
  52. data/lib/familia/identifier_extractor.rb +60 -0
  53. data/lib/familia/logging.rb +268 -112
  54. data/lib/familia/refinements.rb +0 -1
  55. data/lib/familia/settings.rb +7 -7
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -2
  58. data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
  59. data/pr_agent.toml +31 -0
  60. data/pr_compliance_checklist.yaml +45 -0
  61. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  62. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  63. data/try/edge_cases/json_serialization_try.rb +2 -2
  64. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  65. data/try/edge_cases/race_conditions_try.rb +1 -1
  66. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  67. data/try/edge_cases/string_coercion_try.rb +5 -5
  68. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  69. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  70. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  71. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  72. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  77. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  79. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  80. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  81. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  82. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  83. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  84. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  85. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  86. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  87. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  88. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  89. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  94. data/try/features/expiration/expiration_try.rb +2 -2
  95. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  96. data/try/features/feature_dependencies_try.rb +1 -1
  97. data/try/features/feature_improvements_try.rb +1 -1
  98. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  99. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  100. data/try/features/quantization/quantization_try.rb +1 -1
  101. data/try/features/real_feature_integration_try.rb +17 -14
  102. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  103. data/try/features/relationships/indexing_try.rb +34 -5
  104. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  105. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  106. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  107. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  108. data/try/features/relationships/relationships_api_changes_try.rb +5 -5
  109. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  110. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  111. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  112. data/try/features/relationships/relationships_performance_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  114. data/try/features/relationships/relationships_try.rb +1 -1
  115. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  116. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  117. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  118. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  119. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  121. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  122. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
  123. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  124. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  125. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  126. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
  127. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
  128. data/try/{core → integration/connection}/pools_try.rb +1 -1
  129. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  130. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  131. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  135. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  136. data/try/{core → integration}/create_method_try.rb +23 -23
  137. data/try/integration/cross_component_try.rb +1 -1
  138. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  139. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  140. data/try/{core → integration}/database_consistency_try.rb +11 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
  144. data/try/{models → integration/models}/customer_try.rb +1 -1
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +2 -2
  147. data/try/{core → integration}/persistence_operations_try.rb +163 -11
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  156. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  157. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  158. data/try/{core → unit/core}/connection_try.rb +1 -1
  159. data/try/{core → unit/core}/errors_try.rb +1 -1
  160. data/try/{core → unit/core}/extensions_try.rb +1 -1
  161. data/try/unit/core/familia_logger_try.rb +110 -0
  162. data/try/{core → unit/core}/familia_try.rb +1 -1
  163. data/try/{core → unit/core}/middleware_try.rb +41 -1
  164. data/try/{core → unit/core}/settings_try.rb +1 -1
  165. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  166. data/try/{core → unit/core}/tools_try.rb +1 -1
  167. data/try/{core → unit/core}/utils_try.rb +17 -14
  168. data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
  169. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  176. data/try/{data_types → unit/data_types}/string_try.rb +2 -2
  177. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  178. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
  179. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +4 -4
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
  182. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  183. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
  193. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  194. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  195. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  196. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  198. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  199. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  200. metadata +147 -126
  201. data/lib/familia/distinguisher.rb +0 -85
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/try/{debugging → support/debugging}/README.md +0 -0
  205. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  206. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  207. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  227. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  228. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  229. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  230. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  231. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  233. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  234. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  235. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,247 @@
1
+ # DataType Transaction Support Tryouts
2
+ #
3
+ # Tests transaction support for DataType objects, covering both parent-owned
4
+ # DataTypes (delegating to parent) and standalone DataTypes (managing their
5
+ # own connections). Validates atomic operations, connection context handling,
6
+ # and integration with the transaction mode system.
7
+
8
+ require_relative '../../support/helpers/test_helpers'
9
+
10
+ # Setup - Create test model with various DataType fields
11
+ class TransactionTestUser < Familia::Horreum
12
+ logical_database 2
13
+ identifier_field :userid
14
+ field :userid
15
+ field :name
16
+ field :email
17
+
18
+ # Instance-level DataTypes
19
+ sorted_set :scores
20
+ hashkey :profile
21
+ set :tags
22
+ list :activity
23
+ counter :visits
24
+ string :bio
25
+ end
26
+
27
+ @user = TransactionTestUser.new(userid: 'txn_user_001')
28
+ @user.name = 'Transaction Tester'
29
+ @user.save
30
+
31
+ ## Parent-owned SortedSet can execute transaction
32
+ result = @user.scores.transaction do |conn|
33
+ conn.zadd(@user.scores.dbkey, 100, 'level1')
34
+ conn.zadd(@user.scores.dbkey, 200, 'level2')
35
+ conn.zadd(@user.scores.dbkey, 300, 'level3')
36
+ end
37
+ [result.is_a?(MultiResult), @user.scores.members.sort]
38
+ #=> [true, ["level1", "level2", "level3"]]
39
+
40
+ ## Parent-owned HashKey can execute transaction
41
+ result = @user.profile.transaction do |conn|
42
+ conn.hset(@user.profile.dbkey, 'city', 'San Francisco')
43
+ conn.hset(@user.profile.dbkey, 'country', 'USA')
44
+ conn.hget(@user.profile.dbkey, 'city')
45
+ end
46
+ [result.is_a?(MultiResult), result.results.last, @user.profile['country']]
47
+ #=> [true, "San Francisco", "USA"]
48
+
49
+ ## Parent-owned UnsortedSet can execute transaction
50
+ result = @user.tags.transaction do |conn|
51
+ conn.sadd(@user.tags.dbkey, 'ruby')
52
+ conn.sadd(@user.tags.dbkey, 'redis')
53
+ conn.scard(@user.tags.dbkey)
54
+ end
55
+ [result.is_a?(MultiResult), result.results.last, @user.tags.members.sort]
56
+ #=> [true, 2, ["redis", "ruby"]]
57
+
58
+ ## Parent-owned List can execute transaction
59
+ result = @user.activity.transaction do |conn|
60
+ conn.rpush(@user.activity.dbkey, 'login')
61
+ conn.rpush(@user.activity.dbkey, 'view_profile')
62
+ conn.rpush(@user.activity.dbkey, 'logout')
63
+ conn.llen(@user.activity.dbkey)
64
+ end
65
+ [result.is_a?(MultiResult), result.results.last, @user.activity.members]
66
+ #=> [true, 3, ["login", "view_profile", "logout"]]
67
+
68
+ ## Parent-owned Counter can execute transaction
69
+ result = @user.visits.transaction do |conn|
70
+ conn.set(@user.visits.dbkey, 0)
71
+ conn.incr(@user.visits.dbkey)
72
+ conn.incr(@user.visits.dbkey)
73
+ conn.get(@user.visits.dbkey)
74
+ end
75
+ [result.is_a?(MultiResult), result.results.last.to_i, @user.visits.value]
76
+ #=> [true, 2, 2]
77
+
78
+ ## Parent-owned StringKey can execute transaction
79
+ result = @user.bio.transaction do |conn|
80
+ conn.set(@user.bio.dbkey, 'Ruby developer')
81
+ conn.append(@user.bio.dbkey, ' and Redis enthusiast')
82
+ conn.get(@user.bio.dbkey)
83
+ end
84
+ [result.is_a?(MultiResult), result.results.last, @user.bio.value]
85
+ #=> [true, "Ruby developer and Redis enthusiast", "Ruby developer and Redis enthusiast"]
86
+
87
+ ## Standalone SortedSet can execute transaction
88
+ leaderboard = Familia::SortedSet.new('game:leaderboard')
89
+ leaderboard.delete!
90
+ result = leaderboard.transaction do |conn|
91
+ conn.zadd(leaderboard.dbkey, 500, 'player1')
92
+ conn.zadd(leaderboard.dbkey, 600, 'player2')
93
+ conn.zadd(leaderboard.dbkey, 450, 'player3')
94
+ conn.zcard(leaderboard.dbkey)
95
+ end
96
+ [result.is_a?(MultiResult), result.results.last, leaderboard.members.size]
97
+ #=> [true, 3, 3]
98
+
99
+ ## Standalone HashKey can execute transaction
100
+ cache = Familia::HashKey.new('app:cache')
101
+ cache.delete!
102
+ result = cache.transaction do |conn|
103
+ conn.hset(cache.dbkey, 'key1', 'value1')
104
+ conn.hset(cache.dbkey, 'key2', 'value2')
105
+ conn.hkeys(cache.dbkey)
106
+ end
107
+ [result.is_a?(MultiResult), result.results.last.sort, cache.keys.sort]
108
+ #=> [true, ["key1", "key2"], ["key1", "key2"]]
109
+
110
+ ## Standalone UnsortedSet can execute transaction
111
+ global_tags = Familia::UnsortedSet.new('app:tags')
112
+ global_tags.delete!
113
+ result = global_tags.transaction do |conn|
114
+ conn.sadd(global_tags.dbkey, 'tag1')
115
+ conn.sadd(global_tags.dbkey, 'tag2')
116
+ conn.smembers(global_tags.dbkey)
117
+ end
118
+ [result.is_a?(MultiResult), result.results.last.sort, global_tags.members.sort]
119
+ #=> [true, ["tag1", "tag2"], ["tag1", "tag2"]]
120
+
121
+ ## Standalone StringKey can execute transaction
122
+ session_data = Familia::StringKey.new('session:abc123')
123
+ session_data.delete!
124
+ result = session_data.transaction do |conn|
125
+ conn.set(session_data.dbkey, '{"user_id": 123}')
126
+ conn.expire(session_data.dbkey, 3600)
127
+ conn.get(session_data.dbkey)
128
+ end
129
+ [result.is_a?(MultiResult), result.results.last, session_data.value]
130
+ #=> [true, "{\"user_id\": 123}", "{\"user_id\": 123}"]
131
+
132
+ ## Transaction with logical_database option works
133
+ custom_cache = Familia::HashKey.new('custom:cache', logical_database: 3)
134
+ custom_cache.delete!
135
+ result = custom_cache.transaction do |conn|
136
+ conn.hset(custom_cache.dbkey, 'setting', 'enabled')
137
+ conn.hget(custom_cache.dbkey, 'setting')
138
+ end
139
+ [result.is_a?(MultiResult), result.results.last]
140
+ #=> [true, "enabled"]
141
+
142
+ ## Transaction provides correct connection object type
143
+ conn_class = nil
144
+ @user.scores.transaction do |conn|
145
+ conn_class = conn.class.name
146
+ end
147
+ conn_class
148
+ #=> "Redis::MultiConnection"
149
+
150
+ ## Transaction with direct_access works correctly
151
+ result = @user.profile.transaction do |trans_conn|
152
+ trans_conn.hset(@user.profile.dbkey, 'status', 'active')
153
+
154
+ # direct_access should use the same transaction connection
155
+ @user.profile.direct_access do |conn, key|
156
+ conn.object_id == trans_conn.object_id &&
157
+ conn.hset(key, 'verified', 'true')
158
+ end
159
+ end
160
+ [@user.profile['status'], @user.profile['verified']]
161
+ #=> ["active", "true"]
162
+
163
+ ## Transaction atomicity - all commands succeed or none
164
+ test_zset = Familia::SortedSet.new('atomic:test')
165
+ test_zset.delete!
166
+ test_zset.add('initial', 1)
167
+
168
+ begin
169
+ test_zset.transaction do |conn|
170
+ conn.zadd(test_zset.dbkey, 100, 'member1')
171
+ conn.zadd(test_zset.dbkey, 200, 'member2')
172
+ raise 'Intentional error to test rollback'
173
+ end
174
+ rescue => e
175
+ # Transaction should have rolled back
176
+ test_zset.members
177
+ end
178
+ #=> ["initial"]
179
+
180
+ ## Nested transactions with parent-owned DataTypes work
181
+ outer_result = @user.scores.transaction do |outer_conn|
182
+ outer_conn.zadd(@user.scores.dbkey, 999, 'outer_member')
183
+
184
+ inner_result = @user.tags.transaction do |inner_conn|
185
+ inner_conn.sadd(@user.tags.dbkey, 'nested_tag')
186
+ end
187
+
188
+ inner_result.is_a?(MultiResult)
189
+ end
190
+ [outer_result.is_a?(MultiResult), @user.tags.member?('nested_tag')]
191
+ #=> [true, true]
192
+
193
+ ## Transaction respects transaction modes (permissive)
194
+ begin
195
+ original_mode = Familia.transaction_mode
196
+ Familia.configure { |config| config.transaction_mode = :permissive }
197
+
198
+ # Force a cached connection to trigger fallback
199
+ @user.class.instance_variable_set(:@dbclient, Familia.create_dbclient)
200
+
201
+ result = @user.scores.transaction do |conn|
202
+ # Should be IndividualCommandProxy in fallback mode
203
+ conn.class == Familia::Connection::IndividualCommandProxy &&
204
+ conn.zadd(@user.scores.dbkey, 888, 'fallback_test')
205
+ end
206
+
207
+ result.is_a?(MultiResult)
208
+ ensure
209
+ @user.class.remove_instance_variable(:@dbclient)
210
+ Familia.configure { |config| config.transaction_mode = original_mode }
211
+ end
212
+ #=> true
213
+
214
+ ## Transaction with empty block returns empty MultiResult
215
+ result = @user.scores.transaction { |conn| }
216
+ [result.is_a?(MultiResult), result.results.empty?]
217
+ #=> [true, true]
218
+
219
+ ## Transaction connection uses parent's logical_database
220
+ # TransactionTestUser has logical_database 2
221
+ # Parent-owned DataType delegates to parent, verify via class setting
222
+ @user.scores.delete!
223
+ @user.scores.transaction do |conn|
224
+ conn.zadd(@user.scores.dbkey, 1, 'test_member')
225
+ end
226
+ TransactionTestUser.logical_database
227
+ #=> 2
228
+
229
+ ## Multiple DataType types in single transaction
230
+ result = @user.scores.transaction do |conn|
231
+ # Can operate on different DataTypes using same connection
232
+ conn.zadd(@user.scores.dbkey, 777, 'multi_test')
233
+ conn.hset(@user.profile.dbkey, 'multi', 'yes')
234
+ conn.sadd(@user.tags.dbkey, 'multi_tag')
235
+ conn.rpush(@user.activity.dbkey, 'multi_action')
236
+ end
237
+ [
238
+ result.is_a?(MultiResult),
239
+ @user.scores.member?('multi_test'),
240
+ @user.profile['multi'],
241
+ @user.tags.member?('multi_tag'),
242
+ @user.activity.members.include?('multi_action')
243
+ ]
244
+ #=> [true, true, "yes", true, true]
245
+
246
+ # Cleanup
247
+ @user.destroy!
@@ -3,7 +3,7 @@
3
3
  # Database consistency verification and edge case testing
4
4
  # Complements persistence_operations_try.rb with deeper consistency checks
5
5
 
6
- require_relative '../helpers/test_helpers'
6
+ require_relative '../support/helpers/test_helpers'
7
7
 
8
8
  # Test class with different field types for consistency verification
9
9
  class ConsistencyTestModel < Familia::Horreum
@@ -55,15 +55,18 @@ key_parts = dbkey.split(':')
55
55
  # Refresh and verify data integrity
56
56
  @serial_test.refresh!
57
57
  [@serial_test.name, @serial_test.active, @serial_test.metadata]
58
- #=> ['Serialization Test', 'true', {:key=>'value', :array=>[1, 2, 3]}]
58
+ #=> ["Serialization Test", true, {"key"=>"value", "array"=>[1, 2, 3]}]
59
59
 
60
60
  ## Hash field count matches object field count
61
+ @serial_test = ConsistencyTestModel.new(id: next_test_id)
62
+ @serial_test.save
61
63
  expected_fields = @serial_test.class.persistent_fields.length
62
64
  redis_field_count = Familia.dbclient.hlen(@serial_test.dbkey)
63
65
  actual_object_fields = @serial_test.to_h.keys.length
64
- # All should match (redis may have fewer due to nil exclusion)
65
- [expected_fields >= redis_field_count, redis_field_count, actual_object_fields]
66
- #=> [true, 5, 5]
66
+ # The JSON Serializer stores all fields (including nil as "null")
67
+ # Expected fields (5) >= redis count (5) >= to_h count (5, even though email is nil)
68
+ [expected_fields, redis_field_count, actual_object_fields]
69
+ #=> [5, 5, 5]
67
70
 
68
71
  ## Memory vs persistence state consistency after save
69
72
  @consistency_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Memory Test', email: 'test@example.com')
@@ -73,9 +76,9 @@ actual_object_fields = @serial_test.to_h.keys.length
73
76
  memory_name = @consistency_obj.name
74
77
  memory_email = @consistency_obj.email
75
78
 
76
- # Get persistence state
77
- redis_name = Familia.dbclient.hget(@consistency_obj.dbkey, 'name')
78
- redis_email = Familia.dbclient.hget(@consistency_obj.dbkey, 'email')
79
+ # Get persistence state (deserialize from JSON storage)
80
+ redis_name = @consistency_obj.deserialize_value(Familia.dbclient.hget(@consistency_obj.dbkey, 'name'))
81
+ redis_email = @consistency_obj.deserialize_value(Familia.dbclient.hget(@consistency_obj.dbkey, 'email'))
79
82
 
80
83
  [memory_name == redis_name, memory_email == redis_email]
81
84
  #=> [true, true]
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'time'
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../support/helpers/test_helpers'
6
6
 
7
7
  ## Has all datatype relativess
8
8
  registered_types = Familia::DataType.registered_types.keys
@@ -1,6 +1,6 @@
1
1
  # try/core/familia_members_methods_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  # Tests for new methods: demodularize, familia_name, and resolve_class
6
6
 
@@ -1,6 +1,6 @@
1
1
  # try/models/customer_safedump_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  # Setup
6
6
  @now = Familia.now.to_i
@@ -39,7 +39,11 @@ require_relative '../helpers/test_helpers'
39
39
 
40
40
  ## Safe dump includes correct updated timestamp
41
41
  @safe_dump[:updated]
42
- #=> @now
42
+ #=:> Float
43
+
44
+ ## Safe dump includes correct updated timestamp
45
+ @safe_dump[:updated].to_i
46
+ #=> @now.to_i
43
47
 
44
48
  ## Safe dump includes correct secrets_created count
45
49
  @customer.secrets_created.increment
@@ -1,7 +1,7 @@
1
1
  # try/models/customer_try.rb
2
2
 
3
3
  # Customer Tryouts
4
- require_relative '../helpers/test_helpers'
4
+ require_relative '../../support/helpers/test_helpers'
5
5
 
6
6
  # Setup
7
7
  @now = Time.now.to_f
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test DataType base functionality
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  Familia.debug = false
8
8
 
@@ -1,6 +1,6 @@
1
1
  # try/models/familia_object_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -71,7 +71,7 @@ Customer.all_customers.size
71
71
 
72
72
  ## Familia class clear
73
73
  Customer.all_customers.delete!
74
- #=> true
74
+ #=> 1
75
75
 
76
76
  ## Familia class replace 1 of 4
77
77
  Customer.message.value = 'msg1'
@@ -3,7 +3,7 @@
3
3
  # Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
4
4
  # This test addresses gaps that allowed the exists? bug to go undetected
5
5
 
6
- require_relative '../helpers/test_helpers'
6
+ require_relative '../support/helpers/test_helpers'
7
7
 
8
8
  # Use a simple test class to isolate persistence behavior
9
9
  class PersistenceTestModel < Familia::Horreum
@@ -13,6 +13,27 @@ class PersistenceTestModel < Familia::Horreum
13
13
  field :value
14
14
  end
15
15
 
16
+ # Create model with expiration feature for save_fields testing
17
+ class ExpirationPersistenceTest < Familia::Horreum
18
+ feature :expiration
19
+ identifier_field :id
20
+ field :id
21
+ field :name
22
+ field :email
23
+ field :status
24
+ field :metadata
25
+
26
+ default_expiration 3600 # 1 hour
27
+ end
28
+
29
+ # Simple model without expiration feature
30
+ class SimpleModel < Familia::Horreum
31
+ identifier_field :id
32
+ field :id
33
+ field :name
34
+ field :value
35
+ end
36
+
16
37
  # Clean up any existing test data
17
38
  cleanup_keys = []
18
39
  begin
@@ -111,9 +132,9 @@ result = @sine_new.save_if_not_exists
111
132
  [result, @sine_new.exists?]
112
133
  #=> [true, true]
113
134
 
114
- ## save_if_not_exists raises error for existing object
135
+ ## save_if_not_exists! raises error for existing object
115
136
  @sine_duplicate = PersistenceTestModel.new(id: @sine_new.identifier, name: 'Duplicate')
116
- @sine_duplicate.save_if_not_exists
137
+ @sine_duplicate.save_if_not_exists!
117
138
  #=!> Familia::RecordExistsError
118
139
 
119
140
  ## save_if_not_exists with update_expiration: false
@@ -128,14 +149,10 @@ original_name = 'Original Name'
128
149
  @sine_fail_test.save_if_not_exists
129
150
  # Now create duplicate and verify state doesn't change on failure
130
151
  @sine_fail_duplicate = PersistenceTestModel.new(id: @sine_fail_test.identifier, name: 'Changed Name')
131
- begin
132
- @sine_fail_duplicate.save_if_not_exists
133
- false # Should not reach here
134
- rescue Familia::RecordExistsError
135
- # State should be unchanged
136
- @sine_fail_duplicate.name == 'Changed Name'
137
- end
138
- #=> true
152
+ result = @sine_fail_duplicate.save_if_not_exists
153
+ # save_if_not_exists returns false on failure, state should be unchanged
154
+ [result == false, @sine_fail_duplicate.name == 'Changed Name']
155
+ #=> [true, true]
139
156
 
140
157
  # =============================================
141
158
  # 4. create Method Coverage (MISSING from current tests)
@@ -291,7 +308,142 @@ actual_key = @key_obj.dbkey
291
308
  # Cleanup
292
309
  # =============================================
293
310
 
311
+ # =============================================
312
+ # 8. save_fields Method Coverage
313
+ # =============================================
314
+
315
+ ## save_fields basic functionality with specified fields
316
+ @save_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Original Name', email: 'test@example.com', status: 'active')
317
+ @save_fields_obj.save
318
+ # Modify fields locally
319
+ @save_fields_obj.name = 'Updated Name'
320
+ @save_fields_obj.status = 'inactive'
321
+ @save_fields_obj.metadata = { updated: true }
322
+ # Save only specific fields
323
+ result = @save_fields_obj.save_fields(:name, :metadata)
324
+ [result.class == ExpirationPersistenceTest, @save_fields_obj.exists?]
325
+ #=> [true, true]
326
+
327
+ ## Verify only specified fields were saved
328
+ @save_fields_obj.refresh!
329
+ [@save_fields_obj.name, @save_fields_obj.status, @save_fields_obj.metadata]
330
+ #=> ['Updated Name', 'active', { 'updated' => true }]
331
+
332
+ ## save_fields with update_expiration: true (default)
333
+ @exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Expiration Test')
334
+ @exp_obj.save
335
+ original_ttl = @exp_obj.ttl
336
+ # Wait a moment to ensure TTL decreases
337
+ sleep 0.1
338
+ @exp_obj.name = 'Updated with TTL'
339
+ @exp_obj.save_fields(:name) # Should update expiration by default
340
+ new_ttl = @exp_obj.ttl
341
+ # TTL should be refreshed (closer to default_expiration)
342
+ # Allow for small timing variations
343
+ new_ttl >= (ExpirationPersistenceTest.default_expiration - 10)
344
+ #=> true
345
+
346
+ ## save_fields with update_expiration: false
347
+ @no_exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'No Exp Update')
348
+ @no_exp_obj.save
349
+ # Wait briefly and get TTL
350
+ sleep 0.1
351
+ original_ttl = @no_exp_obj.ttl
352
+ @no_exp_obj.name = 'Updated without TTL'
353
+ @no_exp_obj.save_fields(:name, update_expiration: false)
354
+ new_ttl = @no_exp_obj.ttl
355
+ # TTL should be approximately the same (slightly less due to time passing)
356
+ (new_ttl - original_ttl).abs < 2
357
+ #=> true
358
+
359
+ ## save_fields with multiple fields
360
+ @multi_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Multi', email: 'multi@test.com')
361
+ @multi_fields_obj.save
362
+ @multi_fields_obj.name = 'Multi Updated'
363
+ @multi_fields_obj.email = 'updated@test.com'
364
+ @multi_fields_obj.status = 'new_status'
365
+ result = @multi_fields_obj.save_fields(:name, :email, :status)
366
+ @multi_fields_obj.refresh!
367
+ [@multi_fields_obj.name, @multi_fields_obj.email, @multi_fields_obj.status]
368
+ #=> ['Multi Updated', 'updated@test.com', 'new_status']
369
+
370
+ ## save_fields with string field names
371
+ @string_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'String Fields')
372
+ @string_fields_obj.save
373
+ @string_fields_obj.name = 'Updated via String'
374
+ result = @string_fields_obj.save_fields('name') # String instead of symbol
375
+ @string_fields_obj.refresh!
376
+ @string_fields_obj.name
377
+ #=> 'Updated via String'
378
+
379
+ ## save_fields error handling - empty fields
380
+ @empty_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Empty Test')
381
+ @empty_fields_obj.save
382
+ @empty_fields_obj.save_fields()
383
+ #=!> ArgumentError
384
+
385
+ ## save_fields error handling - unknown field
386
+ @unknown_field_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Unknown Field')
387
+ @unknown_field_obj.save
388
+ @unknown_field_obj.save_fields(:nonexistent_field)
389
+ #=!> ArgumentError
390
+
391
+ ## save_fields with nil values
392
+ @nil_values_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Nil Values', status: 'initial')
393
+ @nil_values_obj.save
394
+ @nil_values_obj.status = nil
395
+ @nil_values_obj.save_fields(:status)
396
+ @nil_values_obj.refresh!
397
+ @nil_values_obj.status
398
+ #=> nil
399
+
400
+ ## save_fields with complex data types (Hash, Array)
401
+ @complex_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Complex')
402
+ @complex_obj.save
403
+ @complex_obj.metadata = {
404
+ tags: ['ruby', 'redis'],
405
+ config: { timeout: 30, retries: 3 },
406
+ enabled: true
407
+ }
408
+ @complex_obj.save_fields(:metadata)
409
+ @complex_obj.refresh!
410
+ expected_metadata = {
411
+ 'tags' => ['ruby', 'redis'],
412
+ 'config' => { 'timeout' => 30, 'retries' => 3 },
413
+ 'enabled' => true
414
+ }
415
+ @complex_obj.metadata == expected_metadata
416
+ #=> true
417
+
418
+ ## save_fields transactional behavior
419
+ @transaction_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Transaction Test')
420
+ @transaction_obj.save
421
+ @transaction_obj.name = 'Updated in Transaction'
422
+ @transaction_obj.email = 'transaction@test.com'
423
+ # All fields should be saved atomically
424
+ @transaction_obj.save_fields(:name, :email)
425
+ @transaction_obj.refresh!
426
+ [@transaction_obj.name, @transaction_obj.email]
427
+ #=> ['Updated in Transaction', 'transaction@test.com']
428
+
429
+ ## save_fields performance with model without expiration feature
430
+
431
+ @simple_obj = SimpleModel.new(id: next_test_id, name: 'Simple', value: 'test')
432
+ @simple_obj.save
433
+ @simple_obj.name = 'Simple Updated'
434
+ # Should work without expiration feature (update_expiration param ignored)
435
+ result = @simple_obj.save_fields(:name, update_expiration: true)
436
+ @simple_obj.refresh!
437
+ @simple_obj.name
438
+ #=> 'Simple Updated'
439
+
440
+ # =============================================
441
+ # Cleanup
442
+ # =============================================
443
+
294
444
  # Clean up test data
295
445
  test_keys = Familia.dbclient.keys('persistencetestmodel:*')
296
446
  test_keys.concat(Familia.dbclient.keys('encryptedpersistencetest:*')) if defined?(EncryptedPersistenceTest)
447
+ test_keys.concat(Familia.dbclient.keys('expirationpersistencetest:*'))
448
+ test_keys.concat(Familia.dbclient.keys('simplemodel:*'))
297
449
  Familia.dbclient.del(*test_keys) if test_keys.any?