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
@@ -5,7 +5,7 @@ module Familia
5
5
  module Relationships
6
6
  # Score encoding using bit flags for permissions
7
7
  #
8
- # Encodes permissions as bit flags in the decimal portion of Redis sorted set scores:
8
+ # Encodes permissions as bit flags in the decimal portion of Valkey/Redis sorted set scores:
9
9
  # - Integer part: Unix timestamp for time-based ordering
10
10
  # - Decimal part: 8-bit permission flags (0-255)
11
11
  #
@@ -48,7 +48,7 @@ module Familia
48
48
  viewer: PERMISSION_FLAGS[:read],
49
49
  editor: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit],
50
50
  moderator: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit] | PERMISSION_FLAGS[:delete],
51
- admin: 0b11111111 # All permissions
51
+ admin: 0b11111111, # All permissions
52
52
  }.freeze
53
53
 
54
54
  # Categorical masks for efficient broad queries
@@ -57,7 +57,7 @@ module Familia
57
57
  content_editor: 0b00001110, # Can modify content (append|write|edit)
58
58
  administrator: 0b11110000, # Has any admin powers
59
59
  privileged: 0b11111110, # Has beyond read-only
60
- owner: 0b11111111 # All permissions
60
+ owner: 0b11111111, # All permissions
61
61
  }.freeze
62
62
 
63
63
  class << self
@@ -74,7 +74,7 @@ module Familia
74
74
  #
75
75
  # @param timestamp [Time, Integer] The timestamp to encode
76
76
  # @param permission [Symbol, Integer, Array] Permission(s) to encode
77
- # @return [Float] Encoded score suitable for Redis sorted sets
77
+ # @return [Float] Encoded score suitable for Valkey/Redis sorted sets
78
78
  def permission_encode(timestamp, permission)
79
79
  encode_score(timestamp, permission)
80
80
  end
@@ -88,26 +88,26 @@ module Familia
88
88
  {
89
89
  timestamp: decoded[:timestamp],
90
90
  permissions: decoded[:permissions],
91
- permission_list: decoded[:permission_list]
91
+ permission_list: decoded[:permission_list],
92
92
  }
93
93
  end
94
94
 
95
- # Encode a timestamp and permissions into a Redis score
95
+ # Encode a timestamp and permissions into a Valkey/Redis score
96
96
  #
97
97
  # @param timestamp [Time, Integer] The timestamp to encode
98
98
  # @param permissions [Integer, Symbol, Array] Permissions to encode
99
- # @return [Float] Encoded score suitable for Redis sorted sets
99
+ # @return [Float] Encoded score suitable for Valkey/Redis sorted sets
100
100
  #
101
101
  # @example Basic encoding with bit flag
102
- # encode_score(Time.now, 5) # read(1) + write(4) = 5
102
+ # encode_score(Familia.now, 5) # read(1) + write(4) = 5
103
103
  # #=> 1704067200.005
104
104
  #
105
105
  # @example Permission symbol encoding
106
- # encode_score(Time.now, :read)
106
+ # encode_score(Familia.now, :read)
107
107
  # #=> 1704067200.001
108
108
  #
109
109
  # @example Multiple permissions
110
- # encode_score(Time.now, [:read, :write, :delete])
110
+ # encode_score(Familia.now, [:read, :write, :delete])
111
111
  # #=> 1704067200.037
112
112
  def encode_score(timestamp, permissions = 0)
113
113
  time_part = timestamp.respond_to?(:to_i) ? timestamp.to_i : timestamp
@@ -127,7 +127,7 @@ module Familia
127
127
  time_part + (permission_bits / METADATA_PRECISION)
128
128
  end
129
129
 
130
- # Decode a Redis score back into timestamp and permissions
130
+ # Decode a Valkey/Redis score back into timestamp and permissions
131
131
  #
132
132
  # @param score [Float] The encoded score
133
133
  # @return [Hash] Hash with :timestamp, :permissions, and :permission_list keys
@@ -144,7 +144,7 @@ module Familia
144
144
  {
145
145
  timestamp: time_part,
146
146
  permissions: permission_bits,
147
- permission_list: decode_permission_flags(permission_bits)
147
+ permission_list: decode_permission_flags(permission_bits),
148
148
  }
149
149
  end
150
150
 
@@ -211,7 +211,7 @@ module Familia
211
211
  #
212
212
  # @param min_permissions [Array<Symbol>, nil] Minimum required permissions
