diff --git a/lib/tasks/extract_fixtures.rake b/lib/tasks/extract_fixtures.rake index 787eecaa949..fcd729b409b 100644 --- a/lib/tasks/extract_fixtures.rake +++ b/lib/tasks/extract_fixtures.rake @@ -18,20 +18,73 @@ desc 'Create YAML test fixtures from data in an existing database. Defaults to development database. Set RAILS_ENV to override.' +module Psych + module Visitors + class YAMLTree + # Override default time format + # https://github.com/ruby/ruby/blob/v3_3_6/ext/psych/lib/psych/visitors/yaml_tree.rb#L484-L490 + def format_time time, utc = time.utc? + if utc + time.strftime("%Y-%m-%d %H:%M:%S") + else + time.strftime("%Y-%m-%d %H:%M:%S %:z") + end + end + end + end +end + task :extract_fixtures => :environment do - sql = "SELECT * FROM %s" - skip_tables = ["schema_info"] + dir = ENV['DIR'] || './tmp/fixtures' + time_offset = ENV['TIME_OFFSET'] || '' + tables = ENV['TABLES']&.split(',') || [] + skip_tables = ENV['SKIP_TABLES']&.split(',') || [] + table_filters = ENV['TABLE_FILTERS']&.split(';')&.map {|tf| tf.split(":", 2)}&.to_h || {} + omit_default_or_nil = ActiveRecord::Type::Boolean.new.cast(ENV.fetch('OMIT_DEFAULT_OR_NIL', 'false')) + + FileUtils.mkdir_p(dir) + if time_offset.present? && !time_offset.match?(/^([+-](0[0-9]|1[0-4]):[0-5][0-9])$/) + abort("Invalid TIME_OFFSET format. Use +HH:MM or -HH:MM (e.g. +09:00)") + end + skip_tables += ["schema_migrations", "ar_internal_metadata"] + ActiveRecord::Base.establish_connection - (ActiveRecord::Base.connection.tables - skip_tables).each do |table_name| + tables = tables.present? ? tables : ActiveRecord::Base.connection.tables + (tables - skip_tables).each do |table_name| + columns = ActiveRecord::Base.connection.columns(table_name) + column_names = columns.map(&:name) + has_id_column = column_names.include?('id') + order_columns = has_id_column ? 'id' : column_names.join(', ') + where_clause = table_filters.has_key?(table_name) ? "WHERE #{table_filters[table_name]}" : '' + sql = "SELECT * FROM #{table_name} #{where_clause} ORDER BY #{order_columns}" + data = ActiveRecord::Base.connection.select_all(sql) + if data.empty? + next + end i = "000" - File.open("#{Rails.root}/#{table_name}.yml", 'w' ) do |file| - data = ActiveRecord::Base.connection.select_all(sql % table_name) + File.open(File.join(dir, "#{table_name}.yml"), 'w') do |file| file.write data.inject({}) { |hash, record| - # cast extracted values - ActiveRecord::Base.connection.columns(table_name).each { |col| - record[col.name] = col.type_cast(record[col.name]) if record[col.name] - } - hash["#{table_name}_#{i.succ!}"] = record + # omit default or nil values or cast extracted values with formatting time + columns.each do |col| + if omit_default_or_nil && ( + (!col.default.nil? && !record[col.name].nil? && record[col.name].to_s == col.default) || + (col.default.nil? && record[col.name].nil?) + ) + record.delete(col.name) + next + elsif record[col.name] + record[col.name] = ActiveRecord::Type.lookup(col.type).deserialize(record[col.name]) + if col.type == :datetime && record[col.name].is_a?(Time) + if time_offset.present? + record[col.name] = record[col.name].localtime(time_offset) + else + record[col.name] = record[col.name].getutc + end + end + end + end + key_id = has_id_column ? sprintf('%03d', record['id']) : i.succ! + hash["#{table_name}_#{key_id}"] = record hash }.to_yaml end