familia 2.0.0.pre15 → 2.0.0.pre17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,332 @@
1
+ # try/horreum/destroy_related_fields_cleanup_try.rb
2
+
3
+ # Horreum destroy! Related Fields Cleanup Tryouts
4
+ #
5
+ # Tests that when a Horreum instance is destroyed, all its related fields
6
+ # (lists, sets, sorted sets, hashes, etc.) are also properly cleaned up
7
+ # to prevent orphaned Redis keys.
8
+ #
9
+ # This addresses the bug where destroy! only deleted the main object key
10
+ # but left related field keys in the database.
11
+
12
+ require_relative '../helpers/test_helpers'
13
+
14
+ MANY_FIELD_MULTIPLIER = 10
15
+
16
+ # Test model with various related fields
17
+ class ::DestroyTestUser < Familia::Horreum
18
+ identifier_field :user_id
19
+ field :user_id
20
+ field :name
21
+ field :email
22
+
23
+ # Various DataType relations to test cleanup
24
+ list :activity_log
25
+ set :tags
26
+ zset :scores
27
+ hashkey :settings
28
+ string :status_message
29
+ end
30
+
31
+ class ::CustomOptionsUser < Familia::Horreum
32
+ identifier_field :user_id
33
+ field :user_id
34
+
35
+ # Related fields with various options
36
+ list :custom_log, ttl: 3600
37
+ set :custom_tags, default: ['default_tag']
38
+ zset :custom_scores, class: self
39
+ end
40
+
41
+ class ::ParentModel < Familia::Horreum
42
+ identifier_field :parent_id
43
+ field :parent_id
44
+ list :children_ids
45
+ set :child_tags
46
+ end
47
+
48
+ class ::ChildModel < Familia::Horreum
49
+ identifier_field :child_id
50
+ field :child_id
51
+ field :parent_id
52
+ list :child_activities
53
+ end
54
+
55
+ class ::ManyFieldsModel < Familia::Horreum
56
+ identifier_field :model_id
57
+ field :model_id
58
+
59
+ # Create many different types of related fields
60
+ MANY_FIELD_MULTIPLIER.times do |i|
61
+ list :"list_#{i}"
62
+ set :"set_#{i}"
63
+ zset :"zset_#{i}"
64
+ hashkey :"hash_#{i}"
65
+ end
66
+ end
67
+
68
+ # From transaction_fallback_integration_try.rb for bug verification test
69
+ class ::IntegrationTestUser < Familia::Horreum
70
+ identifier_field :user_id
71
+ field :user_id
72
+ field :name
73
+ field :email
74
+ field :status
75
+ list :activity_log
76
+ set :tags
77
+ zset :scores
78
+ end
79
+
80
+ ## Related fields are cleaned up when instance is destroyed
81
+ user = DestroyTestUser.new(user_id: 'cleanup_test_001', name: 'Test User')
82
+
83
+ # Populate related fields with data
84
+ user.activity_log.add('login')
85
+ user.activity_log.add('profile_update')
86
+
87
+ user.tags.add('premium')
88
+ user.tags.add('verified')
89
+
90
+ user.scores.add('game_score', 100)
91
+ user.scores.add('quiz_score', 85)
92
+
93
+ user.settings['theme'] = 'dark'
94
+ user.settings['notifications'] = 'enabled'
95
+
96
+ user.status_message.value = 'Online'
97
+
98
+ user.save
99
+
100
+ # Verify data exists before destruction
101
+ keys_before = [
102
+ user.dbkey,
103
+ user.activity_log.dbkey,
104
+ user.tags.dbkey,
105
+ user.scores.dbkey,
106
+ user.settings.dbkey,
107
+ user.status_message.dbkey,
108
+ ]
109
+
110
+ keys_exist_before = keys_before.all? { |key| user.dbclient.exists(key) > 0 }
111
+
112
+ # Destroy the user
113
+ destroy_result = user.destroy!
114
+
115
+ # Verify all keys are cleaned up after destruction
116
+ keys_exist_after = keys_before.any? { |key| user.dbclient.exists(key) > 0 }
117
+
118
+ # Should successfully destroy and clean up all related keys
119
+ destroy_result && keys_exist_before && !keys_exist_after
120
+ #=> true
121
+
122
+ ## Class-level destroy! also cleans up related fields
123
+
124
+ user = DestroyTestUser.new(user_id: 'class_destroy_test_001')
125
+
126
+ # Add some related field data
127
+ user.activity_log.add('created')
128
+ user.tags.add('new_user')
129
+ user.scores.add('initial_score', 0)
130
+ user.save
131
+
132
+ # Verify keys exist
133
+ keys_before = [
134
+ user.dbkey,
135
+ user.activity_log.dbkey,
136
+ user.tags.dbkey,
137
+ user.scores.dbkey,
138
+ ]
139
+ keys_exist_before = keys_before.all? { |key| user.dbclient.exists(key) > 0 }
140
+
141
+ # Use class-level destroy!
142
+ destroy_result = DestroyTestUser.destroy!('class_destroy_test_001')
143
+
144
+ # Verify all keys are cleaned up
145
+ keys_exist_after = keys_before.any? { |key| user.dbclient.exists(key) > 0 }
146
+
147
+ destroy_result && keys_exist_before && !keys_exist_after
148
+ #=> true
149
+
150
+ ## Empty related fields don't cause errors during cleanup
151
+ user = DestroyTestUser.new(user_id: 'empty_fields_test_001')
152
+ user.save
153
+
154
+ # Don't add any data to related fields - they should be empty
155
+
156
+ # Destroy should work without errors even with empty related fields
157
+ result = user.destroy!
158
+
159
+ # Verify main key is gone
160
+ main_key_gone = user.dbclient.exists(user.dbkey) == 0
161
+
162
+ result && main_key_gone
163
+ #=> true
164
+
165
+ ## Related fields with custom options are handled properly
166
+ user = CustomOptionsUser.new(user_id: 'custom_options_test_001')
167
+
168
+ # Add data to custom related fields
169
+ user.custom_log.add('custom_event')
170
+ user.custom_tags.add('custom_tag')
171
+ user.custom_scores.add('custom_score', 50)
172
+ user.save
173
+
174
+ # Verify keys exist
175
+ keys_before = [
176
+ user.dbkey,
177
+ user.custom_log.dbkey,
178
+ user.custom_tags.dbkey,
179
+ user.custom_scores.dbkey,
180
+ ]
181
+ keys_exist_before = keys_before.all? { |key| user.dbclient.exists(key) > 0 }
182
+
183
+ # Destroy and verify cleanup
184
+ destroy_result = user.destroy!
185
+ keys_exist_after = keys_before.any? { |key| user.dbclient.exists(key) > 0 }
186
+
187
+ # Clean up the test class to avoid pollution
188
+ Object.send(:remove_const, :CustomOptionsUser) if Object.const_defined?(:CustomOptionsUser)
189
+
190
+ destroy_result && keys_exist_before && !keys_exist_after
191
+ #=> true
192
+
193
+ ## Nested destruction handles complex related field hierarchies
194
+
195
+ parent = ParentModel.new(parent_id: 'parent_001')
196
+ child1 = ChildModel.new(child_id: 'child_001', parent_id: 'parent_001')
197
+ child2 = ChildModel.new(child_id: 'child_002', parent_id: 'parent_001')
198
+
199
+ # Set up relationships
200
+ parent.children_ids.add('child_001')
201
+ parent.children_ids.add('child_002')
202
+ parent.child_tags.add('family_tag')
203
+
204
+ child1.child_activities.add('child1_activity')
205
+ child2.child_activities.add('child2_activity')
206
+
207
+ parent.save
208
+ child1.save
209
+ child2.save
210
+
211
+ # Verify all keys exist
212
+ all_keys = [
213
+ parent.dbkey, parent.children_ids.dbkey, parent.child_tags.dbkey,
214
+ child1.dbkey, child1.child_activities.dbkey,
215
+ child2.dbkey, child2.child_activities.dbkey
216
+ ]
217
+ keys_exist_before = all_keys.all? { |key| parent.dbclient.exists(key) > 0 }
218
+
219
+ # Destroy parent - should clean up all parent's related fields
220
+ parent_destroy_result = parent.destroy!
221
+
222
+ # Parent and its related fields should be gone
223
+ parent_keys = [parent.dbkey, parent.children_ids.dbkey, parent.child_tags.dbkey]
224
+ parent_keys_gone = parent_keys.none? { |key| parent.dbclient.exists(key) > 0 }
225
+
226
+ # Child objects should still exist (they're separate objects)
227
+ child_keys = [child1.dbkey, child1.child_activities.dbkey, child2.dbkey, child2.child_activities.dbkey]
228
+ child_keys_exist = child_keys.all? { |key| child1.dbclient.exists(key) > 0 }
229
+
230
+ # Clean up children
231
+ child1.destroy!
232
+ child2.destroy!
233
+
234
+ # Clean up test classes
235
+ Object.send(:remove_const, :ParentModel) if Object.const_defined?(:ParentModel)
236
+ Object.send(:remove_const, :ChildModel) if Object.const_defined?(:ChildModel)
237
+
238
+ parent_destroy_result && keys_exist_before && parent_keys_gone && child_keys_exist
239
+ #=> true
240
+
241
+ ## Performance check - destroying object with many related fields
242
+ model = ManyFieldsModel.new(model_id: 'many_fields_001')
243
+
244
+ # Add data to some of the fields
245
+ MANY_FIELD_MULTIPLIER.times do |i|
246
+ model.send(:"list_#{i}").add("item_#{i}")
247
+ model.send(:"set_#{i}").add("tag_#{i}")
248
+ model.send(:"zset_#{i}").add("score_#{i}", i * 10)
249
+ model.send(:"hash_#{i}")["key_#{i}"] = "value_#{i}"
250
+ end
251
+
252
+ model.save
253
+
254
+ destroy_result = model.destroy!
255
+
256
+ # Should result in success and also complete in a reasonable amount of
257
+ # time (under 100ms for this test). I acknowledge this is flaky.
258
+ [destroy_result.class, destroy_result.successful?, destroy_result.results.size]
259
+ #=> [MultiResult, true, 41]
260
+ #=%> 100
261
+
262
+ ## Verify transaction_fallback_integration_try.rb bug is fixed
263
+ # Recreate the scenario from the failing test
264
+ user = IntegrationTestUser.new(user_id: 'bugfix_test_001')
265
+
266
+ # Add data to related fields like the original test
267
+ user.activity_log.add('user_created')
268
+ user.activity_log.add('profile_updated')
269
+ user.tags.add('premium')
270
+ user.tags.add('verified')
271
+ user.scores.add('game_score', 100)
272
+ user.scores.add('quiz_score', 85)
273
+
274
+ # Save the user so the main object key exists
275
+ user.save
276
+
277
+ # Verify keys exist before destruction
278
+ keys_before = [
279
+ user.dbkey,
280
+ user.activity_log.dbkey,
281
+ user.tags.dbkey,
282
+ user.scores.dbkey,
283
+ ]
284
+ keys_exist_before = keys_before.all? { |key| user.dbclient.exists(key) > 0 }
285
+
286
+ # The original destroy! call that was leaving orphaned keys
287
+ destroy_result = user.destroy!
288
+
289
+ # Now all related keys should be properly cleaned up
290
+ keys_exist_after = keys_before.any? { |key| user.dbclient.exists(key) > 0 }
291
+
292
+ Object.send(:remove_const, :ManyFieldsModel) if Object.const_defined?(:ManyFieldsModel)
293
+
294
+ destroy_result && keys_exist_before && !keys_exist_after
295
+ #=> true
296
+
297
+ ## Test destroy! with init hook that depends on identifier
298
+ # This verifies that the temp instance initialization fix works correctly
299
+ class TestModelWithInit < Familia::Horreum
300
+ identifier_field :user_id
301
+ field :user_id
302
+ field :region
303
+ list :activities
304
+
305
+ def init(*args, **kwargs)
306
+ # Set region based on user_id (simulates real-world logic)
307
+ self.region = user_id.split('-').first if user_id
308
+ end
309
+ end
310
+
311
+ # Create object - init should set region based on user_id
312
+ init_obj = TestModelWithInit.new(user_id: "us-west-123")
313
+ init_obj.save
314
+ init_obj.activities << "login"
315
+ init_obj.activities << "purchase"
316
+
317
+ # Verify init worked and region is set
318
+ region_set_correctly = init_obj.region == "us"
319
+
320
+ # Verify related field key includes region (would be nil without fix)
321
+ activities_key = init_obj.activities.dbkey
322
+
323
+ # Destroy using class method - temp instance init should execute with identifier
324
+ TestModelWithInit.destroy!("us-west-123")
325
+
326
+ # Verify all keys are cleaned up (including activities with correct key)
327
+ activities_cleaned = TestModelWithInit.dbclient.exists(activities_key).zero?
328
+
329
+ Object.send(:remove_const, :TestModelWithInit) if Object.const_defined?(:TestModelWithInit)
330
+
331
+ region_set_correctly && activities_cleaned
332
+ #=> true
@@ -10,9 +10,9 @@ Familia.debug = false
10
10
  #=> ["tryouts-29@test.com", "John Doe"]
