familia 2.0.0.pre15 → 2.0.0.pre17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -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
27
+ #
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
50
33
  #
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
34
+ # # O(1) lookups with Valkey/Redis hashes
35
+ # indexed_by :display_name, :domain_index, target: Customer
54
36
  #
55
- # # Context-aware membership (no method collisions)
56
- # member_of Customer, :domains
57
- # member_of Team, :domains
58
- # member_of Organization, :domains
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,43 +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
101
-
102
82
  # Register the feature with Familia
103
83
  Familia::Base.add_feature Relationships, :relationships
104
84
 
105
85
  # Feature initialization
106
86
  def self.included(base)
107
87
  Familia.ld "[#{base}] Relationships included"
108
- base.extend ClassMethods
109
- base.include InstanceMethods
88
+ base.extend ModelClassMethods
89
+ base.include ModelInstanceMethods
110
90
 
111
91
  # Include all relationship submodules and their class methods
112
92
  base.include ScoreEncoding
113
- base.include RedisOperations
114
93
 
115
- base.include Tracking
116
- base.extend Tracking::ClassMethods
94
+ base.include Participation
95
+ base.extend Participation::ModelClassMethods
117
96
 
118
97
  base.include Indexing
119
- base.extend Indexing::ClassMethods
120
-
121
- base.include Membership
122
- base.extend Membership::ClassMethods
123
-
124
- base.include Cascading
125
- base.extend Cascading::ClassMethods
126
-
127
- base.include Querying
128
- base.extend Querying::ClassMethods
98
+ base.extend Indexing::ModelClassMethods
129
99
  end
130
100
 
131
101
  # Error classes
@@ -134,7 +104,7 @@ module Familia
134
104
  class InvalidScoreError < RelationshipError; end
135
105
  class CascadeError < RelationshipError; end
136
106
 
137
- module ClassMethods
107
+ module ModelClassMethods
138
108
  # Define the identifier for this class (replaces identifier_field)
139
109
  # This is a compatibility wrapper around the existing identifier_field method
140
110
  #
@@ -149,21 +119,6 @@ module Familia
149
119
  identifier_field
150
120
  end
151
121
 
152
- # Generate a secure temporary identifier
153
- def generate_identifier
154
- SecureRandom.hex(8)
155
- end
156
-
157
- # Get all relationship configurations for this class
158
- def relationship_configs
159
- configs = {}
160
-
161
- configs[:tracking] = tracking_relationships if respond_to?(:tracking_relationships)
162
- configs[:indexing] = indexing_relationships if respond_to?(:indexing_relationships)
163
- configs[:membership] = membership_relationships if respond_to?(:membership_relationships)
164
-
165
- configs
166
- end
167
122
 
168
123
  # Validate relationship configurations
169
124
  def validate_relationships!
@@ -172,25 +127,14 @@ module Familia
172
127
  # Check for method name collisions
173
128
  method_names = []
174
129
 
175
- if respond_to?(:tracking_relationships)
176
- tracking_relationships.each do |config|
177
- context_name = config[:context_class_name].downcase
178
- collection_name = config[:collection_name]
179
-
180
- method_names << "in_#{context_name}_#{collection_name}?"
181
- method_names << "add_to_#{context_name}_#{collection_name}"
182
- method_names << "remove_from_#{context_name}_#{collection_name}"
183
- end
184
- end
185
-
186
- if respond_to?(:membership_relationships)
187
- membership_relationships.each do |config|
188
- owner_name = config[:owner_class_name].downcase
130
+ if respond_to?(:participation_relationships)
131
+ participation_relationships.each do |config|
132
+ target_name = config[:target_class_name].downcase
189
133
  collection_name = config[:collection_name]
190
134
 
191
- method_names << "in_#{owner_name}_#{collection_name}?"
192
- method_names << "add_to_#{owner_name}_#{collection_name}"
193
- 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}"
194
138
  end
195
139
  end
196
140
 
@@ -209,20 +153,13 @@ module Familia
209
153
  true
210
154
  end
211
155
 
212
- # Create a new instance with relationships initialized
213
- def create_with_relationships(attributes = {})
214
- instance = new(attributes)
215
- instance.initialize_relationships
216
- instance
217
- end
218
-
219
156
  # Class method wrapper for create_temp_key
