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
@@ -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
  #
@@ -81,15 +81,26 @@ module Familia
81
81
  # - Custom generators allow domain-specific security requirements
82
82
  #
83
83
  module ObjectIdentifier
84
+ Familia::Base.add_feature self, :object_identifier, depends_on: []
85
+
84
86
  DEFAULT_GENERATOR = :uuid_v7
85
87
 
86
88
  def self.included(base)
87
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
88
- base.extend ClassMethods
89
+ Familia.trace :LOADED, self, base if Familia.debug?
90
+ base.extend ModelClassMethods
89
91
 
90
92
  # Ensure default generator is set in feature options
91
93
  base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
92
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
+
93
104
  # Register the objid field using a simple custom field type
94
105
  base.register_field_type(ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false))
95
106
  end
@@ -99,7 +110,7 @@ module Familia
99
110
  # Object identifier fields automatically generate unique identifiers when first
100
111
  # accessed if not already set. The generation strategy is configurable via
101
112
  # feature options. These fields preserve any values set during initialization
102
- # to ensure data integrity when loading existing objects from Redis.
113
+ # to ensure data integrity when loading existing objects from the database.
103
114
  #
104
115
  # The field type tracks the generator used for each objid to provide provenance
105
116
  # information for security-sensitive operations like external identifier generation.
@@ -149,6 +160,9 @@ module Familia
149
160
  generator = options[:generator] || DEFAULT_GENERATOR
150
161
  instance_variable_set(:"@#{field_name}_generator_used", generator)
151
162
 
163
+ # Update mapping from objid to model primary key
164
+ self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
165
+
152
166
  generated_id
153
167
  end
154
168
  end
@@ -164,7 +178,7 @@ module Familia
164
178
  # Override setter to preserve values during initialization
165
179
  #
166
180
  # This ensures that values passed during object initialization
167
- # (e.g., when loading from Redis) are preserved and not overwritten
181
+ # (e.g., when loading from Valkey/Redis) are preserved and not overwritten
168
182
  # by the lazy generation logic.
169
183
  #
170
184
  # @param klass [Class] The class to define the method on
@@ -175,9 +189,19 @@ module Familia
175
189
 
176
190
  handle_method_conflict(klass, :"#{method_name}=") do
177
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
+
178
199
  instance_variable_set(:"@#{field_name}", value)
179
200
 
180
- # 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),
181
205
  # we cannot determine the original generator, so we clear the provenance
182
206
  # tracking to indicate unknown origin. This prevents false assumptions
183
207
  # about the security properties of externally-provided identifiers.
@@ -203,7 +227,7 @@ module Familia
203
227
  end
204
228
  end
205
229
 
206
- module ClassMethods
230
+ module ModelClassMethods
207
231
  # Generate a new object identifier using the configured strategy
208
232
  #
209
233
  # @return [String] A new unique identifier
@@ -218,7 +242,7 @@ module Familia
218
242
  when :uuid_v4
219
243
  SecureRandom.uuid_v4
220
244
  when :hex
221
- Familia.generate_hex_id
245
+ Familia.generate_id(16)
222
246
  when Proc
223
247
  generator.call
224
248
  else
@@ -241,14 +265,21 @@ module Familia
241
265
 
242
266
  if Familia.debug?
243
267
  reference = caller(1..1).first
244
- Familia.trace :FIND_BY_OBJID, Familia.dbclient, objid, reference
268
+ Familia.trace :FIND_BY_OBJID, nil, objid, reference
245
269
  end
246
270
 
247
- # Use the object identifier as the key for lookup
248
- # This is a simple stub implementation - would need more sophisticated
249
- # search logic in a real application
250
- 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)
251
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)
252
283
  nil
253
284
  end
254
285
  end
@@ -281,6 +312,15 @@ module Familia
281
312
  self.objid = value
282
313
  end
283
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
+
284
324
  # Initialize object identifier configuration
285
325
  #
286
326
  # Called during object initialization to set up the ID generation strategy.
@@ -298,10 +338,8 @@ module Familia
298
338
 
299
339
  options = self.class.feature_options(:object_identifier)
300
340
  generator = options[:generator] || DEFAULT_GENERATOR
301
- Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
341
+ Familia.trace :OBJID_INIT, nil, "Generator strategy: #{generator}"
302
342
  end
303
-
304
- Familia::Base.add_feature self, :object_identifier, depends_on: []
305
343
  end
306
344
  end
307
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,17 +245,18 @@ module Familia
245
245
  # NoDefault.qstamp() # Uses 10.minutes as fallback quantum
246
246
  #
247
247
  module Quantization
248
+ Familia::Base.add_feature self, :quantization
248
249
 
249
250
  using Familia::Refinements::TimeLiterals
250
251
 
251
252
  def self.included(base)
252
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
253
- base.extend ClassMethods
253
+ Familia.trace :LOADED, self, base if Familia.debug?
254
+ base.extend ModelClassMethods
254
255
  end
255
256
 
256
- # Familia::Quantization::ClassMethods
257
+ # Familia::Quantization::ModelClassMethods
257
258
  #
258
- module ClassMethods
259
+ module ModelClassMethods
259
260
  # Generates a quantized timestamp based on the given parameters
260
261
  #
261
262
  # This method rounds the current time to the nearest quantum and optionally
@@ -284,9 +285,7 @@ module Familia
284
285
  #
285
286
  def qstamp(quantum = nil, pattern: nil, time: nil)
286
287
  # Handle array input format: [quantum, pattern]
287
- if quantum.is_a?(Array)
288
- quantum, pattern = quantum
289
- end
288
+ quantum, pattern = quantum if quantum.is_a?(Array)
290
289
 
291
290
  # Use default quantum if none specified
292
291
  # Priority: provided quantum > class default_expiration > 10.minutes fallback
@@ -321,11 +320,11 @@ module Familia
321
320
  end_bucket = qstamp(quantum, time: end_time)
322
321
 
323
322
  while current <= end_bucket
324
- if pattern
325
- timestamps << Time.at(current).strftime(pattern)
326
- else
327
- timestamps << current
328
- end
323
+ timestamps << if pattern
324
+ Time.at(current).strftime(pattern)
325
+ else
326
+ current
327
+ end
329
328
  current += quantum
330
329
  end
331
330
 
@@ -350,7 +349,7 @@ module Familia
350
349
  bucket_start = qstamp(quantum, time: Time.at(bucket_time))
351
350
  bucket_end = bucket_start + quantum - 1
352
351
 
353
- timestamp >= bucket_start && timestamp <= bucket_end
352
+ timestamp.between?(bucket_start, bucket_end)
354
353
  end
355
354
  end
356
355
 
@@ -395,10 +394,6 @@ module Familia
395
394
  base_id = respond_to?(:identifier) ? identifier : object_id
396
395
  "#{base_id}#{separator}#{timestamp}"
397
396
  end
398
-
399
- extend ClassMethods
400
-
401
- Familia::Base.add_feature self, :quantization
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