11
11
 
12
12
  ## Keyword argument initialization works (order independent)
13
- @customer2 = Customer.new(name: 'Jane Smith', custid: 'jane@test.com', email: 'jane@example.com')
13
+ @customer2 = Customer.new(name: 'Jane Windows', custid: 'jane@test.com', email: 'jane@example.com')
14
14
  [@customer2.custid, @customer2.name, @customer2.email]
15
- #=> ["jane@test.com", "Jane Smith", "jane@example.com"]
15
+ #=> ["jane@test.com", "Jane Windows", "jane@example.com"]
16
16
 
17
17
  ## Keyword arguments are order independent (different order, same result)
18
18
  @customer3 = Customer.new(email: 'bob@example.com', custid: 'bob@test.com', name: 'Bob Jones')
@@ -50,11 +50,11 @@ Familia.debug = false
50
50
 
51
51
  ## to_h works correctly with keyword-initialized objects
52
52
  @customer2.to_h["name"]
53
- #=> "Jane Smith"
53
+ #=> "Jane Windows"
54
54
 
55
55
  ## to_a works correctly with keyword-initialized objects
56
56
  @customer2.to_a[4] # name field should be the fifth field defined in the class
57
- #=> "Jane Smith"
57
+ #=> "Jane Windows"
58
58
 
