familia 2.0.0.pre7 → 2.0.0.pre10

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.gitignore +9 -9
  5. data/.rubocop.yml +19 -0
  6. data/.yardopts +22 -1
  7. data/CHANGELOG.md +184 -0
  8. data/CLAUDE.md +8 -5
  9. data/Gemfile +1 -1
  10. data/Gemfile.lock +3 -3
  11. data/README.md +97 -2
  12. data/changelog.d/README.md +66 -0
  13. data/changelog.d/fragments/.keep +0 -0
  14. data/changelog.d/template.md.j2 +29 -0
  15. data/docs/archive/.gitignore +2 -0
  16. data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
  17. data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
  18. data/docs/archive/FAMILIA_UPDATE.md +226 -0
  19. data/docs/archive/README.md +67 -0
  20. data/docs/guides/.gitignore +2 -0
  21. data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
  22. data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
  23. data/docs/guides/relationships-methods.md +266 -0
  24. data/examples/relationships_basic.rb +90 -157
  25. data/familia.gemspec +4 -4
  26. data/lib/familia/connection.rb +4 -21
  27. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
  28. data/lib/familia/features/external_identifiers.rb +111 -0
  29. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
  30. data/lib/familia/features/object_identifiers.rb +194 -0
  31. data/lib/familia/features/relationships/cascading.rb +0 -1
  32. data/lib/familia/features/relationships/indexing.rb +160 -176
  33. data/lib/familia/features/relationships/membership.rb +16 -22
  34. data/lib/familia/features/relationships/querying.rb +7 -12
  35. data/lib/familia/features/relationships/score_encoding.rb +1 -3
  36. data/lib/familia/features/relationships/tracking.rb +61 -22
  37. data/lib/familia/features/relationships.rb +15 -8
  38. data/lib/familia/features/transient_fields.rb +8 -10
  39. data/lib/familia/features.rb +16 -13
  40. data/lib/familia/horreum/core/serialization.rb +2 -5
  41. data/lib/familia/horreum/subclass/definition.rb +36 -0
  42. data/lib/familia/horreum.rb +15 -24
  43. data/lib/familia/version.rb +1 -3
  44. data/setup.cfg +12 -0
  45. data/try/core/errors_try.rb +1 -1
  46. data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
  47. data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
  48. data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
  49. data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
  50. data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
  51. data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
  52. data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
  53. data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
  54. data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
  55. data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
  56. data/try/features/relationships/relationships_api_changes_try.rb +339 -0
  57. data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
  58. data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
  59. data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
  60. data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
  61. data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
  62. data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
  63. data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
  64. data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
  65. data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
  66. data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
  67. metadata +80 -60
  68. /data/docs/{wiki → guides}/API-Reference.md +0 -0
  69. /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
  70. /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
  71. /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
  72. /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
  73. /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
  74. /data/docs/{wiki → guides}/Home.md +0 -0
  75. /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
  76. /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
  77. /data/docs/{wiki → guides}/Security-Model.md +0 -0
  78. /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
  79. /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
  80. /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
  81. /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
  82. /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
  83. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
  84. /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
  85. /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
  86. /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
  87. /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
  88. /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
  89. /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
  90. /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
  91. /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -0,0 +1,266 @@
