familia 2.0.0.pre15 → 2.0.0.pre16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -8,7 +8,7 @@ module Familia
8
8
  #
9
9
  # Object identifiers are:
10
10
  # - Unique across the system
11
- # - Persistent (stored in Redis/Valkey)
11
+ # - Persistent (stored in Valkey/Redis)
12
12
  # - Lazily generated (only when first accessed)
13
13
  # - Configurable (multiple generation strategies available)
14
14
  # - Preserved during initialization (existing IDs never regenerated)
@@ -50,7 +50,7 @@ module Familia
50
50
  #
51
51
  # # Custom generation strategy
52
52
  # class TimestampedItem < Familia::Horreum
53
- # feature :object_identifier, generator: -> { "item_#{Time.now.to_i}_#{SecureRandom.hex(4)}" }
53
+ # feature :object_identifier, generator: -> { "item_#{Familia.now.to_i}_#{SecureRandom.hex(4)}" }
54
54
  # field :data
55
55
  # end
56
56
  #
@@ -60,9 +60,9 @@ module Familia
60
60
  # Data Integrity Guarantees:
61
61
  #
62
62
  # The feature preserves the object identifier passed during initialization,
63
- # ensuring that existing objects loaded from Redis maintain their IDs:
63
+ # ensuring that existing objects loaded from Valkey/Redis maintain their IDs:
64
64
  #
65
- # # Loading existing object from Redis preserves ID
65
+ # # Loading existing object from Valkey/Redis preserves ID
66
66
  # existing = User.new(objid: 'existing-uuid-value', email: 'existing@example.com')
67
67
  # existing.objid # => "existing-uuid-value" (preserved, not regenerated)
68
68
  #
@@ -71,7 +71,7 @@ module Familia
71
71
  # - Lazy Generation: IDs generated only when first accessed
72
72
  # - Thread-Safe: Generator strategy configured once during initialization
73
73
  # - Memory Efficient: No unnecessary ID generation for unused objects
74
- # - Redis Efficient: Only persists non-nil values to conserve memory
74
+ # - Valkey/Redis Efficient: Only persists non-nil values to conserve memory
75
75
  #
76
76
  # Security Considerations:
77
77
  #
@@ -86,12 +86,21 @@ module Familia
86
86
  DEFAULT_GENERATOR = :uuid_v7
87
87
 
88
88
  def self.included(base)
89
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
90
- base.extend ClassMethods
89
+ Familia.trace :LOADED, self, base if Familia.debug?
90
+ base.extend ModelClassMethods
91
91
 
92
92
  # Ensure default generator is set in feature options
93
93
  base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
94
94
 
95
+ # Add class-level mapping for objid -> id lookups.
96
+ #
97
+ # If the model uses objid as it's primary key, this mapping will be
98
+ # redundant to the builtin functionality of horreum clases, that
99
+ # automatically populate ModelClass.instances sorted set. However,
100
+ # if the model uses any other field as primary key, this mapping
101
+ # is necessary to lookup objects by their objid.
102
+ base.class_hashkey :objid_lookup
103
+
95
104
  # Register the objid field using a simple custom field type
96
105
  base.register_field_type(ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false))
97
106
  end
@@ -101,7 +110,7 @@ module Familia
101
110
  # Object identifier fields automatically generate unique identifiers when first
102
111
  # accessed if not already set. The generation strategy is configurable via
103
112
  # feature options. These fields preserve any values set during initialization
104
- # to ensure data integrity when loading existing objects from Redis.
113
+ # to ensure data integrity when loading existing objects from the database.
105
114
  #
106
115
  # The field type tracks the generator used for each objid to provide provenance
107
116
  # information for security-sensitive operations like external identifier generation.
@@ -151,6 +160,9 @@ module Familia
151
160
  generator = options[:generator] || DEFAULT_GENERATOR
152
161
  instance_variable_set(:"@#{field_name}_generator_used", generator)
153
162
 
163
+ # Update mapping from objid to model primary key
164
+ self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
165
+
154
166
  generated_id
155
167
  end
156
168
  end
@@ -166,7 +178,7 @@ module Familia
166
178
  # Override setter to preserve values during initialization
167
179
  #
168
180
  # This ensures that values passed during object initialization
169
- # (e.g., when loading from Redis) are preserved and not overwritten
181
+ # (e.g., when loading from Valkey/Redis) are preserved and not overwritten
170
182
  # by the lazy generation logic.
171
183
  #
