duty_free 1.0.8 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,6 @@ module DutyFree
7
7
  # Helpful suggestions to get started creating a template
8
8
  # Pass in -1 for hops if you want to traverse all possible links
9
9
  def suggest_template(hops = 0, do_has_many = false, show_output = true, this_klass = self)
10
- ::DutyFree.instance_variable_set(:@errored_assocs, [])
11
10
  ::DutyFree.instance_variable_set(:@errored_columns, [])
12
11
  uniques, _required = ::DutyFree::SuggestTemplate._suggest_unique_column(this_klass, nil, '')
13
12
  template, required = ::DutyFree::SuggestTemplate._suggest_template(hops, do_has_many, this_klass)
@@ -17,7 +16,6 @@ module DutyFree
17
16
  all: template,
18
17
  as: {}
19
18
  }
20
- # puts "Errors: #{::DutyFree.instance_variable_get(:@errored_assocs).inspect}"
21
19
 
22
20
  if show_output
23
21
  path = this_klass.name.split('::').map(&:underscore).join('/')
@@ -32,161 +30,369 @@ module DutyFree
32
30
  end
33
31
  end
34
32
 
35
- def self._suggest_template(hops, do_has_many, this_klass, poison_links = [], path = '')
36
- errored_assocs = ::DutyFree.instance_variable_get(:@errored_assocs)
37
- this_primary_key = Array(this_klass.primary_key)
38
- # Find all associations, and track all belongs_tos
39
- this_belongs_tos = []
40
- assocs = {}
41
- this_klass.reflect_on_all_associations.each do |assoc|
42
- # PolymorphicReflection AggregateReflection RuntimeReflection
43
- is_belongs_to = assoc.belongs_to?
44
- # Figure out if it's belongs_to, has_many, or has_one
45
- belongs_to_or_has_many =
46
- if is_belongs_to
47
- 'belongs_to'
48
- elsif (is_habtm = assoc.macro == :has_and_belongs_to_many)
49
- 'has_and_belongs_to_many'
50
- elsif assoc.macro == :has_many
51
- 'has_many'
52
- else
53
- 'has_one'
33
+ def self._suggest_template(hops, do_has_many, this_klass)
34
+ poison_links = []
35
+ requireds = []
36
+ errored_assocs = []
37
+ buggy_bts = []
38
+ is_papertrail = Object.const_defined?('::PaperTrail::Version')
39
+ # Get a list of all polymorphic models by searching out every has_many that has an :as option
40
+ all_polymorphics = Hash.new { |h, k| h[k] = [] }
41
+ _all_models.each do |model|
42
+ model.reflect_on_all_associations.select do |assoc|
43
+ # If the model name can not be found then we get something like this here:
44
+ # 1: from /home/lorin/.rvm/gems/ruby-2.6.5/gems/activerecord-5.2.4.6/lib/active_record/reflection.rb:418:in `compute_class'
45
+ # /home/lorin/.rvm/gems/ruby-2.6.5/gems/activerecord-5.2.4.6/lib/active_record/inheritance.rb:196:in `compute_type': uninitialized constant Site::SiteTeams (NameError)
46
+ ret = nil
47
+ begin
48
+ ret = assoc.polymorphic? ||
49
+ (assoc.options.include?(:as) && !(is_papertrail && assoc.klass&.<=(PaperTrail::Version)))
50
+ rescue NameError
51
+ false
54
52
  end
55
- # Always process belongs_to, and also process has_one and has_many if do_has_many is chosen.
56
- # Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.)
57
- if is_habtm
58
- puts "* In the #{this_klass.name} model there's a problem with: \"has_and_belongs_to_many :#{assoc.name}\" because join table \"#{assoc.join_table}\" does not exist. You can create it with a create_join_table migration." unless ActiveRecord::Base.connection.table_exists?(assoc.join_table)
59
- # %%% Search for other associative candidates to use instead of this HABTM contraption
60
- puts "* In the #{this_klass.name} model there's a problem with: \"has_and_belongs_to_many :#{assoc.name}\" because it includes \"through: #{assoc.options[:through].inspect}\" which is pointless and should be removed." if assoc.options.include?(:through)
61
- end
62
- if (is_through = assoc.is_a?(ActiveRecord::Reflection::ThroughReflection)) && assoc.options.include?(:as)
63
- puts "* In the #{this_klass.name} model there's a problem with: \"has_many :#{assoc.name} through: #{assoc.options[:through].inspect}\" because it also includes \"as: #{assoc.options[:as].inspect}\", so please choose either for this line to be a \"has_many :#{assoc.name} through:\" or to be a polymorphic \"has_many :#{assoc.name} as:\". It can't be both."
53
+ ret
54
+ end.each do |assoc|
55
+ poly_hm = if assoc.belongs_to?
56
+ poly_key = [model, assoc.name.to_sym] if assoc.polymorphic?
57
+ nil
58
+ else
59
+ poly_key = [assoc.klass, assoc.options[:as]]
60
+ [model, assoc.macro, assoc.name.to_sym]
61
+ end
62
+ next if all_polymorphics.include?(poly_key) && all_polymorphics[poly_key].include?(poly_hm)
63
+
64
+ poly_hms = all_polymorphics[poly_key]
65
+ poly_hms << poly_hm if poly_hm
64
66
  end