1
+ # Relationship Methods
2
+
3
+ Here are the methods automatically generated for each relationship type in the new clean API:
4
+
5
+ ## member_of Relationships
6
+
7
+ When you declare:
8
+ ```ruby
9
+ class Domain < Familia::Horreum
10
+ member_of Customer, :domains
11
+ end
12
+ ```
13
+
14
+ **Generated methods on Domain instances:**
15
+ - `add_to_customer_domains(customer)` - Add this domain to customer's domains collection
16
+ - `remove_from_customer_domains(customer)` - Remove this domain from customer's domains collection
17
+ - `in_customer_domains?(customer)` - Check if this domain is in customer's domains collection
18
+
19
+ **Collection << operator support:**
20
+ ```ruby
21
+ customer.domains << domain # Clean Ruby-like syntax (equivalent to domain.add_to_customer_domains(customer))
22
+ ```
23
+
24
+ The method names follow the pattern: `{action}_to_{lowercase_class_name}_{collection_name}`
25
+
26
+ ## tracked_in Relationships
27
+
28
+ ### Class-Level Tracking (class_tracked_in)
29
+ When you declare:
30
+ ```ruby
31
+ class Customer < Familia::Horreum
32
+ class_tracked_in :all_customers, score: :created_at
33
+ end
34
+ ```
35
+
36
+ **Generated class methods:**
37
+ - `Customer.add_to_all_customers(customer)` - Add customer to class-level tracking
38
+ - `Customer.remove_from_all_customers(customer)` - Remove customer from class-level tracking
39
+ - `Customer.all_customers` - Access the sorted set collection directly
40
+
41
+ **Automatic behavior:**
42
+ - Objects are automatically added to class-level tracking collections when saved
43
+ - No manual calls required for basic tracking
44
+
45
+ ### Relationship Tracking (tracked_in with parent class)
46
+ When you declare:
47
+ ```ruby
48
+ class User < Familia::Horreum
49
+ tracked_in Team, :active_users, score: :last_seen
50
+ end
51
+ ```
52
+
53
+ **Generated methods:**
54
+ - Team instance methods for managing the active_users collection
55
+ - Automatic score calculation based on the provided lambda or field
56
+
57
+ ## indexed_by Relationships
58
+
59
+ The `indexed_by` method creates Redis hash-based indexes for O(1) field lookups with automatic management.
60
+
61
+ ### Class-Level Indexing (class_indexed_by)
62
+ When you declare:
63
+ ```ruby
64
+ class Customer < Familia::Horreum
65
+ class_indexed_by :email, :email_lookup
66
+ end
67
+ ```
68
+
69
+ **Generated methods:**
70
+ - **Instance methods**: `customer.add_to_class_email_lookup`, `customer.remove_from_class_email_lookup`
71
+ - **Class methods**: `Customer.email_lookup` (returns hash), `Customer.find_by_email(email)`
72
+
73
+ **Automatic behavior:**
74
+ - Objects are automatically added to class-level indexes when saved
75
+ - Index updates happen transparently on field changes
76
+
77
+ Redis key pattern: `customer:email_lookup`
78
+
79
+ ### Relationship-Scoped Indexing (indexed_by with parent:)
80
+ When you declare:
81
+ ```ruby
82
+ class Domain < Familia::Horreum
83
+ indexed_by :name, :domain_index, parent: Customer
84
+ end
85
+ ```
86
+
87
+ **Generated class methods on Customer:**
88
+ - `Customer#find_by_name(domain_name)` - Find domain by name within this customer
89
+ - `Customer#find_all_by_name(domain_names)` - Find multiple domains by names
90
+
91
+ Redis key pattern: `domain:domain_index` (all stored at class level for consistency)
92
+
93
+ ### When to Use Each Context
94
+ - **Class-level context (`class_indexed_by`)**: Use for system-wide lookups where the field value should be unique across all instances
95
+ - Examples: email addresses, usernames, API keys
96
+ - **Relationship context (`parent:` parameter)**: Use for relationship-scoped lookups where the field value is unique within a specific context
97
+ - Examples: domain names per customer, project names per team
98
+
99
+ ## Complete Example
100
+
101
+ From the relationships example file, you can see the new clean API in action:
102
+
103
+ ```ruby
104
+ # Domain declares membership in Customer collections
105
+ class Domain < Familia::Horreum
106
+ member_of Customer, :domains
107
+ class_tracked_in :active_domains, score: -> { status == 'active' ? Time.now.to_i : 0 }
108
+ end
109
+
110
+ class Customer < Familia::Horreum
111
+ class_indexed_by :email, :email_lookup
112
+ class_tracked_in :all_customers, score: :created_at
113
+ end
114
+ ```
115
+
116
+ **Usage with automatic behavior:**
117
+ ```ruby
118
+ # Create and save objects (automatic indexing and tracking)
119
+ customer = Customer.new(email: "admin@acme.com", name: "Acme Corp")
120
+ customer.save # Automatically added to email_lookup and all_customers
121
+
122
+ domain = Domain.new(name: "acme.com", status: "active")
123
+ domain.save # Automatically added to active_domains
124
+
125
+ # Clean relationship syntax
126
+ customer.domains << domain # Ruby-like collection syntax
127
+
128
+ # Query relationships
129
+ domain.in_customer_domains?(customer) # => true
130
+ customer.domains.member?(domain.identifier) # => true
131
+
132
+ # O(1) lookups with automatic management
133
+ found_id = Customer.email_lookup.get("admin@acme.com")
134
+ ```
135
+
136
+ ## Method Naming Conventions
137
+
138
+ The relationship system uses consistent naming patterns:
139
+ - **member_of**: `{add_to|remove_from|in}_#{parent_class.downcase}_#{collection_name}`
140
+ - **class_tracked_in**: `{add_to|remove_from}_#{collection_name}` (class methods)
141
+ - **class_indexed_by**: `{add_to|remove_from}_class_#{index_name}` (instance methods)
142
+ - **indexed_by with parent**: `{add_to|remove_from}_#{parent_class.downcase}_#{index_name}` (instance methods)
143
+
144
+ ## Key Benefits
145
+
146
+ - **Automatic management**: Save operations update indexes and tracking automatically
147
+ - **Ruby-idiomatic**: Use `<<` operator for natural collection syntax
148
+ - **Consistent storage**: All indexes stored at class level for architectural simplicity
149
+ - **Clean API**: Removed complex global vs parent conditionals for simpler method generation
150
+
151
+
152
+ ## Context Parameter Usage Patterns
153
+
154
+ The `context` parameter in `indexed_by` is a fundamental architectural decision that determines index scope and ownership. Here are practical patterns for when to use each approach:
155
+
156
+ ### Global Context Pattern
157
+ Use `class_indexed_by` when field values should be unique system-wide:
158
+
159
+ ```ruby
160
+ class User < Familia::Horreum
161
+ feature :relationships
162
+
163
+ identifier_field :user_id
164
+ field :user_id, :email, :username
165
+
166
+ # System-wide unique email lookup
167
+ class_indexed_by :email, :email_lookup
168
+ class_indexed_by :username, :username_lookup
169
+ end
170
+
171
+ # Usage:
172
+ user.add_to_global_email_lookup
173
+ found_user_id = User.email_lookup.get("john@example.com")
174
+ ```
175
+
176
+ **Redis keys generated**: `global:email_lookup`, `global:username_lookup`
177
+
178
+ ### Parent Context Pattern
179
+ Use `parent: SomeClass` when field values are unique within a specific parent context:
180
+
181
+ ```ruby
182
+ class Customer < Familia::Horreum
183
+ feature :relationships
184
+
185
+ identifier_field :custid
186
+ field :custid, :name
187
+ sorted_set :domains
188
+ end
189
+
190
+ class Domain < Familia::Horreum
191
+ feature :relationships
192
+
193
+ identifier_field :domain_id
194
+ field :domain_id, :name, :subdomain
195
+
196
+ # Domains are unique per customer (customer can't have duplicate domain names)
197
+ indexed_by :name, :domain_index, parent: Customer
198
+ indexed_by :subdomain, :subdomain_index, parent: Customer
199
+ end
200
+
201
+ # Usage:
202
+ customer = Customer.new(custid: "cust_123")
203
+ customer.find_by_name("example.com") # Find domain within this customer
204
+ customer.find_all_by_subdomain(["www", "api"]) # Find multiple subdomains
205
+ ```
206
+
207
+ **Redis keys generated**: `customer:cust_123:domain_index`, `customer:cust_123:subdomain_index`
208
+
209
+ ### Mixed Pattern Example
210
+ A real-world example showing both patterns:
211
+
212
+ ```ruby
213
+ class ApiKey < Familia::Horreum
214
+ feature :relationships
215
+
216
+ identifier_field :key_id
217
+ field :key_id, :key_hash, :name, :scope
218
+
219
+ # API key hashes must be globally unique
220
+ class_indexed_by :key_hash, :global_key_lookup
221
+
222
+ # But key names can be reused across different customers
223
+ indexed_by :name, :customer_key_lookup, parent: Customer
224
+ indexed_by :scope, :scope_lookup, parent: Customer
225
+ end
226
+
227
+ # Usage examples:
228
+ # Global lookup (system-wide unique)
229
+ ApiKey.key_lookup.get("sha256:abc123...")
230
+
231
+ # Scoped lookup (unique per customer)
232
+ customer = Customer.new(custid: "cust_456")
233
+ customer.find_by_name("production-api-key")
234
+ customer.find_all_by_scope(["read", "write"])
235
+ ```
236
+
237
+ ### Migration Guide
238
+ If you have existing code with old syntax, here's how to update it:
239
+
240
+ ```ruby
241
+ # ❌ Old syntax (pre-refactoring)
242
+ indexed_by :email_lookup, field: :email
243
+ indexed_by :email, :email_lookup, context: :global
244
+ tracked_in :global, :all_users, score: :created_at
245
+
246
+ # ✅ New syntax - Class-level scope
247
+ class_indexed_by :email, :email_lookup
248
+ class_tracked_in :all_users, score: :created_at
249
+
250
+ # ✅ New syntax - Relationship scope
251
+ indexed_by :email, :customer_email_lookup, parent: Customer
252
+ tracked_in Customer, :user_activity, score: :last_seen
253
+ ```
254
+
255
+ **Key Changes**:
256
+ 1. **Class-level relationships**: Use `class_` prefix (`class_tracked_in`, `class_indexed_by`)
257
+ 2. **Relationship-scoped**: Use `parent:` parameter instead of `:global` symbol
258
+ 3. **Automatic management**: Objects automatically added to class-level collections on save
259
+ 4. **Clean syntax**: Collections support `<<` operator for Ruby-like relationship building
260
+ 5. **Simplified storage**: All indexes stored at class level (parent is conceptual only)
261
+
262
+ **Behavioral Changes**:
263
+ - Save operations now automatically update indexes and class-level tracking
264
+ - No more manual `add_to_*` calls required for basic functionality
265
+ - `<<` operator works naturally with all collection types
266
+ - Method generation simplified without complex global/parent conditionals
@@ -3,92 +3,100 @@
3
3
  # Basic Relationships Example
