familia 2.0.0.pre5 → 2.0.0.pre7

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # examples/redis_command_validation_example.rb
4
+ #
5
+ # Comprehensive example demonstrating Redis command validation for Familia
6
+ # This example shows how to validate that Redis operations execute exactly
7
+ # as expected, with particular focus on atomic operations.
8
+
9
+ require_relative '../lib/familia'
10
+ require_relative '../lib/familia/validation'
11
+
12
+ # Enable database logging for visibility
13
+ Familia.enable_database_logging = true
14
+ Familia.enable_database_counter = true
15
+
16
+ # Example models for validation demonstration
17
+ class Account < Familia::Horreum
18
+ identifier_field :account_id
19
+ field :account_id
20
+ field :balance
21
+ field :status
22
+ field :last_updated
23
+ end
24
+
25
+ class TransferService
26
+ def self.atomic_transfer(from_account, to_account, amount)
27
+ # Proper atomic implementation using Familia transaction
28
+ from_balance = from_account.balance.to_i - amount
29
+ to_balance = to_account.balance.to_i + amount
30
+
31
+ Familia.transaction do |conn|
32
+ conn.hset(from_account.dbkey, 'balance', from_balance.to_s)
33
+ conn.hset(to_account.dbkey, 'balance', to_balance.to_s)
34
+ conn.hset(from_account.dbkey, 'last_updated', Time.now.to_i.to_s)
35
+ conn.hset(to_account.dbkey, 'last_updated', Time.now.to_i.to_s)
36
+ end
37
+
38
+ # Update local state
39
+ from_account.balance = from_balance.to_s
40
+ to_account.balance = to_balance.to_s
41
+ end
42
+
43
+ def self.non_atomic_transfer(from_account, to_account, amount)
44
+ # Non-atomic implementation (BAD - for demonstration)
45
+ from_account.balance = (from_account.balance.to_i - amount).to_s
46
+ to_account.balance = (to_account.balance.to_i + amount).to_s
47
+
48
+ from_account.save
49
+ to_account.save
50
+ end
51
+ end
52
+
53
+ puts "🧪 Redis Command Validation Framework Demo"
54
+ puts "=" * 50
55
+
56
+ # Clean up any existing test data
57
+ cleanup_keys = Familia.dbclient.keys("account:*")
58
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
59
+
60
+ # Example 1: Basic Command Recording
61
+ puts "\n1. Basic Command Recording"
62
+ puts "-" * 30
63
+
64
+ CommandRecorder = Familia::Validation::CommandRecorder
65
+ CommandRecorder.start_recording
66
+
67
+ account = Account.new(account_id: "acc001", balance: "1000", status: "active")
68
+ account.save
69
+
70
+ commands = CommandRecorder.stop_recording
71
+ puts "Recorded #{commands.command_count} commands:"
72
+ commands.commands.each { |cmd| puts " #{cmd}" }
73
+
74
+ # Example 2: Transaction Detection
75
+ puts "\n2. Transaction Detection"
76
+ puts "-" * 30
77
+
78
+ CommandRecorder.start_recording
79
+
80
+ acc1 = Account.new(account_id: "acc002", balance: "2000")
81
+ acc2 = Account.new(account_id: "acc003", balance: "500")
82
+ acc1.save
83
+ acc2.save
84
+
85
+ TransferService.atomic_transfer(acc1, acc2, 500)
86
+
87
+ commands = CommandRecorder.stop_recording
88
+ puts "Commands executed: #{commands.command_count}"
89
+ puts "Transactions detected: #{commands.transaction_count}"
90
+
91
+ if commands.transaction_blocks.any?
92
+ tx = commands.transaction_blocks.first
93
+ puts "Transaction commands: #{tx.command_count}"
94
+ tx.commands.each { |cmd| puts " [TX] #{cmd}" }
95
+ end
96
+
97
+ # Example 3: Validation with Expectations DSL
98
+ puts "\n3. Command Validation with Expectations"
99
+ puts "-" * 30
100
+
101
+ begin
102
+ validator = Familia::Validation::Validator.new
103
+
104
+ # This should pass - we expect the exact Redis commands
105
+ result = validator.validate do |expect|
106
+ expect.transaction do |tx|
107
+ tx.hset("account:acc004:object", "balance", "1500")
108
+ .hset("account:acc005:object", "balance", "1000")
109
+ .hset("account:acc004:object", "last_updated", Familia::Validation::ArgumentMatcher.new(:any_string))
110
+ .hset("account:acc005:object", "last_updated", Familia::Validation::ArgumentMatcher.new(:any_string))
111
+ end
112
+
113
+ # Execute the operation
114
+ acc4 = Account.new(account_id: "acc004", balance: "2000")
115
+ acc5 = Account.new(account_id: "acc005", balance: "500")
116
+ acc4.save
117
+ acc5.save
118
+
119
+ TransferService.atomic_transfer(acc4, acc5, 500)
120
+ end
121
+
122
+ puts "Validation result: #{result.valid? ? 'PASS ✅' : 'FAIL ❌'}"
123
+ puts "Summary: #{result.summary}"
124
+
125
+ rescue => e
126
+ puts "Validation demo encountered error: #{e.message}"
127
+ puts "This is expected as the framework needs Redis middleware integration"
128
+ end
129
+
130
+ # Example 4: Performance Analysis
131
+ puts "\n4. Performance Analysis"
132
+ puts "-" * 30
133
+
134
+ begin
135
+ commands = Familia::Validation.capture_commands do
136
+ # Create multiple accounts
137
+ accounts = []
138
+ (1..5).each do |i|
139
+ account = Account.new(account_id: "perf#{i}", balance: "1000")
140
+ account.save
141
+ accounts << account
142
+ end
143
+
144
+ # Perform operations
145
+ accounts[0].balance = "1100"
146
+ accounts[0].save
147
+ end
148
+
149
+ analyzer = Familia::Validation::PerformanceAnalyzer.new(commands)
150
+ analysis = analyzer.analyze
151
+
152
+ puts "Performance Analysis:"
153
+ puts " Total Commands: #{analysis[:total_commands]}"
154
+ puts " Command Types: #{analysis[:command_type_breakdown].keys.join(', ')}"
155
+ puts " Efficiency Score: #{analysis[:efficiency_score]}/100"
156
+
157
+ rescue => e
158
+ puts "Performance analysis encountered error: #{e.message}"
159
+ end
160
+
161
+ # Example 5: Atomicity Validation
162
+ puts "\n5. Atomicity Validation"
163
+ puts "-" * 30
164
+
165
+ begin
166
+ # Test atomic vs non-atomic operations
167
+ acc6 = Account.new(account_id: "acc006", balance: "3000")
168
+ acc7 = Account.new(account_id: "acc007", balance: "1000")
169
+ acc6.save
170
+ acc7.save
171
+
172
+ # This should detect that atomic operations are properly used
173
+ validator = Familia::Validation::Validator.new(strict_atomicity: true)
174
+
175
+ commands = validator.capture_redis_commands do
176
+ TransferService.atomic_transfer(acc6, acc7, 1000)
177
+ end
178
+
179
+ atomicity_validator = Familia::Validation::AtomicityValidator.new(commands)
180
+ result = atomicity_validator.validate
181
+
182
+ puts "Atomicity validation: #{result.valid? ? 'PASS ✅' : 'FAIL ❌'}"
183
+
184
+ rescue => e
185
+ puts "Atomicity validation encountered error: #{e.message}"
186
+ end
187
+
188
+ puts "\n6. Framework Architecture Overview"
189
+ puts "-" * 30
190
+ puts "
191
+ The Redis Command Validation Framework provides:
192
+
193
+ 🔍 Command Recording
194
+ - Captures all Redis commands with full context
195
+ - Tracks transaction boundaries (MULTI/EXEC)
196
+ - Records timing and performance metrics
197
+
198
+ 📝 Expectations DSL
199
+ - Fluent API for defining expected command sequences
200
+ - Support for pattern matching and flexible ordering
201
+ - Transaction and pipeline validation
202
+
203
+ ✅ Validation Engine
204
+ - Compares actual vs expected commands
205
+ - Validates atomicity of operations
206
+ - Provides detailed mismatch reports
207
+
208
+ 🧪 Test Helpers
209
+ - Integration with tryouts framework
210
+ - Methods like assert_redis_commands, assert_atomic_operation
211
+ - Automatic setup and cleanup
212
+
213
+ ⚡ Performance Analysis
214
+ - Command efficiency scoring
215
+ - N+1 pattern detection
216
+ - Transaction overhead analysis
217
+
218
+ Key Benefits:
219
+ • Brass-tacks Redis command validation
220
+ • Atomic operation verification
221
+ • Performance optimization insights
222
+ • Clear diagnostic messages
223
+ • Thread-safe operation
224
+ "
225
+
226
+ # Cleanup
227
+ cleanup_keys = Familia.dbclient.keys("account:*")
228
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
229
+
230
+ puts "\n🎉 Demo complete! The validation framework is ready for use."
231
+ puts " See try/validation/ for comprehensive test examples."
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Basic Relationships Example
4
+ # This example demonstrates the core features of Familia's relationships system
5
+
6
+ require_relative '../lib/familia'
7
+
8
+ # Configure Familia for the example
9
+ Familia.configure do |config|
10
+ config.redis_uri = ENV.fetch('REDIS_URI', 'redis://localhost:6379/15')
11
+ end
12
+
13
+ puts "=== Familia Relationships Basic Example ==="
14
+ puts
15
+
16
+ # Define our model classes
17
+ class Customer < Familia::Horreum
18
+ feature :relationships
19
+
20
+ identifier_field :custid
21
+ field :custid, :name, :email, :plan
22
+
23
+ # Define collections for tracking relationships
24
+ set :domains # Simple set of domain IDs
25
+ list :projects # Ordered list of project IDs
26
+ sorted_set :activity # Activity feed with timestamps
27
+
28
+ # Create indexes for fast lookups
29
+ indexed_by :email_lookup, field: :email
30
+ indexed_by :plan_lookup, field: :plan
31
+
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
38
+ end
39
+
40
+ class Domain < Familia::Horreum
41
+ feature :relationships
42
+
43
+ identifier_field :domain_id
44
+ field :domain_id, :name, :dns_zone, :status
45
+
46
+ # Declare membership in customer collections
47
+ member_of Customer, :domains, type: :set
48
+
49
+ # Track domains by status
50
+ tracked_in :active_domains, type: :sorted_set,
51
+ score: ->(domain) { domain.status == 'active' ? Time.now.to_i : 0 }
52
+ end
53
+
54
+ class Project < Familia::Horreum
55
+ feature :relationships
56
+
57
+ identifier_field :project_id
58
+ field :project_id, :name, :priority
59
+
60
+ # Member of customer projects list (ordered)
61
+ member_of Customer, :projects, type: :list
62
+ end
63
+
64
+ puts "=== 1. Basic Object Creation ==="
65
+
66
+ # Create some sample objects
67
+ customer = Customer.new(
68
+ custid: "cust_#{SecureRandom.hex(4)}",
69
+ name: "Acme Corporation",
70
+ email: "admin@acme.com",
71
+ plan: "enterprise"
72
+ )
73
+
74
+ domain1 = Domain.new(
75
+ domain_id: "dom_#{SecureRandom.hex(4)}",
76
+ name: "acme.com",
77
+ dns_zone: "acme.com.",
78
+ status: "active"
79
+ )
80
+
81
+ domain2 = Domain.new(
82
+ domain_id: "dom_#{SecureRandom.hex(4)}",
83
+ name: "staging.acme.com",
84
+ dns_zone: "staging.acme.com.",
85
+ status: "active"
86
+ )
87
+
88
+ project = Project.new(
89
+ project_id: "proj_#{SecureRandom.hex(4)}",
90
+ name: "Website Redesign",
91
+ priority: "high"
92
+ )
93
+
94
+ puts "✓ Created customer: #{customer.name} (#{customer.custid})"
95
+ puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
96
+ puts "✓ Created project: #{project.name}"
97
+ puts
98
+
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"
105
+
106
+ # Add customer to global tracking
107
+ Customer.add_to_all_customers(customer)
108
+ puts "✓ Added customer to global customer tracking"
109
+
110
+ # Establish member_of relationships (bidirectional)
111
+ domain1.add_to_customer_domains(customer.custid)
112
+ customer.domains.add(domain1.identifier)
113
+
114
+ domain2.add_to_customer_domains(customer.custid)
115
+ customer.domains.add(domain2.identifier)
116
+
117
+ project.add_to_customer_projects(customer.custid)
118
+ customer.projects.add(project.identifier)
119
+
120
+ puts "✓ Established domain ownership relationships"
121
+ puts "✓ Established project ownership relationships"
122
+
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
128
+
129
+ puts "=== 3. Querying Relationships ==="
130
+
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
+
135
+ enterprise_customers = Customer.plan_lookup.get("enterprise")
136
+ puts "Enterprise customer found: #{enterprise_customers}"
137
+
138
+ # Test membership queries
139
+ 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)}"
142
+
143
+ puts "\nCustomer collections:"
144
+ puts " Customer has #{customer.domains.size} domains"
145
+ puts " Customer has #{customer.projects.size} projects"
146
+ puts " Domain IDs: #{customer.domains.members}"
147
+ puts " Project IDs: #{customer.projects.members}"
148
+
149
+ # Test tracked_in collections
150
+ all_customers_count = Customer.all_customers.size
151
+ puts "\nGlobal tracking:"
152
+ puts " Total customers in system: #{all_customers_count}"
153
+
154
+ active_domains_count = Domain.active_domains.size
155
+ puts " Active domains in system: #{active_domains_count}"
156
+ puts
157
+
158
+ puts "=== 4. Range Queries ==="
159
+
160
+ # 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')
163
+ puts "Recent customers (last 24h): #{recent_customers.size}"
164
+
165
+ # 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)}"
170
+ end
171
+ puts
172
+
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
+
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 ==="
206
+
207
+ # Remove relationships
208
+ puts "Cleaning up relationships..."
209
+
210
+ # Remove from member_of relationships
211
+ domain1.remove_from_customer_domains(customer.custid)
212
+ customer.domains.remove(domain1.identifier)
213
+ puts "✓ Removed #{domain1.name} from customer domains"
214
+
215
+ # Remove from tracking collections
216
+ Domain.active_domains.remove(domain2.identifier)
217
+ puts "✓ Removed #{domain2.name} from active domains"
218
+
219
+ # Verify cleanup
220
+ puts "\nAfter cleanup:"
221
+ puts " Customer domains: #{customer.domains.size}"
222
+ puts " Active domains: #{Domain.active_domains.size}"
223
+ puts
224
+
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! ==="
265
+ 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"
272
+ puts
273
+ puts "See docs/wiki/Relationships-Guide.md for comprehensive documentation"
data/lib/familia/base.rb CHANGED
@@ -35,7 +35,7 @@ module Familia
35
35
 
