├── test ├── support │ ├── seeds.rb │ ├── seeds │ │ ├── 1.csv │ │ └── 2.csv │ ├── autorun.rb │ ├── connection_helper.rb │ ├── connection.rb │ └── config.rb ├── models │ ├── book.rb │ ├── rent.rb │ └── user.rb ├── cases │ ├── helper.rb │ ├── dump_test.rb │ ├── manifest_test.rb │ ├── restore_test.rb │ ├── db_config.rb │ └── test_case.rb ├── config.rb ├── schema │ └── schema.rb └── config.yml ├── lib ├── seed │ ├── version.rb │ ├── mysql.rb │ ├── manifest.rb │ ├── configuration.rb │ └── snapshot.rb ├── seed_snapshot │ └── version.rb └── seed-snapshot.rb ├── bin ├── setup └── console ├── .gitignore ├── Rakefile ├── gemfiles ├── ar_7.0.gemfile ├── ar_7.1.gemfile ├── ar_7.2.gemfile └── ar_8.0.gemfile ├── Gemfile ├── seed-snapshot.gemspec ├── LICENSE.txt ├── .github └── workflows │ └── main.yml └── README.md /test/support/seeds.rb: -------------------------------------------------------------------------------- 1 | puts 'seeds' 2 | -------------------------------------------------------------------------------- /test/support/seeds/1.csv: -------------------------------------------------------------------------------- 1 | 1,2,3,4,5 2 | 6,7,8,9,10 3 | -------------------------------------------------------------------------------- /lib/seed/version.rb: -------------------------------------------------------------------------------- 1 | module SeedSnapshot 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/seed_snapshot/version.rb: -------------------------------------------------------------------------------- 1 | module SeedSnapshot 2 | VERSION = "0.7.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/support/autorun.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | 3 | require 'minitest' 4 | 5 | Minitest.autorun 6 | -------------------------------------------------------------------------------- /test/support/seeds/2.csv: -------------------------------------------------------------------------------- 1 | a,b,c,d 2 | e,f,g,h 3 | i,j,k,l 4 | m,n,o,p 5 | q,r,s,t 6 | u,v,w,x 7 | y,z,, 8 | -------------------------------------------------------------------------------- /test/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | belongs_to :user 3 | has_many :rent 4 | end 5 | -------------------------------------------------------------------------------- /test/models/rent.rb: -------------------------------------------------------------------------------- 1 | class Rent < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :book 4 | end 5 | -------------------------------------------------------------------------------- /test/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :books 3 | has_many :rents, through: :books 4 | end 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | .DS_Store 12 | /vendor 13 | 14 | debug.log 15 | -------------------------------------------------------------------------------- /test/cases/helper.rb: -------------------------------------------------------------------------------- 1 | require 'cases/db_config' 2 | 3 | require 'support/autorun' 4 | require 'cases/test_case' 5 | 6 | require 'models/rent' 7 | require 'models/book' 8 | require 'models/user' 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "lib" << "test" 6 | t.test_files = Dir.glob("test/**/*_test.rb") 7 | end 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /gemfiles/ar_7.0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'bundler' 4 | gem 'rake' 5 | gem 'pry' 6 | gem 'minitest' 7 | gem 'erubis' 8 | gem 'mysql2', '>= 0.4.4', '< 0.6.0' 9 | gem 'activerecord', '~> 7.0.0' 10 | -------------------------------------------------------------------------------- /gemfiles/ar_7.1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'bundler' 4 | gem 'rake' 5 | gem 'pry' 6 | gem 'minitest' 7 | gem 'erubis' 8 | gem 'mysql2', '>= 0.4.4', '< 0.6.0' 9 | gem 'activerecord', '~> 7.1.0' 10 | -------------------------------------------------------------------------------- /gemfiles/ar_7.2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'bundler' 4 | gem 'rake' 5 | gem 'pry' 6 | gem 'minitest' 7 | gem 'erubis' 8 | gem 'mysql2', '>= 0.4.4', '< 0.6.0' 9 | gem 'activerecord', '~> 7.2.0' 10 | -------------------------------------------------------------------------------- /gemfiles/ar_8.0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'bundler' 4 | gem 'rake' 5 | gem 'pry' 6 | gem 'minitest' 7 | gem 'erubis' 8 | gem 'mysql2', '>= 0.4.4', '< 0.6.0' 9 | gem 'activerecord', '~> 8.0.0' 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'bundler' 4 | gem 'rake' 5 | gem 'pry' 6 | gem 'minitest' 7 | gem 'erubis' 8 | gem 'mysql2', '>= 0.4.4', '< 0.6.0' 9 | 10 | # Specify your gem's dependencies in seed-snapshot.gemspec 11 | gemspec 12 | -------------------------------------------------------------------------------- /test/config.rb: -------------------------------------------------------------------------------- 1 | TEST_ROOT = File.expand_path(File.dirname(__FILE__)) 2 | ASSETS_ROOT = TEST_ROOT + "/assets" 3 | FIXTURES_ROOT = TEST_ROOT + "/fixtures" 4 | MIGRATIONS_ROOT = TEST_ROOT + "/migrations" 5 | SCHEMA_ROOT = TEST_ROOT + "/schema" 6 | -------------------------------------------------------------------------------- /test/support/connection_helper.rb: -------------------------------------------------------------------------------- 1 | module ConnectionHelper 2 | def run_without_connection 3 | original_connection = ActiveRecord::Base.remove_connection 4 | yield original_connection 5 | ensure 6 | ActiveRecord::Base.establish_connection(original_connection) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "seed-snapshot" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | require "pry" 11 | Pry.start 12 | -------------------------------------------------------------------------------- /test/schema/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table :rents, force: true do |t| 3 | t.integer :book_id 4 | t.integer :user_id 5 | t.timestamps null: false 6 | end 7 | 8 | create_table :books, force: true do |t| 9 | t.integer :user_id 10 | t.timestamps null: false 11 | end 12 | 13 | create_table :users, force: true do |t| 14 | t.timestamps null: false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/config.yml: -------------------------------------------------------------------------------- 1 | default_connection: 'mysql2' 2 | 3 | connections: 4 | mysql2: 5 | arunit: 6 | host: 127.0.0.1 7 | port: 3306 8 | username: <%= ENV['DB_USERNAME'] || 'root' %> 9 | password: <%= ENV['DB_PASSWORD'] || '' %> 10 | encoding: utf8 11 | collation: utf8_unicode_ci 12 | arunit2: 13 | host: 127.0.0.1 14 | port: 3306 15 | username: <%= ENV['DB_USERNAME'] || 'root' %> 16 | password: <%= ENV['DB_PASSWORD'] || '' %> 17 | encoding: utf8 18 | -------------------------------------------------------------------------------- /test/support/connection.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/logger' 2 | 3 | module ARTest 4 | def self.connection_name 5 | ENV['ARCONN'] || config['default_connection'] 6 | end 7 | 8 | def self.connection_config 9 | config['connections'][connection_name] 10 | end 11 | 12 | def self.connect 13 | puts "Using #{connection_name}" 14 | ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) 15 | ActiveRecord::Base.configurations = connection_config 16 | ActiveRecord::Base.establish_connection :arunit 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/cases/dump_test.rb: -------------------------------------------------------------------------------- 1 | require 'cases/helper' 2 | 3 | class DumpTest < SeedSnapshot::TestCase 4 | def setup 5 | SeedSnapshot.clean 6 | end 7 | 8 | def test_exists_snapshot 9 | assert_equal SeedSnapshot.exists?, false 10 | end 11 | 12 | def test_dump 13 | SeedSnapshot.dump 14 | assert_equal SeedSnapshot.exists?, true 15 | 16 | lines = open(SeedSnapshot.configuration.current_version_path) { |f| f.readlines } 17 | assert lines.any? { |l| l.include?(User.table_name) } 18 | assert lines.any? { |l| l.include?(Book.table_name) } 19 | assert lines.any? { |l| l.include?(Rent.table_name) } 20 | assert lines.none? { |l| l.include?('ar_internal_metadata') } 21 | assert lines.none? { |l| l.include?('schema_migrations') } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/cases/manifest_test.rb: -------------------------------------------------------------------------------- 1 | require 'cases/helper' 2 | 3 | class ManifestTest < SeedSnapshot::TestCase 4 | def setup 5 | m = SeedSnapshot.manifest 6 | m.appends(paths) 7 | m.save 8 | end 9 | 10 | def paths 11 | Dir.glob('./test/support/seeds/**/*') 12 | end 13 | 14 | def test_diff_if_not_added 15 | m1 = SeedSnapshot.manifest 16 | m1.appends(paths) 17 | 18 | assert_equal m1.diff?, false 19 | end 20 | 21 | # virtual diff 22 | def test_diff_if_added 23 | m1 = SeedSnapshot.manifest 24 | m1.appends(paths) 25 | 26 | m2 = SeedSnapshot.manifest 27 | m2.appends(paths) 28 | m2.appends(['./test/support/seeds.rb']) 29 | 30 | # has diff 31 | assert_equal m2.current.hash != m1.latest.hash, true 32 | end 33 | 34 | def test_save 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /seed-snapshot.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'seed_snapshot/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'seed-snapshot' 8 | spec.version = SeedSnapshot::VERSION 9 | spec.authors = ['yo_waka'] 10 | spec.email = ['y.wakahara@gmail.com'] 11 | 12 | spec.summary = %q{Easy dump/restore tool for ActiveRecord.} 13 | spec.description = %q{Easy dump/restore tool for ActiveRecord.} 14 | spec.homepage = 'https://github.com/waka/seed-snapshot' 15 | spec.license = 'MIT' 16 | 17 | spec.require_paths = ['lib'] 18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | 20 | spec.add_runtime_dependency 'activerecord', '>= 7.0' 21 | end 22 | -------------------------------------------------------------------------------- /test/cases/restore_test.rb: -------------------------------------------------------------------------------- 1 | require 'cases/helper' 2 | 3 | class RestoreTest < SeedSnapshot::TestCase 4 | def setup 5 | SeedSnapshot.clean 6 | 7 | create_models 8 | SeedSnapshot.dump(classes: [User, Book], ignore_classes: [Rent]) 9 | destroy_models 10 | end 11 | 12 | def create_models 13 | user = User.create! 14 | book = Book.create!(user_id: user.id) 15 | Rent.create!(user_id: user.id, book_id: book.id) 16 | User.create! 17 | end 18 | 19 | def destroy_models 20 | User.destroy_all 21 | Book.destroy_all 22 | Rent.destroy_all 23 | end 24 | 25 | def test_restore 26 | assert_equal User.count, 0 27 | assert_equal Book.count, 0 28 | assert_equal Rent.count, 0 29 | 30 | SeedSnapshot.restore 31 | assert_equal User.count, 2 32 | assert_equal Book.count, 1 33 | assert_equal Rent.count, 0 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/support/config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'erb' 3 | require 'fileutils' 4 | require 'pathname' 5 | 6 | module ARTest 7 | class << self 8 | def config 9 | @config ||= read_config 10 | end 11 | 12 | private 13 | 14 | def config_file 15 | Pathname.new(ENV['ARCONFIG'] || TEST_ROOT + '/config.yml') 16 | end 17 | 18 | def read_config 19 | erb = ERB.new(config_file.read) 20 | expand_config(YAML.parse(erb.result(binding)).transform) 21 | end 22 | 23 | def expand_config(config) 24 | config['connections'].each do |adapter, connection| 25 | dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']] 26 | dbs.each do |name, db_name| 27 | unless connection[name].is_a?(Hash) 28 | connection[name] = { 'database' => connection[name] } 29 | end 30 | 31 | connection[name]['database'] ||= db_name 32 | connection[name]['adapter'] ||= adapter 33 | end 34 | end 35 | 36 | config 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/seed-snapshot.rb: -------------------------------------------------------------------------------- 1 | require 'seed/configuration' 2 | require 'seed/manifest' 3 | require 'seed/mysql' 4 | require 'seed/snapshot' 5 | 6 | module SeedSnapshot 7 | # @param Array[ActiveRecord::Base] classes 8 | # @param Array[ActiveRecord::Base] ignore_classes 9 | # @param Boolean force 10 | def self.dump(classes: [], ignore_classes: [], force: false) 11 | snapshot = Seed::Snapshot.new(self.configuration) 12 | snapshot.dump(classes, ignore_classes, force) 13 | end 14 | 15 | def self.restore 16 | snapshot = Seed::Snapshot.new(self.configuration) 17 | snapshot.restore 18 | end 19 | 20 | # @return Boolean 21 | def self.exists? 22 | snapshot = Seed::Snapshot.new(self.configuration) 23 | snapshot.exist_path? 24 | end 25 | 26 | def self.clean 27 | snapshot = Seed::Snapshot.new(self.configuration) 28 | snapshot.clean 29 | end 30 | 31 | # @return Seed::Manifest 32 | def self.manifest 33 | Seed::Manifest.new(self.configuration) 34 | end 35 | 36 | # @return Seed::Configuration 37 | def self.configuration 38 | Seed::Configuration.new 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/cases/db_config.rb: -------------------------------------------------------------------------------- 1 | require 'config' 2 | 3 | require 'active_record' 4 | require 'seed-snapshot' 5 | 6 | require 'support/config' 7 | require 'support/connection' 8 | 9 | # create database if it doesn't exist 10 | def create_database_if_not_exists 11 | ARTest.connection_config.each do |_, config| 12 | begin 13 | ActiveRecord::Base.establish_connection(config.except('database')) 14 | ActiveRecord::Base.connection.create_database(config['database'], charset: config['encoding'], collation: config['collation']) 15 | rescue ActiveRecord::DatabaseAlreadyExists 16 | # Database already exists, nothing to do 17 | rescue => e 18 | raise "Failed to create database '#{config['database']}': #{e.message}" 19 | ensure 20 | ActiveRecord::Base.remove_connection 21 | end 22 | end 23 | end 24 | 25 | create_database_if_not_exists 26 | 27 | # connect to the database 28 | ARTest.connect 29 | 30 | def load_schema 31 | original_stdout = $stdout 32 | $stdout = StringIO.new 33 | 34 | load SCHEMA_ROOT + '/schema.rb' 35 | ensure 36 | $stdout = original_stdout 37 | end 38 | 39 | load_schema 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 yo_waka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/seed/mysql.rb: -------------------------------------------------------------------------------- 1 | module Seed 2 | class Mysql 3 | def self.dump(file_path, username:, password:, host:, port:, database:, tables:, ignore_tables:, client_version:) 4 | host = 'localhost' unless host 5 | 6 | additional_options = 7 | if client_version.match?(/\A8/) 8 | '--skip-column-statistics' 9 | else 10 | '' 11 | end 12 | 13 | cmd = "MYSQL_PWD=#{password} mysqldump -u #{username} -h #{host} -P #{port} #{database} -t #{allow_tables_option(tables)} #{ignore_tables_option(ignore_tables)} --no-tablespaces #{additional_options} > #{file_path}" 14 | system cmd 15 | end 16 | 17 | def self.restore(file_path, username:, password:, host:, port:, database:) 18 | host = 'localhost' unless host 19 | cmd = "MYSQL_PWD=#{password} mysql -u #{username} -h #{host} -P #{port} #{database} < #{file_path}" 20 | system cmd 21 | end 22 | 23 | def self.allow_tables_option(tables) 24 | tables.join(' ') 25 | end 26 | 27 | def self.ignore_tables_option(tables) 28 | tables.map { |table| "--ignore-table=#{table}" }.join(' ') 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/seed/manifest.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Seed 4 | class Manifest 5 | def initialize(configuration) 6 | @configuration = configuration 7 | @paths = [] 8 | end 9 | 10 | def appends(paths = []) 11 | @paths << paths 12 | @paths.flatten! 13 | end 14 | 15 | def diff? 16 | self.current.hash != self.latest.hash 17 | end 18 | 19 | def save 20 | @configuration.make_tmp_dir 21 | 22 | open(self.manifest_path, File::WRONLY | File::CREAT | File::TRUNC) do |io| 23 | JSON.dump(self.current, io) 24 | end 25 | end 26 | 27 | # @return [Hash] 28 | def current 29 | @current ||= self.generate_manifest 30 | end 31 | 32 | # @return [Hash] 33 | def latest 34 | @configuration.make_tmp_dir 35 | 36 | open(self.manifest_path, File::RDONLY | File::CREAT) do |io| 37 | JSON.load(io) rescue {} 38 | end 39 | end 40 | 41 | def manifest_path 42 | @configuration.base_path.join('seed_manifest.json') 43 | end 44 | 45 | def generate_manifest 46 | hash = {} 47 | @paths.each do |path| 48 | next unless File.exist?(path) 49 | content = File.read(path) 50 | hash[path] = Digest::SHA256.new.update(content).hexdigest 51 | end 52 | hash 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | - '**.txt' 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | - '**.txt' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | name: test (ruby:${{ matrix.ruby }}, ar:${{ matrix.ar }}) 20 | 21 | services: 22 | mysql: 23 | image: mysql:8.0 24 | env: 25 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 26 | MYSQL_DATABASE: 'activerecord_unittest' 27 | ports: 28 | - 3306:3306 29 | options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | ruby: ['3.1', '3.2', '3.3'] 35 | ar: ['7.0', '7.1', '7.2', '8.0'] 36 | exclude: 37 | - ruby: '3.1' 38 | ar: '8.0' 39 | 40 | env: 41 | BUNDLE_GEMFILE: gemfiles/ar_${{ matrix.ar }}.gemfile 42 | 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Ruby ${{ matrix.ruby }} 48 | uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: ${{ matrix.ruby }} 51 | bundler-cache: true 52 | 53 | - name: Run tests 54 | run: bundle exec rake 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seed-snapshot 2 | 3 | The library that easily and quickly dumps and restores seed data. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | group :test do 11 | gem 'seed-snapshot' 12 | end 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install seed-snapshot 22 | 23 | ## Usage 24 | 25 | Configuration in RSpec with DatabaseCleaner (optional). 26 | 27 | ``` 28 | require 'seed-snapshot' 29 | 30 | RSpec.configure do |config| 31 | tables = [User, Item] 32 | 33 | config.before(:suite) do 34 | DatabaseCleaner.strategy = :transaction 35 | 36 | if SeedSnapshot.exists? 37 | SeedSnapshot.restore(tables) 38 | else 39 | # load seed data normally 40 | end 41 | end 42 | 43 | config.before(:each) do 44 | DatabaseCleaner.start 45 | end 46 | 47 | config.after(:each) do 48 | DatabaseCleaner.clean 49 | end 50 | 51 | config.after(:suite) do 52 | unless SeedSnapshot.exists? 53 | SeedSnapshot.dump(tables) 54 | end 55 | end 56 | end 57 | ``` 58 | 59 | ## Development 60 | 61 | After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 62 | 63 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 64 | 65 | ## Contributing 66 | 67 | Bug reports and pull requests are welcome on GitHub at https://github.com/waka/seed-snapshot. 68 | 69 | 70 | ## License 71 | 72 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 73 | 74 | -------------------------------------------------------------------------------- /lib/seed/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require 'active_record' 3 | 4 | module Seed 5 | class Configuration 6 | def initialize 7 | # error if adapter is not mysql 8 | raise 'seed-snapshot support only MySQL' unless adapter_name == 'Mysql2' 9 | end 10 | 11 | def adapter_name 12 | @adapter_name ||= ActiveRecord::Base.connection.adapter_name 13 | end 14 | 15 | def schema_version 16 | @schema_version ||= Digest::SHA1.hexdigest(get_all_versions.sort.join) 17 | end 18 | 19 | def client_version 20 | @client_version ||= ActiveRecord::Base.connection.raw_connection.info[:version] 21 | end 22 | 23 | def database_options 24 | @options ||= ActiveRecord::Base.connection_db_config.configuration_hash 25 | end 26 | 27 | # ${Rails.root}/tmp/dump 28 | def base_path 29 | Pathname.new(Dir.pwd).join('tmp').join('dump') 30 | end 31 | 32 | # ${Rails.root}/tmp/dump/123456789.sql' 33 | def current_version_path 34 | base_path.join(schema_version.to_s + '.sql') 35 | end 36 | 37 | def make_tmp_dir 38 | FileUtils.mkdir_p(base_path) unless File.exist?(base_path) 39 | end 40 | 41 | private 42 | 43 | def get_all_versions 44 | if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new('7.1') 45 | migration_paths = ::ActiveRecord::Migrator.migrations_paths 46 | ::ActiveRecord::MigrationContext.new(migration_paths).get_all_versions 47 | elsif ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new('6.0') 48 | migration_paths = ::ActiveRecord::Migrator.migrations_paths 49 | ::ActiveRecord::MigrationContext.new(migration_paths, ::ActiveRecord::SchemaMigration).get_all_versions 50 | elsif ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new('5.2') 51 | migration_paths = ::ActiveRecord::Migrator.migrations_paths 52 | ::ActiveRecord::MigrationContext.new(migration_paths).get_all_versions 53 | else 54 | ::ActiveRecord::Migrator.get_all_versions 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/seed/snapshot.rb: -------------------------------------------------------------------------------- 1 | module Seed 2 | class Snapshot 3 | def initialize(configuration) 4 | @configuration = configuration 5 | end 6 | 7 | def dump(classes = [], ignore_classes = [], force_dump = false) 8 | @configuration.make_tmp_dir 9 | 10 | if exist_path? && !force_dump 11 | puts 'Dump file already exists for current schema.' 12 | return 13 | end 14 | 15 | Mysql.dump( 16 | @configuration.current_version_path, 17 | **options.merge({ 18 | tables: tables(classes), 19 | ignore_tables: ignore_tables(ignore_classes), 20 | client_version: @configuration.client_version 21 | }) 22 | ) 23 | end 24 | 25 | def restore 26 | @configuration.make_tmp_dir 27 | 28 | unless exist_path? 29 | puts "Dump file does not exist for current schema." 30 | return 31 | end 32 | 33 | Mysql.restore( 34 | @configuration.current_version_path, 35 | **options 36 | ) 37 | end 38 | 39 | def clean 40 | unless exist_path? 41 | puts "Dump file does not exist for current schema." 42 | return 43 | end 44 | 45 | version_path = @configuration.current_version_path 46 | File.delete(version_path) 47 | end 48 | 49 | def exist_path? 50 | version_path = @configuration.current_version_path 51 | File.exist?(version_path) 52 | end 53 | 54 | def options 55 | db = @configuration.database_options 56 | { 57 | username: db[:username], 58 | password: db[:password], 59 | host: db[:host], 60 | port: db[:port], 61 | database: db[:database] 62 | } 63 | end 64 | 65 | def tables(classes) 66 | classes.map do |cls| 67 | cls.table_name 68 | end 69 | end 70 | 71 | IGNORED_TABLES = [ 72 | 'ar_internal_metadata', 73 | 'schema_migrations' 74 | ].freeze 75 | 76 | def ignore_tables(classes) 77 | db = @configuration.database_options[:database] 78 | 79 | # mysqldump `--ignore-table` options require database name. 80 | tables(classes).concat(IGNORED_TABLES).map {|t| "#{db}.#{t}" } 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/cases/test_case.rb: -------------------------------------------------------------------------------- 1 | module SeedSnapshot 2 | class TestCase < Minitest::Test 3 | def setup 4 | end 5 | 6 | def teardown 7 | SQLCounter.clear_log 8 | end 9 | 10 | def capture_sql 11 | SQLCounter.clear_log 12 | yield 13 | SQLCounter.log_all.dup 14 | end 15 | end 16 | 17 | class SQLCounter 18 | class << self 19 | attr_accessor :ignored_sql, :log, :log_all 20 | def clear_log; self.log = []; self.log_all = []; end 21 | end 22 | 23 | self.clear_log 24 | 25 | self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] 26 | 27 | # FIXME: this needs to be refactored so specific database can add their own 28 | # ignored SQL, or better yet, use a different notification for the queries 29 | # instead examining the SQL content. 30 | oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] 31 | mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] 32 | postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] 33 | sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] 34 | 35 | [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| 36 | ignored_sql.concat db_ignored_sql 37 | end 38 | 39 | attr_reader :ignore 40 | 41 | def initialize(ignore = Regexp.union(self.class.ignored_sql)) 42 | @ignore = ignore 43 | end 44 | 45 | def call(name, start, finish, message_id, values) 46 | sql = values[:sql] 47 | 48 | # FIXME: this seems bad. we should probably have a better way to indicate 49 | # the query was cached 50 | return if 'CACHE' == values[:name] 51 | 52 | self.class.log_all << sql 53 | self.class.log << sql unless ignore =~ sql 54 | end 55 | end 56 | 57 | ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) 58 | end 59 | --------------------------------------------------------------------------------