65
- next if is_through || is_habtm || (!is_belongs_to && !do_has_many) || errored_assocs.include?(assoc)
67
+ end
66
68
 
67
- if is_belongs_to && assoc.options[:polymorphic] # Polymorphic belongs_to?
68
- # Load all models
69
- # %%% Note that this works in Rails 5.x, but may not work in Rails 6.0 and later, which uses the Zeitwerk loader by default:
70
- Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
71
- # Find all current possible polymorphic relations
72
- ActiveRecord::Base.descendants.each do |model|
73
- # Skip auto-generated HABTM_DestinationModel models
74
- next if model.respond_to?(:table_name_resolver) &&
75
- model.name.start_with?('HABTM_') &&
76
- model.table_name_resolver.is_a?(
77
- ActiveRecord::Associations::Builder::HasAndBelongsToMany::JoinTableResolver::KnownClass
78
- )
69
+ bad_polymorphic_hms = []
70
+ # "path" starts with ''
71
+ # "template" starts with [], and we hold on to the root piece here so we can return it after
72
+ # going through all the layers, at which point it will have grown to include the entire hierarchy.
73
+ this_layer = [[do_has_many, this_klass, '', (whole_template = [])]]
74
+ loop do
75
+ next_layer = []
76
+ this_layer.each do |klass_item|
77
+ do_has_many, this_klass, path, template = klass_item
78
+ this_primary_key = Array(this_klass.primary_key)
79
+ # Find all associations, and track all belongs_tos
80
+ this_belongs_tos = []
81
+ assocs = {}
82
+ this_klass.reflect_on_all_associations.each do |assoc|
83
+ # PolymorphicReflection AggregateReflection RuntimeReflection
84
+ is_belongs_to = assoc.belongs_to?
85
+ # Figure out if it's belongs_to, has_many, or has_one
86
+ belongs_to_or_has_many =
87
+ if is_belongs_to
88
+ 'belongs_to'
89
+ elsif (is_habtm = assoc.macro == :has_and_belongs_to_many)
90
+ 'has_and_belongs_to_many'
91
+ elsif assoc.macro == :has_many
92
+ 'has_many'
93
+ else
94
+ 'has_one'
95
+ end
96
+ # Always process belongs_to, and also process has_one and has_many if do_has_many is chosen.
97
+ # Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.)
98
+ if is_habtm
99
+ puts "* #{this_klass.name} model - problem with: \"has_and_belongs_to_many :#{assoc.name}\" because join table \"#{assoc.join_table}\" does not exist. You can create it with a create_join_table migration." unless ActiveRecord::Base.connection.table_exists?(assoc.join_table)
100
+ # %%% Search for other associative candidates to use instead of this HABTM contraption
101
+ puts "* #{this_klass.name} model - problem with: \"has_and_belongs_to_many :#{assoc.name}\" because it includes \"through: #{assoc.options[:through].inspect}\" which is pointless and should be removed." if assoc.options.include?(:through)
102
+ end
103
+ if (is_through = assoc.is_a?(ActiveRecord::Reflection::ThroughReflection)) && assoc.options.include?(:as)
104
+ puts "* #{this_klass.name} model - problem with: \"has_many :#{assoc.name} through: #{assoc.options[:through].inspect}\" because it also includes \"as: #{assoc.options[:as].inspect}\", " \
105
+ "so please choose either for this line to be a \"has_many :#{assoc.name} through:\" or to be a polymorphic \"has_many :#{assoc.name} as:\". It can't be both."
106
+ end
107
+ next if is_through || is_habtm || (!is_belongs_to && !do_has_many) || errored_assocs.include?(assoc)
79
108
 