220
157
  def create_temp_key(base_name, ttl = 300)
221
- timestamp = Time.now.to_i
158
+ timestamp = Familia.now.to_i
222
159
  random_suffix = SecureRandom.hex(3)
223
160
  temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
224
161
 
225
- # Set immediate expiry to ensure cleanup even if operation fails
162
+ # UnsortedSet immediate expiry to ensure cleanup even if operation fails
226
163
  if respond_to?(:dbclient)
227
164
  dbclient.expire(temp_key, ttl)
228
165
  else
@@ -236,116 +173,60 @@ module Familia
236
173
  include ScoreEncoding
237
174
 
238
175
  private
239
-
240
- # Simple constantize method to convert string to constant
241
- def constantize_class_name(class_name)
242
- class_name.split('::').reduce(Object) { |mod, name| mod.const_get(name) }
243
- rescue NameError
244
- # If the class doesn't exist, return nil
245
- nil
246
- end
247
176
  end
248
177
 
249
- module InstanceMethods
250
- # Get the identifier value for this instance
251
- # Uses the existing Horreum identifier infrastructure
252
- def identifier
253
- id_field = self.class.identifier_field
254
- send(id_field) if respond_to?(id_field)
255
- end
256
-
257
- # Set the identifier value for this instance
258
- def identifier=(value)
259
- id_field = self.class.identifier_field
260
- send("#{id_field}=", value) if respond_to?("#{id_field}=")
261
- end
262
-
263
- # Initialize relationships (called after object creation)
264
- def initialize_relationships
265
- # This can be overridden by subclasses to set up initial relationships
266
- 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
267
181
 
268
182
  # Override save to update relationships automatically
269
183
  def save(update_expiration: true)
270
184
  result = super
271
185
 
272
- if result
186
+ if result && respond_to?(:update_all_indexes)
273
187
  # Automatically update all indexes when object is saved
274
- if respond_to?(:update_all_indexes)
275
- update_all_indexes
276
- end
277
-
278
- # Auto-add to class-level tracking collections
279
- if respond_to?(:add_to_class_tracking_collections)
280
- add_to_class_tracking_collections
281
- end
188
+ update_all_indexes
282
189
 
283
- # NOTE: Relationship-specific membership and tracking updates are done explicitly
190
+ # NOTE: Relationship-specific participation updates are done explicitly
284
191
  # since we need to know which specific collections this object should be in
285
192
  end
286
193
 
287
194
  result
288
195
  end
289
196
 
290
- # Override destroy to handle cascade operations
291
- def destroy!
292
- # Execute cascade operations before destroying the object
293
- execute_cascade_operations if respond_to?(:execute_cascade_operations)
294
-
295
- super
296
- end
297
-
298
197
  # Get comprehensive relationship status for this object
299
198
  def relationship_status
300
199
  status = {
301
200
  identifier: identifier,
302
- tracking_memberships: [],
303
- membership_collections: [],
304
- index_memberships: []
201
+ current_participations: [],
202
+ index_memberships: [],
305
203
  }
306
204
 
307
- # Get tracking memberships
308
- if respond_to?(:tracking_collections_membership)
309
- status[:tracking_memberships] = tracking_collections_membership
310
- end
311
-
312
- # Get membership collections
313
- 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)
314
207
 
315
208
  # Get index memberships
316
- status[:index_memberships] = indexing_memberships if respond_to?(:indexing_memberships)
209
+ status[:index_memberships] = current_indexings if respond_to?(:current_indexings)
317
210
 
318
211
  status
319
212
  end
320
213
 
321
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
322
222
  def cleanup_all_relationships!
323
- # Remove from tracking collections
324
- remove_from_all_tracking_collections if respond_to?(:remove_from_all_tracking_collections)
325
-
326
- # Remove from membership collections
327
- 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.'
328
225
 
329
226
  # Remove from indexes
330
227
  remove_from_all_indexes if respond_to?(:remove_from_all_indexes)
331
228
  end
332
229
 
333
- # Dry run for relationship cleanup (preview what would be affected)
334
- def cleanup_preview
335
- preview = {
336
- tracking_collections: [],
337
- membership_collections: [],
338
- index_entries: []
339
- }
340
-
341
- if respond_to?(:cascade_dry_run)
342
- cascade_preview = cascade_dry_run
343
- preview.merge!(cascade_preview)
344
- end
345
-
346
- preview
347
- end
348
-
349
230
  # Validate that this object's relationships are consistent
