mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Convert rake task to spec
This commit is contained in:
@@ -1,141 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :ofn do
|
||||
namespace :data do
|
||||
desc 'Generate Migrations for Missing Foreign Keys'
|
||||
task generate_foreign_key_migrations: :environment do
|
||||
Rails.application.eager_load!
|
||||
model_classes = filter_model_classes
|
||||
|
||||
orphaned_records = generate_migrations_and_counts(model_classes)
|
||||
orphaned_records_queries, orphaned_records_counts = orphaned_records
|
||||
|
||||
write_queries_to_file(orphaned_records_queries)
|
||||
|
||||
summarize_counts(orphaned_records_queries, orphaned_records_counts)
|
||||
end
|
||||
|
||||
def filter_model_classes
|
||||
Dir.glob(Rails.root.join('app/models/**/*.rb').to_s)
|
||||
.map { |file| File.basename(file, '.rb').camelize }
|
||||
.reject { |model| ["Gateway", "PayPalExpress", "Bogus", "BogusSimple"].include?(model) }
|
||||
end
|
||||
|
||||
def generate_migrations_and_counts(model_classes)
|
||||
orphaned_records_queries = []
|
||||
orphaned_records_counts = []
|
||||
previous_models = {}
|
||||
|
||||
ActiveRecord::Base.descendants.each do |model_class|
|
||||
next unless model_classes.include?(model_class.name.demodulize)
|
||||
|
||||
model_class.reflect_on_all_associations(:belongs_to).each do |association|
|
||||
orphans = process_association(model_class, association, previous_models)
|
||||
if orphans
|
||||
orphaned_records_queries << orphans[0]
|
||||
orphaned_records_counts << orphans[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[orphaned_records_queries, orphaned_records_counts]
|
||||
end
|
||||
|
||||
def process_association(model_class, association, previous_models)
|
||||
return if association.options[:polymorphic]
|
||||
|
||||
foreign_key_table_name = determine_foreign_key_table_name(model_class, association)
|
||||
foreign_key_column = "#{association.options[:foreign_key] || association.name}_id"
|
||||
|
||||
# Filter out duplicate migrations
|
||||
return if duplicate_migration?(model_class, foreign_key_table_name, previous_models)
|
||||
|
||||
previous_models[model_class.table_name] ||= []
|
||||
previous_models[model_class.table_name] << foreign_key_table_name
|
||||
|
||||
generate_migration(model_class, association, foreign_key_table_name, foreign_key_column)
|
||||
|
||||
query = generate_orphaned_records_query(model_class, foreign_key_table_name,
|
||||
foreign_key_column)
|
||||
|
||||
count = execute_count_query(query)
|
||||
|
||||
[query, count]
|
||||
end
|
||||
|
||||
def determine_foreign_key_table_name(model_class, association)
|
||||
if association.options[:class_name]
|
||||
class_name = association.options[:class_name].underscore.parameterize
|
||||
foreign_key_table_name = class_name.tableize
|
||||
else
|
||||
foreign_key_table_name = association.class_name.underscore.parameterize.tableize
|
||||
namespace = model_class.name.deconstantize
|
||||
|
||||
unless association.class_name.deconstantize == namespace || namespace == "" ||
|
||||
ActiveRecord::Base.connection.table_exists?(foreign_key_table_name)
|
||||
foreign_key_table_name = "#{namespace.underscore}_#{foreign_key_table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
foreign_key_table_name
|
||||
end
|
||||
|
||||
def generate_migration(model_class, association, foreign_key_table_name, foreign_key_column)
|
||||
migration_name = "add_foreign_key_to_#{model_class.table_name}_#{foreign_key_table_name}"
|
||||
migration_class_name = migration_name.camelize
|
||||
migration_file_name = "db/migrate/#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_" \
|
||||
"#{migration_name}.rb"
|
||||
|
||||
File.open(migration_file_name, 'w') do |file|
|
||||
file.puts <<~MIGRATION
|
||||
class #{migration_class_name} < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_foreign_key :#{model_class.table_name}, :#{foreign_key_table_name}, column: :#{foreign_key_column}
|
||||
end
|
||||
end
|
||||
MIGRATION
|
||||
end
|
||||
|
||||
puts "Migration generated: #{migration_file_name}"
|
||||
end
|
||||
|
||||
def generate_orphaned_records_query(model_class, foreign_key_table_name, foreign_key_column)
|
||||
<<~SQL.squish
|
||||
SELECT COUNT(*) FROM #{model_class.table_name}
|
||||
LEFT JOIN #{foreign_key_table_name} ON #{model_class.table_name}.#{foreign_key_column} = #{foreign_key_table_name}.id
|
||||
WHERE #{foreign_key_table_name}.id IS NULL
|
||||
SQL
|
||||
end
|
||||
|
||||
def execute_count_query(query)
|
||||
result = ActiveRecord::Base.connection.select_all(query)
|
||||
result[0]["count"].to_i if result.present?
|
||||
end
|
||||
|
||||
def write_queries_to_file(queries)
|
||||
File.open("lib/tasks/data/orphaned_records_queries.sql", 'w') do |file|
|
||||
file.puts queries.join("\n\n")
|
||||
end
|
||||
end
|
||||
|
||||
def summarize_counts(queries, counts)
|
||||
puts "\nSummary:"
|
||||
if counts.any?(&:positive?)
|
||||
puts "Some orphaned records were found in the following queries:"
|
||||
queries.each_with_index do |query, index|
|
||||
if counts[index] > 0
|
||||
puts "Query #{index + 1}: #{query}"
|
||||
puts "Count: #{counts[index]}"
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "No orphaned records were found. Migrations are safe to proceed."
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate_migration?(model_class, foreign_key_table_name, previous_models)
|
||||
model_class.connection.foreign_key_exists?(model_class.table_name, foreign_key_table_name) ||
|
||||
previous_models[model_class.table_name]&.include?(foreign_key_table_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
97
spec/models/database_spec.rb
Normal file
97
spec/models/database_spec.rb
Normal file
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe "Database" do
|
||||
excluded_models = ["Gateway", "PayPalExpress", "Bogus", "BogusSimple"].freeze
|
||||
|
||||
it "should have foreign keys for models with a belongs_to relationship" do
|
||||
pending "Consider adding foreign keys"
|
||||
Rails.application.eager_load!
|
||||
model_classes = filter_model_classes
|
||||
|
||||
migrations = generate_migrations(model_classes)
|
||||
|
||||
expect(migrations.length).to eq(0)
|
||||
end
|
||||
|
||||
def filter_model_classes
|
||||
Dir.glob(Rails.root.join('app/models/**/*.rb').to_s)
|
||||
.map { |file| File.basename(file, '.rb').camelize }
|
||||
.reject { |model| excluded_models.include?(model) }
|
||||
end
|
||||
|
||||
def generate_migrations(model_classes)
|
||||
migrations = []
|
||||
previous_models = {}
|
||||
|
||||
ActiveRecord::Base.descendants.each do |model_class|
|
||||
next unless model_classes.include?(model_class.name.demodulize)
|
||||
|
||||
model_class.reflect_on_all_associations(:belongs_to).each do |association|
|
||||
migration = process_association(model_class, association, previous_models)
|
||||
migrations << migration unless migration.nil?
|
||||
end
|
||||
end
|
||||
|
||||
if migrations
|
||||
puts "Foreign key(s) appear to be absent from the database. " \
|
||||
"You can add it/them using the following migration(s):"
|
||||
puts migrations.join("\n")
|
||||
puts "To disable this warning, add the class name(s) of the model(s) to EXCLUDED_MODELS " \
|
||||
"in /spec/models/missing_foreign_keys_spec.rb"
|
||||
end
|
||||
|
||||
migrations
|
||||
end
|
||||
|
||||
def process_association(model_class, association, previous_models)
|
||||
return if association.options[:polymorphic]
|
||||
|
||||
foreign_key_table_name = determine_foreign_key_table_name(model_class, association)
|
||||
foreign_key_column = "#{association.options[:foreign_key] || association.name}_id"
|
||||
|
||||
# Filter out duplicate migrations
|
||||
return if duplicate_migration?(model_class, foreign_key_table_name, previous_models)
|
||||
|
||||
previous_models[model_class.table_name] ||= []
|
||||
previous_models[model_class.table_name] << foreign_key_table_name
|
||||
|
||||
generate_migration(model_class, association, foreign_key_table_name, foreign_key_column)
|
||||
end
|
||||
|
||||
def determine_foreign_key_table_name(model_class, association)
|
||||
if association.options[:class_name]
|
||||
class_name = association.options[:class_name].underscore.parameterize
|
||||
foreign_key_table_name = class_name.tableize
|
||||
else
|
||||
foreign_key_table_name = association.class_name.underscore.parameterize.tableize
|
||||
namespace = model_class.name.deconstantize
|
||||
|
||||
unless association.class_name.deconstantize == namespace || namespace == "" ||
|
||||
ActiveRecord::Base.connection.table_exists?(foreign_key_table_name)
|
||||
foreign_key_table_name = "#{namespace.underscore}_#{foreign_key_table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
foreign_key_table_name
|
||||
end
|
||||
|
||||
def generate_migration(model_class, _association, foreign_key_table_name, foreign_key_column)
|
||||
migration_name = "add_foreign_key_to_#{model_class.table_name}_#{foreign_key_table_name}"
|
||||
migration_class_name = migration_name.camelize
|
||||
|
||||
<<~MIGRATION
|
||||
class #{migration_class_name} < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_foreign_key :#{model_class.table_name}, :#{foreign_key_table_name}, column: :#{foreign_key_column}
|
||||
end
|
||||
end
|
||||
MIGRATION
|
||||
end
|
||||
|
||||
def duplicate_migration?(model_class, foreign_key_table_name, previous_models)
|
||||
model_class.connection.foreign_key_exists?(model_class.table_name, foreign_key_table_name) ||
|
||||
previous_models[model_class.table_name]&.include?(foreign_key_table_name)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user