4
4
  # This example demonstrates the core features of Familia's relationships system
5
5
 
6
- require_relative '../lib/familia'
6
+ require 'familia'
7
7
 
8
8
  # Configure Familia for the example
9
+ # Note: Individual models can specify logical_database if needed
9
10
  Familia.configure do |config|
10
- config.redis_uri = ENV.fetch('REDIS_URI', 'redis://localhost:6379/15')
11
+ config.uri = 'redis://localhost:6379/'
11
12
  end
12
13
 
13
- puts "=== Familia Relationships Basic Example ==="
14
+ puts '=== Familia Relationships Basic Example ==='
14
15
  puts
15
16
 
16
17
  # Define our model classes
17
18
  class Customer < Familia::Horreum
19
+ logical_database 15 # Use test database
18
20
  feature :relationships
19
21
 
20
22
  identifier_field :custid
21
- field :custid, :name, :email, :plan
23
+ field :custid
24
+ field :name
25
+ field :email
26
+ field :plan
22
27
 
23
28
  # Define collections for tracking relationships
24
29
  set :domains # Simple set of domain IDs
25
30
  list :projects # Ordered list of project IDs
26
31
  sorted_set :activity # Activity feed with timestamps
27
32
 
28
- # Create indexes for fast lookups
29
- indexed_by :email_lookup, field: :email
30
- indexed_by :plan_lookup, field: :plan
33
+ # Create indexes for fast lookups (using class_ prefix for class-level)
34
+ class_indexed_by :email, :email_lookup # i.e. Customer.email_lookup
35
+ class_indexed_by :plan, :plan_lookup # i.e. Customer.plan_lookup
31
36
 