59
59
  ## Session has limited fields (only sessid defined)
60
60
  @session1 = Session.new('sess123')
@@ -103,12 +103,16 @@ Familia.debug = false
103
103
  [@customer6, @complex].map(&:delete!)
104
104
  #=> [true, true]
105
105
 
106
- ## "Cleaning up" test objects that were never saved returns false.
106
+ ## "Cleaning up" test objects that were never saved returns true regardless
107
+ ## b/c it takes place in a transaction and it's the transaction's success
108
+ ## that successful? is based on. If you look at the MultiResult#results,
109
+ ## it's an array of 0s, except for the @customer1 that had been saved.
110
+ ## That's b/c DEL command returns the number of keys deleted.
107
111
  @customer1.save
108
112
  [
109
113
  @customer1, @customer2, @customer3, @customer4, @customer6, @customer7,
110
114
  @session1, @session2, @session3,
111
115
  @domain1, @domain2,
112
116
  @partial, @complex
113
- ].map(&:destroy!)
114
- #=> [true, false, false, false, false, false, false, false, false, false, false, false, false]
117
+ ].map(&:destroy!).map(&:areyouhappynow?)
118
+ #==> result.all?(true)
@@ -56,7 +56,7 @@ tags = @test_user.tags
56
56
  scores = @test_user.scores
