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,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