real_data_tests 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8d2dd0beed227113467c6660eaa2101c603ef7c127ad3bd7fb19c835a641b1b
4
- data.tar.gz: ac0e5fdb541110b7a523a6fa8a894d49d1c10ab9eeab86003f9ae13ebbf1968a
3
+ metadata.gz: 1c5ba3c2d2b4bb4f9ec9a6c539ddd809b6840d3eb7e6cbfb33d02f524177b3ff
4
+ data.tar.gz: e64f18ddd2a0bab9c0f3aad9ab628fb7bd86097644807c5d99596aa3c3af55c4
5
5
  SHA512:
6
- metadata.gz: e048bb72149153484dc179e7628ea3fc41e083906e7cbbd8c1a970b147aa1a232c5d32a6d26553de22c8c3b21a281913515bf1c51b43700c80ba0ac3dc35ffe6
7
- data.tar.gz: 3859be802e7aea039691af38cc309bb56c81832bb5740d510e9f6438d10873a66d1efa8aae167a8cdc2a7e3f24b8b4c26f92d2481303d948797bbbd6a5d08d4d
6
+ metadata.gz: 14a8d12cb5fd01582b2bf22d47d7fa518f2f0be2258e15c477f42c29c1c95b9d362fc20f486fe583f655eeac5b15ceba48c888eb49e9bc47444ab3ffcf79feb6
7
+ data.tar.gz: 1c2a99d52fa1b740c07152bf12d6802711bba8af8af38d94924d66ddda06d2eab76e6f459b532af3c29e2a0767bfd4ee7977aa23293b3a616225a5b340010a89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-01-14
4
+ ### Fixed
5
+ - Fixed circular dependency handling in RecordCollector to correctly limit record collection
6
+ - Moved prevention logic earlier in the collection process to stop circular dependencies before record collection
7
+ - Improved tracking of visited associations for more accurate prevention
8
+ - Added better logging for dependency prevention decisions
9
+ - Fixed test case for circular dependency prevention in nested associations
10
+
11
+ ## [0.3.0] - 2025-01-13
12
+ ### Added
13
+ - **Polymorphic Association Support**:
14
+ - RecordCollector now supports tracking and collecting records from polymorphic associations.
15
+ - Polymorphic `belongs_to`, `has_many`, and `has_one` associations are automatically detected and processed during data collection.
16
+ - Added tracking for polymorphic types in `@collection_stats` to provide detailed insights into polymorphic relationships.
17
+ - Graceful handling of missing records in polymorphic associations using error logging.
18
+
19
+ ### Fixed
20
+ - Improved error handling for `ActiveRecord::RecordNotFound` exceptions when loading polymorphic associations.
21
+ - Correctly initializes and updates association statistics for polymorphic associations in `@collection_stats`.
22
+
3
23
  ## [0.2.1] - 2025-01-13
4
24
  ### Fixed
5
25
  - Fixed JSONB field handling to output '{}' instead of empty string for blank values
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- real_data_tests (0.2.0)
4
+ real_data_tests (0.2.1)
5
5
  activerecord (>= 5.0)
6
6
  faker (~> 3.0)
7
7
  pg (>= 1.1)
data/README.md CHANGED
@@ -90,6 +90,37 @@ Rails.application.config.after_initialize do
90
90
  end