32
- # Track in global collections
33
- tracked_in :all_customers, type: :sorted_set, score: :created_at
34
-
35
- def created_at
36
- Time.now.to_i
37
- end
37
+ # Track in class-level collections (using class_ prefix for class-level)
38
+ class_tracked_in :all_customers, score: :created_at # i.e. Customer.all_customers
38
39
  end
39
40
 
40
41
  class Domain < Familia::Horreum
42
+ logical_database 15 # Use test database
41
43
  feature :relationships
42
44
 
43
45
  identifier_field :domain_id
44
- field :domain_id, :name, :dns_zone, :status
46
+ field :domain_id
47
+ field :name
48
+ field :dns_zone
49
+ field :status
45
50
 
46
51
  # Declare membership in customer collections
47
- member_of Customer, :domains, type: :set
52
+ member_of Customer, :domains #, type: :set
48
53
 
49
- # Track domains by status
50
- tracked_in :active_domains, type: :sorted_set,
51
- score: ->(domain) { domain.status == 'active' ? Time.now.to_i : 0 }
54
+ # Track domains by status (using class_ prefix for class-level)
55
+ class_tracked_in :active_domains,
56
+ score: -> { status == 'active' ? Time.now.to_i : 0 }
52
57
  end
53
58
 
54
59
  class Project < Familia::Horreum
