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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- 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.
|
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(
|
data/lib/familia/connection.rb
CHANGED
@@ -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)
|
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)
|
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(&
|
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'
|