172
184
  # @param klass [Class] The class to define the method on
@@ -177,9 +189,19 @@ module Familia
177
189
 
178
190
  handle_method_conflict(klass, :"#{method_name}=") do
179
191
  klass.define_method :"#{method_name}=" do |value|
192
+ # Remove old mapping if objid is changing
193
+ old_value = instance_variable_get(:"@#{field_name}")
194
+ if old_value && old_value != value
195
+ Familia.logger.info("Removing objid mapping for #{old_value}")
196
+ self.class.objid_lookup.remove_field(old_value)
197
+ end
198
+
180
199
  instance_variable_set(:"@#{field_name}", value)
181
200
 
182
- # When setting objid from external source (e.g., loading from Redis),
201
+ # Update mapping from objid to this new identifier
202
+ self.class.objid_lookup[value] = identifier unless value.nil? || identifier.nil?
203
+
204
+ # When setting objid from external source (e.g., loading from Valkey/Redis),
183
205
  # we cannot determine the original generator, so we clear the provenance
184
206
  # tracking to indicate unknown origin. This prevents false assumptions
185
207
  # about the security properties of externally-provided identifiers.
@@ -205,7 +227,7 @@ module Familia
205
227
  end
206
228
  end
207
229
 
208
- module ClassMethods
230
+ module ModelClassMethods
209
231
  # Generate a new object identifier using the configured strategy
210
232
  #
211
233
  # @return [String] A new unique identifier
@@ -220,7 +242,7 @@ module Familia
220
242
  when :uuid_v4
221
243
  SecureRandom.uuid_v4
222
244
  when :hex
223
- Familia.generate_hex_id
245
+ Familia.generate_id(16)
224
246
  when Proc
225
247
  generator.call
226
248
  else
@@ -243,14 +265,21 @@ module Familia
243
265
 
244
266
  if Familia.debug?
245
267
  reference = caller(1..1).first
246
- Familia.trace :FIND_BY_OBJID, Familia.dbclient, objid, reference
268
+ Familia.trace :FIND_BY_OBJID, nil, objid, reference
247
269
  end
248
270
 
249
- # Use the object identifier as the key for lookup
250
- # This is a simple stub implementation - would need more sophisticated
251
- # search logic in a real application
252
- find_by_id(objid)
271
+ # Look up the primary ID from the external ID mapping
272
+ primary_id = objid_lookup[objid]
273
+
274
+ # If there is no mapping for this instance's objid, perhaps
275
+ # the object dbkey is already using the objid.
276
+ primary_id = objid if primary_id.nil?
277
+
278
+ find_by_id(primary_id)
253
279
  rescue Familia::NotFound
280
+ # If the object was deleted but mapping wasn't cleaned up
281
+ # we could autoclean here, as long as we log it.
282
+ # objid_lookup.remove_field(objid)
254
283
  nil
255
284
  end
256
285
  end
@@ -283,6 +312,15 @@ module Familia
283
312
  self.objid = value
284
313
  end
285
314
 
315
+ def destroy!
316
+ # Clean up objid mapping when object is destroyed
317
+ current_objid = instance_variable_get(:@objid)
318
+
319
+ self.class.objid_lookup.remove_field(current_objid) if current_objid
320
+
321
+ super if defined?(super)
322
+ end
323
+
286
324
  # Initialize object identifier configuration
287
325
  #
288
326
  # Called during object initialization to set up the ID generation strategy.
@@ -300,9 +338,8 @@ module Familia
300
338
 
301
339
  options = self.class.feature_options(:object_identifier)
302
340
  generator = options[:generator] || DEFAULT_GENERATOR
303
- Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
341
+ Familia.trace :OBJID_INIT, nil, "Generator strategy: #{generator}"
304
342
  end
305
-
306
343
  end
307
344
  end
308
345
  end
@@ -106,7 +106,7 @@ module Familia
106
106
  # activity.save
107
107
  # end
108
108
  #
109
- # def self.activity_for_hour(time = Time.now)
109
+ # def self.activity_for_hour(time = Familia.now)
110
110
  # bucket_id = "activity:#{qstamp(1.hour, time: time, pattern: '%Y%m%d%H')}"
111
111
  # find(bucket_id)
112
112
  # end
@@ -129,7 +129,7 @@ module Familia
129
129
  # interval: interval.to_i)
130
130
  # end
131
131
  #
132
- # metric.data_points.add(timestamp, value)
132
+ # metric.data_points.add(value, timestamp)
133
133
  # metric.timestamp = timestamp