91
91
  ```
92
92
 
93
+ ## Polymorphic Association Support
94
+
95
+ Real Data Tests supports collecting records through polymorphic associations. This feature allows you to:
96
+ - Automatically detect and collect records for polymorphic `belongs_to`, `has_many`, and `has_one` associations.
97
+ - Track and report the types of records collected through polymorphic associations in detailed collection statistics.
98
+
99
+ ### Example
100
+ If your model includes a polymorphic association like this:
101
+
102
+ ```ruby
103
+ class Payment < ApplicationRecord
104
+ belongs_to :billable, polymorphic: true
105
+ end
106
+ ```
107
+
108
+ Real Data Tests will:
109
+ 1. Collect the associated `billable` records regardless of their type (e.g., `InsuranceCompany`, `Patient`).
110
+ 2. Include the `billable_type` in the collection statistics for transparency and reporting.
111
+
112
+ ### Configuration for Polymorphic Associations
113
+ Polymorphic associations are automatically handled based on your existing configuration. You can also explicitly include or limit polymorphic associations, like so:
114
+
115
+ ```ruby
116
+ RealDataTests.configure do |config|
117
+ config.include_associations_for 'Payment', :billable
118
+ config.limit_association 'Payment.billable', 5
119
+ end
120
+ ```
121
+
122
+ This ensures a robust and flexible way to handle even the most complex relationships in your data.
123
+
93
124
  ## Using Presets
94
125
 
95
126
  Real Data Tests allows you to define multiple configuration presets for different data extraction needs. This is particularly useful when you need different association rules and anonymization settings for different testing scenarios.
@@ -268,6 +299,22 @@ This is particularly useful when:
268
299
  - You want to collect an association from one model but not another
269
300
  - You need to maintain a clean separation of concerns in your test data
270
301
 
302
+ ### Polymorphic Associations
303
+ Polymorphic associations are fully supported. Include and configure them as needed:
304
+
305
+ ```ruby
306
+ RealDataTests.configure do |config|
307
+ config.include_associations_for 'Payment', :billable
308
+ end
309
+ ```
310
+
311
+ You can also limit or prevent reciprocal loading for polymorphic associations:
312
+
313
+ ```ruby
314
+ config.limit_association 'Payment.billable', 10
315
+ config.prevent_reciprocal 'Payment.billable'
316
+ ```
317
+
271
318
  ### Association Loading Control
272
319
 
273
320
  You can further refine how associations are loaded using limits and reciprocal prevention:
@@ -61,7 +61,10 @@ module RealDataTests
61
61
  class PresetConfig
62
62
  attr_reader :association_filter_mode, :association_filter_list,
63
63
  :model_specific_associations, :association_limits,
64
- :prevent_reciprocal_loading, :anonymization_rules
64
+ :prevent_reciprocal_loading, :anonymization_rules,
65
+ :prevented_reciprocals
66
+
67
+ attr_accessor :max_depth
65
68
 
66
69
  def initialize
67
70
  @association_filter_mode = nil
@@ -70,6 +73,26 @@ module RealDataTests
70
73
  @association_limits = {}
71
74
  @prevent_reciprocal_loading = {}
72
75
  @anonymization_rules = {}
76
+ @prevented_reciprocals = Set.new
77
+ @max_depth = 10
78
+ end
79
+
80
+ def prevent_circular_dependency(klass, association_name)
81
+ key = if klass.is_a?(String)
82
+ "#{klass}:#{association_name}"
83
+ else
84
+ "#{klass.name}:#{association_name}"
85
+ end
86
+ @prevented_reciprocals << key
87
+ end
88
+
89
+ def has_circular_dependency?(klass, association_name)
90
+ key = if klass.is_a?(String)
91
+ "#{klass}:#{association_name}"
92
+ else
93
+ "#{klass.name}:#{association_name}"
94
+ end
95
+ @prevented_reciprocals.include?(key)
73
96
  end
74
97
 
75
98
  def include_associations(*associations)
@@ -102,9 +125,14 @@ module RealDataTests
102
125
  @association_limits[path]
103
126
  end
104
127
 
128
+ def set_association_limit(model_name, association_name, limit)
129
+ path = "#{model_name}.#{association_name}"
130
+ @association_limits[path] = limit
131
+ end
132
+
105
133
  def prevent_reciprocal?(record_class, association_name)
106
134
  path = "#{record_class.name}.#{association_name}"
107
- @prevent_reciprocal_loading[path]
135
+ @prevent_reciprocal_loading[path] || has_circular_dependency?(record_class, association_name)
108
136
  end
109
137
 
110
138
  def prevent_reciprocal(path)
@@ -1,84 +1,182 @@
1
1
  module RealDataTests
2
2
  class RecordCollector
3
+ attr_reader :collection_stats, :collected_records
4
+
3
5
  def initialize(record)
4
6
  @record = record
5
7
  @collected_records = Set.new
6
- @collection_stats = Hash.new { |h, k| h[k] = { count: 0, associations: Hash.new(0) } }
8
+ @collection_stats = {}
7
9
  @processed_associations = Set.new
8
10
  @association_path = []
11
+ @current_depth = 0
12
+ @visited_associations = {}
13
+
14
+ # Initialize stats for the record's class
15
+ @collection_stats[record.class.name] = {
16
+ count: 0,
17
+ associations: Hash.new(0),
18
+ polymorphic_types: {}
19
+ }
20
+
21
+ record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
22
+ if assoc.polymorphic?
23
+ @collection_stats[record.class.name][:polymorphic_types][assoc.name.to_s] ||= Set.new
24
+ end
25
+ end
26
+
27
+ puts "\nInitializing RecordCollector for #{record.class.name}##{record.id}"
28
+ record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
29
+ if assoc.polymorphic?
30
+ type = record.public_send("#{assoc.name}_type")
31
+ id = record.public_send("#{assoc.name}_id")
32
+ puts "Found polymorphic belongs_to '#{assoc.name}' with type: #{type}, id: #{id}"
33
+ end
34
+ end
9
35
  end
10
36
 
11
37
  def collect
12
38
  puts "\nStarting record collection from: #{@record.class.name}##{@record.id}"
13
- filter_mode = RealDataTests.configuration.association_filter_mode
14
- filter_list = RealDataTests.configuration.association_filter_list
39
+ filter_mode = RealDataTests.configuration.current_preset.association_filter_mode
40
+ filter_list = RealDataTests.configuration.current_preset.association_filter_list
15
41
  puts "Using #{filter_mode || 'no'} filter with #{filter_list.any? ? filter_list.join(', ') : 'no associations'}"
16
- collect_record(@record)
42
+ collect_record(@record, 0)
17
43
  print_collection_stats
18
44
  @collected_records.to_a
19
45
  end
20
46
 
21
47
  private
22
48
 
23
- def should_process_association?(record, association)
24
- association_key = "#{record.class.name}##{record.id}:#{association.name}"
25
- return false if @processed_associations.include?(association_key)
26
- @processed_associations.add(association_key)
27
-
28
- # Use the enhanced should_process_association? method
29
- return false unless RealDataTests.configuration.should_process_association?(record, association.name)
30
-
31
- # Check for prevented reciprocal loading
32
- if RealDataTests.configuration.prevent_reciprocal?(record.class, association.name)
33
- puts " Skipping prevented reciprocal association: #{association.name} on #{record.class.name}"
34
- return false
35
- end
36
-
37
- true
38
- end
39
-
40
- def collect_record(record)
49
+ def collect_record(record, depth)
41
50
  return if @collected_records.include?(record)
42
51
  return unless record # Guard against nil records
52
+ return if depth > RealDataTests.configuration.current_preset.max_depth
43
53
 
54
+ puts "\nCollecting record: #{record.class.name}##{record.id}"
44
55
  @collected_records.add(record)
56
+
57
+ # Initialize stats structure
58
+ @collection_stats[record.class.name] ||= {
59
+ count: 0,
60
+ associations: {},
61
+ polymorphic_types: {}
62
+ }
45
63
  @collection_stats[record.class.name][:count] += 1
46
- collect_associations(record)
64
+
65
+ # Track types for polymorphic belongs_to associations
66
+ record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
67
+ next unless assoc.polymorphic?
68
+
69
+ type = record.public_send("#{assoc.name}_type")
70
+ @collection_stats[record.class.name][:polymorphic_types][assoc.name.to_sym] ||= Set.new
71
+
72
+ begin
73
+ associated_record = record.public_send(assoc.name)
74
+ if associated_record
75
+ puts " Adding polymorphic type '#{type}' for #{assoc.name}"
76
+ @collection_stats[record.class.name][:polymorphic_types][assoc.name.to_sym] << associated_record.class.name
77
+ else
78
+ puts " Skipping polymorphic type for #{assoc.name} due to missing associated record"
79
+ end
80
+ rescue StandardError => e
81
+ puts " Error loading polymorphic association #{assoc.name}: #{e.message}"
82
+ end
83
+ end
84
+
85
+ collect_associations(record, depth)
47
86
  end
48
87
 
49
- def collect_associations(record)
88
+ def collect_associations(record, depth)
50
89
  return unless record.class.respond_to?(:reflect_on_all_associations)
90
+ return if depth >= RealDataTests.configuration.current_preset.max_depth
51
91
 
52
92
  associations = record.class.reflect_on_all_associations
53
93
  puts "\nProcessing associations for: #{record.class.name}##{record.id}"
54
94
  puts "Found #{associations.length} associations"
55
95
 
56
96
  associations.each do |association|
57
- should_process = RealDataTests.configuration.should_process_association?(record, association.name)
97
+ association_key = "#{record.class.name}##{record.id}:#{association.name}"
98
+ puts " Checking if should process: #{association_key}"
58
99
 
59
- unless should_process
60
- puts " Skipping #{RealDataTests.configuration.association_filter_mode == :whitelist ? 'non-whitelisted' : 'blacklisted'} association: #{association.name} for #{record.class.name}"
61
- next
100
+ if RealDataTests.configuration.current_preset.prevent_reciprocal?(record.class, association.name)
101
+ track_key = "#{record.class.name}:#{association.name}"
102
+ @visited_associations[track_key] ||= Set.new
103
+
104
+ # Skip if we've already processed this association type for this class
105
+ if @visited_associations[track_key].any?
106
+ puts " Skipping prevented reciprocal association: #{track_key}"
107
+ next
108
+ end
109
+ @visited_associations[track_key].add(record.id)
62
110
  end
63
111
 
64
- puts " Processing #{association.macro} association: #{association.name}"
65
- process_association(record, association)
112
+ next unless should_process_association?(record, association, depth)
113
+
114
+ puts " Processing #{association.macro} #{association.polymorphic? ? 'polymorphic ' : ''}association: #{association.name}"
115
+ process_association(record, association, depth)
66
116
  end
67
117
  end
68
118
 
69
- def process_association(record, association)
119
+ def should_process_association?(record, association, depth = 0)
120
+ return false if depth >= RealDataTests.configuration.current_preset.max_depth
121
+
122
+ association_key = "#{record.class.name}##{record.id}:#{association.name}"
123
+ return false if @processed_associations.include?(association_key)
124
+
125
+ # Check if the association is allowed by configuration
126
+ should_process = RealDataTests.configuration.current_preset.should_process_association?(record, association.name)
127
+ puts " Configuration says: #{should_process}"
128
+
129
+ if should_process
130
+ @processed_associations.add(association_key)
131
+ true
132
+ else
133
+ false
134
+ end
135
+ end
136
+
137
+ def process_association(record, association, depth)
138
+ @association_path.push(association.name)
139
+
70
140
  begin
141
+ if detect_circular_dependency?(record, association)
142
+ puts " Skipping circular dependency for #{association.name} on #{record.class.name}##{record.id}"
143
+ return
144
+ end
145
+
71
146
  related_records = fetch_related_records(record, association)
72
147
  count = related_records.length
73
148
  puts " Found #{count} related #{association.name} records"
74
- @collection_stats[record.class.name][:associations][association.name] += count
75
149
 
76
- related_records.each { |related_record| collect_record(related_record) }
150
+ @collection_stats[record.class.name][:associations][association.name.to_s] ||= 0
151
+ @collection_stats[record.class.name][:associations][association.name.to_s] += count
152
+
153
+ related_records.each { |related_record| collect_record(related_record, depth + 1) }
77
154
  rescue => e
78
155
  puts " Error processing association #{association.name}: #{e.message}"
156
+ ensure
157
+ @association_path.pop
79
158
  end
80
159
  end
81
160
 
161
+ def self_referential_association?(klass, association)
162
+ return false unless association.options[:class_name]
163
+ return false if association.polymorphic?
164
+ association.options[:class_name] == klass.name
165
+ end
166
+
167
+ def detect_circular_dependency?(record, association)
168
+ return false unless association.belongs_to?
169
+ return false if association.polymorphic?
170
+
171
+ target_class = association.klass
172
+ return false unless target_class
173
+
174
+ path_key = "#{target_class.name}:#{association.name}"
175
+ visited_count = @association_path.count { |assoc| "#{target_class.name}:#{assoc}" == path_key }
176
+
177
+ visited_count > 1
178
+ end
179
+
82
180
  def fetch_related_records(record, association)
83
181
  case association.macro
84
182
  when :belongs_to, :has_one
@@ -86,13 +184,12 @@ module RealDataTests
86
184
  when :has_many, :has_and_belongs_to_many
87
185
  relation = record.public_send(association.name)
88
186
 
89
- # Apply configured limit if it exists
90
- if limit = RealDataTests.configuration.get_association_limit(record.class, association.name)
91
- puts " Applying configured limit of #{limit} records for #{record.class.name}.#{association.name}"
92
- relation = relation.limit(limit)
187
+ if limit = RealDataTests.configuration.current_preset.get_association_limit(record.class, association.name)
188
+ puts " Applying configured limit of #{limit} records for #{record.class.name}.#{association.name}"
189
+ relation = relation[0...limit]
93
190
  end
94
191
 
95
- relation.loaded? ? relation.to_a : relation.load.to_a
192
+ relation
96
193
  else
97
194
  []
98
195
  end
@@ -103,12 +200,20 @@ module RealDataTests
103
200
  @collection_stats.each do |model, stats|
104
201
  puts "\n#{model}:"
105
202
  puts " Total records: #{stats[:count]}"
203
+
106
204
  if stats[:associations].any?
107
205
  puts " Associations:"
108
206
  stats[:associations].each do |assoc_name, count|
109
207
  puts " #{assoc_name}: #{count} records"
110
208
  end
111
209
  end
210
+
211
+ if stats[:polymorphic_types].any?
212
+ puts " Polymorphic Types:"
213
+ stats[:polymorphic_types].each do |assoc_name, types|
214
+ puts " #{assoc_name}: #{types.to_a.join(', ')}"
215
+ end
216
+ end
112
217
  end
113
218
  puts "\nTotal unique records collected: #{@collected_records.size}"
114
219
  puts "==============================\n"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RealDataTests
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: real_data_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Dias
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-13 00:00:00.000000000 Z
11
+ date: 2025-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails