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
@@ -25,14 +25,11 @@ class TestDomain < Familia::Horreum
25
25
  field :created_at
26
26
  field :permission_level
27
27
 
28
- # Basic tracking with simplified score
29
- tracked_in TestCustomer, :domains, score: :created_at
30
- class_tracked_in :all_domains, score: :created_at
28
+ # Basic participation with simplified score
29
+ participates_in TestCustomer, :domains, score: :created_at
30
+ class_participates_in :all_domains, score: :created_at
31
31
 
32
32
  # Note: Indexing features removed for stability
33
-
34
- # Basic membership
35
- member_of TestCustomer, :domains
36
33
  end
37
34
 
38
35
  class TestTag < Familia::Horreum
@@ -42,8 +39,8 @@ class TestTag < Familia::Horreum
42
39
  field :name
43
40
  field :created_at
44
41
 
45
- # Global tracking
46
- class_tracked_in :all_tags, score: :created_at
42
+ # Global participation
43
+ class_participates_in :all_tags, score: :created_at
47
44
  end
48
45
 
49
46
  # Setup
@@ -51,10 +48,10 @@ end
51
48
  @domain = TestDomain.new(
52
49
  domain_id: 'dom_789',
53
50
  display_domain: 'example.com',
54
- created_at: Time.now.to_i,
51
+ created_at: Familia.now.to_i,
55
52
  permission_level: :write
56
53
  )
57
- @tag = TestTag.new(name: 'important', created_at: Time.now.to_i)
54
+ @tag = TestTag.new(name: 'important', created_at: Familia.now.to_i)
58
55
 
59
56
  # =============================================
60
57
  # 1. V2 Feature Integration Tests