213
213
  # @param max_permissions [Array<Symbol>, nil] Maximum allowed permissions
214
- # @return [Array<Float>] Min and max scores for Redis range queries
214
+ # @return [Array<Float>] Min and max scores for Valkey/Redis range queries
215
215
  #
216
216
  # @example
217
217
  # permission_range([:read], [:read, :write])
@@ -231,20 +231,20 @@ module Familia
231
231
 
232
232
  # Get current timestamp as score (no permissions)
233
233
  #
234
- # @return [Float] Current time as Redis score
234
+ # @return [Float] Current time as Valkey/Redis score
235
235
  def current_score
236
- encode_score(Time.now, 0)
236
+ encode_score(Familia.now, 0)
237
237
  end
238
238
 
239
- # Create score range for Redis operations based on time bounds
239
+ # Create score range for db operations based on time bounds
240
240
  #
241
241
  # @param start_time [Time, nil] Start time (nil for -inf)
242
242
  # @param end_time [Time, nil] End time (nil for +inf)
243
243
  # @param min_permissions [Array<Symbol>, nil] Minimum required permissions
244
- # @return [Array] Array suitable for Redis ZRANGEBYSCORE operations
244
+ # @return [Array] Array suitable for Valkey/Redis ZRANGEBYSCORE operations
245
245
  #
246
246
  # @example Time range
247
- # score_range(1.hour.ago, Time.now)
247
+ # score_range(1.hour.ago, Familia.now)
248
248
  # #=> [1704063600.0, 1704067200.255]
249
249
  #
250
250
  # @example Permission filter
@@ -367,13 +367,13 @@ module Familia
367
367
  # @param category [Symbol] Category to create range for
368
368
  # @param start_time [Time, nil] Optional start time filter
369
369
  # @param end_time [Time, nil] Optional end time filter
370
- # @return [Array<String>] Min and max range strings for Redis queries
370
+ # @return [Array<String>] Min and max range strings for Valkey/Redis queries
371
371
  def category_score_range(category, start_time = nil, end_time = nil)
372
372
  PERMISSION_CATEGORIES[category] || 0
373
373
 
374
374
  # Any permission matching the category mask
375
375
  min_score = start_time ? start_time.to_i : 0
376
- max_score = end_time ? end_time.to_i : Time.now.to_i
376
+ max_score = end_time ? end_time.to_i : Familia.now.to_i
377
377
 
378
378
  # Return range that includes any matching permissions
379
379
  ["#{min_score}.000", "#{max_score}.999"]
@@ -2,80 +2,61 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require_relative 'relationships/score_encoding'
5
- require_relative 'relationships/redis_operations'
6
- require_relative 'relationships/tracking'
5
+ require_relative 'relationships/participation'
7
6
  require_relative 'relationships/indexing'
8
- require_relative 'relationships/membership'
9
- require_relative 'relationships/cascading'
10
- require_relative 'relationships/querying'
11
- require_relative 'relationships/permission_management'
12
7
 
13
8
  module Familia
14
9
  module Features
15
10
  # Unified Relationships feature for Familia v2
16
11
  #
17
12
  # This feature merges the functionality of relatable_objects and relationships
18
- # into a single, Redis-native implementation that embraces the "where does this appear?"
13
+ # into a single, Valkey/Redis-native implementation that embraces the "where does this appear?"
19
14
  # philosophy rather than "who owns this?".
20
15
  #
21
- # Key improvements in v2:
22
- # - Multi-presence: Objects can exist in multiple collections simultaneously
23
- # - Score encoding: Metadata embedded in Redis scores for efficiency
24
- # - Collision-free: Method names include collection names to prevent conflicts
25
- # - Redis-native: All operations use Redis commands, no Ruby iteration
26
- # - Atomic operations: Multi-collection updates happen atomically
27
- #
28
- # Breaking changes from v1:
29
- # - Single feature: Use `feature :relationships` instead of separate features
30
- # - Simplified identifier: Use `identifier :field` instead of `identifier_field :field`
31
- # - No ownership concept: Remove `owned_by`, use multi-presence instead
32
- # - Method naming: Generated methods include collection names for uniqueness
33
- # - Score encoding: Scores can carry metadata like permissions
34
- #
35
16
  # @example Basic usage
36
17
  # class Domain < Familia::Horreum
37
- # feature :relationships
38
18
  #
39
- # identifier :domain_id
19
+ # identifier_field :domain_id
20
+ #
40
21
  # field :domain_id