60
+ logical_database 15 # Use test database
55
61
  feature :relationships
56
62
 
57
63
  identifier_field :project_id
58
- field :project_id, :name, :priority
64
+ field :project_id
65
+ field :name
66
+ field :priority
59
67
 
60
68
  # Member of customer projects list (ordered)
61
69
  member_of Customer, :projects, type: :list
62
70
  end
63
71
 
64
- puts "=== 1. Basic Object Creation ==="
72
+ puts '=== 1. Basic Object Creation ==='
65
73
 
66
74
  # Create some sample objects
67
75
  customer = Customer.new(
68
- custid: "cust_#{SecureRandom.hex(4)}",
69
- name: "Acme Corporation",
70
- email: "admin@acme.com",
71
- plan: "enterprise"
76
+ # custid: "cust_#{SecureRandom.hex(4)}",
77
+ name: 'Acme Corporation',
78
+ email: 'admin@acme.com',
79
+ plan: 'enterprise'
72
80
  )
73
81
 
74
82
  domain1 = Domain.new(
75
- domain_id: "dom_#{SecureRandom.hex(4)}",
76
- name: "acme.com",
77
- dns_zone: "acme.com.",
78
- status: "active"
83
+ # domain_id: "dom_#{SecureRandom.hex(4)}",
84
+ name: 'acme.com',
85
+ dns_zone: 'acme.com.',
86
+ status: 'active'
79
87
  )
80
88
 
81
89
  domain2 = Domain.new(
82
- domain_id: "dom_#{SecureRandom.hex(4)}",
83
- name: "staging.acme.com",
84
- dns_zone: "staging.acme.com.",
85
- status: "active"
90
+ # domain_id: "dom_#{SecureRandom.hex(4)}",
91
+ name: 'staging.acme.com',
92
+ dns_zone: 'staging.acme.com.',
93
+ status: 'active'
86
94
  )
87
95
 