57
57
  prefs = @test_user.preferences
58
58
  [sessions.class.name, tags.class.name, scores.class.name, prefs.class.name]
59
- #=> ["Familia::List", "Familia::Set", "Familia::SortedSet", "Familia::HashKey"]
59
+ #=> ["Familia::ListKey", "Familia::UnsortedSet", "Familia::SortedSet", "Familia::HashKey"]
60
60
 
61
61
  ## Database types use correct dbkeys
62
62
  @test_user.sessions.dbkey
@@ -72,7 +72,7 @@ prefs = @test_user.preferences
72
72
  @test_user.sessions.size
73
73
  #=> 2
74
74
 
75
- ## Can work with Set Database type
75
+ ## Can work with UnsortedSet Database type
76
76
  @test_user.tags.clear
77
77
  @test_user.tags.add('ruby', 'valkey', 'web')
78
78
  @test_user.tags.size
@@ -80,8 +80,8 @@ prefs = @test_user.preferences
80
80
 
81
81
  ## Can work with SortedSet Database type
82
82
  @test_user.scores.clear
83
- @test_user.scores.add(100, 'level1')
84
- @test_user.scores.add(200, 'level2')
83
+ @test_user.scores.add('level1', 100)
84
+ @test_user.scores.add('level2', 200)
85
85
  @test_user.scores.size