@@ -72,10 +69,6 @@ TestDomain.included_modules.map(&:name).include?('Familia::Features::Relationshi
72
69
  @domain.respond_to?(:permission_encode)
73
70
  #=> true
74
71
 
75
- ## Redis operations functionality is available
76
- @domain.respond_to?(:atomic_operation)
77
- #=> true
78
-
79
72
  ## Identifier method works (wraps identifier_field)
80
73
  TestDomain.identifier_field
81
74
  #=> :domain_id
@@ -89,7 +82,7 @@ TestDomain.identifier_field
89
82
  # =============================================
90
83
 
91
84
  ## Permission encoding creates proper score
92
- @score = @domain.permission_encode(Time.now, :write)
85
+ @score = @domain.permission_encode(Familia.now, :write)
93
86
  @score.to_s.match?(/\d+\.\d+/)
94
87
  #=> true
95
88
 
@@ -99,16 +92,16 @@ decoded[:permission_list].include?(:write)
99
92
  #=> true
100
93
 
101
94
  ## Score encoding preserves timestamp ordering
102
- @early_score = @domain.encode_score(Time.now - 3600, 100) # 1 hour ago
103
- @late_score = @domain.encode_score(Time.now, 100)
95
+ @early_score = @domain.encode_score(Familia.now - 3600, 100) # 1 hour ago
96
+ @late_score = @domain.encode_score(Familia.now, 100)
104
97
  @late_score > @early_score
105
98
  #=> true
106
99
 
107
100
  # =============================================
108
- # 3. Tracking Relationships (tracked_in)
101
+ # 3. Participation Relationships (participates_in)
109
102
  # =============================================
110
103
 
111
- ## Save operation manages tracking relationships
104
+ ## Save operation manages participation relationships
112
105
  @customer.save
113
106
  @domain.save
114
107
 
@@ -129,24 +122,24 @@ decoded[:permission_list].include?(:write)
129
122
  #=> true
130
123
 
131
124
  ## Domain can check membership in customer domains (collision-free naming)
132
- @domain.respond_to?(:in_testcustomer_domains?)
125
+ @domain.respond_to?(:in_test_customer_domains?)
133
126
  #=> true
134
127
 
135
128
  ## Domain can add itself to customer domains (collision-free naming)
136
- @domain.respond_to?(:add_to_testcustomer_domains)
129
+ @domain.respond_to?(:add_to_test_customer_domains)
137
130
  #=> true
138
131
 
139
132
  ## Domain can remove itself from customer domains (collision-free naming)
140
- @domain.respond_to?(:remove_from_testcustomer_domains)
133
+ @domain.respond_to?(:remove_from_test_customer_domains)
141
134
  #=> true
142
135
 
143
136
  ## Add domain to customer collection
144
- @domain.add_to_testcustomer_domains(@customer)
145
- @domain.in_testcustomer_domains?(@customer)
137
+ @domain.add_to_test_customer_domains(@customer)
138
+ @domain.in_test_customer_domains?(@customer)
146
139
  #=> true
147
140
 
148
141
  ## Score is properly encoded
149
- score = @domain.score_in_testcustomer_domains(@customer)
142
+ score = @domain.score_in_test_customer_domains(@customer)
150
143
  score.is_a?(Float) && score > 0
151
144
  #=> true
152
145
 
@@ -154,8 +147,8 @@ score.is_a?(Float) && score > 0
154
147
  # 4. Basic Functionality Verification
155
148
  # =============================================
156
149
 
157
- ## Domain tracking methods work correctly
158
- @domain.respond_to?(:score_in_testcustomer_domains)
150
+ ## Domain participation methods work correctly
151
+ @domain.respond_to?(:score_in_test_customer_domains)
159
152
  #=> true
160
153
 
161
154
  ## Score calculation methods are available
@@ -163,23 +156,23 @@ score.is_a?(Float) && score > 0
163
156
  #=> true
164
157
 
165
158
  # =============================================
166
- # 5. Basic Membership Relationships (member_of)
159
+ # 5. Bidirectional Participation Methods
167
160
  # =============================================
168
161
 
169
- ## Member_of generates collision-free methods with collection names
170
- @domain.respond_to?(:add_to_testcustomer_domains)
162
+ ## participates_in generates collision-free bidirectional methods with collection names
163
+ @domain.respond_to?(:add_to_test_customer_domains)
171
164
  #=> true
172
165
 
173
- ## Basic membership operations work
174
- @domain.remove_from_testcustomer_domains(@customer)
175
- @domain.in_testcustomer_domains?(@customer)
166
+ ## Basic bidirectional participation operations work
167
+ @domain.remove_from_test_customer_domains(@customer)
168
+ @domain.in_test_customer_domains?(@customer)
176
169
  #=> false
177
170
 
178
171
  # =============================================
179
172
  # 6. Basic Global Tag Tracking Test
180
173
  # =============================================
181
174
 
182
- ## Tag can be tracked globally
175
+ ## Tag can be participating globally
183
176
  @tag.save
184
177
  @tag.respond_to?(:add_to_class_all_tags)
185
178
  #=> true
@@ -213,10 +206,6 @@ temp_key = @domain.create_temp_key("test_operation", 60)
213
206
  temp_key.start_with?("temp:")
214
207
  #=> true
215
208
 
216
- ## Batch operations are available
217
- @domain.respond_to?(:batch_zadd)
218
- #=> true
219
-
220
209
  ## Score range queries work
221
210
  @domain.respond_to?(:score_range)
222
211
  #=> true
@@ -17,8 +17,8 @@ Customer.safe_dump_fields
17
17
  @cust.custid = 'test@example.com'
18
18
  @cust.role = 'user'
19
19
  @cust.verified = true
20
- @cust.created = Time.now.to_i
21
- @cust.updated = Time.now.to_i
20
+ @cust.created = Familia.now.to_i
21
+ @cust.updated = Familia.now.to_i
22
22
  @safe_dump = @cust.safe_dump
23
23
  @safe_dump.keys.sort
24
24
  #=> [:active, :created, :custid, :role, :secrets_created, :updated, :verified]
@@ -8,6 +8,8 @@ Familia.debug = false
8
8
  Familia.dbclient.flushdb
9
9
 
10
10
  class SecretService < Familia::Horreum
11
+ feature :transient_fields
12
+
11
13
  identifier_field :name
12
14
 
13
15
  field :name
@@ -94,7 +96,7 @@ SecretService.fields.sort
94
96
  @service.endpoint_url
95
97
  #=> "https://api.example.com"
96
98
 
97
- ## Set transient fields again after refresh
99
+ ## UnsortedSet transient fields again after refresh
98
100
  @service.api_key = 'new-api-key-after-refresh'
99
101
  @service.password = 'new-password-after-refresh'
100
102
  @service.token = 'new-token-after-refresh'
@@ -14,7 +14,7 @@ class Customer
14
14
  transient_field :temp_data
15
15
  end
16
16
 
17
- # Set some values
17
+ # UnsortedSet some values
18
18
  service.name = 'Test Customer'
19
19
  service.temp_data = 'secret-info'
20
20
 
@@ -0,0 +1,86 @@
1
+ # Test cleanup helper for managing anonymous classes and test isolation
2
+ #
3
+ # This module provides utilities to create and cleanup test classes that
4
+ # inherit from Familia classes. Without proper cleanup, anonymous classes
5
+ # pollute the Familia.members registry causing test failures.
6
+
7
+ module TestCleanup
8
+ @test_classes = []
9
+
10
+ class << self
11
+ attr_reader :test_classes
12
+
13
+ # Create a test class that inherits from the given base class.
14
+ # The created class is automatically tracked for cleanup.
15
+ #
16
+ # @param base_class [Class] The class to inherit from (e.g., Familia::Horreum)
17
+ # @param block [Proc] Block to define the class body
18
+ # @return [Class] The created test class
19
+ def create_test_class(base_class, &block)
20
+ test_class = Class.new(base_class, &block)
21
+ track_test_class(test_class)
22
+ test_class
23
+ end
24
+
25
+ # Track a test class for cleanup. Use this when you create test classes
26
+ # directly with Class.new instead of using create_test_class.
27
+ #
28
+ # @param klass [Class] The test class to track
29
+ # @return [Class] The tracked class
30
+ def track_test_class(klass)
31
+ @test_classes << klass unless @test_classes.include?(klass)
32
+ klass
33
+ end
34
+
35
+ # Remove all tracked test classes from Familia.members and clear
36
+ # the tracking array. This should be called in test teardown.
37
+ #
38
+ # @return [Array<Class>] The classes that were removed
39
+ def cleanup_test_classes
40
+ removed_classes = []
41
+
42
+ @test_classes.each do |test_class|
43
+ if Familia.members.include?(test_class)
44
+ Familia.unload_member(test_class)
45
+ removed_classes << test_class
46
+ end
47
+ end
48
+
49
+ @test_classes.clear
50
+ removed_classes
51
+ end
52
+
53
+ # Clean up all anonymous classes from Familia.members.
54
+ # This is a more aggressive cleanup that removes any class with nil name.
55
+ #
56
+ # @return [Array<Class>] The anonymous classes that were removed
57
+ def cleanup_anonymous_classes
58
+ Familia.clear_anonymous_members
59
+ end
60
+
61
+ # Perform complete test cleanup - both tracked and anonymous classes
62
+ #
63
+ # @return [Hash] Summary of cleanup performed
64
+ def complete_cleanup
65
+ tracked_removed = cleanup_test_classes
66
+ anonymous_removed = cleanup_anonymous_classes
67
+
68
+ {
69
+ tracked_classes_removed: tracked_removed.size,
70
+ anonymous_classes_removed: anonymous_removed.size,
71
+ total_removed: tracked_removed.size + anonymous_removed.size
72
+ }
73
+ end
74
+ end
75
+ end
76
+
77
+ # Automatically perform cleanup if we're in test mode
78
+ # This ensures cleanup happens even if tests don't explicitly call it
79
+ at_exit do
80
+ if Familia.test_mode? && TestCleanup.test_classes.any?
81
+ cleanup_result = TestCleanup.complete_cleanup
82
+ if cleanup_result[:total_removed] > 0
83
+ puts "[TestCleanup] Cleaned up #{cleanup_result[:total_removed]} test classes on exit"
84
+ end
85
+ end
86
+ end
@@ -10,6 +10,7 @@ require_relative '../../lib/familia'
10
10
 
11
11
  Familia.enable_database_logging = true
12
12
  Familia.enable_database_counter = true
13
+ Familia.uri = 'redis://127.0.0.1:2525'
13
14
 
14
15
  class Bone < Familia::Horreum
15
16
  using Familia::Refinements::TimeLiterals
@@ -44,12 +45,10 @@ class Customer < Familia::Horreum
44
45
 
45
46
  using Familia::Refinements::TimeLiterals
46
47
 
47
- logical_database 15 # Use something other than the default DB
48
+ logical_database 3 # Use something other than the default DB
48
49
  default_expiration 5.years
49
50
 
50
51
  feature :safe_dump
51
- # feature :expiration
52
- # feature :api_version
53
52
 
54
53
  # Use new SafeDump DSL instead of @safe_dump_fields
55
54
  safe_dump_field :custid
@@ -65,7 +64,7 @@ class Customer < Familia::Horreum
65
64
  # We use a callable here since `:active?` is not a valid method symbol.
66
65
  safe_dump_field :active, ->(cust) { cust.active? }
67
66
 
68
- class_sorted_set :values, key: 'onetime:customer'
67
+ class_sorted_set :values, key: 'familia:customer'
69
68
  class_hashkey :domains
70
69
 
71
70
  hashkey :stripe_customer
@@ -91,9 +90,9 @@ class Customer < Familia::Horreum
91
90
  field :reset_requested #=> Boolean
92
91
 
93
92
  hashkey :password_reset #=> Familia::HashKey
94
- list :sessions #=> Familia::List
93
+ list :sessions #=> Familia::ListKey
95
94
 
96
- class_list :customers, suffix: []
95
+ class_list :all_customers, suffix: [] # no suffix
97
96
  class_string :message
98
97
 
99
98
  class_zset :instances, class: self, reference: true
@@ -108,7 +107,7 @@ end
108
107
  class Session < Familia::Horreum
109
108
  using Familia::Refinements::TimeLiterals
110
109
 
111
- logical_database 14 # don't use Onetime's default DB
110
+ logical_database 2 # a non-default database
112
111
  default_expiration 180.minutes
113
112
 
114
113
  identifier_field :sessid
@@ -0,0 +1,212 @@
1
+ # try/horreum/auto_indexing_on_save_try.rb
2
+
3
+ #
4
+ # Auto-indexing on save functionality tests
5
+ # Tests automatic index population when Familia::Horreum objects are saved
6
+ #
7
+
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Test classes for auto-indexing functionality
11
+ class ::AutoIndexUser < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :user_id
15
+ field :user_id
16
+ field :email
17
+ field :username
18
+ field :department
19
+
20
+ # Class-level unique indexes (should auto-populate on save)
21
+ unique_index :email, :email_index
22
+ unique_index :username, :username_index
23
+ end
24
+
25
+ class ::AutoIndexCompany < Familia::Horreum
26
+ feature :relationships
27
+
28
+ identifier_field :company_id
29
+ field :company_id
30
+ field :name
31
+ end
32
+
33
+ class ::AutoIndexEmployee < Familia::Horreum
34
+ feature :relationships
35
+
36
+ identifier_field :emp_id
37
+ field :emp_id
38
+ field :badge_number
39
+ field :department
40
+
41
+ # Instance-scoped indexes (should NOT auto-populate - require parent context)
42
+ unique_index :badge_number, :badge_index, within: AutoIndexCompany
43
+ multi_index :department, :dept_index, within: AutoIndexCompany
44
+ end
45
+
46
+ # Setup
47
+ @user_id = "user_#{rand(1000000)}"
48
+ @user = AutoIndexUser.new(user_id: @user_id, email: 'test@example.com', username: 'testuser', department: 'engineering')
49
+
50
+ @company_id = "comp_#{rand(1000000)}"
51
+ @company = AutoIndexCompany.new(company_id: @company_id, name: 'Test Corp')
52
+
53
+ @emp_id = "emp_#{rand(1000000)}"
54
+ @employee = AutoIndexEmployee.new(emp_id: @emp_id, badge_number: 'BADGE123', department: 'sales')
55
+
56
+ # =============================================
57
+ # 1. Class-Level Unique Index Auto-Population
58
+ # =============================================
59
+
60
+ ## Unique index is empty before save
61
+ AutoIndexUser.email_index.has_key?('test@example.com')
62
+ #=> false
63
+
64
+ ## Save automatically populates unique index
65
+ @user.save
66
+ AutoIndexUser.email_index.has_key?('test@example.com')
67
+ #=> true
68
+
69
+ ## Auto-populated index maps to correct identifier
70
+ AutoIndexUser.email_index.get('test@example.com')
71
+ #=> @user_id
72
+
73
+ ## Finder method works after auto-indexing
74
+ found = AutoIndexUser.find_by_email('test@example.com')
75
+ found&.user_id
76
+ #=> @user_id
77
+
78
+ ## Multiple unique indexes auto-populate on same save
79
+ AutoIndexUser.username_index.get('testuser')
80
+ #=> @user_id
81
+
82
+ ## Subsequent saves maintain index (idempotent)
83
+ @user.save
84
+ AutoIndexUser.email_index.get('test@example.com')
85
+ #=> @user_id
86
+
87
+ ## Changing indexed field and saving adds new entry (old entry remains unless manually removed)
88
+ # Note: Auto-indexing is idempotent addition only - updates require manual update_in_class_* calls
89
+ @user.email = 'newemail@example.com'
90
+ @user.save
91
+ # New email is indexed, but old email remains (expected behavior - use update_in_class_* for proper updates)
92
+ [AutoIndexUser.email_index.has_key?('test@example.com'), AutoIndexUser.email_index.get('newemail@example.com') == @user_id]
93
+ #=> [true, true]
94
+
95
+ # =============================================
96
+ # 2. Instance-Scoped Indexes (Manual Only)
97
+ # =============================================
98
+
99
+ ## Instance-scoped indexes do NOT auto-populate on save
100
+ @employee.save
101
+ @company.badge_index.has_key?('BADGE123')
102
+ #=> false
103
+
104
+ ## Instance-scoped indexes remain manual (require parent context)
105
+ @employee.add_to_auto_index_company_badge_index(@company)
106
+ @company.badge_index.has_key?('BADGE123')
107
+ #=> true
108
+
109
+ # =============================================
110
+ # 3. Edge Cases and Error Handling
111
+ # =============================================
112
+
113
+ ## Nil field values handled gracefully
114
+ @user_nil_id = "user_nil_#{rand(1000000)}"
115
+ @user_nil = AutoIndexUser.new(user_id: @user_nil_id, email: nil, username: nil, department: nil)
116
+ @user_nil.save
117
+ AutoIndexUser.email_index.has_key?('')
118
+ #=> false
119
+
120
+ ## Empty string field values handled gracefully
121
+ @user_empty_id = "user_empty_#{rand(1000000)}"
122
+ @user_empty = AutoIndexUser.new(user_id: @user_empty_id, email: '', username: '', department: '')
123
+ @user_empty.save
124
+ # Empty strings are indexed (they're valid string values, just empty)
125
+ AutoIndexUser.email_index.has_key?('')
126
+ #=> true
127
+
128
+ ## Auto-indexing works with create method
129
+ @user2_id = "user_#{rand(1000000)}"
130
+ @user2 = AutoIndexUser.create(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
131
+ AutoIndexUser.find_by_email('create@example.com')&.user_id
132
+ #=> @user2_id
133
+
134
+ ## Auto-indexing idempotent with multiple saves
135
+ @user2.save
136
+ @user2.save
137
+ @user2.save
138
+ AutoIndexUser.email_index.get('create@example.com')
139
+ #=> @user2_id
140
+
141
+ ## Field update followed by save adds new entry (use update_in_class_* for proper updates)
142
+ old_email = @user2.email
143
+ @user2.email = 'updated@example.com'
144
+ @user2.save
145
+ # Both old and new emails are indexed (auto-indexing doesn't remove old values)
146
+ # For proper updates that remove old values, use: @user2.update_in_class_email_index(old_email)
147
+ [AutoIndexUser.email_index.has_key?(old_email), AutoIndexUser.email_index.get('updated@example.com') == @user2_id]
148
+ #=> [true, true]
149
+
150
+ # =============================================
151
+ # 4. Integration with Other Features
152
+ # =============================================
153
+
154
+ ## Auto-indexing works with transient fields
155
+ class ::AutoIndexWithTransient < Familia::Horreum
156
+ feature :transient_fields
157
+ feature :relationships
158
+
159
+ identifier_field :id
160
+ field :id
161
+ field :email
162
+ transient_field :temp_value
163
+
164
+ unique_index :email, :email_index
165
+ end
166
+
167
+ @transient_id = "trans_#{rand(1000000)}"
168
+ @transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: 'transient@example.com', temp_value: 'ignored')
169
+ @transient_obj.save
170
+ AutoIndexWithTransient.find_by_email('transient@example.com')&.id
171
+ #=> @transient_id
172
+
173
+ ## Auto-indexing works regardless of other features
174
+ # Just verify that the feature system doesn't interfere
175
+ @transient_obj.class.respond_to?(:indexing_relationships)
176
+ #=> true
177
+
178
+ # =============================================
179
+ # 5. Performance and Behavior Verification
180
+ # =============================================
181
+
182
+ ## Auto-indexing has negligible overhead (no existence checks)
183
+ # This test verifies the design: we use idempotent commands (HSET, SADD)
184
+ # rather than checking if the index exists before updating
185
+ @user4_id = "user_#{rand(1000000)}"
186
+ @user4 = AutoIndexUser.new(user_id: @user4_id, email: 'perf@example.com', username: 'perfuser', department: 'ops')
187
+
188
+ # Save multiple times - all should succeed with same result
189
+ @user4.save
190
+ @user4.save
191
+ @user4.save
192
+
193
+ AutoIndexUser.email_index.get('perf@example.com')
194
+ #=> @user4_id
195
+
196
+ ## Auto-indexing only processes class-level indexes
197
+ # Verify no errors when instance-scoped indexes present
198
+ @employee2_id = "emp_#{rand(1000000)}"
199
+ @employee2 = AutoIndexEmployee.new(emp_id: @employee2_id, badge_number: 'BADGE456', department: 'engineering')
200
+ @employee2.save # Should not error, just skip instance-scoped indexes
201
+ @employee2.emp_id
202
+ #=> @employee2_id
203
+
204
+ # Teardown - clean up test objects
205
+ [@user, @user2, @user4, @user_nil, @user_empty, @company, @employee, @employee2, @transient_obj].each do |obj|
206
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
207
+ end
208
+
209
+ # Clean up class-level indexes
210
+ [AutoIndexUser.email_index, AutoIndexUser.username_index].each do |index|
211
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
212
+ end
@@ -52,7 +52,7 @@ Familia.debug = false
52
52
  #=> true
53
53
 
54
54
  ## Horreum object fields have a fast attribute method (1 of 2)
55
- Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
55
+ Familia.trace :LOAD, nil, @customer.uri if Familia.debug?
56
56
  @customer.name! 'Jane Doe'
57
57
  #=> true
58
58
 
@@ -69,7 +69,8 @@ Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
69
69
 
70
70
  ## Horreum objects can be destroyed
71
71
  @customer.destroy!
72
- #=> true
72
+ #=:> MultiResult
73
+ #==> result.successful?
73
74
 
74
75
  ## All horrerum objects have a key field
75
76
  @customer.identifier
@@ -1,4 +1,6 @@
1
- # Test Horreum Redis commands
1
+ # try/horreum/commands_try.rb
2
+
3
+ # Test Horreum Valkey/Redis commands
2
4
 
3
5
  require_relative '../helpers/test_helpers'
4
6
 
@@ -0,0 +1,86 @@
1
+ # try/horreum/defensive_initialization_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ # Test defensive initialization behavior
6
+ class User < Familia::Horreum
7
+ field :email
8
+ list :sessions
9
+ zset :metrics
10
+
11
+ def initialize(email = nil)
12
+ # This is the common mistake - overriding initialize without calling super
13
+ @email = email
14
+ # Missing: super() or initialize_relatives
15
+ end
16
+ end
17
+
18
+ class SafeUser < Familia::Horreum
19
+ field :email
20
+ list :sessions
21
+ zset :metrics
22
+
23
+ def init
24
+ # This is the correct way - using the init hook
25
+ # Fields are already set by initialize, no need to override
26
+ end
27
+ end
28
+
29
+ # Setup instances for testing
30
+ @user = User.new("test@example.com")
31
+ @safe_user = SafeUser.new
32
+ @safe_user.email = "safe@example.com"
33
+
34
+ ## Test that accessing relationships after bad initialize triggers lazy initialization
35
+ @user.email
36
+ #=> "test@example.com"
37
+
38
+ ## Test that sessions works with lazy initialization
39
+ @user.sessions.class
40
+ #=> Familia::ListKey
41
+
42
+ ## Test that metrics also works with lazy initialization
43
+ @user.metrics.class
44
+ #=> Familia::SortedSet
45
+
46
+ ## Test that safe user works normally
47
+ @safe_user.email
48
+ #=> "safe@example.com"
49
+
50
+ ## Test that safe user sessions work
51
+ @safe_user.sessions.class
52
+ #=> Familia::ListKey
53
+
54
+ ## Test that relatives_initialized flag prevents double initialization
55
+ @user.singleton_class.instance_variable_get(:@relatives_initialized)
56
+ #=> true
57
+
58
+ ## Test that manual initialize_relatives call is no-op
59
+ @user.initialize_relatives
60
+ @user.sessions.class
61
+ #=> Familia::ListKey
62
+
63
+ ## Test that the original problem is now fixed - bad override still works
64
+ class BadUser < Familia::Horreum
65
+ field :email
66
+ list :sessions
67
+
68
+ def initialize(email)
69
+ # Bad: overriding initialize without calling super
70
+ @email = email
71
+ # Missing: super() or initialize_relatives
72
+ end
73
+ end
74
+
75
+ @bad_user = BadUser.new("bad@example.com")
76
+ @bad_user.email
77
+ #=> "bad@example.com"
78
+
79
+ ## Test that relationships work despite bad initialize (lazy initialization kicks in)
80
+ @bad_user.sessions.class
81
+ #=> Familia::ListKey
82
+
83
+ ## Test that the bad user can actually use the relationships
84
+ @bad_user.sessions.add("session_123")
85
+ @bad_user.sessions.size > 0
86
+ #=> true