88
96
  project = Project.new(
89
- project_id: "proj_#{SecureRandom.hex(4)}",
90
- name: "Website Redesign",
91
- priority: "high"
97
+ # project_id: "proj_#{SecureRandom.hex(4)}",
98
+ name: 'Website Redesign',
99
+ priority: 'high'
92
100
  )
93
101
 
94
102
  puts "✓ Created customer: #{customer.name} (#{customer.custid})"
@@ -96,49 +104,44 @@ puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
96
104
  puts "✓ Created project: #{project.name}"
97
105
  puts
98
106
 
99
- puts "=== 2. Establishing Relationships ==="
100
-
101
- # Add objects to indexed lookups
102
- Customer.add_to_email_lookup(customer)
103
- Customer.add_to_plan_lookup(customer)
104
- puts "✓ Added customer to email and plan indexes"
107
+ puts '=== 2. Establishing Relationships ==='
105
108
 
106
- # Add customer to global tracking
107
- Customer.add_to_all_customers(customer)
108
- puts "Added customer to global customer tracking"
109
+ # Save objects to automatically update indexes and class-level tracking
110
+ customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
111
+ puts 'Customer automatically added to indexes and tracking on save'
109
112
 
110
- # Establish member_of relationships (bidirectional)
111
- domain1.add_to_customer_domains(customer.custid)
112
- customer.domains.add(domain1.identifier)
113
+ # Establish member_of relationships using clean << operator syntax
114
+ customer.domains << domain1 # Clean Ruby-like syntax
115
+ customer.domains << domain2 # Same as domain1.add_to_customer_domains(customer)
116
+ customer.projects << project # Same as project.add_to_customer_projects(customer)
113
117
 
114
- domain2.add_to_customer_domains(customer.custid)
115
- customer.domains.add(domain2.identifier)
118
+ puts '✓ Established domain ownership relationships using << operator'
119
+ puts '✓ Established project ownership relationships using << operator'
116
120
 
117
- project.add_to_customer_projects(customer.custid)
118
- customer.projects.add(project.identifier)
121
+ # Save domains to automatically add them to class-level tracking
122
+ domain1.save # Automatically adds to active_domains if status == 'active'
123
+ domain2.save # Automatically adds to active_domains if status == 'active'
124
+ puts '✓ Domains automatically added to status tracking on save'
125
+ puts
119
126
 
120
- puts "✓ Established domain ownership relationships"
121
- puts "✓ Established project ownership relationships"
127
+ puts '=== 3. Querying Relationships ==='
122
128
 
123
- # Track domains in status collections
124
- Domain.add_to_active_domains(domain1)
125
- Domain.add_to_active_domains(domain2)
126
- puts "✓ Added domains to active tracking"
127
- puts
129
+ record = Customer.get_by_email('admin@acme.com')
130
+ puts "Email lookup: #{record&.custid || 'not found'}"
128
131
 
129
- puts "=== 3. Querying Relationships ==="
132
+ Customer.get_by_plan('enterprise') # raises MoreThanOne
130
133
 
131
- # Test indexed lookups
132
- found_customer_id = Customer.email_lookup.get(customer.email)
133
- puts "Email lookup for #{customer.email}: #{found_customer_id}"
134
+ results = Customer.find_by_email('admin@acme.com')
135
+ puts "Email lookup: #{results&.size} found"
134
136
 
135
- enterprise_customers = Customer.plan_lookup.get("enterprise")
136
- puts "Enterprise customer found: #{enterprise_customers}"
137
+ results = Customer.find_by_plan('enterprise')
138
+ puts "Enterprise lookup: #{results&.size} found"
139
+ puts
137
140
 
138
141
  # Test membership queries
139
142
  puts "\nDomain membership checks:"
140
- puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer.custid)}"
141
- puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer.custid)}"
143
+ puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer)}"
144
+ puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer)}"
142
145
 
143
146
  puts "\nCustomer collections:"
144
147
  puts " Customer has #{customer.domains.size} domains"