36
36
  def add_feature(klass, feature_name, depends_on: [])
37
37
  @features_available ||= {}
38
- Familia.ld "[#{self}] Adding feature #{klass} as #{feature_name.inspect}"
38
+ Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
39
39
 
40
40
  # Create field definition object
41
41
  feature_def = FeatureDefinition.new(
@@ -171,7 +171,7 @@ module Familia
171
171
  result = dbclient.multi do |conn|
172
172
  Fiber[:familia_transaction] = conn
173
173
  begin
174
- block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
174
+ block_result = yield(conn)
175
175
  ensure
176
176
  Fiber[:familia_transaction] = nil # cleanup reference
177
177
  end
@@ -220,7 +220,7 @@ module Familia
220
220
  result = dbclient.pipelined do |conn|
221
221
  Fiber[:familia_pipeline] = conn
222
222
  begin
223
- block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
223
+ block_result = yield(conn)
224
224
  ensure
225
225
  Fiber[:familia_pipeline] = nil # cleanup reference
226
226
  end
@@ -244,7 +244,7 @@ module Familia
244
244
  # conn.expire("custom_key", 3600)
245
245
  # end
246
246
  #
247
- def with_connection(&block)
247
+ def with_connection(&)
248
248
  yield dbclient
249
249
  end
250
250
 
@@ -0,0 +1,38 @@
1
+ # lib/familia/data_type/types/counter.rb
2
+
3
+ module Familia
4
+ class Counter < String
5
+ def initialize(*args)
6
+ super
7
+ @opts[:default] ||= 0
8
+ end
9
+
10
+ # Enhanced counter semantics
11
+ def reset(val = 0)
12
+ set(val).to_s.eql?('OK')
13
+ end
14
+
15
+ def increment_if_less_than(threshold, amount = 1)
16
+ current = to_i
17
+ return false if current >= threshold
18
+
19
+ incrementby(amount)
20
+ true
21
+ end
22
+
23
+ def atomic_increment_and_get(amount = 1)
24
+ incrementby(amount)
25
+ end
26
+
27
+ # Override to ensure integer serialization
28
+ def value=(val)
29
+ super(val.to_i)
30
+ end
31
+
32
+ def value
33
+ super.to_i
34
+ end
35
+ end
36
+ end
37
+
38
+ Familia::DataType.register Familia::Counter, :counter
@@ -61,6 +61,24 @@ module Familia
61
61
  end
62
62
  alias all hgetall
63
63
 
64
+ # Sets field in the hash stored at key to value, only if field does not yet exist.
65
+ # If field already exists, this operation has no effect.
66
+ # @param field [String] The field name
67
+ # @param val [Object] The value to set
68
+ # @return [Integer] 1 if field is a new field and value was set, 0 if field already exists
69
+ def hsetnx(field, val)
70
+ ret = dbclient.hsetnx dbkey, field.to_s, serialize_value(val)
71
+ update_expiration if ret == 1
72
+ ret
73
+ rescue TypeError => e
74
+ Familia.le "[hsetnx] #{e.message}"
75
+ Familia.ld "[hsetnx] #{dbkey} #{field}=#{val}" if Familia.debug
76
+ echo :hsetnx, caller(1..1).first if Familia.debug # logs via echo to the db and back
77
+ klass = val.class
78
+ msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
79
+ raise e.class, msg
80
+ end
81
+
64
82
  def key?(field)
65
83
  dbclient.hexists dbkey, field.to_s
66
84
  end
@@ -0,0 +1,43 @@
1
+ # lib/familia/data_type/types/lock.rb
2
+
3
+ module Familia
4
+ class Lock < String
5
+ def initialize(*args)
6
+ super
7
+ @opts[:default] = nil
8
+ end
9
+
10
+ # Acquire a lock with optional TTL
11
+ # @param token [String] Unique token to identify lock holder (auto-generated if nil)
12
+ # @param ttl [Integer, nil] Time-to-live in seconds. nil = no expiration, <=0 rejected
13
+ # @return [String, false] Returns token if acquired successfully, false otherwise
14
+ def acquire(token = SecureRandom.uuid, ttl: 10)
15
+ success = setnx(token)
16
+ # Handle both integer (1/0) and boolean (true/false) return values
17
+ return false unless success == 1 || success == true
18
+ return del && false if ttl&.<=(0)
19
+ return del && false if ttl&.positive? && !expire(ttl)
20
+ token
21
+ end
22
+
23
+ def release(token)
24
+ # Lua script to atomically check token and delete
25
+ script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
26
+ dbclient.eval(script, [dbkey], [token]) == 1
27
+ end
28
+
29
+ def locked?
30
+ !value.nil?
31
+ end
32
+
33
+ def held_by?(token)
34
+ value == token
35
+ end
36
+
37
+ def force_unlock!
38
+ del
39
+ end
40
+ end
41
+ end
42
+
43
+ Familia::DataType.register Familia::Lock, :lock
@@ -108,12 +108,19 @@ module Familia
108
108
  ret
109
109
  end
110
110
 
111
+ def del
112
+ ret = dbclient.del dbkey
113
+ ret.positive?
114
+ end
115
+
111
116
  def nil?
112
117
  value.nil?
113
118
  end
114
119
 
115
120
  Familia::DataType.register self, :string
116
- Familia::DataType.register self, :counter
117
- Familia::DataType.register self, :lock
118
121
  end
119
122
  end
123
+
124
+ # Both subclass String
125
+ require_relative 'lock'
126
+ require_relative 'counter'