41
22
  # field :display_name
42
23
  # field :created_at
43
24
  # field :permission_bits
44
25
  #
45
- # # Multi-presence tracking with score encoding
46
- # tracked_in Customer, :domains,
47
- # score: -> { permission_encode(created_at, permission_bits) }
48
- # tracked_in Team, :domains, score: :added_at
49
- # tracked_in Organization, :all_domains, score: :created_at
26
+ # feature :relationships
50
27
  #
51
- # # O(1) lookups with Redis hashes
52
- # indexed_by :display_name, :domain_index, context: Customer
53
- # indexed_by :display_name, :global_domain_index, context: :global
28
+ # # Multi-presence participation with score encoding
29
+ # participates_in Customer, :domains,
30
+ # score: -> { permission_encode(created_at, permission_bits) }
31
+ # participates_in Team, :domains, score: :added_at
32
+ # participates_in Organization, :all_domains, score: :created_at
54
33
  #
55
- # # Context-aware membership (no method collisions)
56
- # member_of Customer, :domains
57
- # member_of Team, :domains
58
- # member_of Organization, :domains
34
+ # # O(1) lookups with Valkey/Redis hashes
35
+ # indexed_by :display_name, :domain_index, target: Customer
36
+ #
37
+ # # Participation with bidirectional control (no method collisions)
38
+ # participates_in Customer, :domains
39
+ # participates_in Team, :domains, bidirectional: false
40
+ # participates_in Organization, :domains, type: :set
59
41
  # end
60
42
  #
61
43
  # @example Generated methods (collision-free)
62
- # # Tracking methods
44
+ # # Participation methods
63
45
  # Customer.domains # => Familia::SortedSet
64
46
  # Customer.add_domain(domain, score) # Add to customer's domains
65
47
  # domain.in_customer_domains?(customer) # Check membership
66
48
  #
67
49
  # # Indexing methods
68
50
  # Customer.find_by_display_name(name) # O(1) lookup
69
- # Domain.find_by_display_name(name) # Global lookup
70
51
  #
71
- # # Membership methods (collision-free naming)
52
+ # # Bidirectional methods (collision-free naming)
72
53
  # domain.add_to_customer_domains(customer) # Specific collection
73
54
  # domain.add_to_team_domains(team) # Different collection
74
55
  # domain.in_customer_domains?(customer) # Check specific membership
75
56
  #
76
57
  # @example Score encoding for permissions
77
58
  # # Encode permission in score
78
- # score = domain.permission_encode(Time.now, :write)
59
+ # score = domain.permission_encode(Familia.now, :write)
79
60
  # # => 1704067200.004 (timestamp + permission bits)
80
61
  #
81
62
  # # Decode permission from score
@@ -89,42 +70,32 @@ module Familia
89
70
  # # Atomic updates across multiple collections
90
71
  # domain.update_multiple_presence([
91
72
  # { key: "customer:123:domains", score: current_score },
92
- # { key: "team:456:domains", score: permission_encode(Time.now, :read) }
73
+ # { key: "team:456:domains", score: permission_encode(Familia.now, :read) }
93
74
  # ], :add, domain.identifier)
94
75
  #
95
- # # Set operations on collections
76
+ # # UnsortedSet operations on collections
96
77
  # accessible = Domain.union_collections([
97
78
  # { owner: customer, collection: :domains },
98
79
  # { owner: team, collection: :domains }
99
80
  # ], min_permission: :read)
100
81
  module Relationships
82
+ # Register the feature with Familia
83
+ Familia::Base.add_feature Relationships, :relationships
84
+
101
85
  # Feature initialization
102
86
  def self.included(base)
103
- puts "[DEBUG] Relationships included in #{base}"
104
- base.extend ClassMethods
105
- base.include InstanceMethods
87
+ Familia.ld "[#{base}] Relationships included"
88
+ base.extend ModelClassMethods
89
+ base.include ModelInstanceMethods
106
90
 
107
91
  # Include all relationship submodules and their class methods
108
92
  base.include ScoreEncoding
109
- base.include RedisOperations
110
93
 
111
- puts '[DEBUG] Including Tracking module'
112
- base.include Tracking
113
- puts '[DEBUG] Extending with Tracking::ClassMethods'
114
- base.extend Tracking::ClassMethods
115
- puts "[DEBUG] Base now responds to tracked_in: #{base.respond_to?(:tracked_in)}"
94
+ base.include Participation
95
+ base.extend Participation::ModelClassMethods
116
96
 