@@ -147,69 +150,37 @@ puts " Domain IDs: #{customer.domains.members}"
147
150
  puts " Project IDs: #{customer.projects.members}"
148
151
 
149
152
  # Test tracked_in collections
150
- all_customers_count = Customer.all_customers.size
151
- puts "\nGlobal tracking:"
153
+ all_customers_count = Customer.values.size
154
+ puts "\nClass-level tracking:"
152
155
  puts " Total customers in system: #{all_customers_count}"
153
156
 
154
157
  active_domains_count = Domain.active_domains.size
155
158
  puts " Active domains in system: #{active_domains_count}"
156
159
  puts
157
160
 
158
- puts "=== 4. Range Queries ==="
161
+ puts '=== 4. Range Queries ==='
159
162
 
160
163
  # Get recent customers (last 24 hours)
161
- yesterday = (Time.now - 24.hours).to_i
162
- recent_customers = Customer.all_customers.range_by_score(yesterday, '+inf')
164
+ yesterday = (Time.now - (24 * 3600)).to_i # 24 hours ago
165
+ recent_customers = Customer.values.rangebyscore(yesterday, '+inf')
163
166
  puts "Recent customers (last 24h): #{recent_customers.size}"
164
167
 
165
168
  # Get all active domains by score
166
- active_domain_scores = Domain.active_domains.range_by_score(1, '+inf', with_scores: true)
167
- puts "Active domains with timestamps:"
168
- active_domain_scores.each do |domain_id, timestamp|
169
- puts " #{domain_id}: active since #{Time.at(timestamp.to_i)}"
169
+ active_domain_scores = Domain.active_domains.rangebyscore(1, '+inf', with_scores: true)
170
+ puts 'Active domains with timestamps:'
171
+ active_domain_scores.each_slice(2) do |domain_id, timestamp|
172
+ puts " #{domain_id}: active since #{Time.at(timestamp.to_i)} #{timestamp.inspect}"
170
173
  end
171
174
  puts
172
175
 
173
- puts "=== 5. Batch Operations ==="
174
-
175
- # Create additional test data
176
- additional_customers = []
177
- 3.times do |i|
178
- cust = Customer.new(
179
- custid: "batch_cust_#{i}",
180
- name: "Customer #{i}",
181
- email: "customer#{i}@example.com",
182
- plan: i.even? ? "basic" : "premium"
183
- )
184
- additional_customers << cust
185
-
186
- # Add to indexes and tracking
187
- Customer.add_to_email_lookup(cust)
188
- Customer.add_to_plan_lookup(cust)
189
- Customer.add_to_all_customers(cust)
190
- end
191
-
192
- puts "✓ Created and indexed #{additional_customers.size} additional customers"
193
176
 
194
- # Query by plan
195
- basic_customers = Customer.plan_lookup.get("basic")
196
- premium_customers = Customer.plan_lookup.get("premium")
197
- enterprise_customers = Customer.plan_lookup.get("enterprise")
198
-
199
- puts "\nCustomer distribution by plan:"
200
- puts " Basic: #{basic_customers ? 1 : 0} customers"
201
- puts " Premium: #{premium_customers ? 1 : 0} customers"
202
- puts " Enterprise: #{enterprise_customers ? 1 : 0} customers"
203
- puts
204
-
205
- puts "=== 6. Relationship Cleanup ==="
177
+ puts '=== 6. Relationship Cleanup ==='
206
178
 
207
179
  # Remove relationships
208
- puts "Cleaning up relationships..."
180
+ puts 'Cleaning up relationships...'
209
181
 
210
182
  # Remove from member_of relationships
211
- domain1.remove_from_customer_domains(customer.custid)
212
- customer.domains.remove(domain1.identifier)
183
+ domain1.remove_from_customer_domains(customer)
213
184
  puts "✓ Removed #{domain1.name} from customer domains"
214
185
 
215
186
  # Remove from tracking collections
