Files
openfoodnetwork/spec/models/database_spec.rb
Maikel Linke 301add4992 Remove faulty migration generation
The long timestamps don't play well with Rails default timestamps for
migrations. They are always seen as the newest. Not using long
timestamps leads to duplicate timestamps though.

The better solution is to use `rails generate migration` and copy the
`change` method.
2024-01-15 14:36:23 +11:00

147 lines
5.2 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Database" do
let(:models_todo) {
["Spree::CreditCard", "Spree::Adjustment",
"StripeAccount", "ColumnPreference",
"Spree::LineItem", "Spree::ShippingMethod",
"Spree::ShippingRate"].freeze
}
it "should have foreign keys for models with a belongs_to relationship" do
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 do |file|
relative_path = Pathname.new(file).relative_path_from(Rails.root.join('app/models')).to_s
subdirectory = File.dirname(relative_path)
base_name = File.basename(file, '.rb').camelize
subdirectory == "." ? base_name : "#{subdirectory.camelize}::#{base_name}"
end
end
def generate_migrations(model_classes)
migrations = []
filter = lambda { |model| models_todo.include?(model) }
pending_models = model_classes.select(&filter)
model_classes.reject!(&filter)
ActiveRecord::Base.descendants.each do |model_class|
next unless model_classes.include?(model_class.name)
model_class.reflect_on_all_associations(:belongs_to).each do |association|
migration = process_association(model_class, association)
migrations << migration unless migration.nil?
end
end
print_missing_foreign_key_warnings(migrations)
puts "The following models are marked as todo in #{__FILE__}:"
puts pending_models.join(", ")
migrations
end
def print_missing_foreign_key_warnings(migrations)
return if migrations.empty?
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 "\nTo disable this warning, add the class name(s) of the model(s) to models_todo " \
"in #{__FILE__}"
end
def process_association(model_class, association)
return if association.options[:polymorphic] || association.options[:optional]
foreign_key_table_name = determine_foreign_key_table_name(model_class, association)
foreign_key_column = association.options[:foreign_key] || "#{association.name}_id"
foreign_keys = model_class.connection.foreign_keys(model_class.table_name)
# Check if there is a foreign key that already exists for the column
return if foreign_keys.any? { |fk|
fk.column == foreign_key_column &&
fk.to_table == foreign_key_table_name
}
generate_migration(model_class, 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, foreign_key_table_name, foreign_key_column)
migration_name = "add_foreign_key_to_#{model_class.table_name}_" \
"#{foreign_key_table_name}_#{foreign_key_column}"
migration_class_name = migration_name.camelize
orphaned_records_query = generate_orphaned_records_query(model_class, foreign_key_table_name,
foreign_key_column)
migration = <<~MIGRATION
# Orphaned records can be found before running this migration with the following SQL:
#{orphaned_records_query}
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
migration
end
def generate_orphaned_records_query(model_class, foreign_key_table_name, foreign_key_column)
<<~SQL # rubocop:disable Rails/SquishedSQLHeredocs # Using squish deletes the newlines
# 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
# AND #{model_class.table_name}.#{foreign_key_column} IS NOT NULL
SQL
end
# Generates a unique timestamp.
#
# We may create multiple migrations within the same second, maybe even millisecond.
# So we add precision to milliseconds and increment on conflict.
def generate_timestamp
@last_creation_time ||= Time.new.utc(0)
creation_time = Time.now.utc
if creation_time <= @last_creation_time
creation_time += 0.001.seconds
end
@last_creation_time = creation_time
creation_time.utc.strftime('%Y%m%d%H%M%S%L')
end
end