117
97
  base.include Indexing
118
- base.extend Indexing::ClassMethods
119
-
120
- base.include Membership
121
- base.extend Membership::ClassMethods
122
-
123
- base.include Cascading
124
- base.extend Cascading::ClassMethods
125
-
126
- base.include Querying
127
- base.extend Querying::ClassMethods
98
+ base.extend Indexing::ModelClassMethods
128
99
  end
129
100
 
130
101
  # Error classes
@@ -133,7 +104,7 @@ module Familia
133
104
  class InvalidScoreError < RelationshipError; end
134
105
  class CascadeError < RelationshipError; end
135
106
 
136
- module ClassMethods
107
+ module ModelClassMethods
137
108
  # Define the identifier for this class (replaces identifier_field)
138
109
  # This is a compatibility wrapper around the existing identifier_field method
139
110
  #
@@ -148,21 +119,6 @@ module Familia
148
119
  identifier_field
149
120
  end
150
121
 
151
- # Generate a secure temporary identifier
152
- def generate_identifier
153
- SecureRandom.hex(8)
154
- end
155
-
156
- # Get all relationship configurations for this class
157
- def relationship_configs
158
- configs = {}
159
-
160
- configs[:tracking] = tracking_relationships if respond_to?(:tracking_relationships)
161
- configs[:indexing] = indexing_relationships if respond_to?(:indexing_relationships)
162
- configs[:membership] = membership_relationships if respond_to?(:membership_relationships)
163
-
164
- configs
165
- end
166
122
 
167
123
  # Validate relationship configurations
168
124
  def validate_relationships!
@@ -171,25 +127,14 @@ module Familia
171
127
  # Check for method name collisions
172
128
  method_names = []
173
129
 
174
- if respond_to?(:tracking_relationships)
175
- tracking_relationships.each do |config|
176
- context_name = config[:context_class_name].downcase
130
+ if respond_to?(:participation_relationships)
131
+ participation_relationships.each do |config|
132
+ target_name = config[:target_class_name].downcase
177
133
  collection_name = config[:collection_name]
178
134
 
179
- method_names << "in_#{context_name}_#{collection_name}?"
180
- method_names << "add_to_#{context_name}_#{collection_name}"
181
- method_names << "remove_from_#{context_name}_#{collection_name}"
182
- end
183
- end
184
-
185
- if respond_to?(:membership_relationships)
186
- membership_relationships.each do |config|
187
- owner_name = config[:owner_class_name].downcase
188
- collection_name = config[:collection_name]
189
-
190
- method_names << "in_#{owner_name}_#{collection_name}?"
191
- method_names << "add_to_#{owner_name}_#{collection_name}"
192
- method_names << "remove_from_#{owner_name}_#{collection_name}"
135
+ method_names << "in_#{target_name}_#{collection_name}?"
136
+ method_names << "add_to_#{target_name}_#{collection_name}"
137
+ method_names << "remove_from_#{target_name}_#{collection_name}"
193
138
  end
194
139
  end
195
140
 
@@ -208,20 +153,13 @@ module Familia
208
153
  true
209
154
  end
210
155
 
211
- # Create a new instance with relationships initialized
212
- def create_with_relationships(attributes = {})
213
- instance = new(attributes)
214
- instance.initialize_relationships
215
- instance
216
- end
217
-
218
156
  # Class method wrapper for create_temp_key
219
157
  def create_temp_key(base_name, ttl = 300)
220
- timestamp = Time.now.to_i
158
+ timestamp = Familia.now.to_i
221
159
  random_suffix = SecureRandom.hex(3)
222
160
  temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
223
161
 
224
- # Set immediate expiry to ensure cleanup even if operation fails
162
+ # UnsortedSet immediate expiry to ensure cleanup even if operation fails
225
163
  if respond_to?(:dbclient)
226
164
  dbclient.expire(temp_key, ttl)
227
165
  else
@@ -235,116 +173,60 @@ module Familia
235
173
  include ScoreEncoding
236
174
 
237
175
  private
238
-
239
- # Simple constantize method to convert string to constant
240
- def constantize_class_name(class_name)
241
- class_name.split('::').reduce(Object) { |mod, name| mod.const_get(name) }
242
- rescue NameError
243
- # If the class doesn't exist, return nil
244
- nil
245
- end
246
176
  end
247
177
 