@@ -222,52 +193,14 @@ puts " Customer domains: #{customer.domains.size}"
222
193
  puts " Active domains: #{Domain.active_domains.size}"
223
194
  puts
224
195
 
225
- puts "=== 7. Advanced Usage - Permission Encoding ==="
226
-
227
- # Demonstrate basic score encoding for permissions
228
- class DocumentAccess
229
- # Permission flags (powers of 2)
230
- READ = 1
231
- WRITE = 2
232
- DELETE = 4
233
- ADMIN = 8
234
-
235
- def self.encode_permissions(timestamp, permissions)
236
- "#{timestamp}.#{permissions}".to_f
237
- end
238
-
239
- def self.decode_permissions(score)
240
- parts = score.to_s.split('.')
241
- timestamp = parts[0].to_i
242
- permissions = parts[1] ? parts[1].to_i : 0
243
- [timestamp, permissions]
244
- end
245
- end
246
-
247
- # Example permission encoding
248
- now = Time.now.to_i
249
- read_write_permissions = DocumentAccess::READ | DocumentAccess::WRITE # 3
250
- admin_permissions = DocumentAccess::ADMIN # 8
251
-
252
- encoded_score = DocumentAccess.encode_permissions(now, read_write_permissions)
253
- timestamp, permissions = DocumentAccess.decode_permissions(encoded_score)
254
-
255
- puts "Permission encoding example:"
256
- puts " Original: timestamp=#{now}, permissions=#{read_write_permissions}"
257
- puts " Encoded score: #{encoded_score}"
258
- puts " Decoded: timestamp=#{timestamp}, permissions=#{permissions}"
259
- puts " Has read access: #{(permissions & DocumentAccess::READ) != 0}"
260
- puts " Has write access: #{(permissions & DocumentAccess::WRITE) != 0}"
261
- puts " Has delete access: #{(permissions & DocumentAccess::DELETE) != 0}"
262
- puts
263
-
264
- puts "=== Example Complete! ==="
196
+ puts '=== Example Complete! ==='
265
197
  puts
266
- puts "Key takeaways:"
267
- puts "tracked_in: Use for activity feeds, leaderboards, time-series data"
268
- puts "indexed_by: Use for fast O(1) lookups by field values"
269
- puts "• member_of: Use for bidirectional ownership/membership"
270
- puts "Score encoding: Combine timestamps with metadata for rich queries"
271
- puts "Batch operations: Use Redis pipelines for efficiency"
198
+ puts 'Key takeaways:'
199
+ puts 'class_tracked_in: Automatic class-level collections updated on save'
200
+ puts 'class_indexed_by: Automatic class-level indexes updated on save'
201
+ puts '• member_of: Use << operator for clean Ruby-like collection syntax'
202
+ puts 'indexed_by with parent:: Use for relationship-scoped indexes'
203
+ puts 'Save operations: Automatically update indexes and class-level tracking'
204
+ puts '• << operator: Works naturally with all collection types (sets, lists, sorted sets)'
272
205
  puts
273
- puts "See docs/wiki/Relationships-Guide.md for comprehensive documentation"
206
+ puts 'See docs/wiki/Relationships-Guide.md for comprehensive documentation'
data/familia.gemspec CHANGED
@@ -19,10 +19,10 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.required_ruby_version = Gem::Requirement.new('>= 3.4')
21
21
 
22
- spec.add_dependency 'benchmark'
23
- spec.add_dependency 'connection_pool'
24
- spec.add_dependency 'csv'
25
- spec.add_dependency 'logger'
22
+ spec.add_dependency 'benchmark', '~> 0.1'
23
+ spec.add_dependency 'connection_pool', '~> 2.4'
24
+ spec.add_dependency 'csv', '~> 3.1'
25
+ spec.add_dependency 'logger', '~> 1.6'
26
26
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
27
27
  spec.add_dependency 'stringio', '~> 3.1.1'
28
28
  spec.add_dependency 'uri-valkey', '~> 1.4'