diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a664334 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,79 @@ +name: Tests + +on: + push: + branches: + - '**' + tags-ignore: + - 'v*' + pull_request: + +jobs: + test: + name: Ruby ${{ matrix.ruby }}, ActiveRecord ${{ matrix.activerecord }}, ${{ matrix.database }} + continue-on-error: ${{ matrix.ruby == 'head' }} + strategy: + fail-fast: false + matrix: + include: + - ruby: "head" + activerecord: "head" + database: sqlite + - ruby: "3.3" + activerecord: "7.1" + database: postgresql + - ruby: "3.3" + activerecord: "7.1" + database: mysql + - ruby: "3.3" + activerecord: "7.1" + database: sqlite + - ruby: "3.2" + activerecord: "7.0" + database: sqlite + - ruby: "3.1" + activerecord: "6.1" + database: sqlite + - ruby: "3.0" + activerecord: "6.0" + database: sqlite + + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + mysql: + image: mysql:8.4 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: evil_seed_test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + env: + CI: true + ACTIVERECORD_VERSION: "${{ matrix.activerecord }}" + DB: "${{ matrix.database }}" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rake diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dc76acb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,82 +0,0 @@ -dist: xenial -language: ruby -cache: bundler -sudo: false - -addons: - postgresql: "10" - apt: - sources: - - travis-ci/sqlite3 - packages: - - sqlite3 - -services: - - mysql - - postgresql - -before_install: gem install bundler -v '~> 1.17' - -matrix: - include: - - rvm: 3.0.2 - gemfile: gemfiles/activerecord_master.gemfile - env: "DB=sqlite" - - rvm: 3.0.2 - gemfile: gemfiles/activerecord_master.gemfile - env: "DB=postgresql" - - rvm: 3.0.2 - gemfile: gemfiles/activerecord_master.gemfile - env: "DB=mysql" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_1.gemfile - env: "DB=sqlite" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_1.gemfile - env: "DB=postgresql" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_1.gemfile - env: "DB=mysql" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_0.gemfile - env: "DB=sqlite" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_0.gemfile - env: "DB=postgresql" - - rvm: 2.7.4 - gemfile: gemfiles/activerecord_6_0.gemfile - env: "DB=mysql" - - rvm: 2.6.8 - gemfile: gemfiles/activerecord_5_2.gemfile - env: "DB=sqlite" - - rvm: 2.6.8 - gemfile: gemfiles/activerecord_5_2.gemfile - env: "DB=postgresql" - - rvm: 2.6.8 - gemfile: gemfiles/activerecord_5_2.gemfile - env: "DB=mysql" - - rvm: 2.5.9 - gemfile: gemfiles/activerecord_5_1.gemfile - env: "DB=sqlite" - - rvm: 2.5.9 - gemfile: gemfiles/activerecord_5_1.gemfile - env: "DB=postgresql" - - rvm: 2.5.9 - gemfile: gemfiles/activerecord_5_1.gemfile - env: "DB=mysql" - - rvm: 2.4.10 - gemfile: gemfiles/activerecord_5_0.gemfile - env: "DB=sqlite" - - rvm: 2.4.10 - gemfile: gemfiles/activerecord_5_0.gemfile - env: "DB=postgresql" - - rvm: 2.4.10 - gemfile: gemfiles/activerecord_5_0.gemfile - env: "DB=mysql" - - rvm: 2.3.8 - gemfile: gemfiles/activerecord_4_2.gemfile - env: "DB=postgresql" - - rvm: 2.3.8 - gemfile: gemfiles/activerecord_4_2.gemfile - env: "DB=sqlite" - # Please note that gem can't be tested against MySQL on ActiveRecord 4.2 (Dump and restore test doesn't work)! diff --git a/Appraisals b/Appraisals deleted file mode 100644 index 4043724..0000000 --- a/Appraisals +++ /dev/null @@ -1,48 +0,0 @@ -appraise 'activerecord-5-0' do - gem 'activerecord', '~> 5.0.0' - gem "pg", ">= 0.20", "< 2.0" - gem 'mysql2', '~> 0.4.4' - gem 'sqlite3', '~> 1.3.6' -end - -appraise 'activerecord-5-1' do - gem 'activerecord', '~> 5.1.0' - gem "pg", ">= 0.20", "< 2.0" - gem 'mysql2', '~> 0.4.4' - gem 'sqlite3', '~> 1.3' -end - -appraise 'activerecord-5-2' do - gem 'activerecord', '~> 5.2.0' - gem "pg", ">= 0.20", "< 2.0" - gem 'mysql2', '~> 0.4.4' - gem 'sqlite3', '~> 1.3' -end - -appraise 'activerecord-6-0' do - gem 'activerecord', '~> 6.0.0' - gem "pg", ">= 0.20", "< 2.0" - gem 'mysql2', '~> 0.4.4' - gem 'sqlite3', '~> 1.4' -end - -appraise 'activerecord-6-0' do - gem 'activerecord', '~> 6.0.0' - gem "pg", ">= 0.20", "< 2.0" - gem 'mysql2', '~> 0.4.4' - gem 'sqlite3', '~> 1.4' -end - -appraise 'activerecord-6-1' do - gem 'activerecord', '~> 6.1.0' - gem "pg", "~> 1.1" - gem 'mysql2', '~> 0.5' - gem 'sqlite3', '~> 1.4' -end - -appraise 'activerecord-master' do - gem 'activerecord', git: 'https://github.com/rails/rails.git' - gem "pg", "~> 1.1" - gem 'mysql2', '~> 0.5' - gem 'sqlite3', '~> 1.4' -end diff --git a/Gemfile b/Gemfile index 4892193..7ffd3a3 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,16 @@ source 'https://rubygems.org' # Specify your gem's dependencies in evil-seed.gemspec gemspec + +activerecord_version = ENV.fetch("ACTIVERECORD_VERSION", "~> 7.1") +case activerecord_version.upcase +when "HEAD" + git "https://github.com/rails/rails.git" do + gem "activerecord" + gem "rails" + end +else + activerecord_version = "~> #{activerecord_version}.0" if activerecord_version.match?(/^\d+\.\d+$/) + gem "activerecord", activerecord_version + gem "sqlite3", "~> 1.4" +end diff --git a/README.md b/README.md index cf770e3..b3f0158 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,19 @@ EvilSeed.configure do |config| # Anonymization is a handy DSL for transformations allowing you to transform model attributes in declarative fashion # Please note that model setters will NOT be called: results of the blocks will be assigned to - config.anonymize("User") + config.anonymize("User") do name { Faker::Name.name } email { Faker::Internet.email } login { |login| "#{login}-test" } end + + # You can ignore columns for any model. This is specially useful when working + # with encrypted columns. + # + # This will remove the columns even if the model is not a root node and is + # dumped via an association. + config.ignore_columns("Profile", :name) +end ``` ### Creating dump diff --git a/evil-seed.gemspec b/evil-seed.gemspec index 7b2f23b..6f290f2 100644 --- a/evil-seed.gemspec +++ b/evil-seed.gemspec @@ -37,5 +37,4 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rubocop' spec.add_development_dependency 'bundler' spec.add_development_dependency 'pry' - spec.add_development_dependency 'appraisal' end diff --git a/gemfiles/.bundle/config b/gemfiles/.bundle/config deleted file mode 100644 index c127f80..0000000 --- a/gemfiles/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_RETRY: "1" diff --git a/gemfiles/activerecord_5_0.gemfile b/gemfiles/activerecord_5_0.gemfile deleted file mode 100644 index 179fca4..0000000 --- a/gemfiles/activerecord_5_0.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 5.0.0" -gem "pg", ">= 0.20", "< 2.0" -gem "mysql2", "~> 0.4.4" -gem "sqlite3", "~> 1.3.6" - -gemspec path: "../" diff --git a/gemfiles/activerecord_5_1.gemfile b/gemfiles/activerecord_5_1.gemfile deleted file mode 100644 index 6c9182b..0000000 --- a/gemfiles/activerecord_5_1.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 5.1.0" -gem "pg", ">= 0.20", "< 2.0" -gem "mysql2", "~> 0.4.4" -gem "sqlite3", "~> 1.3" - -gemspec path: "../" diff --git a/gemfiles/activerecord_5_2.gemfile b/gemfiles/activerecord_5_2.gemfile deleted file mode 100644 index e28520e..0000000 --- a/gemfiles/activerecord_5_2.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 5.2.0" -gem "pg", ">= 0.20", "< 2.0" -gem "mysql2", "~> 0.4.4" -gem "sqlite3", "~> 1.3" - -gemspec path: "../" diff --git a/gemfiles/activerecord_6_0.gemfile b/gemfiles/activerecord_6_0.gemfile deleted file mode 100644 index 8020940..0000000 --- a/gemfiles/activerecord_6_0.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 6.0.0" -gem "pg", ">= 0.20", "< 2.0" -gem "mysql2", "~> 0.4.4" -gem "sqlite3", "~> 1.4" - -gemspec path: "../" diff --git a/gemfiles/activerecord_6_1.gemfile b/gemfiles/activerecord_6_1.gemfile deleted file mode 100644 index 64594c2..0000000 --- a/gemfiles/activerecord_6_1.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 6.1.0" -gem "pg", "~> 1.1" -gem "mysql2", "~> 0.5" -gem "sqlite3", "~> 1.4" - -gemspec path: "../" diff --git a/gemfiles/activerecord_master.gemfile b/gemfiles/activerecord_master.gemfile deleted file mode 100644 index c1099ab..0000000 --- a/gemfiles/activerecord_master.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", git: "https://github.com/rails/rails.git" -gem "pg", "~> 1.1" -gem "mysql2", "~> 0.5" -gem "sqlite3", "~> 1.4" - -gemspec path: "../" diff --git a/lib/evil_seed/configuration.rb b/lib/evil_seed/configuration.rb index 4dc73d8..df032ca 100644 --- a/lib/evil_seed/configuration.rb +++ b/lib/evil_seed/configuration.rb @@ -7,7 +7,7 @@ module EvilSeed # This module holds configuration for creating dump: which models and their constraints class Configuration - attr_accessor :record_dumper_class, :verbose, :verbose_sql, :unscoped, :dont_nullify, :skip_columns + attr_accessor :record_dumper_class, :verbose, :verbose_sql, :unscoped, :dont_nullify def initialize @record_dumper_class = RecordDumper @@ -15,7 +15,7 @@ def initialize @verbose_sql = false @unscoped = true @dont_nullify = false - @skip_columns = {} + @ignored_columns = Hash.new { |h, k| h[k] = [] } end def roots @@ -38,14 +38,18 @@ def anonymize(model_class, &block) customizers[model_class.to_s] << Anonymizer.new(model_class, &block) end + def ignore_columns(model_class, *columns) + @ignored_columns[model_class] += columns + end + # Customizer objects for every model # @return [Hash{String => Array<#call>}] def customizers @customizers ||= Hash.new { |h, k| h[k] = [] } end - def add_skip_columns(table_name, column_names) - @skip_columns[table_name] = column_names + def ignored_columns_for(model_class) + @ignored_columns[model_class] end end end diff --git a/lib/evil_seed/record_dumper.rb b/lib/evil_seed/record_dumper.rb index a443b35..4e8e266 100644 --- a/lib/evil_seed/record_dumper.rb +++ b/lib/evil_seed/record_dumper.rb @@ -50,21 +50,32 @@ def transform_and_anonymize(attributes) end end + def insertable_column_names + model_class.columns_hash.reject do |k,v| + v.respond_to?(:virtual?) ? v.virtual? : false + end.keys + end + def insert_statement connection = model_class.connection table_name = connection.quote_table_name(model_class.table_name) - columns = model_class.column_names.select { |c| !configuration.skip_columns[model_class.table_name]&.include?(c) } - columns = columns.map { |c| connection.quote_column_name(c) } - "INSERT INTO #{table_name} (#{columns.join(', ')}) VALUES\n" + columns = insertable_column_names.map { |c| connection.quote_column_name(c) }.join(', ') + "INSERT INTO #{table_name} (#{columns}) VALUES\n" end def write!(attributes) - puts("-- #{relation_dumper.association_path}\n") if configuration.verbose_sql + # Remove non-insertable columns from attributes + attributes = prepare(attributes.slice(*insertable_column_names)) + + if configuration.verbose_sql + puts("-- #{relation_dumper.association_path}\n") + puts(@tuples_written.zero? ? insert_statement : ",\n") + puts(" (#{attributes.join(', ')})") + end + @output.write("-- #{relation_dumper.association_path}\n") && @header_written = true unless @header_written - puts(@tuples_written.zero? ? insert_statement : ",\n") if configuration.verbose_sql @output.write(@tuples_written.zero? ? insert_statement : ",\n") - puts(" (#{prepare(attributes).join(', ')})") if configuration.verbose_sql - @output.write(" (#{prepare(attributes).join(', ')})") + @output.write(" (#{attributes.join(', ')})") @tuples_written += 1 @output.write(";\n") && @tuples_written = 0 if @tuples_written == MAX_TUPLES_PER_INSERT_STMT end @@ -77,7 +88,6 @@ def finalize! def prepare(attributes) attributes.map do |key, value| - next if configuration.skip_columns[model_class.table_name]&.include?(key) type = model_class.attribute_types[key] model_class.connection.quote(type.serialize(value)) end.flatten.compact diff --git a/lib/evil_seed/relation_dumper.rb b/lib/evil_seed/relation_dumper.rb index 0ed0102..07443c4 100644 --- a/lib/evil_seed/relation_dumper.rb +++ b/lib/evil_seed/relation_dumper.rb @@ -70,6 +70,9 @@ def call private def dump! + original_ignored_columns = model_class.ignored_columns + model_class.ignored_columns += Array(configuration.ignored_columns_for(model_class.sti_name)) + model_class.send(:reload_schema_from_cache) if ActiveRecord.version < Gem::Version.new("6.1.0.rc1") # See https://github.com/rails/rails/pull/37581 if identifiers.present? puts(" # #{search_key} => #{identifiers}") if verbose # Don't use AR::Base#find_each as we will get error on Oracle if we will have more than 1000 ids in IN statement @@ -92,6 +95,8 @@ def dump! end end end + ensure + model_class.ignored_columns = original_ignored_columns end def dump_record!(attributes) diff --git a/lib/evil_seed/version.rb b/lib/evil_seed/version.rb index f99d020..8bd1ad6 100644 --- a/lib/evil_seed/version.rb +++ b/lib/evil_seed/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module EvilSeed - VERSION = '0.3.0' + VERSION = '0.5.0' end diff --git a/test/db/database.yml b/test/db/database.yml index 6dc32f5..f30ac65 100644 --- a/test/db/database.yml +++ b/test/db/database.yml @@ -12,11 +12,18 @@ sqlite: postgresql: <<: *common adapter: postgresql - username: <%= ENV['ES_PG_USER'] || 'postgres' %> + <% if ENV.fetch("GITHUB_ACTIONS", false) %> + host: localhost + username: <%= ENV.fetch("POSTGRES_USER") %> + password: <%= ENV.fetch("POSTGRES_PASSWORD") %> + <% end %> mysql: <<: *common adapter: mysql2 - username: <%= ENV['ES_PG_USER'] || 'root' %> + username: <%= ENV['MYSQL_USER'] || 'root' %> + <% if ENV.fetch("GITHUB_ACTIONS", false) %> + host: 127.0.0.1 + <% end %> flags: - MULTI_STATEMENTS diff --git a/test/db/schema.rb b/test/db/schema.rb index 40face8..30c20de 100644 --- a/test/db/schema.rb +++ b/test/db/schema.rb @@ -55,7 +55,7 @@ def create_schema! create_table :roles do |t| t.string :name - t.string :permissions, array: true + t.string :permissions, **(ENV["DB"] == "postgresql" ? { array: true } : {}) end create_table :user_roles, id: false do |t| diff --git a/test/db/seeds.rb b/test/db/seeds.rb index a0e280f..2a2f77c 100644 --- a/test/db/seeds.rb +++ b/test/db/seeds.rb @@ -12,7 +12,7 @@ ], ) -User.create!( +users = User.create!( [ { login: 'johndoe', email: 'johndoe@example.com', password: 'realhash', forum: forums[0], roles: [roles.second] }, { login: 'janedoe', email: 'janedoe@example.net', password: 'realhash', forum: forums[1], roles: [roles.first] }, @@ -47,3 +47,9 @@ question_attrs = %w[first second third fourth fifth].map { |name| { name: name, forum: forums.first } } Question.create!(question_attrs) + +Profile.create!([ + { user: users[0], name: "Profile for user 0", title: "Title for user 0" }, + { user: users[1], name: "Profile for user 1", title: "Title for user 1" }, + { user: users[2], name: "Profile for user 2", title: "Title for user 2" }, +]) diff --git a/test/evil_seed_test.rb b/test/evil_seed_test.rb index 0c36eb4..3db523e 100644 --- a/test/evil_seed_test.rb +++ b/test/evil_seed_test.rb @@ -17,6 +17,8 @@ def setup config.root('Role') do |root| root.exclude(/\Arole\.(?!roles_users\z)/) # Take only join table and nothing more end + + config.ignore_columns("Profile", :name) end end @@ -59,6 +61,7 @@ def test_it_dumps_and_restores assert Role.find_by(name: 'Superadmin') assert Question.find_by(name: 'fourth') assert Question.find_by(name: 'fifth') + assert Profile.where.not(name: nil).none? end end end diff --git a/test/support/database.rb b/test/support/database.rb index ab5ac28..41e90b5 100644 --- a/test/support/database.rb +++ b/test/support/database.rb @@ -13,7 +13,11 @@ def database ActiveRecord::Migration.verbose = false database_yml_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'db', 'database.yml')) -ActiveRecord::Base.configurations = YAML.safe_load(ERB.new(File.read(database_yml_path)).result, [], [], true) +if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') + ActiveRecord::Base.configurations = YAML.safe_load(ERB.new(File.read(database_yml_path)).result, aliases: true) +else + ActiveRecord::Base.configurations = YAML.safe_load(ERB.new(File.read(database_yml_path)).result, [], [], true) +end def database_config if ActiveRecord.version >= Gem::Version.new("6.1") # See https://github.com/rails/rails/pull/38256