134
134
  # metric.value = value
135
135
  # metric.save
@@ -175,13 +175,13 @@ module Familia
175
175
  #
176
176
  # def self.utc_hourly_key(metric_name)
177
177
  # # Always use UTC for consistent global buckets
178
- # timestamp = qstamp(1.hour, time: Time.now.utc, pattern: '%Y%m%d%H')
178
+ # timestamp = qstamp(1.hour, time: Familia.now, pattern: '%Y%m%d%H')
179
179
  # "global:#{metric_name}:#{timestamp}"
180
180
  # end
181
181
  #
182
182
  # def self.local_daily_key(metric_name, timezone = 'America/New_York')
183
183
  # # Use local timezone for region-specific buckets
184
- # local_time = Time.now.in_time_zone(timezone)
184
+ # local_time = Familia.now.in_time_zone(timezone)
185
185
  # timestamp = qstamp(1.day, time: local_time, pattern: '%Y%m%d')
186
186
  # "#{timezone.gsub('/', '_')}:#{metric_name}:#{timestamp}"
187
187
  # end
@@ -194,7 +194,7 @@ module Familia
194
194
  #
195
195
  # # Cache quantized timestamps to avoid repeated calculations
196
196
  # def self.cached_qstamp(quantum, pattern: nil, time: nil)
197
- # cache_key = "qstamp:#{quantum}:#{pattern}:#{(time || Time.now).to_i / quantum}"
197
+ # cache_key = "qstamp:#{quantum}:#{pattern}:#{(time || Familia.now).to_i / quantum}"
198
198
  # Rails.cache.fetch(cache_key, expires_in: quantum) do
199
199
  # qstamp(quantum, pattern: pattern, time: time)
200
200
  # end
@@ -245,19 +245,18 @@ module Familia
245
245
  # NoDefault.qstamp() # Uses 10.minutes as fallback quantum
246
246
  #
247
247
  module Quantization
248
-
249
248
  Familia::Base.add_feature self, :quantization
250
249
 
251
250
  using Familia::Refinements::TimeLiterals
252
251
 
253
252
  def self.included(base)
254
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
255
- base.extend ClassMethods
253
+ Familia.trace :LOADED, self, base if Familia.debug?
254
+ base.extend ModelClassMethods
256
255
  end
257
256
 
258
- # Familia::Quantization::ClassMethods
257
+ # Familia::Quantization::ModelClassMethods
259
258
  #
260
- module ClassMethods
259
+ module ModelClassMethods
261
260
  # Generates a quantized timestamp based on the given parameters
262
261
  #
263
262
  # This method rounds the current time to the nearest quantum and optionally
@@ -286,9 +285,7 @@ module Familia
286
285
  #
287
286
  def qstamp(quantum = nil, pattern: nil, time: nil)
288
287
  # Handle array input format: [quantum, pattern]
289
- if quantum.is_a?(Array)
290
- quantum, pattern = quantum
291
- end
288
+ quantum, pattern = quantum if quantum.is_a?(Array)
292
289
 
293
290
  # Use default quantum if none specified
294
291
  # Priority: provided quantum > class default_expiration > 10.minutes fallback
@@ -323,11 +320,11 @@ module Familia
323
320
  end_bucket = qstamp(quantum, time: end_time)
324
321
 
325
322
  while current <= end_bucket
326
- if pattern
327
- timestamps << Time.at(current).strftime(pattern)
328
- else
329
- timestamps << current
330
- end
323
+ timestamps << if pattern
324
+ Time.at(current).strftime(pattern)
325
+ else
326
+ current
327
+ end
331
328
  current += quantum
332
329
  end
333
330
 
@@ -352,7 +349,7 @@ module Familia
352
349
  bucket_start = qstamp(quantum, time: Time.at(bucket_time))
353
350
  bucket_end = bucket_start + quantum - 1
354
351
 
355
- timestamp >= bucket_start && timestamp <= bucket_end
352
+ timestamp.between?(bucket_start, bucket_end)
356
353
  end
357
354
  end
358
355
 
@@ -397,8 +394,6 @@ module Familia
397
394
  base_id = respond_to?(:identifier) ? identifier : object_id
398
395
  "#{base_id}#{separator}#{timestamp}"
399
396
  end
400
-
401
- extend ClassMethods
402
397
  end
403
398
  end
404
399
  end