86
86
  #=> 2
87
87
 
@@ -108,16 +108,21 @@ prefs = @test_user.preferences
108
108
  @test_product.views.value
109
109
  #=> 6
110
110
 
111
- ## Database types maintain parent reference
112
- @test_user.sessions.parent == @test_user
113
- #=> true
111
+ ## Database types maintain ParentDefinition reference, not the parent itself
112
+ @test_user.sessions.parent
113
+ #=/> @test_user
114
+ #=:> Familia::Horreum::ParentDefinition
115
+
116
+ ## Database types maintain ParentDefinition reference, not the parent itself
117
+ @test_user.sessions.parent
118
+ #=> Familia::Horreum::ParentDefinition.from_parent(@test_user)
114
119
 
115
120
  ## Database types know their field name
116
121
  @test_user.tags.keystring
117
122
  #=> :tags
118
123
 
119
124
  ## Can check if Database types exist
120
- @test_user.scores.add(50, 'test')
125
+ @test_user.scores.add('test', 50)
121
126
  before_exists = @test_user.scores.exists?
122
127
  @test_user.scores.clear
123
128
  after_exists = @test_user.scores.exists?
@@ -130,15 +135,18 @@ after_exists = @test_user.scores.exists?
130
135
  @test_user.preferences.exists?
131
136
  #=> false
132
137
 
133
- ## Parent object destruction does not clean up relations
134
- @test_user.sessions.add('cleanup_test')
138
+ ## Parent object destruction DOES clean up relations (since v2.0.0.pre16)
139
+ @test_user.sessions.push('cleanup_test')
135
140
  @test_user.destroy!
136
141
  @test_user.sessions.exists?
137
- #=> true
142
+ #=> false
138
143
 
139
144
  ## If the parent instance is still in memory, can use it
140
145
  ## to access and clear the child field.
141
- @test_user.sessions.clear
142
- #=> true
146
+ @test_user.sessions.add(Familia.now)
147
+ @test_user.sessions.size
148
+ #=> 1
149
+
150
+ @test_user.sessions.delete!
143
151
 
144
152
  @test_product.destroy!
@@ -13,8 +13,8 @@ Familia.debug = false
13
13
  #=> true
14
14
 
15
15
  ## save_if_not_exists saves new customer successfully
16
- Familia.dbclient.set('debug:starting_save_if_not_exists_tests', Time.now.to_s)
17
- @test_id = "#{Time.now.to_i}-#{rand(1000)}"
16
+ Familia.dbclient.set('debug:starting_save_if_not_exists_tests', Familia.now.to_s)
17
+ @test_id = "#{Familia.now.to_i}-#{rand(1000)}"
18
18
  @new_customer = Customer.new "new-customer-#{@test_id}@test.com"
19
19
  @new_customer.name = 'New Customer'
20
20
  @new_customer.save_if_not_exists
@@ -34,7 +34,7 @@ Familia.dbclient.set('debug:starting_save_if_not_exists_tests', Time.now.to_s)
34
34
  #=> true
35
35
 
36
36
  ## End of save_if_not_exists tests
37
- Familia.dbclient.set('debug:ending_save_if_not_exists_tests', Time.now.to_s)
37
+ Familia.dbclient.set('debug:ending_save_if_not_exists_tests', Familia.now.to_s)
38
38
 
39
39
  ## save_if_not_exists persists data correctly
40
40
  @another_new_customer.refresh!
@@ -62,7 +62,7 @@ Familia.dbclient.set('debug:ending_save_if_not_exists_tests', Time.now.to_s)
62
62
  #=> "John Doe"
63
63
 
64
64
  ## batch_update can update multiple fields atomically, to_h
65
- @result = @customer.batch_update(name: 'Jane Smith', email: 'jane@example.com')
65
+ @result = @customer.batch_update(name: 'Jane Windows', email: 'jane@example.com')
66
66
  @result.to_h
67
67
  #=> {:success=>true, :results=>[0, 0]}
68
68
 
@@ -80,12 +80,12 @@ Familia.dbclient.set('debug:ending_save_if_not_exists_tests', Time.now.to_s)
80
80
 
81
81
  ## batch_update updates object fields in memory, confirm fields changed
82
82
  [@customer.name, @customer.email]