350
231
  def validate_relationships!
351
232
  errors = []
@@ -353,11 +234,11 @@ module Familia
353
234
  # Validate identifier exists
354
235
  errors << 'Object identifier is nil' unless identifier
355
236
 
356
- # Validate tracking memberships
357
- if respond_to?(:tracking_collections_membership)
358
- tracking_collections_membership.each do |membership|
237
+ # Validate participation memberships
238
+ if respond_to?(:current_participations)
239
+ current_participations.each do |membership|
359
240
  score = membership[:score]
360
- 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)
361
242
  end
362
243
  end
363
244
 
@@ -366,107 +247,25 @@ module Familia
366
247
  true
367
248
  end
368
249
 
369
- # Refresh relationship data from Redis (useful after external changes)
370
- def refresh_relationships!
371
- # Clear any cached relationship data
372
- @relationship_status = nil
373
- @tracking_memberships = nil
374
- @membership_collections = nil
375
- @index_memberships = nil
376
-
377
- # Reload fresh data
378
- relationship_status
379
- end
380
-
381
- # Create a snapshot of current relationship state (for debugging)
382
- def relationship_snapshot
383
- {
384
- timestamp: Time.now,
385
- identifier: identifier,
386
- class: self.class.name,
387
- status: relationship_status,
388
- redis_keys: find_related_redis_keys
389
- }
390
- end
391
-
392
- # Direct Redis access for instance methods
393
- def redis
250
+ # Direct Valkey/Redis access for instance methods
251
+ def dbclient
394
252
  self.class.dbclient
395
253
  end
396
254
 
397
255
  # Instance method wrapper for create_temp_key
398
256
  def create_temp_key(base_name, ttl = 300)
399
- timestamp = Time.now.to_i
257
+ timestamp = Familia.now.to_i
400
258
  random_suffix = SecureRandom.hex(3)
401
259
  temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
402
260
 
403
- # Set immediate expiry to ensure cleanup even if operation fails
404
- redis.expire(temp_key, ttl)
261
+ # UnsortedSet immediate expiry to ensure cleanup even if operation fails
262
+ dbclient.expire(temp_key, ttl)
405
263
 
406
264
  temp_key
407
265
  end
408
266
 
409
- # Instance method wrapper for cleanup_temp_keys
410
- def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100)
411
- cursor = 0
412
-
413
- loop do
414
- cursor, keys = redis.scan(cursor, match: pattern, count: batch_size)
415
-
416
- if keys.any?
417
- # Check TTL and remove keys that should have expired
418
- keys.each_slice(batch_size) do |key_batch|
419
- redis.pipelined do |pipeline|
420
- key_batch.each do |key|
421
- ttl = redis.ttl(key)
422
- pipeline.del(key) if ttl == -1 # Key exists but has no TTL
423
- end
424
- end
425
- end
426
- end
427
-
428
- break if cursor.zero?
429
- end
430
- end
431
-
432
267
  private
433
-
434
- # Find all Redis keys related to this object
435
- def find_related_redis_keys
436
- related_keys = []
437
- id = identifier
438
- return related_keys unless id
439
-
440
- # Scan for keys that might contain this object
441
- patterns = [
442
- '*:*:*', # General pattern for relationship keys
443
- "*#{id}*" # Keys containing the identifier
444
- ]
445
-
446
- patterns.each do |pattern|
447
- redis.scan_each(match: pattern, count: 100) do |key|
448
- # Check if this key actually contains our object
449
- key_type = redis.type(key)
450
-
451
- case key_type
452
- when 'zset'
453
- related_keys << key if redis.zscore(key, id)
454
- when 'set'
455
- related_keys << key if redis.sismember(key, id)
456
- when 'list'
457
- related_keys << key if redis.lpos(key, id)
458
- when 'hash'
459
- # For hash keys, check if any field values match our identifier
460
- hash_values = redis.hvals(key)
461
- related_keys << key if hash_values.include?(id.to_s)
462
- end
463
- end
464
- end
465
-
466
- related_keys.uniq
467
- end
468
268
  end
469
-
470
269
  end
471
270
  end
472
271
  end