248
- module InstanceMethods
249
- # Get the identifier value for this instance
250
- # Uses the existing Horreum identifier infrastructure
251
- def identifier
252
- id_field = self.class.identifier_field
253
- send(id_field) if respond_to?(id_field)
254
- end
255
-
256
- # Set the identifier value for this instance
257
- def identifier=(value)
258
- id_field = self.class.identifier_field
259
- send("#{id_field}=", value) if respond_to?("#{id_field}=")
260
- end
261
-
262
- # Initialize relationships (called after object creation)
263
- def initialize_relationships
264
- # This can be overridden by subclasses to set up initial relationships
265
- end
178
+ module ModelInstanceMethods
179
+ # NOTE: identifier and identifier= methods are provided by Horreum base class
180
+ # No need to override them here - use the existing infrastructure
266
181
 
267
182
  # Override save to update relationships automatically
268
183
  def save(update_expiration: true)
269
184
  result = super
270
185
 
271
- if result
186
+ if result && respond_to?(:update_all_indexes)
272
187
  # Automatically update all indexes when object is saved
273
- if respond_to?(:update_all_indexes)
274
- update_all_indexes
275
- end
276
-
277
- # Auto-add to class-level tracking collections
278
- if respond_to?(:add_to_class_tracking_collections)
279
- add_to_class_tracking_collections
280
- end
188
+ update_all_indexes
281
189
 
282
- # NOTE: Relationship-specific membership and tracking updates are done explicitly
190
+ # NOTE: Relationship-specific participation updates are done explicitly
283
191
  # since we need to know which specific collections this object should be in
284
192
  end
285
193
 
286
194
  result
287
195
  end
288
196
 
289
- # Override destroy to handle cascade operations
290
- def destroy!
291
- # Execute cascade operations before destroying the object
292
- execute_cascade_operations if respond_to?(:execute_cascade_operations)
293
-
294
- super
295
- end
296
-
297
197
  # Get comprehensive relationship status for this object
298
198
  def relationship_status
299
199
  status = {
300
200
  identifier: identifier,
301
- tracking_memberships: [],
302
- membership_collections: [],
303
- index_memberships: []
201
+ current_participations: [],
202
+ index_memberships: [],
304
203
  }
305
204
 
306
- # Get tracking memberships
307
- if respond_to?(:tracking_collections_membership)
308
- status[:tracking_memberships] = tracking_collections_membership
309
- end
310
-
311
- # Get membership collections
312
- status[:membership_collections] = membership_collections if respond_to?(:membership_collections)
205
+ # Get participation memberships
206
+ status[:current_participations] = current_participations if respond_to?(:current_participations)
313
207
 
314
208
  # Get index memberships
315
- status[:index_memberships] = indexing_memberships if respond_to?(:indexing_memberships)
209
+ status[:index_memberships] = current_indexings if respond_to?(:current_indexings)
316
210
 
317
211
  status
318
212
  end
319
213
 
320
214
  # Comprehensive cleanup - remove from all relationships
215
+ #
216
+ # @deprecated This method is poorly implemented and will be removed in v3.0.
217
+ # The participation collection removal logic was repetitive and difficult to debug.
218
+ # A cleaner implementation will be provided in a future version.
219
+ # See pull #115 for details.
220
+ #
221
+ # @note Currently only removes from indexes, not participation collections
321
222
  def cleanup_all_relationships!
322
- # Remove from tracking collections
323
- remove_from_all_tracking_collections if respond_to?(:remove_from_all_tracking_collections)
324
-
325
- # Remove from membership collections
326
- remove_from_all_memberships if respond_to?(:remove_from_all_memberships)
223
+ warn '[DEPRECATED] cleanup_all_relationships! will be removed in v3.0. See pull #115.'
224
+ warn 'Not currently removing from participation collections. Only indexes will be cleaned.'
327
225
 
328
226
  # Remove from indexes
329
227
  remove_from_all_indexes if respond_to?(:remove_from_all_indexes)
330
228
  end
331
229
 
332
- # Dry run for relationship cleanup (preview what would be affected)
333
- def cleanup_preview
334
- preview = {
335
- tracking_collections: [],
336
- membership_collections: [],
337
- index_entries: []
338
- }
339
-
340
- if respond_to?(:cascade_dry_run)
341
- cascade_preview = cascade_dry_run
342
- preview.merge!(cascade_preview)
343
- end
344
-
345
- preview
346
- end
347
-
348
230
  # Validate that this object's relationships are consistent