83
- #=> ["Jane Smith", "jane@example.com"]
83
+ #=> ["Jane Windows", "jane@example.com"]
84
84
 
85
- ## batch_update persists to Redis
85
+ ## batch_update persists to Valkey/Redis
86
86
  @customer.refresh!
87
87
  [@customer.name, @customer.email]
88
- #=> ["Jane Smith", "jane@example.com"]
88
+ #=> ["Jane Windows", "jane@example.com"]
89
89
 
90
90
  ## batch_update with update_expiration: false works
91
91
  @customer.batch_update(name: 'Bob Jones', update_expiration: false)
@@ -139,7 +139,7 @@ end
139
139
  result.size
140
140
  #=> 2
141
141
 
142
- ## refresh! reloads from Redis
142
+ ## refresh! reloads from Valkey/Redis
143
143
  @customer.refresh!
144
144
  @customer.hget('temp_field')
145
145
  #=> "temp_value"
@@ -151,13 +151,14 @@ result.successful?
151
151
 
152
152
  ## destroy! removes object from Database (1 of 2)
153
153
  @customer.destroy!
154
- #=> true
154
+ #=:> MultiResult
155
+ #==> result.successful?
155
156
 
156
157
  ## After destroy!, dbkey no longer exists (2 of 2)
157
158
  @customer.exists?
158
159
  #=> false
159
160
 
160
- ## destroy! removes object from Redis, not the in-memory object (2 of 2)
161
+ ## destroy! removes object from Valkey/Redis, not the in-memory object (2 of 2)
161
162
  @customer.refresh!
162
163
  @customer.name
163
164
  #=> "Bob Jones"
@@ -183,7 +184,7 @@ result.successful?
183
184
  [@fresh_customer.role, @fresh_customer.planid]
184
185
  #=> ["admin", "premium"]
185
186
 
186
- ## Fresh customer changes persist to Redis
187
+ ## Fresh customer changes persist to Valkey/Redis
187
188
  @fresh_customer.refresh!
188
189
  [@fresh_customer.role, @fresh_customer.planid]
189
190
  #=> ["admin", "premium"]
@@ -1,3 +1,5 @@
1
+ # try/horreum/settings_try.rb
2
+
1
3
  # Test Horreum settings
2
4
 
3
5
  require_relative '../helpers/test_helpers'
@@ -3,7 +3,7 @@
3
3
  require_relative '../helpers/test_helpers'
4
4
 
5
5
  class TestUser < Familia::Horreum
6
- using Familia::Refinements::SnakeCase
6
+ using Familia::Refinements::StylizeWords
7
7
 
8
8
  identifier_field :email
9
9
  field :email
@@ -40,14 +40,14 @@ end
40
40
  user_class.prefix
41
41
  #=!> Familia::Problem
42
42
 
43
- ## RedisType relations with Horreum expiration
43
+ ## DataType relations with Horreum expiration
44
44
  user_class = TestUser
45
45
 
46
46
  user = user_class.new(email: "test@example.com")
47
47
  user.save
48
48
  user.expire(1800)
49
49
 
50
- # Create related RedisType
50
+ # Create related DataType
51
51
  tags = user.tags
52
52
  tags << "ruby" << "redis"
53
53
 
@@ -8,7 +8,7 @@ require_relative '../helpers/test_helpers'
8
8
  class MemorySecurityTester
9
9
  def self.test_redacted_string
10
10
  results = {
11
- timestamp: Time.now,
11
+ timestamp: Familia.now,
12
12
  tests: []
13
13
  }
14
14
 
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # See example output at end.
7
7
 
8
- # Set CONTAINER_ID to $CONTAINER_ID or the first argument
8
+ # UnsortedSet CONTAINER_ID to $CONTAINER_ID or the first argument
9
9
  CONTAINER_ID=${CONTAINER_ID:-$1}
10
10
 
11
11
  if [ -z "$CONTAINER_ID" ]; then
@@ -59,7 +59,7 @@ docker exec $CONTAINER_ID bash -c '
59
59
  # $
60
60
  # $ docker run --rm -d -p 3000:3000 \
61
61
  # -e SECRET=$SECRET \
62
- # -e REDIS_URL=redis://host.docker.internal:6379/0 \
62
+ # -e REDIS_URL=redis://host.docker.internal:2525/0 \
63
63
  # ghcr.io/onetimesecret/devtimesecret-lite:latest