80
- # Find applicable polymorphic has_many associations from each real model
81
- model.reflect_on_all_associations.each do |poly_assoc|
82
- next unless poly_assoc.macro == :has_many && poly_assoc.inverse_of == assoc
109
+ # Polymorphic belongs_to?
110
+ # (checking all_polymorphics in order to handle the rare case when the belongs_to side of a polymorphic association is missing "polymorphic: true")
111
+ if is_belongs_to && (
112
+ assoc.options[:polymorphic] ||
113
+ (
114
+ all_polymorphics.include?(poly_key = [assoc.active_record, assoc.name.to_sym]) &&
115
+ all_polymorphics[poly_key].map { |p| p&.first }.include?(this_klass)
116
+ )
117
+ )
118
+ # Find all current possible polymorphic relations
119
+ _all_models.each do |model|
120
+ # Skip auto-generated HABTM_DestinationModel models
121
+ next if model.respond_to?(:table_name_resolver) &&
122
+ model.name.start_with?('HABTM_') &&
123
+ model.table_name_resolver.is_a?(
124
+ ActiveRecord::Associations::Builder::HasAndBelongsToMany::JoinTableResolver::KnownClass
125
+ )
83
126
 
84
- this_belongs_tos += (fkeys = [poly_assoc.type, poly_assoc.foreign_key])
85
- assocs["#{assoc.name}_#{poly_assoc.active_record.name.underscore}".to_sym] = [[fkeys, assoc.active_record], poly_assoc.active_record]
86
- end
87
- end
88
- else
89
- # Is it a polymorphic has_many, which is defined using as: :somethingable ?
90
- is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
91
- begin
92
- # Standard has_one, or has_many, and belongs_to uses assoc.klass.
93
- # Also polymorphic belongs_to uses assoc.klass.
94
- assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass
95
- rescue NameError # For models which cannot be found by name
96
- end
97
- new_assoc =
98
- if assoc_klass.nil?
99
- puts "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model."
100
- nil # Cause this one to be excluded
101
- elsif is_belongs_to
102
- this_belongs_tos << (fk = assoc.foreign_key.to_s)
103
- [[[fk], assoc.active_record], assoc_klass]
104
- else # has_many or has_one
105
- inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
106
- missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
107
- if missing_key_columns.empty?
108
- puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
109
- # puts "Has columns #{inverse_foreign_keys.inspect}"
110
- [[inverse_foreign_keys, assoc_klass], assoc_klass]
111
- else
112
- if inverse_foreign_keys.length > 1
113
- puts "* The #{assoc_klass.name} model is missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance."
114
- else
115
- print "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\"."
127
+ # Find applicable polymorphic has_many associations from each real model
128
+ model.reflect_on_all_associations.each do |poly_assoc|
129
+ next unless [:has_many, :has_one].include?(poly_assoc.macro) && poly_assoc.inverse_of == assoc
116
130
 
