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,438 @@
|
|
1
|
+
# lib/familia/features/relationships/cascading.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Cascading module for handling cascade operations during object lifecycle
|
7
|
+
# Supports multi-presence scenarios where objects exist in multiple collections
|
8
|
+
module Cascading
|
9
|
+
# Cascade strategies
|
10
|
+
STRATEGIES = {
|
11
|
+
remove: :remove_from_collections,
|
12
|
+
ignore: :ignore_collections,
|
13
|
+
cascade: :cascade_destroy_dependents
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# Class-level cascade configurations
|
17
|
+
def self.included(base)
|
18
|
+
base.extend ClassMethods
|
19
|
+
base.include InstanceMethods
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# Get cascade strategies for all relationships
|
25
|
+
def cascade_strategies
|
26
|
+
strategies = {}
|
27
|
+
|
28
|
+
# Collect strategies from tracking relationships
|
29
|
+
if respond_to?(:tracking_relationships)
|
30
|
+
tracking_relationships.each do |config|
|
31
|
+
key = "#{config[:context_class_name]}.#{config[:collection_name]}"
|
32
|
+
strategies[key] = {
|
33
|
+
type: :tracking,
|
34
|
+
strategy: config[:on_destroy] || :remove,
|
35
|
+
config: config
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Collect strategies from membership relationships
|
41
|
+
if respond_to?(:membership_relationships)
|
42
|
+
membership_relationships.each do |config|
|
43
|
+
key = "#{config[:owner_class_name]}.#{config[:collection_name]}"
|
44
|
+
strategies[key] = {
|
45
|
+
type: :membership,
|
46
|
+
strategy: config[:on_destroy] || :remove,
|
47
|
+
config: config
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Collect strategies from indexing relationships
|
53
|
+
if respond_to?(:indexing_relationships)
|
54
|
+
indexing_relationships.each do |config|
|
55
|
+
key = if config[:context_class_name] == 'global'
|
56
|
+
"global.#{config[:index_name]}"
|
57
|
+
else
|
58
|
+
"#{config[:context_class_name]}.#{config[:index_name]}"
|
59
|
+
end
|
60
|
+
strategies[key] = {
|
61
|
+
type: :indexing,
|
62
|
+
strategy: :remove, # Indexes should always be cleaned up
|
63
|
+
config: config
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
strategies
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Instance methods for cascade operations
|
73
|
+
module InstanceMethods
|
74
|
+
# Execute cascade operations during destroy
|
75
|
+
def execute_cascade_operations
|
76
|
+
strategies = self.class.cascade_strategies
|
77
|
+
|
78
|
+
# Group operations by strategy for efficient execution
|
79
|
+
remove_operations = []
|
80
|
+
cascade_operations = []
|
81
|
+
|
82
|
+
strategies.each_value do |strategy_info|
|
83
|
+
case strategy_info[:strategy]
|
84
|
+
when :remove
|
85
|
+
remove_operations << strategy_info
|
86
|
+
when :cascade
|
87
|
+
cascade_operations << strategy_info
|
88
|
+
when :ignore
|
89
|
+
# Do nothing
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Execute remove operations first (cleanup this object's presence)
|
94
|
+
execute_remove_operations(remove_operations) if remove_operations.any?
|
95
|
+
|
96
|
+
# Then execute cascade operations (may trigger other destroys)
|
97
|
+
execute_cascade_operations_recursive(cascade_operations) if cascade_operations.any?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Remove this object from all collections without cascading
|
101
|
+
def remove_from_all_collections
|
102
|
+
strategies = self.class.cascade_strategies
|
103
|
+
remove_operations = strategies.values.reject { |s| s[:strategy] == :ignore }
|
104
|
+
execute_remove_operations(remove_operations)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check if destroying this object would trigger cascades
|
108
|
+
def cascade_impact
|
109
|
+
strategies = self.class.cascade_strategies
|
110
|
+
impact = {
|
111
|
+
removals: 0,
|
112
|
+
cascades: 0,
|
113
|
+
affected_collections: [],
|
114
|
+
cascade_targets: []
|
115
|
+
}
|
116
|
+
|
117
|
+
strategies.each do |key, strategy_info|
|
118
|
+
case strategy_info[:strategy]
|
119
|
+
when :remove
|
120
|
+
impact[:removals] += 1
|
121
|
+
impact[:affected_collections] << key
|
122
|
+
when :cascade
|
123
|
+
impact[:cascades] += 1
|
124
|
+
impact[:affected_collections] << key
|
125
|
+
|
126
|
+
# Estimate cascade targets (this is expensive, use carefully)
|
127
|
+
targets = estimate_cascade_targets(strategy_info)
|
128
|
+
impact[:cascade_targets].concat(targets)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
impact
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# Execute removal operations atomically
|
138
|
+
def execute_remove_operations(remove_operations)
|
139
|
+
return if remove_operations.empty?
|
140
|
+
|
141
|
+
dbclient.pipelined do |pipeline|
|
142
|
+
remove_operations.each do |operation|
|
143
|
+
case operation[:type]
|
144
|
+
when :tracking
|
145
|
+
remove_from_tracking_collections(pipeline, operation[:config])
|
146
|
+
when :membership
|
147
|
+
remove_from_membership_collections(pipeline, operation[:config])
|
148
|
+
when :indexing
|
149
|
+
remove_from_indexing_collections(pipeline, operation[:config])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Remove from tracking collections
|
156
|
+
def remove_from_tracking_collections(pipeline, config)
|
157
|
+
context_class_name = config[:context_class_name]
|
158
|
+
collection_name = config[:collection_name]
|
159
|
+
|
160
|
+
# Find all collections this object is tracked in
|
161
|
+
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
162
|
+
|
163
|
+
dbclient.scan_each(match: pattern) do |key|
|
164
|
+
pipeline.zrem(key, identifier)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Remove from membership collections
|
169
|
+
def remove_from_membership_collections(pipeline, config)
|
170
|
+
owner_class_name = config[:owner_class_name]
|
171
|
+
collection_name = config[:collection_name]
|
172
|
+
type = config[:type]
|
173
|
+
|
174
|
+
# Find all collections this object is a member of
|
175
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
176
|
+
|
177
|
+
dbclient.scan_each(match: pattern) do |key|
|
178
|
+
case type
|
179
|
+
when :sorted_set
|
180
|
+
pipeline.zrem(key, identifier)
|
181
|
+
when :set
|
182
|
+
pipeline.srem(key, identifier)
|
183
|
+
when :list
|
184
|
+
pipeline.lrem(key, 0, identifier)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Remove from indexing collections
|
190
|
+
def remove_from_indexing_collections(pipeline, config)
|
191
|
+
context_class_name = config[:context_class_name]
|
192
|
+
index_name = config[:index_name]
|
193
|
+
field = config[:field]
|
194
|
+
|
195
|
+
field_value = send(field) if respond_to?(field)
|
196
|
+
return unless field_value
|
197
|
+
|
198
|
+
if context_class_name == 'global'
|
199
|
+
index_key = "global:#{index_name}"
|
200
|
+
pipeline.hdel(index_key, field_value.to_s)
|
201
|
+
else
|
202
|
+
# Find all indexes this object appears in
|
203
|
+
pattern = "#{context_class_name.downcase}:*:#{index_name}"
|
204
|
+
|
205
|
+
dbclient.scan_each(match: pattern) do |key|
|
206
|
+
pipeline.hdel(key, field_value.to_s)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Execute cascade operations that may trigger dependent destroys
|
212
|
+
def execute_cascade_operations_recursive(cascade_operations)
|
213
|
+
cascade_operations.each do |operation|
|
214
|
+
case operation[:type]
|
215
|
+
when :tracking
|
216
|
+
cascade_tracking_dependents(operation[:config])
|
217
|
+
when :membership
|
218
|
+
cascade_membership_dependents(operation[:config])
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Cascade destroy for tracking relationships
|
224
|
+
def cascade_tracking_dependents(config)
|
225
|
+
# This is a complex operation that depends on the specific business logic
|
226
|
+
# For now, we'll provide a framework that can be customized
|
227
|
+
|
228
|
+
context_class_name = config[:context_class_name]
|
229
|
+
collection_name = config[:collection_name]
|
230
|
+
|
231
|
+
# Find all contexts that track this object
|
232
|
+
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
233
|
+
|
234
|
+
dbclient.scan_each(match: pattern) do |key|
|
235
|
+
# Check if this object is the only member
|
236
|
+
if dbclient.zcard(key) == 1 && dbclient.zscore(key, identifier)
|
237
|
+
context_id = key.split(':')[1]
|
238
|
+
|
239
|
+
# Optionally destroy the context if it becomes empty
|
240
|
+
# This is application-specific logic
|
241
|
+
trigger_cascade_callback(:tracking, context_class_name, context_id, collection_name)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Cascade destroy for membership relationships
|
247
|
+
def cascade_membership_dependents(config)
|
248
|
+
# Similar to tracking, this depends on business logic
|
249
|
+
|
250
|
+
owner_class_name = config[:owner_class_name]
|
251
|
+
collection_name = config[:collection_name]
|
252
|
+
type = config[:type]
|
253
|
+
|
254
|
+
# Find all owners that contain this object
|
255
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
256
|
+
|
257
|
+
dbclient.scan_each(match: pattern) do |key|
|
258
|
+
# Check if this object exists in the collection
|
259
|
+
is_member = case type
|
260
|
+
when :sorted_set
|
261
|
+
dbclient.zscore(key, identifier) != nil
|
262
|
+
when :set
|
263
|
+
dbclient.sismember(key, identifier)
|
264
|
+
when :list
|
265
|
+
dbclient.lpos(key, identifier) != nil
|
266
|
+
end
|
267
|
+
|
268
|
+
if is_member
|
269
|
+
owner_id = key.split(':')[1]
|
270
|
+
trigger_cascade_callback(:membership, owner_class_name, owner_id, collection_name)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Trigger application-specific cascade callbacks
|
276
|
+
def trigger_cascade_callback(relationship_type, class_name, object_id, collection_name)
|
277
|
+
# This method can be overridden by applications to implement
|
278
|
+
# custom cascade logic
|
279
|
+
|
280
|
+
callback_method = "on_cascade_#{relationship_type}_#{collection_name}"
|
281
|
+
|
282
|
+
return unless respond_to?(callback_method, true)
|
283
|
+
|
284
|
+
send(callback_method, class_name, object_id)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Estimate objects that would be affected by cascading (expensive operation)
|
288
|
+
def estimate_cascade_targets(strategy_info)
|
289
|
+
targets = []
|
290
|
+
|
291
|
+
case strategy_info[:type]
|
292
|
+
when :tracking
|
293
|
+
config = strategy_info[:config]
|
294
|
+
context_class_name = config[:context_class_name]
|
295
|
+
collection_name = config[:collection_name]
|
296
|
+
|
297
|
+
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
298
|
+
dbclient.scan_each(match: pattern) do |key|
|
299
|
+
if dbclient.zscore(key, identifier)
|
300
|
+
context_id = key.split(':')[1]
|
301
|
+
targets << {
|
302
|
+
type: :context,
|
303
|
+
class: context_class_name,
|
304
|
+
id: context_id,
|
305
|
+
collection: collection_name
|
306
|
+
}
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
when :membership
|
311
|
+
config = strategy_info[:config]
|
312
|
+
owner_class_name = config[:owner_class_name]
|
313
|
+
collection_name = config[:collection_name]
|
314
|
+
type = config[:type]
|
315
|
+
|
316
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
317
|
+
dbclient.scan_each(match: pattern) do |key|
|
318
|
+
is_member = case type
|
319
|
+
when :sorted_set
|
320
|
+
dbclient.zscore(key, identifier) != nil
|
321
|
+
when :set
|
322
|
+
dbclient.sismember(key, identifier)
|
323
|
+
when :list
|
324
|
+
dbclient.lpos(key, identifier) != nil
|
325
|
+
end
|
326
|
+
|
327
|
+
if is_member
|
328
|
+
owner_id = key.split(':')[1]
|
329
|
+
targets << {
|
330
|
+
type: :owner,
|
331
|
+
class: owner_class_name,
|
332
|
+
id: owner_id,
|
333
|
+
collection: collection_name
|
334
|
+
}
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
targets
|
340
|
+
end
|
341
|
+
|
342
|
+
# Dry run cascade operations (for testing/preview)
|
343
|
+
def cascade_dry_run
|
344
|
+
strategies = self.class.cascade_strategies
|
345
|
+
|
346
|
+
preview = {
|
347
|
+
removals: [],
|
348
|
+
cascades: [],
|
349
|
+
affected_keys: []
|
350
|
+
}
|
351
|
+
|
352
|
+
strategies.each do |key, strategy_info|
|
353
|
+
case strategy_info[:strategy]
|
354
|
+
when :remove
|
355
|
+
affected_keys = find_affected_keys(strategy_info)
|
356
|
+
preview[:removals] << {
|
357
|
+
relationship: key,
|
358
|
+
keys: affected_keys,
|
359
|
+
count: affected_keys.length
|
360
|
+
}
|
361
|
+
preview[:affected_keys].concat(affected_keys)
|
362
|
+
|
363
|
+
when :cascade
|
364
|
+
cascade_targets = estimate_cascade_targets(strategy_info)
|
365
|
+
preview[:cascades] << {
|
366
|
+
relationship: key,
|
367
|
+
targets: cascade_targets,
|
368
|
+
count: cascade_targets.length
|
369
|
+
}
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
preview[:affected_keys].uniq!
|
374
|
+
preview
|
375
|
+
end
|
376
|
+
|
377
|
+
# Find all Redis keys that would be affected by removing this object
|
378
|
+
def find_affected_keys(strategy_info)
|
379
|
+
affected_keys = []
|
380
|
+
|
381
|
+
case strategy_info[:type]
|
382
|
+
when :tracking
|
383
|
+
config = strategy_info[:config]
|
384
|
+
context_class_name = config[:context_class_name]
|
385
|
+
collection_name = config[:collection_name]
|
386
|
+
pattern = "#{context_class_name.downcase}:*:#{collection_name}"
|
387
|
+
|
388
|
+
dbclient.scan_each(match: pattern) do |key|
|
389
|
+
affected_keys << key if dbclient.zscore(key, identifier)
|
390
|
+
end
|
391
|
+
|
392
|
+
when :membership
|
393
|
+
config = strategy_info[:config]
|
394
|
+
owner_class_name = config[:owner_class_name]
|
395
|
+
collection_name = config[:collection_name]
|
396
|
+
type = config[:type]
|
397
|
+
pattern = "#{owner_class_name.downcase}:*:#{collection_name}"
|
398
|
+
|
399
|
+
dbclient.scan_each(match: pattern) do |key|
|
400
|
+
is_member = case type
|
401
|
+
when :sorted_set
|
402
|
+
dbclient.zscore(key, identifier) != nil
|
403
|
+
when :set
|
404
|
+
dbclient.sismember(key, identifier)
|
405
|
+
when :list
|
406
|
+
dbclient.lpos(key, identifier) != nil
|
407
|
+
end
|
408
|
+
affected_keys << key if is_member
|
409
|
+
end
|
410
|
+
|
411
|
+
when :indexing
|
412
|
+
config = strategy_info[:config]
|
413
|
+
context_class_name = config[:context_class_name]
|
414
|
+
index_name = config[:index_name]
|
415
|
+
field = config[:field]
|
416
|
+
|
417
|
+
field_value = send(field) if respond_to?(field)
|
418
|
+
if field_value
|
419
|
+
if context_class_name == 'global'
|
420
|
+
index_key = "global:#{index_name}"
|
421
|
+
affected_keys << index_key if dbclient.hexists(index_key, field_value.to_s)
|
422
|
+
else
|
423
|
+
pattern = "#{context_class_name.downcase}:*:#{index_name}"
|
424
|
+
dbclient.scan_each(match: pattern) do |key|
|
425
|
+
affected_keys << key if dbclient.hexists(key, field_value.to_s)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
affected_keys
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|