@@ -0,0 +1,97 @@
1
+ <!--lib/familia/features/relationships/README.md-->
2
+
3
+ ## Core Modules
4
+
5
+ **relationships.rb** - Main orchestrator that unifies all relationship functionality into a single feature, providing the public API and coordinating between all submodules.
6
+
7
+ **indexing.rb** - O(1) lookup capability via Valkey/Redis hashes and sets. Enables fast field-based searches when parent-scoped (within: ParentClass). Creates instance methods on parent class for scoped lookups.
8
+
9
+ **participation.rb** - Multi-presence management where objects can exist in multiple collections simultaneously with score-encoded metadata (timestamps, permissions, etc.). All add/remove operations use transactions for atomicity.
10
+
11
+ ## Quick API Guide
12
+
13
+ **participates_in** - Collection membership ("this object belongs in that collection")
14
+ ```ruby
15
+ participates_in Organization, :members, score: :joined_at, bidirectional: true
16
+ # Creates: org.members, org.add_member(), customer.add_to_organization_members()
17
+ ```
18
+
19
+ **unique_index** - Fast unique lookups ("find object by unique field value")
20
+ ```ruby
21
+ unique_index :email, :email_index, within: Organization # Scoped: org.find_by_email()
22
+ ```
23
+
24
+ **multi_index** - Fast multi-value lookups ("find all objects by field value")
25
+ ```ruby
26
+ multi_index :department, :dept_index, within: Organization
27
+ # Creates: org.sample_from_department(), org.find_all_by_department()
28
+ ```
29
+
30
+ ## Key Philosophy
31
+
32
+ The entire system embraces "where does this appear?" rather than "who owns this?" - enabling objects to exist in multiple contexts simultaneously while maintaining fast lookups and atomic operations.
33
+
34
+ ## When to Use Which
35
+
36
+ <details>
37
+ <summary>📋 participates_in vs indexing - Decision Guide</summary>
38
+
39
+ ### participates_in - Collection Membership
40
+ - **Purpose**: "This object belongs in that collection"
41
+ - **Storage**: SortedSet/Set/List of object IDs with optional scores
42
+ - **Use for**: Membership relationships, ordered lists, scored collections
43
+ - **Example**: Customers in an Organization, Tasks in a Project
44
+ - **Atomicity**: Transactions for all operations (collection + reverse index)
45
+
46
+ ```ruby
47
+ participates_in Organization, :members, score: :joined_at
48
+ # Creates: org.members (SortedSet), org.add_member(), customer.add_to_organization_members()
49
+ ```
50
+
51
+ ### unique_index - Fast Unique Lookups
52
+ - **Purpose**: "Find THE object by unique field value"
53
+ - **Storage**: HashKey for O(1) field-to-object mapping
54
+ - **Use for**: Email lookups, username searches, unique IDs
55
+ - **Example**: Find customer by email, find employee by badge number
56
+ - **Atomicity**: Transactions for updates (remove old + add new)
57
+
58
+ ```ruby
59
+ unique_index :email, :email_index, within: Organization
60
+ # Creates: org.find_by_email(), org.find_all_by_email()
61
+ ```
62
+
63
+ ### multi_index - Fast Multi-Value Lookups
64
+ - **Purpose**: "Find ALL objects by shared field value"
65
+ - **Storage**: UnsortedSet for O(1) field-to-objects mapping
66
+ - **Use for**: Grouping by department, status, category, tags
67
+ - **Example**: All employees in a department, all tasks with status
68
+ - **Atomicity**: Transactions for updates (remove from old set + add to new set)
69
+
70
+ ```ruby
71
+ multi_index :department, :dept_index, within: Organization
72
+ # Creates: org.sample_from_department(dept, count), org.find_all_by_department(dept)
73
+ ```
74
+
75
+ </details>
76
+
77
+ > [!NOTE]
78
+ > **Scoping Patterns**: `unique_index` and `multi_index` use the `within:` parameter for instance-scoping, while participation uses distinct method names (`participates_in` vs `class_participates_in`) to reflect fundamentally different semantics (instance collections vs auto-tracking all instances).
79
+
80
+ > [!TIP]
81
+ > **Quick Decision Guide**
82
+ > - Need to store a collection of objects? → `participates_in`
83
+ > - Need to find ONE object by unique field? → `unique_index`
84
+ > - Need to find MANY objects by shared field? → `multi_index`
85
+ > - Combination? → Use all three together (very common)
86
+
87
+ ```ruby
88
+ class Customer < Familia::Horreum
89
+ feature :relationships
90
+
91
+ participates_in Organization, :members # Customer belongs to org
92
+ unique_index :email, :email_index, within: Organization # Find by unique email
93
+ end
94
+ ```
95
+
96
+ > [!NOTE]
97
+ > **Key**: `participates_in` = collections, `unique_index` = unique lookups, `multi_index` = group lookups.
@@ -0,0 +1,104 @@
1
+ # lib/familia/features/relationships/collection_operations.rb
2
+
3
+ module Familia
4
+ module Features
5
+ module Relationships
6
+ # Shared collection operations for Participation module
7
+ # Provides common methods for working with Horreum-managed DataType collections
8
+ # Used by both ParticipantMethods and TargetMethods to reduce duplication
9
+ module CollectionOperations
10
+ using Familia::Refinements::StylizeWords
11
+
12
+ # Ensure a target class has the specified DataType field defined
13
+ # @param target_class [Class] The class that should have the collection
14
+ # @param collection_name [Symbol] Name of the collection field
15
+ # @param type [Symbol] Collection type (:sorted_set, :set, :list)
16
+ def ensure_collection_field(target_class, collection_name, type)
17
+ return if target_class.method_defined?(collection_name)
18
+
19
+ target_class.send(type, collection_name)
20
+ end
21
+
22
+ # Add an item to a collection, handling type-specific operations
23
+ # @param collection [Familia::DataType] The collection to add to
24
+ # @param item [Object] The item to add (must respond to identifier)
25
+ # @param score [Float, nil] Score for sorted sets
26
+ # @param type [Symbol] Collection type
27
+ def add_to_collection(collection, item, type:, score: nil, target_class: nil, collection_name: nil)
28
+ case type
29
+ when :sorted_set
30
+ # Ensure score is never nil for sorted sets
31
+ score ||= calculate_item_score(item, target_class, collection_name)
32
+ collection.add(item.identifier, score)
33
+ when :list
34
+ # Lists use push/unshift operations
35
+ collection.add(item.identifier)
36
+ when :set
37
+ # Sets use simple add
38
+ collection.add(item.identifier)
39
+ else
40
+ raise ArgumentError, "Unknown collection type: #{type}"
41
+ end
42
+ end
43
+
44
+ # Remove an item from a collection
45
+ # @param collection [Familia::DataType] The collection to remove from
46
+ # @param item [Object] The item to remove (must respond to identifier)
47
+ # @param type [Symbol] Collection type
48
+ def remove_from_collection(collection, item, type: nil)
49
+ # All collection types support remove/delete
50
+ collection.remove(item.identifier)
51
+ end
52
+
53
+ # Check if an item is a member of a collection
54
+ # @param collection [Familia::DataType] The collection to check
55
+ # @param item [Object] The item to check (must respond to identifier)
56
+ # @return [Boolean] True if item is in collection
57
+ def member_of_collection?(collection, item)
58
+ collection.member?(item.identifier)
59
+ end
60
+
61
+ # Bulk add items to a collection using DataType methods
62
+ # @param collection [Familia::DataType] The collection to add to
63
+ # @param items [Array] Array of items to add
64
+ # @param type [Symbol] Collection type
65
+ def bulk_add_to_collection(collection, items, type:, target_class: nil, collection_name: nil)
66
+ return if items.empty?
67
+
68
+ case type
69
+ when :sorted_set
70
+ # Add items one by one for sorted sets to ensure proper scoring
71
+ items.each do |item|
72
+ score = calculate_item_score(item, target_class, collection_name)
73
+ collection.add(item.identifier, score)
74
+ end
75
+ when :set, :list
76
+ # For sets and lists, add items one by one using DataType methods
77
+ items.each do |item|
78
+ collection.add(item.identifier)
79
+ end
80
+ else
81
+ raise ArgumentError, "Unknown collection type: #{type}"
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Calculate score for an item
88
+ # @param item [Object] The item to score
89
+ # @param target_class [Class, nil] The target class for participation scoring
90
+ # @param collection_name [Symbol, nil] The collection name for participation scoring
91
+ # @return [Float] The calculated score
92
+ def calculate_item_score(item, target_class = nil, collection_name = nil)
93
+ if item.respond_to?(:calculate_participation_score) && target_class && collection_name
94
+ item.calculate_participation_score(target_class, collection_name)
95
+ elsif item.respond_to?(:current_score)
96
+ item.current_score
97
+ else
98
+ Familia.now.to_f
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end