117
- if (inverses = _find_belongs_tos(assoc_klass, this_klass, errored_assocs)).empty?
118
- if inverse_foreign_keys.first.nil?
119
- puts " Consider adding \"foreign_key: :#{this_klass.name.underscore}_id\" regarding some column in #{assoc_klass.name} to this #{belongs_to_or_has_many} entry."
120
- else
121
- puts " (Cannot find foreign key \"#{inverse_foreign_keys.first.inspect}\" in #{assoc_klass.name}.)"
131
+ this_belongs_tos += (fkeys = [poly_assoc.type, poly_assoc.foreign_key])
132
+ assocs["#{assoc.name}_#{poly_assoc.active_record.name.underscore}".to_sym] = [[fkeys, assoc.active_record], poly_assoc.active_record]
133
+ end
134
+ end
135
+ else
136
+ # Is it a polymorphic has_many, which is defined using as: :somethingable ?
137
+ is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
138
+ begin
139
+ # Standard has_one, or has_many, and belongs_to uses assoc.klass.
140
+ # Also polymorphic belongs_to uses assoc.klass.
141
+ assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass
142
+ rescue NameError # For models which cannot be found by name
143
+ end
144
+ # Skip any PaperTrail audited things
145
+ # rubocop:disable Lint/SafeNavigationConsistency
146
+ next if (Object.const_defined?('PaperTrail::Version') && assoc_klass&.<=(PaperTrail::Version) && assoc.options.include?(:as)) ||
147
+ # And any goofy self-referencing aliases
148
+ (!is_belongs_to && assoc_klass <= assoc.active_record && assoc.foreign_key.to_s == assoc.active_record.primary_key)
149
+
150
+ # rubocop:enable Lint/SafeNavigationConsistency
151
+
152
+ # Avoid getting goofed up by the belongs_to side of a broken polymorphic association
153
+ assoc_klass = nil if assoc.belongs_to? && !(assoc_klass <= ActiveRecord::Base)
154
+
155
+ if !is_polymorphic_hm && assoc.options.include?(:as)
156
+ assoc_klass = assoc.inverse_of.active_record
157
+ is_polymorphic_hm = true
158
+ bad_polymorphic_hm = [assoc_klass, assoc.inverse_of]
159
+ unless bad_polymorphic_hms.include?(bad_polymorphic_hm)
160
+ bad_polymorphic_hms << bad_polymorphic_hm
161
+ puts "* #{assoc_klass.name} model - problem with the polymorphic association \"belongs_to :#{assoc.inverse_of.name}\". You can fix this in one of two ways:"
162
+ puts ' (1) add "polymorphic: true" on this belongs_to line, or'
163
+ poly_hms = all_polymorphics.inject([]) do |s, poly_hm|
164
+ if (key = poly_hm.first).first <= assoc_klass && key.last == assoc.inverse_of.name
165
+ s += poly_hm.last
122
166
  end
167
+ s
168
+ end
169
+ puts " (2) Undo #{assoc_klass.name} polymorphism by removing \"as: :#{assoc.inverse_of.name}\" in these #{poly_hms.length} places:"
170
+ poly_hms.each { |poly_hm| puts " In the #{poly_hm.first.name} class from the line: #{poly_hm[1]} :#{poly_hm.last}" }
171
+ end
172
+ end
173
+
174
+ new_assoc =
175
+ if assoc_klass.nil?
176
+ # In case this is a buggy polymorphic belongs_to, keep track of all of these and at the very end
177
+ # only show the pertinent ones.
178
+ if is_belongs_to
179
+ buggy_bts << [this_klass, assoc]
123
180
  else
124
- puts " Consider adding \"#{inverses.map { |x| "inverse_of: :#{x.name}" }.join(' or ')}\" to this entry."
181
+ puts "* #{this_klass.name} model - problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model."
125
182
  end