64
64
  #
65
65
  # abcd1234
@@ -3,7 +3,7 @@
3
3
  require_relative '../helpers/test_helpers'
4
4
 
5
5
  # Setup
6
- @now = Time.now.to_i
6
+ @now = Familia.now.to_i
7
7
  @customer = Customer.new
8
8
  @customer.custid = 'test+customer_safedump@example.com'
9
9
  @customer.email = 'test+customer_safedump@example.com'
@@ -46,7 +46,7 @@ Customer.find_by_id(ident).planid
46
46
  #=> 1
47
47
 
48
48
  ## Customer can add custom domain via add method
49
- @customer.custom_domains.add(@now, 'example.org')
49
+ @customer.custom_domains.add('example.org', @now)
50
50
  @customer.custom_domains.members.include?('example.org')
51
51
  #=> true
52
52
 
@@ -69,7 +69,7 @@ Customer.find_by_id(ident).planid
69
69
  #=> true
70
70
 
71
71
  ## Customer can be added to class-level sorted set
72
- Customer.instances << @customer
72
+ Customer.instances.add(@customer.identifier)
73
73
  Customer.instances.member?(@customer)
74
74
  #=> true
75
75
 
@@ -89,27 +89,29 @@ Customer.instances.member?(@customer)
89
89
  #=> "reset123"
90
90
 
91
91
  ## Customer can be destroyed
92
- ret = @customer.destroy!
92
+ multi_result = @customer.destroy!
93
93
  cust = Customer.find_by_id('test@example.com')
94
94
  exists = Customer.exists?('test@example.com')
95
- [ret, cust.nil?, exists]
96
- #=> [true, true, false]
95
+ [multi_result.results, cust.nil?, exists]
96
+ #=> [[1, 0, 1, 1, 1, 1, 1], true, false]
97
97
 
98
98
  ## Customer.destroy! can be called on an already destroyed object
99
99
  @customer.destroy!
100
- #=> false
100
+ #=:> MultiResult
101
+ #==> result.successful?
102
+ #=*> result.results
101
103
 
102
104
  ## Customer.logical_database returns the correct database number
103
105
  Customer.logical_database
104
- #=> 15
106
+ #=> 3
105
107
 
106
108
  ## Customer.logical_database returns the correct database number
107
109
  @customer.logical_database
108
- #=> 15
110
+ #=> 3
109
111
 
110
112
  ## @customer.dbclient.connection returns the correct database URI
111
113
  @customer.dbclient.connection
112
- #=> {:host=>"127.0.0.1", :port=>6379, :db=>15, :id=>"redis://127.0.0.1:6379/15", :location=>"127.0.0.1:6379"}
114
+ #=> {:host=>"127.0.0.1", :port=>2525, :db=>3, :id=>"redis://127.0.0.1:2525/3", :location=>"127.0.0.1:2525"}
113
115
 
114
116
  ## @customer.dbclient.uri returns the correct database URI
115
117
  @customer.secrets_created.logical_database
@@ -117,23 +119,19 @@ Customer.logical_database
117
119
 
118
120
  ## @customer.dbclient.uri returns the correct database URI
119
121
  @customer.secrets_created.dbclient.connection
120
- #=> {:host=>"127.0.0.1", :port=>6379, :db=>15, :id=>"redis://127.0.0.1:6379/15", :location=>"127.0.0.1:6379"}
122
+ #=> {:host=>"127.0.0.1", :port=>2525, :db=>3, :id=>"redis://127.0.0.1:2525/3", :location=>"127.0.0.1:2525"}
121
123
 
122
124
  ## Customer.url is nil by default
123
125
  Customer.uri
124
126
  #=> nil
125
127
 
126
- ## Customer.destroy! makes only one call to Redis
127
- DatabaseCommandCounter.count_commands { @customer.destroy! }
128
- #=> 1
129
-
130
128
  ## Customer.logical_database returns the correct database number
131
129
  Customer.instances.logical_database
132
130
  #=> nil
133
131
 
134
132
  ## Customer.logical_database returns the correct database number
135
133
  Customer.instances.uri.to_s
136
- #=> 'redis://127.0.0.1/15'
134
+ #=> 'redis://127.0.0.1/3'
137
135
 
138
136
  # Teardown
139
137
  Customer.instances.delete!