349
231
  def validate_relationships!
350
232
  errors = []
@@ -352,11 +234,11 @@ module Familia
352
234
  # Validate identifier exists
353
235
  errors << 'Object identifier is nil' unless identifier
354
236
 
355
- # Validate tracking memberships
356
- if respond_to?(:tracking_collections_membership)
357
- tracking_collections_membership.each do |membership|
237
+ # Validate participation memberships
238
+ if respond_to?(:current_participations)
239
+ current_participations.each do |membership|
358
240
  score = membership[:score]
359
- errors << "Invalid score in tracking membership: #{membership}" if score && !score.is_a?(Numeric)
241
+ errors << "Invalid score in participation membership: #{membership}" if score && !score.is_a?(Numeric)
360
242
  end
361
243
  end
362
244
 
@@ -365,109 +247,25 @@ module Familia
365
247
  true
366
248
  end
367
249
 
368
- # Refresh relationship data from Redis (useful after external changes)
369
- def refresh_relationships!
370
- # Clear any cached relationship data
371
- @relationship_status = nil
372
- @tracking_memberships = nil
373
- @membership_collections = nil
374
- @index_memberships = nil
375
-
376
- # Reload fresh data
377
- relationship_status
378
- end
379
-
380
- # Create a snapshot of current relationship state (for debugging)
381
- def relationship_snapshot
382
- {
383
- timestamp: Time.now,
384
- identifier: identifier,
385
- class: self.class.name,
386
- status: relationship_status,
387
- redis_keys: find_related_redis_keys
388
- }
389
- end
390
-
391
- # Direct Redis access for instance methods
392
- def redis
250
+ # Direct Valkey/Redis access for instance methods
251
+ def dbclient
393
252
  self.class.dbclient
394
253
  end
395
254
 
396
255
  # Instance method wrapper for create_temp_key
397
256
  def create_temp_key(base_name, ttl = 300)
398
- timestamp = Time.now.to_i
257
+ timestamp = Familia.now.to_i
399
258
  random_suffix = SecureRandom.hex(3)
400
259
  temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
401
260
 
402
- # Set immediate expiry to ensure cleanup even if operation fails
403
- redis.expire(temp_key, ttl)
261
+ # UnsortedSet immediate expiry to ensure cleanup even if operation fails
262
+ dbclient.expire(temp_key, ttl)
404
263
 
405
264
  temp_key
406
265
  end
407
266
 
408
- # Instance method wrapper for cleanup_temp_keys
409
- def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
410
- cursor = 0
411
-
412
- loop do
413
- cursor, keys = redis.scan(cursor, match: pattern, count: batch_size)
414
-
415
- if keys.any?
416
- # Check TTL and remove keys that should have expired
417
- keys.each_slice(batch_size) do |key_batch|
418
- redis.pipelined do |pipeline|
419
- key_batch.each do |key|
420
- ttl = redis.ttl(key)
421
- pipeline.del(key) if ttl == -1 # Key exists but has no TTL
422
- end
423
- end
424
- end
425
- end
426
-
427
- break if cursor.zero?
428
- end
429
- end
430
-
431
267
  private
432
-
433
- # Find all Redis keys related to this object
434
- def find_related_redis_keys
435
- related_keys = []
436
- id = identifier
437
- return related_keys unless id
438
-
439
- # Scan for keys that might contain this object
440
- patterns = [
441
- '*:*:*', # General pattern for relationship keys
442
- "*#{id}*" # Keys containing the identifier
443
- ]
444
-
445
- patterns.each do |pattern|
446
- redis.scan_each(match: pattern, count: 100) do |key|
447
- # Check if this key actually contains our object
448
- key_type = redis.type(key)
449
-
450
- case key_type
451
- when 'zset'
452
- related_keys << key if redis.zscore(key, id)
453
- when 'set'
454
- related_keys << key if redis.sismember(key, id)
455
- when 'list'
456
- related_keys << key if redis.lpos(key, id)
457
- when 'hash'
458
- # For hash keys, check if any field values match our identifier
459
- hash_values = redis.hvals(key)
460
- related_keys << key if hash_values.include?(id.to_s)
461
- end
462
- end
463
- end
464
-
465
- related_keys.uniq
466
- end
467
268
  end
468
-
469
- # Register the feature with Familia
470
- Familia::Base.add_feature Relationships, :relationships
471
269
  end
472
270
  end
473
271
  end