familia 2.0.0.pre14 → 2.0.0.pre16

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