183
+ nil # Cause this one to be excluded
184
+ elsif is_belongs_to
185
+ this_belongs_tos << (foreign_key = assoc.foreign_key.to_s)
186
+ [[[foreign_key], assoc.active_record], assoc_klass]
187
+ elsif _all_tables.include?(assoc_klass.table_name) || # has_many or has_one
188
+ (assoc_klass.table_name.start_with?('public.') && _all_tables.include?(assoc_klass.table_name[7..-1]))
189
+ inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
190
+ missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
191
+ if missing_key_columns.empty?
192
+ puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
193
+ # puts "Has columns #{inverse_foreign_keys.inspect}"
194
+ [[inverse_foreign_keys, assoc_klass], assoc_klass]
195
+ else
196
+ if inverse_foreign_keys.length > 1
197
+ puts "* #{assoc_klass.name} model - missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance."
198
+ else
199
+ puts "* #{this_klass.name} model - problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\"."
200
+
201
+ # Most general related parent class in case we're STI
202
+ root_class = test_class = this_klass
203
+ while (test_class = test_class.superclass) != ActiveRecord::Base
204
+ root_class = test_class unless test_class.abstract_class?
205
+ end
206
+ # If we haven't yet found a match, search for any appropriate unused foreign key that belongs_to the primary class
207
+ is_mentioned_consider = false
208
+ if (inverses = _find_assocs(:belongs_to, assoc_klass, root_class, errored_assocs, assoc.foreign_key)).empty?
209
+ if inverse_foreign_keys.first.nil?
210
+ # So we can rule them out, find the belongs_tos that already are inverses of any other relevant has_many
211
+ hm_assocs = _find_assocs(:has_many, this_klass, assoc_klass, errored_assocs)
212
+ hm_inverses = hm_assocs.each_with_object([]) do |hm, s|
213
+ s << hm.inverse_of if hm.inverse_of
214
+ s
215
+ end
216
+ # Remaining belongs_tos are also good candidates to become an inverse_of, so we'll suggest
217
+ # both to establish a :foreign_key and also duing the inverses.present? check an :inverse_of.
218
+ inverses = _find_assocs(:belongs_to, assoc_klass, root_class, errored_assocs).reject do |bt|
219
+ hm_inverses.include?(bt)
220
+ end
221
+ fks = inverses.map(&:foreign_key)
222
+ # All that and still no matches?
223
+ unless fks.present? || assoc_klass.columns.map(&:name).include?(suggested_fk = "#{root_class.name.underscore}_id")
224
+ # Find any polymorphic association on this model (that we're not already tied to) that could be used.
225
+ poly_hms = all_polymorphics.each_with_object([]) do |p, s|
226
+ if p.first.first == assoc_klass &&
227
+ p.last.none? { |poly_hm| this_klass <= poly_hm.first } # <= to deal with polymorphic inheritance
228
+ s << p.first
229
+ end
230
+ s
231
+ end
232
+
233
+ # Consider all the HMT with through: :contacts, find their source(s)
234
+ poly_hmts = this_klass.reflect_on_all_associations.each_with_object([]) do |a, s|
235
+ if [:has_many, :has_one].include?(a.macro) && a.options[:source] &&
236
+ a.options[:through] == assoc.name
237
+ s << a.options[:source]
238
+ end
239
+ s
240
+ end
241
+ poly_hms_hmts = poly_hms.select { |poly_hm| poly_hmts.include?(poly_hm.last) }
242
+ poly_hms = poly_hms_hmts unless poly_hms_hmts.blank?
243
+
244
+ poly_hms.map! { |poly_hm| "\"as: :#{poly_hm.last}\"" }
245
+ if poly_hms.blank?
246
+ puts " Consider removing this #{belongs_to_or_has_many} because the #{assoc_klass.name} model does not include a column named \"#{suggested_fk}\"."
247
+ else
248
+ puts " Consider adding #{poly_hms.join(' or ')} to establish a valid polymorphic association."
249
+ end
250
+ is_mentioned_consider = true
251
+ end
252
+ unless fks.empty? || fks.include?(assoc.foreign_key.to_s)
253
+ puts " Consider adding #{fks.map { |fk| "\"foreign_key: :#{fk}\"" }.join(' or ')} (or some other appropriate column from #{assoc_klass.name}) to this #{belongs_to_or_has_many} entry."
254
+ is_mentioned_consider = true
255
+ end
256
+ else
257
+ puts " (Cannot find foreign key \"#{inverse_foreign_keys.first.inspect}\" in #{assoc_klass.name}.)"
258
+ end
259
+ end
260
+ if inverses.empty?
261
+ opposite_macro = assoc.belongs_to? ? 'has_many or has_one' : 'belongs_to'
262
+ puts " (Could not identify any inverse #{opposite_macro} association in the #{assoc_klass.name} model.)"
263
+ else
264
+ print is_mentioned_consider ? ' Also consider ' : ' Consider '
265
+ puts "adding \"#{inverses.map { |x| "inverse_of: :#{x.name}" }.join(' or ')}\" to this entry."
266
+ end
267
+ end
268
+ nil
269
+ end
270
+ else
271
+ puts "* Missing table #{assoc_klass.table_name} for class #{assoc_klass.name}"
272
+ puts ' (Maybe try running: bin/rails db:migrate )'
273
+ nil # Related has_* is missing its table
126
274
  end
127
- nil
275
+ if new_assoc.nil?
276
+ errored_assocs << assoc
277
+ else
278
+ assocs[assoc.name] = new_assoc
128
279
  end
129
280
  end
130
- if new_assoc.nil?
131
- errored_assocs << assoc
132
- else
133
- assocs[assoc.name] = new_assoc
281
+ end
282
+
283
+ # Include all columns except for the primary key, any foreign keys, and excluded_columns
284
+ # %%% add EXCLUDED_ALL_COLUMNS || ...
285
+ excluded_columns = %w[created_at updated_at deleted_at]
286
+ (this_klass.columns.map(&:name) - this_primary_key - this_belongs_tos - excluded_columns).each do |column|
287
+ template << column.to_sym
288
+ end
289
+ # Okay, at this point it really searches for the uniques, and in the "strict" (not loose) kind of way
290
+ requireds += _find_requireds(this_klass, false, [this_klass.primary_key]).first.map { |r| "#{path}#{r}".to_sym }
291
+ # Now add the foreign keys and any has_manys in the form of references to associated models
292
+ assocs.each do |k, assoc|
293
+ # # assoc.first describes this foreign key and class, and is used for a "reverse poison"
294
+ # # detection so we don't fold back on ourselves
295
+ next if poison_links.include?(assoc.first)
296
+
297
+ is_has_many = (assoc.first.last == assoc.last)
298
+ if hops.zero?
299
+ # For has_one or has_many, exclude with priority the foreign key column(s) we rode in here on
300
+ priority_excluded_columns = assoc.first.first if is_has_many
301
+ # puts "Excluded: #{priority_excluded_columns.inspect}"
302
+ unique, new_requireds = _suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_")
303
+ template << { k => unique }
304
+ requireds += new_requireds
305
+ else
306
+ new_poison_links =
307
+ if is_has_many
308
+ # binding.pry if assoc.first.last.nil?
309
+ # has_many is simple, just exclude how we got here from the foreign table
310
+ [assoc.first]
311
+ else
312
+ # belongs_to is more involved since there may be multiple foreign keys which point
313
+ # from the foreign table to this primary one, so exclude all these links.
314
+ _find_assocs(:belongs_to, assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
315
+ [[f_assoc.foreign_key.to_s], f_assoc.active_record]
316
+ end
317
+ end
318
+ # puts "New Poison: #{new_poison_links.map{|a| "#{a.first.inspect} - #{a.last.name}"}.join(' / ')}"
319
+ # if (poison_links & new_poison_links).empty?
320
+ # Store the ones to do the next round
321
+ # puts "Test against #{assoc.first.inspect}"
322
+ template << { k => (next_template = []) }
323
+ next_layer << [do_has_many, assoc.last, "#{path}#{k}_", next_template]
324
+ poison_links += (new_poison_links - poison_links)
325
+ # end
326
+ end
134
327
  end
135
328
  end
329
+ break if hops.zero? || next_layer.empty?
330
+
331
+ hops -= 1
332
+ this_layer = next_layer
136
333
  end
334
+ (buggy_bts - bad_polymorphic_hms).each do |bad_bt|
335
+ puts "* #{bad_bt.first.name} model - problem with: \"belongs_to :#{bad_bt.last.name}\" because there is no \"#{bad_bt.last.class_name}\" model."
336
+ end
337
+ [whole_template, requireds]
338
+ end
137
339
 
138
- # Include all columns except for the primary key, any foreign keys, and excluded_columns
139
- # %%% add EXCLUDED_ALL_COLUMNS || ...
140
- excluded_columns = %w[created_at updated_at deleted_at]
141
- template = (this_klass.columns.map(&:name) - this_primary_key - this_belongs_tos - excluded_columns)
142
- template.map!(&:to_sym)
143
- requireds = _find_requireds(this_klass).map { |r| "#{path}#{r}".to_sym }
144
- # Now add the foreign keys and any has_manys in the form of references to associated models
145
- assocs.each do |k, assoc|
146
- # assoc.first describes this foreign key and class, and is used for a "reverse poison"
147
- # detection so we don't fold back on ourselves
148
- next if poison_links.include?(assoc.first)
340
+ # Load all models
341
+ # %%% Note that this works in Rails 5.x, but may not work in Rails 6.0 and later, which uses the Zeitwerk loader by default:
342
+ def self._all_models
343
+ unless ActiveRecord::Base.instance_variable_get(:@eager_loaded_all)
344
+ if ActiveRecord.version < ::Gem::Version.new('4.0')
345
+ Rails.configuration.eager_load_paths
346
+ else
347
+ Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
348
+ end
349
+ ActiveRecord::Base.instance_variable_set(:@eager_loaded_all, true)
350
+ end
351
+ ActiveRecord::Base.descendants
352
+ end
149
353
 
150
- is_has_many = (assoc.first.last == assoc.last)
151
- # puts "#{k} #{hops}"
152
- unique, new_requireds =
153
- if hops.zero?
154
- # For has_one or has_many, exclude with priority the foreign key column(s) we rode in here on
155
- priority_excluded_columns = assoc.first.first if is_has_many
156
- # puts "Excluded: #{priority_excluded_columns.inspect}"
157
- _suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_")
158
- else
159
- new_poison_links =
160
- if is_has_many
161
- # has_many is simple, just exclude how we got here from the foreign table
162
- [assoc.first]
354
+ # Load all tables
355
+ def self._all_tables
356
+ unless (all_tables = ActiveRecord::Base.instance_variable_get(:@_all_tables))
357
+ sql = if ActiveRecord::Base.connection.class.name.end_with?('::SQLite3Adapter')
358
+ "SELECT DISTINCT name AS table_name FROM sqlite_master WHERE type = 'table'"
163
359
  else
164
- # belongs_to is more involved since there may be multiple foreign keys which point
165
- # from the foreign table to this primary one, so exclude all these links.
166
- _find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
167
- [[f_assoc.foreign_key.to_s], f_assoc.active_record]
168
- end
360
+ # For everything else, which would be "::PostgreSQLAdapter", "::MysqlAdapter", or "::Mysql2Adapter":
361
+ "SELECT DISTINCT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_type = 'BASE TABLE'"
169
362
  end
170
- # puts "New Poison: #{new_poison_links.inspect}"
171
- _suggest_template(hops - 1, do_has_many, assoc.last, poison_links + new_poison_links, "#{path}#{k}_")
172
- end
173
- template << { k => unique }
174
- requireds += new_requireds
363
+ # The MySQL version of execute_sql returns arrays instead of a hash when there's just one column asked for.
364
+ all_tables = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |row, s|
365
+ s[row.is_a?(Array) ? row.first : row['table_name']] = nil
366
+ s
367
+ end
368
+ ActiveRecord::Base.instance_variable_set(:@_all_tables, all_tables)
175
369
  end
176
- [template, requireds]
370
+ all_tables
177
371
  end
178
372
 
179
373
  # Find belongs_tos for this model to one more more other klasses
180
- def self._find_belongs_tos(klass, to_klass, errored_assocs)
181
- klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
182
- # .is_a?(ActiveRecord::Reflection::BelongsToReflection)
183
- next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
374
+ def self._find_assocs(macro, klass, to_klass, errored_assocs, using_fk = nil)
375
+ case macro
376
+ when :belongs_to
377
+ klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
378
+ next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
379
+
380
+ begin
381
+ s << bt_assoc if !bt_assoc.options[:polymorphic] && bt_assoc.klass <= to_klass &&
382
+ (using_fk.nil? || bt_assoc.foreign_key == using_fk)
383
+ rescue NameError
384
+ errored_assocs << bt_assoc
385
+ puts "* #{bt_assoc.active_record.name} model - \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}."
386
+ end
387
+ s
388
+ end
389
+ when :has_many # Also :has_one
390
+ klass.reflect_on_all_associations.each_with_object([]) do |hm_assoc, s|
391
+ next if ![:has_many, :has_one].include?(hm_assoc.macro) || errored_assocs.include?(hm_assoc) ||
392
+ (Object.const_defined?('PaperTrail::Version') && hm_assoc.klass <= PaperTrail::Version && hm_assoc.options.include?(:as)) # Skip any PaperTrail associations
184
393
 
185
- begin
186
- s << bt_assoc if !bt_assoc.options[:polymorphic] && bt_assoc.klass == to_klass
187
- rescue NameError
188
- errored_assocs << bt_assoc
189
- puts "* In the #{bt_assoc.active_record.name} model \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}."
394
+ s << hm_assoc if hm_assoc.klass == to_klass && (using_fk.nil? || hm_assoc.foreign_key == using_fk)
395
+ s
190
396
  end
191
397
  end
192
398
  end
@@ -197,36 +403,20 @@ module DutyFree
197
403
  # ...
198
404
  # Not available, so grasping at straws, just search for any available column
199
405
  # %%% add EXCLUDED_UNIQUE_COLUMNS || ...
200
- klass_columns = klass.columns
201
-
202
- # Requireds takes its cues from all attributes having a presence validator
203
- requireds = _find_requireds(klass)
204
- klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns
205
- excluded_columns = %w[created_at updated_at deleted_at]
206
- unique = [(
207
- # Find the first text field of a required if one exists
208
- klass_columns.find { |col| requireds.include?(col.name) && col.type == :string }&.name ||
209
- # Find the first text field, now of a non-required, if one exists
210
- klass_columns.find { |col| col.type == :string }&.name ||
211
- # If no string then look for the first non-PK that is also not a foreign key or created_at or updated_at
212
- klass_columns.find do |col|
213
- requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name)
214
- end&.name ||
215
- # And now the same but not a required, the first non-PK that is also not a foreign key or created_at or updated_at
216
- klass_columns.find do |col|
217
- col.name != klass.primary_key && !excluded_columns.include?(col.name)
218
- end&.name ||
219
- # Finally just accept the PK if nothing else
220
- klass.primary_key
221
- ).to_sym]
222
-
223
- [unique, requireds.map { |r| "#{path}#{r}".to_sym }]
406
+ uniques, requireds = _find_requireds(klass, true, priority_excluded_columns)
407
+ [[uniques.first.to_sym], requireds.map { |r| "#{path}#{r}".to_sym }]
224
408
  end
225
409
 
226
- def self._find_requireds(klass)
410
+ def self._find_requireds(klass, is_loose = false, priority_excluded_columns = nil)
227
411
  errored_columns = ::DutyFree.instance_variable_get(:@errored_columns)
228
- klass.validators.select do |v|
229
- v.is_a?(ActiveRecord::Validations::PresenceValidator)
412
+ # %%% In case we need to exclude foreign keys in the future, this will do it:
413
+ # bts = klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
414
+ # next unless bt_assoc.belongs_to?
415
+
416
+ # s << bt_assoc.name
417
+ # end
418
+ requireds = klass.validators.select do |v|
419
+ v.is_a?(ActiveRecord::Validations::PresenceValidator) # && (v.attributes & bts).empty?
230
420
  end.each_with_object([]) do |v, s|
231
421
  v.attributes.each do |a|
232
422
  attrib = a.to_s
@@ -236,17 +426,39 @@ module DutyFree
236
426
  if klass.columns.map(&:name).include?(attrib)
237
427
  s << attrib
238
428
  else
239
- hm_and_bt_names = klass.reflect_on_all_associations.each_with_object([]) do |assoc, names|
240
- names << assoc.name.to_s if [:belongs_to, :has_many, :has_one].include?(assoc.macro)
241
- names
242
- end
243
- unless hm_and_bt_names.include?(attrib)
244
- puts "* In the #{klass.name} model \"validates_presence_of :#{attrib}\" should be removed as it does not refer to any existing column."
429
+ unless klass.instance_methods.map(&:to_s).include?(attrib)
430
+ puts "* #{klass.name} model - \"validates_presence_of :#{attrib}\" should be removed as it does not refer to any existing column or relation."
245
431
  errored_columns << klass_col
246
432
  end
247
433
  end
248
434
  end
249
435
  end
436
+ klass_columns = klass.columns
437
+
438
+ # Take our cues from all attributes having a presence validator
439
+ klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns
440
+ excluded_columns = %w[created_at updated_at deleted_at]
441
+
442
+ # First find any text fields that are required
443
+ uniques = klass_columns.select { |col| requireds.include?(col.name) && [:string, :text].include?(col.type) }
444
+ # If not that then find any text field, even those not required
445
+ uniques = klass_columns.select { |col| [:string, :text].include?(col.type) } if is_loose && uniques.empty?
446
+ # If still not then look for any required non-PK that is also not a foreign key or created_at or updated_at
447
+ if uniques.empty?
448
+ uniques = klass_columns.select do |col|
449
+ requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name)
450
+ end
451
+ end
452
+ # If still nothing then the same but not a required, any non-PK that is also not a foreign key or created_at or updated_at
453
+ if is_loose && uniques.empty?
454
+ uniques = klass_columns.select do |col|
455
+ col.name != klass.primary_key && !excluded_columns.include?(col.name)
456
+ end
457
+ end
458
+ uniques.map!(&:name)
459
+ # Finally if nothing else then just accept the PK, if there is one
460
+ uniques = [klass.primary_key] if klass.primary_key && uniques.empty? && (!priority_excluded_columns || priority_excluded_columns.exclude?(klass.primary_key))
461
+ [uniques, requireds]
250
462
  end
251
463
 
252
464
  # Show a "pretty" version of IMPORT_TEMPLATE, to be placed in a model