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
@@ -1,737 +0,0 @@
1
- # Relationships Guide
2
-
3
- ## Overview
4
-
5
- The Relationships feature provides a sophisticated system for managing object relationships in Familia applications. It enables objects to track membership, create bidirectional associations, and maintain indexed lookups while supporting advanced features like permission bit encoding and time-based analytics.
6
-
7
- ## Core Concepts
8
-
9
- ### Relationship Types
10
-
11
- The Familia v2.0 relationships system provides three distinct relationship patterns:
12
-
13
- 1. **`tracked_in`** - Multi-presence tracking with score encoding (sorted sets)
14
- 2. **`indexed_by`** - O(1) hash-based lookups by field values
15
- 3. **`member_of`** - Bidirectional membership with collision-free naming
16
-
17
- Each type is optimized for different use cases and provides specific performance characteristics.
18
-
19
- ## Basic Usage
20
-
21
- ### Enabling Relationships
22
-
23
- ```ruby
24
- class Customer < Familia::Horreum
25
- feature :relationships # Enable relationship functionality
26
-
27
- identifier_field :custid
28
- field :custid, :name, :email
29
-
30
- # Collections for relationships
31
- set :domains # Simple set of domain IDs
32
- sorted_set :activity # Activity feed with timestamps
33
-
34
- # Class-level relationship collections (automatically managed)
35
- class_tracked_in :all_customers, score: :created_at
36
- class_indexed_by :email, :email_lookup
37
- end
38
-
39
- class Domain < Familia::Horreum
40
- feature :relationships
41
-
42
- identifier_field :domain_id
43
- field :domain_id, :name, :dns_zone
44
-
45
- # Define bidirectional membership
46
- member_of Customer, :domains
47
- end
48
- ```
49
-
50
- ## Tracked In Relationships
51
-
52
- ### Basic Tracking
53
-
54
- The `tracked_in` relationship creates collections that track object membership with sophisticated scoring:
55
-
56
- ```ruby
57
- class User < Familia::Horreum
58
- feature :relationships
59
-
60
- identifier_field :user_id
61
- field :user_id, :name, :score_value
62
-
63
- # Simple sorted set tracking
64
- class_tracked_in :leaderboard, score: :score_value
65
-
66
- # Time-based tracking with automatic timestamps
67
- class_tracked_in :activity_log, score: :created_at
68
-
69
- # Proc-based scoring for complex calculations
70
- class_tracked_in :performance_metrics, score: -> { (score_value || 0) * 2 }
71
- end
72
-
73
- # Usage
74
- user = User.new(user_id: 'user123', score_value: 85)
75
-
76
- # Add to collections
77
- User.add_to_leaderboard(user) # Uses score_value (85)
78
- User.add_to_activity_log(user) # Uses created_at timestamp
79
- User.add_to_performance_metrics(user) # Uses proc result (170)
80
-
81
- # Query collections
82
- User.leaderboard.score('user123') # => 85.0
83
- User.activity_log.rangebyscore('-inf', '+inf') # All users by time
84
- User.performance_metrics.rank('user123') # User's rank by performance
85
- ```
86
-
87
- ### Score Encoding System
88
-
89
- The relationships feature includes a sophisticated bit encoding system for permissions and metadata:
90
-
91
- ```ruby
92
- class Document < Familia::Horreum
93
- feature :relationships
94
-
95
- identifier_field :doc_id
96
- field :doc_id, :title, :content
97
-
98
- # Permission-based tracking with 8-bit encoding
99
- class_tracked_in :authorized_users, score: :encode_permissions
100
-
101
- private
102
-
103
- def encode_permissions
104
- # Combine timestamp with permission bits
105
- timestamp = Time.now.to_f.floor
106
- permissions = calculate_user_permissions # Returns 0-255
107
- "#{timestamp}.#{permissions}".to_f
108
- end
109
- end
110
- ```
111
-
112
- #### Permission Bit Flags
113
-
114
- The system supports 8 permission flags (0-255 range):
115
-
116
- | Flag | Value | Permission | Description |
117
- |------|-------|------------|-------------|
118
- | read | 1 | Read access | View document content |
119
- | append | 2 | Append access | Add new content |
120
- | write | 4 | Write access | Modify existing content |
121
- | edit | 8 | Edit access | Full content editing |
122
- | configure | 16 | Configure access | Change document settings |
123
- | delete | 32 | Delete access | Remove document |
124
- | transfer | 64 | Transfer access | Change ownership |
125
- | admin | 128 | Admin access | Full administrative control |
126
-
127
- #### Predefined Permission Roles
128
-
129
- ```ruby
130
- # Permission combinations for common roles
131
- ROLES = {
132
- viewer: 1, # read only
133
- editor: 1 | 2 | 4, # read + append + write
134
- moderator: 15, # read + append + write + edit
135
- admin: 255 # all permissions
136
- }
137
-
138
- # Usage with score encoding
139
- class DocumentAccess
140
- include Familia::Features::Relationships::ScoreEncoding
141
-
142
- def grant_access(user_id, role = :viewer)
143
- permissions = ROLES[role]
144
- encoded_score = encode_score_with_permissions(permissions)
145
- Document.authorized_users.add(user_id, encoded_score)
146
- end
147
-
148
- def check_permission(user_id, permission_flag)
149
- score = Document.authorized_users.score(user_id)
150
- return false unless score
151
-
152
- _, permissions = decode_score_with_permissions(score)
153
- (permissions & permission_flag) != 0
154
- end
155
- end
156
-
157
- # Example usage
158
- access = DocumentAccess.new
159
- access.grant_access('user123', :editor)
160
- access.check_permission('user123', 4) # => true (write permission)
161
- access.check_permission('user123', 32) # => false (no delete permission)
162
- ```
163
-
164
- #### Time-Based Queries with Permissions
165
-
166
- ```ruby
167
- # Range queries combining time and permissions
168
- class DocumentAnalytics
169
- def users_with_access_since(timestamp, min_permissions = 1)
170
- min_score = "#{timestamp}.#{min_permissions}".to_f
171
- Document.authorized_users.range_by_score(min_score, '+inf')
172
- end
173
-
174
- def admin_users_last_week
175
- week_ago = (Time.now - 7.days).to_f.floor
176
- admin_permissions = 128
177
- min_score = "#{week_ago}.#{admin_permissions}".to_f
178
-
179
- Document.authorized_users.range_by_score(min_score, '+inf')
180
- end
181
- end
182
- ```
183
-
184
- ## Indexed By Relationships
185
-
186
- ### Hash-Based Lookups
187
-
188
- The `indexed_by` relationship creates O(1) hash-based indexes for field values. The `context` parameter determines index ownership and scope:
189
-
190
- ```ruby
191
- class User < Familia::Horreum
192
- feature :relationships
193
-
194
- field :email, :username, :department
195
-
196
- # Global indexes for system-wide unique lookups
197
- class_indexed_by :email, :email_index
198
- class_indexed_by :username, :username_index
199
-
200
- # Scoped indexes for values unique within a context
201
- indexed_by :department, :department_index, parent: Organization
202
- end
203
-
204
- # Usage for Global Context
205
- user = User.new(email: 'john@example.com', username: 'johndoe')
206
-
207
- # Add to global indexes (instance methods)
208
- user.add_to_global_email_index
209
- user.add_to_global_username_index
210
-
211
- # Fast O(1) lookups (class methods)
212
- user_id = User.email_index.get('john@example.com') # => user.identifier
213
- user_id = User.username_index.get('johndoe') # => user.identifier
214
-
215
- # Batch operations
216
- users = [user1, user2, user3]
217
- users.each { |u| u.add_to_global_email_index }
218
-
219
- # Check if indexed
220
- User.email_index.exists?('john@example.com') # => true
221
-
222
- # Usage for Scoped Context
223
- organization = Organization.new(org_id: 'acme_corp')
224
- organization.find_by_department('engineering') # Find user by department within this org
225
- ```
226
-
227
- ### Context Parameter Usage Patterns
228
-
229
- Understanding when to use global vs class context:
230
-
231
- ```ruby
232
- class Product < Familia::Horreum
233
- feature :relationships
234
-
235
- field :sku, :category, :brand
236
-
237
- # Global context: SKUs must be unique system-wide
238
- class_indexed_by :sku, :sku_index
239
-
240
- # Class context: Categories are unique per brand
241
- indexed_by :category, :category_index, parent: Brand
242
- end
243
-
244
- class Brand < Familia::Horreum
245
- feature :relationships
246
-
247
- identifier_field :brand_id
248
- field :brand_id, :name
249
- sorted_set :products
250
- end
251
-
252
- # Usage patterns:
253
- product = Product.new(sku: 'ELEC001', category: 'laptops', brand: 'apple')
254
-
255
- # Global indexing (system-wide unique SKUs)
256
- product.add_to_global_sku_index
257
- Product.sku_index.get('ELEC001') # => product.identifier
258
-
259
- # Scoped indexing (categories unique per brand)
260
- brand = Brand.new(brand_id: 'apple', name: 'Apple Inc.')
261
- brand.find_by_category('laptops') # Find products in this brand's laptop category
262
- ```
263
-
264
- ### Context Parameter Reference
265
-
266
- The `context` parameter is a **required** architectural decision that determines index scope:
267
-
268
- | Context Type | Usage | Redis Key Pattern | When to Use |
269
- |--------------|--------|------------------|-------------|
270
- | `:global` | `context: :global` | `global:index_name` | Field values unique system-wide (emails, usernames, API keys) |
271
- | Class | `context: SomeClass` | `someclass:123:index_name` | Field values unique within parent object scope (project names per team) |
272
-
273
- #### Generated Methods
274
-
275
- **Global Context** (`context: :global`):
276
- - **Instance methods**: `object.add_to_global_index_name`, `object.remove_from_global_index_name`
277
- - **Class methods**: `Class.index_name` (returns hash), `Class.find_by_field`
278
-
279
- **Class Context** (`context: Customer`):
280
- - **Instance methods**: `object.add_to_customer_index_name(customer)`, `object.remove_from_customer_index_name(customer)`
281
- - **Class methods on context**: `customer.find_by_field(value)`, `customer.find_all_by_field(values)`
282
-
283
- #### Migration from Incorrect Syntax
284
-
285
- ```ruby
286
- # ❌ Old incorrect syntax (will cause ArgumentError)
287
- indexed_by :email_lookup, field: :email
288
-
289
- # ✅ New correct syntax
290
- class_indexed_by :email, :email_lookup # Global scope
291
- indexed_by :email, :customer_lookup, parent: Customer # Scoped per customer
292
- ```
293
-
294
- ## Member Of Relationships
295
-
296
- ### Bidirectional Membership
297
-
298
- The `member_of` relationship creates bidirectional associations with collision-free method naming:
299
-
300
- ```ruby
301
- class Customer < Familia::Horreum
302
- feature :relationships
303
-
304
- identifier_field :custid
305
- field :custid, :name
306
-
307
- # Collections for owned objects
308
- set :domains
309
- list :projects
310
- set :users
311
- end
312
-
313
- class Domain < Familia::Horreum
314
- feature :relationships
315
-
316
- identifier_field :domain_id
317
- field :domain_id, :name
318
-
319
- # Declare membership in customer collections
320
- member_of Customer, :domains, type: :set
321
- end
322
-
323
- class Project < Familia::Horreum
324
- feature :relationships
325
-
326
- identifier_field :project_id
327
- field :project_id, :name
328
-
329
- member_of Customer, :projects, type: :list
330
- end
331
-
332
- class User < Familia::Horreum
333
- feature :relationships
334
-
335
- identifier_field :user_id
336
- field :user_id, :email
337
-
338
- member_of Customer, :users, type: :set
339
- end
340
- ```
341
-
342
- ### Collision-Free Method Generation
343
-
344
- The system automatically generates collision-free methods when multiple classes have the same collection name:
345
-
346
- ```ruby
347
- class Team < Familia::Horreum
348
- feature :relationships
349
- set :users # Same collection name as Customer
350
- end
351
-
352
- class User < Familia::Horreum
353
- feature :relationships
354
-
355
- # Both memberships create unique methods
356
- member_of Customer, :users, type: :set
357
- member_of Team, :users, type: :set
358
- end
359
-
360
- # Generated methods are collision-free:
361
- user = User.new(user_id: 'user123')
362
-
363
- # Add to different collections
364
- user.add_to_customer_users(customer.custid) # Specific to Customer.users
365
- user.add_to_team_users(team.team_id) # Specific to Team.users
366
-
367
- # Check membership
368
- user.in_customer_users?(customer.custid) # => true
369
- user.in_team_users?(team.team_id) # => true
370
-
371
- # Remove from specific collections
372
- user.remove_from_customer_users(customer.custid)
373
- user.remove_from_team_users(team.team_id)
374
- ```
375
-
376
- ### Multi-Context Membership Patterns
377
-
378
- ```ruby
379
- class Document < Familia::Horreum
380
- feature :relationships
381
-
382
- identifier_field :doc_id
383
- field :doc_id, :title
384
-
385
- # Multiple membership contexts
386
- member_of Customer, :documents, type: :set
387
- member_of Project, :documents, type: :list
388
- member_of Team, :shared_docs, type: :sorted_set
389
- end
390
-
391
- # Usage - same document can belong to multiple contexts
392
- doc = Document.new(doc_id: 'doc123', title: 'Requirements')
393
-
394
- # Add to different organizational contexts
395
- doc.add_to_customer_documents(customer.custid)
396
- doc.add_to_project_documents(project.project_id)
397
- doc.add_to_team_shared_docs(team.team_id, score: Time.now.to_i)
398
-
399
- # Query membership across contexts
400
- doc.in_customer_documents?(customer.custid) # => true
401
- doc.in_project_documents?(project.project_id) # => true
402
- doc.in_team_shared_docs?(team.team_id) # => true
403
- ```
404
-
405
- ## Advanced Features
406
-
407
- ### Atomic Multi-Collection Operations
408
-
409
- ```ruby
410
- class BusinessLogic
411
- def transfer_domain(domain, from_customer, to_customer)
412
- # Atomic transfer across multiple collections
413
- Familia.transaction do |conn|
414
- # Remove from old customer
415
- domain.remove_from_customer_domains(from_customer.custid)
416
- from_customer.domains.remove(domain.identifier)
417
-
418
- # Add to new customer
419
- domain.add_to_customer_domains(to_customer.custid)
420
- to_customer.domains.add(domain.identifier)
421
- end
422
- end
423
-
424
- def bulk_permission_update(user_ids, new_permissions)
425
- Document.authorized_users.pipeline do |pipe|
426
- user_ids.each do |user_id|
427
- current_score = Document.authorized_users.score(user_id)
428
- if current_score
429
- timestamp = current_score.floor
430
- new_score = "#{timestamp}.#{new_permissions}".to_f
431
- pipe.zadd(Document.authorized_users.key, new_score, user_id)
432
- end
433
- end
434
- end
435
- end
436
- end
437
- ```
438
-
439
- ### Performance Optimizations
440
-
441
- ```ruby
442
- class OptimizedQueries
443
- # Batch membership checks
444
- def check_multiple_memberships(user_ids, customer)
445
- # Single Redis call instead of multiple
446
- Customer.users.pipeline do |pipe|
447
- user_ids.each { |uid| pipe.sismember(customer.users.key, uid) }
448
- end
449
- end
450
-
451
- # Efficient range queries with permissions
452
- def recent_editors_with_write_access(hours = 24)
453
- since = (Time.now - hours.hours).to_f.floor
454
- write_permission = 4
455
- min_score = "#{since}.#{write_permission}".to_f
456
-
457
- Document.authorized_users.range_by_score(min_score, '+inf')
458
- end
459
-
460
- # Batch index updates
461
- def reindex_users(users)
462
- User.email_index.pipeline do |pipe|
463
- users.each do |user|
464
- pipe.hset(User.email_index.key, user.email, user.identifier)
465
- end
466
- end
467
- end
468
- end
469
- ```
470
-
471
- ## Integration Patterns
472
-
473
- ### Multi-Tenant Applications
474
-
475
- ```ruby
476
- class Organization < Familia::Horreum
477
- feature :relationships
478
-
479
- identifier_field :org_id
480
- field :org_id, :name, :plan
481
-
482
- # Organization collections
483
- set :members
484
- set :projects
485
- sorted_set :activity_feed
486
- end
487
-
488
- class User < Familia::Horreum
489
- feature :relationships
490
-
491
- identifier_field :user_id
492
- field :user_id, :email, :role
493
-
494
- # Multi-tenant membership
495
- member_of Organization, :members, type: :set
496
- class_tracked_in :global_activity, score: :created_at
497
- class_indexed_by :email, :email_lookup
498
- end
499
-
500
- class Project < Familia::Horreum
501
- feature :relationships
502
-
503
- identifier_field :project_id
504
- field :project_id, :name, :status
505
-
506
- member_of Organization, :projects, type: :set
507
- class_tracked_in :status_timeline,
508
- score: ->(proj) { "#{Time.now.to_i}.#{proj.status.hash}" }
509
- end
510
-
511
- # Usage
512
- org = Organization.new(org_id: 'org123', name: 'Acme Corp')
513
- user = User.new(user_id: 'user456', email: 'john@acme.com')
514
- project = Project.new(project_id: 'proj789', name: 'Website')
515
-
516
- # Establish relationships with clean syntax
517
- org.members << user # Clean Ruby-like syntax
518
- org.projects << project
519
-
520
- # Save objects to trigger automatic indexing
521
- user.save # Automatically added to class-level indexes
522
- project.save # Automatically added to class-level tracking
523
-
524
- # Query organization structure
525
- org.members.size # Number of organization members
526
- org.projects.members # All project IDs in organization
527
-
528
- # Automatic class-level indexing (no manual calls needed)
529
- user_id = User.email_lookup.get('john@acme.com') # Fast email lookup
530
- found_user = User.find_by_email('john@acme.com') # Convenience method
531
- ```
532
-
533
- ### Analytics and Reporting
534
-
535
- ```ruby
536
- class AnalyticsService
537
- def user_engagement_report(days = 30)
538
- since = (Time.now - days.days).to_f.floor
539
-
540
- # Get all users active in time period
541
- active_users = User.activity.range_by_score(since, '+inf')
542
-
543
- # Analyze permission levels
544
- permission_breakdown = Document.authorized_users
545
- .range_by_score(since, '+inf', with_scores: true)
546
- .group_by { |user_id, score| decode_permissions(score) }
547
-
548
- {
549
- total_active_users: active_users.size,
550
- permission_breakdown: permission_breakdown.transform_values(&:size),
551
- top_contributors: User.activity.range(0, 9, with_scores: true)
552
- }
553
- end
554
-
555
- def project_status_timeline(project_id)
556
- project = Project.find(project_id)
557
- Project.status_timeline
558
- .range_by_score('-inf', '+inf', with_scores: true)
559
- .select { |id, _| id == project_id }
560
- .map { |_, score| decode_status_change(score) }
561
- end
562
-
563
- private
564
-
565
- def decode_permissions(score)
566
- _, permissions = score.to_s.split('.').map(&:to_i)
567
- case permissions
568
- when 1 then :viewer
569
- when 15 then :moderator
570
- when 255 then :admin
571
- else :custom
572
- end
573
- end
574
-
575
- def decode_status_change(score)
576
- timestamp, status_hash = score.to_s.split('.').map(&:to_i)
577
- {
578
- timestamp: Time.at(timestamp),
579
- status: reverse_status_hash(status_hash)
580
- }
581
- end
582
- end
583
- ```
584
-
585
- ## Testing Relationships
586
-
587
- ### RSpec Testing Patterns
588
-
589
- ```ruby
590
- RSpec.describe "Relationships Feature" do
591
- let(:customer) { Customer.new(custid: 'cust123', name: 'Acme Corp') }
592
- let(:domain) { Domain.new(domain_id: 'dom456', name: 'acme.com') }
593
- let(:user) { User.new(user_id: 'user789', email: 'john@acme.com') }
594
-
595
- describe "tracked_in relationships" do
596
- it "tracks objects with score encoding" do
597
- User.add_to_leaderboard(user)
598
- score = User.leaderboard.score(user.identifier)
599
-
600
- expect(score).to be_a(Float)
601
- expect(User.leaderboard.rank(user.identifier)).to be >= 0
602
- end
603
-
604
- it "supports permission bit encoding" do
605
- # Test permission encoding
606
- encoded = encode_score_with_permissions(15) # moderator permissions
607
- timestamp, permissions = decode_score_with_permissions(encoded)
608
-
609
- expect(permissions).to eq(15)
610
- expect(timestamp).to be_within(1).of(Time.now.to_i)
611
- end
612
- end
613
-
614
- describe "indexed_by relationships" do
615
- it "creates O(1) hash lookups" do
616
- User.add_to_email_lookup(user)
617
- found_id = User.email_lookup.get(user.email)
618
-
619
- expect(found_id).to eq(user.identifier)
620
- end
621
-
622
- it "handles batch operations" do
623
- users = [user, user2, user3]
624
- users.each { |u| User.add_to_email_lookup(u) }
625
-
626
- users.each do |u|
627
- expect(User.email_lookup.get(u.email)).to eq(u.identifier)
628
- end
629
- end
630
- end
631
-
632
- describe "member_of relationships" do
633
- it "creates bidirectional associations" do
634
- domain.add_to_customer_domains(customer.custid)
635
- customer.domains.add(domain.identifier)
636
-
637
- expect(domain.in_customer_domains?(customer.custid)).to be true
638
- expect(customer.domains.member?(domain.identifier)).to be true
639
- end
640
-
641
- it "generates collision-free methods" do
642
- expect(domain).to respond_to(:add_to_customer_domains)
643
- expect(domain).to respond_to(:in_customer_domains?)
644
- expect(domain).to respond_to(:remove_from_customer_domains)
645
- end
646
- end
647
- end
648
- ```
649
-
650
- ### Integration Testing
651
-
652
- ```ruby
653
- RSpec.describe "Relationships Integration" do
654
- scenario "multi-tenant organization with full relationship graph" do
655
- # Create organization structure
656
- org = Organization.create(org_id: 'org123', name: 'Tech Corp')
657
-
658
- # Create users with different roles
659
- admin = User.create(user_id: 'admin1', email: 'admin@tech.com', role: 'admin')
660
- dev = User.create(user_id: 'dev1', email: 'dev@tech.com', role: 'developer')
661
-
662
- # Create projects
663
- project = Project.create(project_id: 'proj1', name: 'Web App')
664
-
665
- # Establish all relationships
666
- admin.add_to_organization_members(org.org_id)
667
- dev.add_to_organization_members(org.org_id)
668
- project.add_to_organization_projects(org.org_id)
669
-
670
- # Add to indexes
671
- User.add_to_email_lookup(admin)
672
- User.add_to_email_lookup(dev)
673
-
674
- # Test relationship integrity
675
- expect(org.members.size).to eq(2)
676
- expect(org.projects.size).to eq(1)
677
-
678
- # Test lookups
679
- expect(User.email_lookup.get('admin@tech.com')).to eq(admin.identifier)
680
- expect(User.email_lookup.get('dev@tech.com')).to eq(dev.identifier)
681
-
682
- # Test membership queries
683
- expect(admin.in_organization_members?(org.org_id)).to be true
684
- expect(project.in_organization_projects?(org.org_id)).to be true
685
- end
686
- end
687
- ```
688
-
689
- ## Best Practices
690
-
691
- ### Relationship Design
692
-
693
- 1. **Choose the Right Type**:
694
- - Use `tracked_in` for activity feeds, leaderboards, time-series data
695
- - Use `indexed_by` for fast lookups by field values
696
- - Use `member_of` for bidirectional ownership/membership
697
-
698
- 2. **Score Encoding Strategy**:
699
- - Combine timestamps with metadata for rich queries
700
- - Use bit flags for permissions (supports 8 flags efficiently)
701
- - Consider sort order requirements when designing scores
702
-
703
- 3. **Performance Optimization**:
704
- - Batch operations when possible using pipelines
705
- - Use appropriate Redis data types for your access patterns
706
- - Index only frequently-queried fields
707
-
708
- ### Memory and Storage
709
-
710
- 1. **Efficient Bit Encoding**:
711
- - 8 bits can encode 256 permission combinations
712
- - Single Redis sorted set score contains time + permissions
713
- - Reduces memory vs. separate permission records
714
-
715
- 2. **Key Design**:
716
- - Relationship keys follow pattern: `class:field:collection`
717
- - Collision-free method names prevent namespace conflicts
718
- - Predictable key structure aids debugging
719
-
720
- 3. **Cleanup Strategies**:
721
- - Remove objects from all relationship collections on deletion
722
- - Use TTL on temporary relationship data
723
- - Regular cleanup of stale indexes
724
-
725
- ### Security Considerations
726
-
727
- 1. **Permission Validation**:
728
- - Always validate permissions before operations
729
- - Use bit flags for efficient permission checking
730
- - Audit permission changes with timestamps
731
-
732
- 2. **Access Control**:
733
- - Verify relationship membership before granting access
734
- - Use consistent permission models across features
735
- - Log relationship changes for audit trails
736
-
737
- The Relationships feature provides a comprehensive foundation for building sophisticated multi-tenant applications with efficient object relationships, permission management, and analytics capabilities.