├── .yardopts ├── lib ├── seed-do │ ├── version.rb │ ├── capistrano3.rb │ ├── capistrano.rb │ ├── railtie.rb │ ├── block_hash.rb │ ├── runner.rb │ ├── active_record_extension.rb │ ├── seeder.rb │ └── writer.rb ├── tasks │ ├── seed_do_capistrano3.rake │ └── seed_do.rake └── seed-do.rb ├── spec ├── fixtures │ ├── seeded_models.rb │ ├── seeded_models_2.rb │ └── seeded_models_3.rb.gz ├── connections │ ├── sqlite3.rb │ ├── mysql2.rb │ └── postgresql.rb ├── runner_spec.rb ├── spec_helper.rb ├── writer_spec.rb └── seeder_spec.rb ├── .gitignore ├── Gemfile ├── .rubocop.yml ├── gemfiles ├── rails_8_0.gemfile ├── rails_8_1.gemfile ├── rails_7_1.gemfile └── rails_7_2.gemfile ├── Rakefile ├── LICENSE ├── seed-do.gemspec ├── .github └── workflows │ └── ci.yml ├── .rubocop_todo.yml └── README.md /.yardopts: -------------------------------------------------------------------------------- 1 | -m markdown 2 | --no-private 3 | -------------------------------------------------------------------------------- /lib/seed-do/version.rb: -------------------------------------------------------------------------------- 1 | module SeedDo 2 | VERSION = '3.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/seed-do/capistrano3.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path('../tasks/seed_do_capistrano3.rake', __dir__) 2 | -------------------------------------------------------------------------------- /spec/fixtures/seeded_models.rb: -------------------------------------------------------------------------------- 1 | SeededModel.seed do |s| 2 | s.id = 1 3 | s.title = 'Foo' 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/seeded_models_2.rb: -------------------------------------------------------------------------------- 1 | SeededModel.seed do |s| 2 | s.id = 2 3 | s.title = 'Bar' 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/seeded_models_3.rb.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willnet/seed-do/HEAD/spec/fixtures/seeded_models_3.rb.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.gem 3 | .DS_Store 4 | /pkg 5 | Gemfile.lock 6 | .bundle 7 | .yardoc 8 | doc 9 | gemfiles/*.gemfile.lock 10 | *.sqlite3 11 | spec/seeded_models.rb 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gem 'mysql2' 3 | gem 'pg' 4 | gem 'rails' 5 | gem 'rubocop', require: false 6 | gem 'rubocop-performance', require: false 7 | gem 'sqlite3' 8 | gemspec 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | plugins: 4 | - rubocop-performance 5 | 6 | AllCops: 7 | NewCops: enable 8 | TargetRubyVersion: 3.2 9 | Performance: 10 | Enabled: true -------------------------------------------------------------------------------- /gemfiles/rails_8_0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord', '~> 8.0.0' 4 | gem 'activesupport', '~> 8.0.0' 5 | gem 'mysql2' 6 | gem 'pg' 7 | gem 'sqlite3' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/rails_8_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord', '~> 8.1.0' 4 | gem 'activesupport', '~> 8.1.0' 5 | gem 'mysql2' 6 | gem 'pg' 7 | gem 'sqlite3' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord', '~> 7.1.0' 4 | gem 'activesupport', '~> 7.1.0' 5 | gem 'mysql2' 6 | gem 'pg' 7 | gem 'sqlite3', '~> 1.4' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord', '~> 7.2.0' 4 | gem 'activesupport', '~> 7.2.0' 5 | gem 'mysql2' 6 | gem 'pg' 7 | gem 'sqlite3', '~> 1.4' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /spec/connections/sqlite3.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.configurations = { 2 | 'test' => { 3 | adapter: 'sqlite3', 4 | database: File.dirname(__FILE__) + '/test.sqlite3' 5 | } 6 | } 7 | 8 | ActiveRecord::Base.establish_connection 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new :spec do |spec| 6 | spec.pattern = 'spec/**/*_spec.rb' 7 | end 8 | 9 | task default: :spec 10 | -------------------------------------------------------------------------------- /spec/connections/mysql2.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.configurations = { 2 | 'test' => { 3 | adapter: 'mysql2', 4 | database: 'seed_do_test', 5 | username: 'root', 6 | host: '127.0.0.1' 7 | } 8 | } 9 | ActiveRecord::Tasks::DatabaseTasks.create_current('test') 10 | -------------------------------------------------------------------------------- /spec/connections/postgresql.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.configurations = { 2 | 'test' => { 3 | adapter: 'postgresql', 4 | database: 'seed_do_test', 5 | username: 'postgres', 6 | host: '127.0.0.1' 7 | } 8 | } 9 | ActiveRecord::Tasks::DatabaseTasks.create_current('test') 10 | -------------------------------------------------------------------------------- /lib/seed-do/capistrano.rb: -------------------------------------------------------------------------------- 1 | Capistrano::Configuration.instance.load do 2 | namespace :db do 3 | desc 'Load seed data into database' 4 | task :seed_do, roles: :db, only: { primary: true } do 5 | run "cd #{release_path} && bundle exec rake RAILS_ENV=#{rails_env} db:seed_do" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/seed_do_capistrano3.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc 'Load seed data into database' 3 | task :seed_do do 4 | on roles(:db) do 5 | within release_path do 6 | with rails_env: fetch(:rails_env) do 7 | execute :bundle, :exec, :rake, 'db:seed_do' 8 | end 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/seed-do/railtie.rb: -------------------------------------------------------------------------------- 1 | module SeedDo 2 | class Railtie < Rails::Railtie 3 | rake_tasks do 4 | load 'tasks/seed_do.rake' 5 | end 6 | 7 | initializer 'seed_do.set_fixture_paths' do 8 | SeedDo.fixture_paths = [ 9 | Rails.root.join('db/fixtures').to_s, 10 | Rails.root.join('db/fixtures/' + Rails.env).to_s 11 | ] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/seed-do/block_hash.rb: -------------------------------------------------------------------------------- 1 | module SeedDo 2 | # @private 3 | class BlockHash 4 | def initialize(proc) 5 | @hash = {} 6 | proc.call(self) 7 | end 8 | 9 | def to_hash 10 | @hash 11 | end 12 | 13 | def method_missing(method_name, *args, &block) 14 | if method_name.to_s =~ /^(.*)=$/ && args.length == 1 && block.nil? 15 | @hash[::Regexp.last_match(1)] = args.first 16 | else 17 | super 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SeedDo::Runner do 4 | it 'should seed data from Ruby and gzipped Ruby files in the given fixtures directory' do 5 | SeedDo.seed(File.dirname(__FILE__) + '/fixtures') 6 | 7 | expect(SeededModel.find(1).title).to eq 'Foo' 8 | expect(SeededModel.find(2).title).to eq 'Bar' 9 | expect(SeededModel.find(3).title).to eq 'Baz' 10 | end 11 | 12 | it 'should seed only the data which matches the filter, if one is given' do 13 | SeedDo.seed(File.dirname(__FILE__) + '/fixtures', /_2/) 14 | 15 | expect(SeededModel.count).to eq 1 16 | expect(SeededModel.find(2).title).to eq 'Bar' 17 | end 18 | 19 | it 'should use the SeedDo.fixtures_paths variable to determine where fixtures are' do 20 | SeedDo.fixture_paths = [File.dirname(__FILE__) + '/fixtures'] 21 | SeedDo.seed 22 | expect(SeededModel.count).to eq 3 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010 Michael Bleigh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /seed-do.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'seed-do/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'seed-do' 8 | s.version = SeedDo::VERSION 9 | s.platform = Gem::Platform::RUBY 10 | s.licenses = ['MIT'] 11 | s.authors = ['Shinichi Maeshima'] 12 | s.email = ['netwillnet@gmail.com'] 13 | s.homepage = 'http://github.com/willnet/seed-do' 14 | s.summary = 'Easily manage seed data in your Active Record application' 15 | s.description = 'Seed Do is an attempt to once and for all solve the problem of inserting and maintaining seed data in a database. It uses a variety of techniques gathered from various places around the web and combines them to create what is hopefully the most robust seed data system around.' 16 | s.required_ruby_version = '>= 3.2' 17 | 18 | s.add_dependency 'activerecord', '>= 7.1' 19 | s.add_dependency 'activesupport', '>= 7.1' 20 | 21 | s.add_development_dependency 'rspec' 22 | 23 | s.files = Dir.glob('{lib}/**/*') + %w[LICENSE README.md] 24 | s.require_path = 'lib' 25 | s.metadata['rubygems_mfa_required'] = 'true' 26 | end 27 | -------------------------------------------------------------------------------- /lib/tasks/seed_do.rake: -------------------------------------------------------------------------------- 1 | require 'seed-do' 2 | 3 | namespace :db do 4 | desc <<-EOS 5 | Loads seed data for the current environment. It will look for 6 | ruby seed files in /db/fixtures/ and 7 | /db/fixtures//. 8 | 9 | By default it will load any ruby files found. You can filter the files 10 | loaded by passing in the FILTER environment variable with a comma-delimited 11 | list of patterns to include. Any files not matching the pattern will 12 | not be loaded. 13 | 14 | You can also change the directory where seed files are looked for 15 | with the FIXTURE_PATH environment variable. 16 | 17 | Examples: 18 | # default, to load all seed files for the current environment 19 | rake db:seed_do 20 | 21 | # to load seed files matching orders or customers 22 | rake db:seed_do FILTER=orders,customers 23 | 24 | # to load files from RAILS_ROOT/features/fixtures 25 | rake db:seed_do FIXTURE_PATH=features/fixtures 26 | EOS 27 | task seed_do: :environment do 28 | filter = /#{ENV['FILTER'].tr(',', '|')}/ if ENV['FILTER'] 29 | 30 | fixture_paths = [ENV['FIXTURE_PATH'], ENV['FIXTURE_PATH'] + '/' + Rails.env] if ENV['FIXTURE_PATH'] 31 | 32 | SeedDo.seed(fixture_paths, filter) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/seed-do.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_support/core_ext/module/attribute_accessors' 3 | require 'seed-do/railtie' if defined?(Rails) 4 | 5 | module SeedDo 6 | autoload :VERSION, 'seed-do/version' 7 | autoload :Seeder, 'seed-do/seeder' 8 | autoload :ActiveRecordExtension, 'seed-do/active_record_extension' 9 | autoload :BlockHash, 'seed-do/block_hash' 10 | autoload :Runner, 'seed-do/runner' 11 | autoload :Writer, 'seed-do/writer' 12 | 13 | # Set `SeedDo.quiet = true` to silence all output 14 | mattr_accessor :quiet, default: false 15 | 16 | # Set this to be an array of paths to directories containing your seed files. If used as a Rails 17 | # plugin, SeedDo will set it to contain `Rails.root/db/fixtures` and 18 | # `Rails.root/db/fixtures/Rails.env` 19 | mattr_accessor :fixture_paths, default: ['db/fixtures'] 20 | 21 | # Load seed data from files 22 | # @param [Array] fixture_paths The paths to look for seed files in 23 | # @param [Regexp] filter If given, only filenames matching this expression will be loaded 24 | def self.seed(fixture_paths = SeedDo.fixture_paths, filter = nil) 25 | Runner.new(fixture_paths, filter).run 26 | end 27 | end 28 | 29 | # @public 30 | ActiveSupport.on_load(:active_record) do 31 | extend SeedDo::ActiveRecordExtension 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'seed-do' 4 | require 'logger' 5 | 6 | SeedDo.quiet = true 7 | 8 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/../debug.log') 9 | ENV['RAILS_ENV'] ||= 'test' 10 | ENV['DB'] ||= 'sqlite3' 11 | 12 | puts "Using #{ENV.fetch('DB', nil)} to run the tests." 13 | require File.dirname(__FILE__) + "/connections/#{ENV.fetch('DB', nil)}.rb" 14 | 15 | ActiveRecord::Schema.define version: 0 do 16 | create_table :seeded_models, force: true do |t| 17 | t.column :login, :string 18 | t.column :first_name, :string 19 | t.column :last_name, :string 20 | t.column :title, :string 21 | end 22 | 23 | create_table :seeded_model_no_primary_keys, id: false, force: true do |t| 24 | t.column :id, :string 25 | end 26 | 27 | create_table :seeded_model_no_sequences, id: false, force: true do |t| 28 | t.column :id, :string 29 | end 30 | 31 | execute('ALTER TABLE seeded_model_no_sequences ADD PRIMARY KEY (id)') if ENV['DB'] == 'postgresql' 32 | end 33 | 34 | class SeededModel < ActiveRecord::Base 35 | validates_presence_of :title 36 | attr_accessor :fail_to_save 37 | 38 | before_save { throw(:abort) if fail_to_save } 39 | end 40 | 41 | class SeededModelNoPrimaryKey < ActiveRecord::Base 42 | end 43 | 44 | class SeededModelNoSequence < ActiveRecord::Base 45 | end 46 | 47 | RSpec.configure do |config| 48 | config.before do 49 | SeededModel.delete_all 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/writer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SeedDo::Writer do 4 | before do 5 | @file_name = File.dirname(__FILE__) + '/seeded_models.rb' 6 | end 7 | 8 | after do 9 | FileUtils.rm(@file_name) 10 | end 11 | 12 | it 'should successfully write some seeds out to a file and then import them back in' do 13 | SeedDo::Writer.write(@file_name, class_name: 'SeededModel') do |writer| 14 | writer << { id: 1, title: 'Mr' } 15 | writer << { id: 2, title: 'Dr' } 16 | end 17 | load @file_name 18 | 19 | expect(SeededModel.find(1).title).to eq 'Mr' 20 | expect(SeededModel.find(2).title).to eq 'Dr' 21 | end 22 | 23 | it 'should support chunking' do 24 | SeedDo::Writer.write(@file_name, class_name: 'SeededModel', chunk_size: 2) do |writer| 25 | writer << { id: 1, title: 'Mr' } 26 | writer << { id: 2, title: 'Dr' } 27 | writer << { id: 3, title: 'Dr' } 28 | end 29 | load @file_name 30 | 31 | expect(SeededModel.count).to eq 3 32 | expect(File.read(@file_name)).to include("# BREAK EVAL\n") 33 | end 34 | 35 | it "should support specifying the output to use 'seed_once' rather than 'seed'" do 36 | SeededModel.seed(id: 1, title: 'Dr') 37 | 38 | SeedDo::Writer.write(@file_name, class_name: 'SeededModel', seed_type: :seed_once) do |writer| 39 | writer << { id: 1, title: 'Mr' } 40 | end 41 | load @file_name 42 | 43 | expect(SeededModel.find(1).title).to eq 'Dr' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/seed-do/runner.rb: -------------------------------------------------------------------------------- 1 | require 'zlib' 2 | require 'active_support/core_ext/array/wrap' 3 | 4 | module SeedDo 5 | # Runs seed files. 6 | # 7 | # It is not recommended to use this class directly. Instead, use {SeedDo.seed SeedDo.seed}, which creates 8 | # an instead of {Runner} and calls {#run #run}. 9 | # 10 | # @see SeedDo.seed SeedDo.seed 11 | class Runner 12 | # @param [Array] fixture_paths The paths where fixtures are located. Will use 13 | # `SeedDo.fixture_paths` if {nil}. If the argument is not an array, it will be wrapped by one. 14 | # @param [Regexp] filter If given, only seed files with a file name matching this pattern will 15 | # be used 16 | def initialize(fixture_paths = nil, filter = nil) 17 | @fixture_paths = Array.wrap(fixture_paths || SeedDo.fixture_paths) 18 | @filter = filter 19 | end 20 | 21 | # Run the seed files. 22 | def run 23 | puts "\n== Filtering seed files against regexp: #{@filter.inspect}" if @filter && !SeedDo.quiet 24 | 25 | filenames.each do |filename| 26 | run_file(filename) 27 | end 28 | end 29 | 30 | private 31 | 32 | def run_file(filename) 33 | puts "\n== Seed from #{filename}" unless SeedDo.quiet 34 | 35 | ActiveRecord::Base.transaction do 36 | open(filename) do |file| 37 | chunked_ruby = +'' 38 | file.each_line do |line| 39 | if line == "# BREAK EVAL\n" 40 | eval(chunked_ruby) 41 | chunked_ruby = +'' 42 | else 43 | chunked_ruby << line 44 | end 45 | end 46 | eval(chunked_ruby) unless chunked_ruby == '' 47 | end 48 | end 49 | end 50 | 51 | def open(filename, &) 52 | if filename[-3..-1] == '.gz' 53 | Zlib::GzipReader.open(filename, &) 54 | else 55 | File.open(filename, &) 56 | end 57 | end 58 | 59 | def filenames 60 | filenames = [] 61 | @fixture_paths.each do |path| 62 | filenames += (Dir[File.join(path, '*.rb')] + Dir[File.join(path, '*.rb.gz')]).sort 63 | end 64 | filenames.uniq! 65 | filenames = filenames.find_all { |filename| filename =~ @filter } if @filter 66 | filenames 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/seed-do/active_record_extension.rb: -------------------------------------------------------------------------------- 1 | module SeedDo 2 | module ActiveRecordExtension 3 | # Load some seed data. There are two ways to do this. 4 | # 5 | # Verbose syntax 6 | # -------------- 7 | # 8 | # This will seed a single record. The `:id` parameter ensures that if a record already exists 9 | # in the database with the same id, then it will be updated with the name and age, rather 10 | # than created from scratch. 11 | # 12 | # Person.seed(:id) do |s| 13 | # s.id = 1 14 | # s.name = "Jon" 15 | # s.age = 21 16 | # end 17 | # 18 | # Note that `:id` is the default attribute used to identify a seed, so it need not be 19 | # specified. 20 | # 21 | # Terse syntax 22 | # ------------ 23 | # 24 | # This is a more succinct way to load multiple records. Note that both `:x` and `:y` are being 25 | # used to identify a seed here. 26 | # 27 | # Point.seed(:x, :y, 28 | # { :x => 3, :y => 10, :name => "Home" }, 29 | # { :x => 5, :y => 9, :name => "Office" } 30 | # ) 31 | def seed(*args, &block) 32 | SeedDo::Seeder.new(self, *parse_seed_do_args(args, block)).seed 33 | end 34 | 35 | # Has the same syntax as {#seed}, but if a record already exists with the same values for 36 | # constraining attributes, it will not be updated. 37 | # 38 | # @example 39 | # Person.seed(:id, :id => 1, :name => "Jon") # => Record created 40 | # Person.seed(:id, :id => 1, :name => "Bob") # => Name changed 41 | # Person.seed_once(:id, :id => 1, :name => "Harry") # => Name *not* changed 42 | def seed_once(*args, &block) 43 | constraints, data = parse_seed_do_args(args, block) 44 | SeedDo::Seeder.new(self, constraints, data, insert_only: true).seed 45 | end 46 | 47 | private 48 | 49 | def parse_seed_do_args(args, block) 50 | if block.nil? 51 | if args.last.is_a?(Array) 52 | # Last arg is an array of data, so assume the rest of the args are constraints 53 | data = args.pop 54 | [args, data] 55 | else 56 | # Partition the args, assuming the first hash is the start of the data 57 | args.partition { |arg| !arg.is_a?(Hash) } 58 | end 59 | else 60 | # We have a block, so assume the args are all constraints 61 | [args, [SeedDo::BlockHash.new(block).to_hash]] 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | rubocop: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Set up Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: '3.4' 18 | bundler-cache: true 19 | - name: Run RuboCop 20 | run: bundle exec rubocop 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | services: 25 | mysql: 26 | image: mysql:8.0 27 | ports: 28 | - 3306:3306 29 | env: 30 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 31 | postgres: 32 | image: postgres:17 33 | ports: 34 | - 5432:5432 35 | env: 36 | POSTGRES_HOST_AUTH_METHOD: trust 37 | POSTGRES_USER: postgres 38 | strategy: 39 | matrix: 40 | include: 41 | - { ruby-version: '3.2', gemfile: 'rails_7_1', database: 'sqlite3' } 42 | - { ruby-version: '3.2', gemfile: 'rails_7_1', database: 'mysql2' } 43 | - { ruby-version: '3.2', gemfile: 'rails_7_1', database: 'postgresql' } 44 | - { ruby-version: '3.2', gemfile: 'rails_7_2', database: 'sqlite3' } 45 | - { ruby-version: '3.2', gemfile: 'rails_7_2', database: 'mysql2' } 46 | - { ruby-version: '3.2', gemfile: 'rails_7_2', database: 'postgresql' } 47 | - { ruby-version: '3.3', gemfile: 'rails_7_2', database: 'sqlite3' } 48 | - { ruby-version: '3.3', gemfile: 'rails_7_2', database: 'mysql2' } 49 | - { ruby-version: '3.3', gemfile: 'rails_7_2', database: 'postgresql' } 50 | - { ruby-version: '3.4', gemfile: 'rails_7_2', database: 'sqlite3' } 51 | - { ruby-version: '3.4', gemfile: 'rails_7_2', database: 'mysql2' } 52 | - { ruby-version: '3.4', gemfile: 'rails_7_2', database: 'postgresql' } 53 | - { ruby-version: '3.2', gemfile: 'rails_8_0', database: 'sqlite3' } 54 | - { ruby-version: '3.2', gemfile: 'rails_8_0', database: 'mysql2' } 55 | - { ruby-version: '3.2', gemfile: 'rails_8_0', database: 'postgresql' } 56 | - { ruby-version: '3.3', gemfile: 'rails_8_0', database: 'sqlite3' } 57 | - { ruby-version: '3.3', gemfile: 'rails_8_0', database: 'mysql2' } 58 | - { ruby-version: '3.3', gemfile: 'rails_8_0', database: 'postgresql' } 59 | - { ruby-version: '3.4', gemfile: 'rails_8_0', database: 'sqlite3' } 60 | - { ruby-version: '3.4', gemfile: 'rails_8_0', database: 'mysql2' } 61 | - { ruby-version: '3.4', gemfile: 'rails_8_0', database: 'postgresql' } 62 | - { ruby-version: '3.2', gemfile: 'rails_8_1', database: 'sqlite3' } 63 | - { ruby-version: '3.2', gemfile: 'rails_8_1', database: 'mysql2' } 64 | - { ruby-version: '3.2', gemfile: 'rails_8_1', database: 'postgresql' } 65 | - { ruby-version: '3.3', gemfile: 'rails_8_1', database: 'sqlite3' } 66 | - { ruby-version: '3.3', gemfile: 'rails_8_1', database: 'mysql2' } 67 | - { ruby-version: '3.3', gemfile: 'rails_8_1', database: 'postgresql' } 68 | - { ruby-version: '3.4', gemfile: 'rails_8_1', database: 'sqlite3' } 69 | - { ruby-version: '3.4', gemfile: 'rails_8_1', database: 'mysql2' } 70 | - { ruby-version: '3.4', gemfile: 'rails_8_1', database: 'postgresql' } 71 | env: 72 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 73 | DB: ${{ matrix.database }} 74 | 75 | steps: 76 | - uses: actions/checkout@v6 77 | - name: Set up Ruby 78 | uses: ruby/setup-ruby@v1 79 | with: 80 | ruby-version: ${{ matrix.ruby-version }} 81 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 82 | - name: Run tests 83 | run: bundle exec rspec -------------------------------------------------------------------------------- /lib/seed-do/seeder.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash/keys' 2 | 3 | module SeedDo 4 | # Creates or updates seed records with data. 5 | # 6 | # It is not recommended to use this class directly. Instead, use `Model.seed`, and `Model.seed_once`, 7 | # where `Model` is your Active Record model. 8 | # 9 | # @see ActiveRecordExtension 10 | class Seeder 11 | # @param [ActiveRecord::Base] model_class The model to be seeded 12 | # @param [Array] constraints A list of attributes which identify a particular seed. If 13 | # a record with these attributes already exists then it will be updated rather than created. 14 | # @param [Array] data Each item in this array is a hash containing attributes for a 15 | # particular record. 16 | # @param [Hash] options 17 | # @option options [Boolean] :quiet (SeedDo.quiet) If true, output will be silenced 18 | # @option options [Boolean] :insert_only (false) If true then existing records which match the 19 | # constraints will not be updated, even if the seed data has changed 20 | def initialize(model_class, constraints, data, options = {}) 21 | @model_class = model_class 22 | @constraints = constraints.to_a.empty? ? [:id] : constraints 23 | @data = data.to_a || [] 24 | @options = options.symbolize_keys 25 | 26 | @options[:quiet] ||= SeedDo.quiet 27 | 28 | validate_constraints! 29 | validate_data! 30 | end 31 | 32 | # Insert/update the records as appropriate. Validation is skipped while saving. 33 | # @return [Array] The records which have been seeded 34 | def seed 35 | records = @model_class.transaction do 36 | @data.map { |record_data| seed_record(record_data.symbolize_keys) } 37 | end 38 | update_id_sequence 39 | records 40 | end 41 | 42 | private 43 | 44 | def validate_constraints! 45 | unknown_columns = @constraints.map(&:to_s) - @model_class.column_names 46 | return if unknown_columns.empty? 47 | 48 | raise(ArgumentError, 49 | "Your seed constraints contained unknown columns: #{column_list(unknown_columns)}. " + 50 | "Valid columns are: #{column_list(@model_class.column_names)}.") 51 | end 52 | 53 | def validate_data! 54 | raise ArgumentError, 'Seed data missing' if @data.empty? 55 | end 56 | 57 | def column_list(columns) 58 | '`' + columns.join('`, `') + '`' 59 | end 60 | 61 | def seed_record(data) 62 | record = find_or_initialize_record(data) 63 | return if @options[:insert_only] && !record.new_record? 64 | 65 | puts " - #{@model_class} #{data.inspect}" unless @options[:quiet] 66 | 67 | record.assign_attributes(data) 68 | record.save(validate: false) || raise(ActiveRecord::RecordNotSaved, 'Record not saved!') 69 | record 70 | end 71 | 72 | def find_or_initialize_record(data) 73 | @model_class.where(constraint_conditions(data)).take || 74 | @model_class.new 75 | end 76 | 77 | def constraint_conditions(data) 78 | @constraints.to_h { |c| [c, data[c.to_sym]] } 79 | end 80 | 81 | def update_id_sequence 82 | return unless %w[PostgreSQL PostGIS].include?(@model_class.connection.adapter_name) 83 | return if @model_class.primary_key.nil? || @model_class.sequence_name.nil? 84 | 85 | quoted_id = @model_class.connection.quote_column_name(@model_class.primary_key) 86 | sequence = @model_class.sequence_name 87 | 88 | if @model_class.connection.postgresql_version >= 100_000 89 | sql = <<-EOS 90 | SELECT setval('#{sequence}', (SELECT GREATEST(MAX(#{quoted_id})+(SELECT seqincrement FROM pg_sequence WHERE seqrelid = '#{sequence}'::regclass), (SELECT seqmin FROM pg_sequence WHERE seqrelid = '#{sequence}'::regclass)) FROM #{@model_class.quoted_table_name}), false) 91 | EOS 92 | else 93 | sql = <<-EOS 94 | SELECT setval('#{sequence}', (SELECT GREATEST(MAX(#{quoted_id})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{@model_class.quoted_table_name}), false) 95 | EOS 96 | end 97 | 98 | @model_class.connection.execute sql 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/seed-do/writer.rb: -------------------------------------------------------------------------------- 1 | module SeedDo 2 | # {Writer} is used to programmatically generated seed files. For example, you might want to write 3 | # a script which converts data in a CSV file to a valid Seed Do seed file, which can then be 4 | # imported. 5 | # 6 | # @example Basic usage 7 | # SeedDo::Writer.write('path/to/file.rb', :class_name => 'Person', :constraints => [:first_name, :last_name]) do |writer| 8 | # writer.add(:first_name => 'Jon', :last_name => 'Smith', :age => 21) 9 | # writer.add(:first_name => 'Emily', :last_name => 'McDonald', :age => 24) 10 | # end 11 | # 12 | # # Writes the following to the file: 13 | # # 14 | # # Person.seed(:first_name, :last_name, 15 | # # {:first_name=>"Jon", :last_name=>"Smith", :age=>21}, 16 | # # {:first_name=>"Emily", :last_name=>"McDonald", :age=>24} 17 | # # ) 18 | class Writer 19 | cattr_accessor :default_options 20 | @@default_options = { 21 | chunk_size: 100, 22 | constraints: [:id], 23 | seed_type: :seed 24 | } 25 | 26 | # @param [Hash] options 27 | # @option options [String] :class_name *Required* The name of the Active Record model to 28 | # generate seeds for 29 | # @option options [Fixnum] :chunk_size (100) The number of seeds to write before generating a 30 | # `# BREAK EVAL` line. (Chunking reduces memory usage when loading seeds.) 31 | # @option options [:seed, :seed_once] :seed_type (:seed) The method to use when generating 32 | # seeds. See {ActiveRecordExtension} for details. 33 | # @option options [Array] :constraints ([:id]) The constraining attributes for the seeds 34 | def initialize(options = {}) 35 | @options = self.class.default_options.merge(options) 36 | raise ArgumentError, 'missing option :class_name' unless @options[:class_name] 37 | end 38 | 39 | # Creates a new instance of {Writer} with the `options`, and then calls {#write} with the 40 | # `io_or_filename` and `block` 41 | def self.write(io_or_filename, options = {}, &) 42 | new(options).write(io_or_filename, &) 43 | end 44 | 45 | # Writes the necessary headers and footers, and yields to a block within which the actual 46 | # seed data should be writting using the `#<<` method. 47 | # 48 | # @param [IO] io_or_filename The IO to which writes will be made. (If an `IO` is given, it is 49 | # your responsibility to close it after writing.) 50 | # @param [String] io_or_filename The filename of a file to make writes to. (Will be opened and 51 | # closed automatically.) 52 | # @yield [self] make calls to `#<<` within the block 53 | def write(io_or_filename, &block) 54 | raise ArgumentError, 'missing block' unless block_given? 55 | 56 | if io_or_filename.respond_to?(:write) 57 | write_to_io(io_or_filename, &block) 58 | else 59 | File.open(io_or_filename, 'w') do |file| 60 | write_to_io(file, &block) 61 | end 62 | end 63 | end 64 | 65 | # Add a seed. Must be called within a block passed to {#write}. 66 | # @param [Hash] seed The attributes for the seed 67 | def <<(seed) 68 | raise 'You must add seeds inside a SeedDo::Writer#write block' unless @io 69 | 70 | buffer = +'' 71 | 72 | if chunk_this_seed? 73 | buffer << seed_footer 74 | buffer << "# BREAK EVAL\n" 75 | buffer << seed_header 76 | end 77 | 78 | buffer << ",\n" 79 | buffer << (' ' + seed.inspect) 80 | 81 | @io.write(buffer) 82 | 83 | @count += 1 84 | end 85 | alias add << 86 | 87 | private 88 | 89 | def write_to_io(io) 90 | @io = io 91 | @count = 0 92 | @io.write(file_header) 93 | @io.write(seed_header) 94 | yield(self) 95 | @io.write(seed_footer) 96 | @io.write(file_footer) 97 | ensure 98 | @io = nil 99 | @count = nil 100 | end 101 | 102 | def file_header 103 | <<~END 104 | # DO NOT MODIFY THIS FILE, it was auto-generated. 105 | # 106 | # Date: #{Time.now} 107 | # Seeding #{@options[:class_name]} 108 | # Written with the command: 109 | # 110 | # #{$0} #{$*.join} 111 | # 112 | END 113 | end 114 | 115 | def file_footer 116 | <<~END 117 | # End auto-generated file. 118 | END 119 | end 120 | 121 | def seed_header 122 | constraints = @options[:constraints] && @options[:constraints].map(&:inspect).join(', ') 123 | "#{@options[:class_name]}.#{@options[:seed_type]}(#{constraints}" 124 | end 125 | 126 | def seed_footer 127 | "\n)\n" 128 | end 129 | 130 | def chunk_this_seed? 131 | @count != 0 && (@count % @options[:chunk_size]) == 0 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/seeder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SeedDo::Seeder do 4 | it 'should work with negative seeds' do 5 | SeededModel.seed(:id) do |s| 6 | s.id = 10 7 | s.login = 'bob2' 8 | s.first_name = 'Bob2' 9 | s.last_name = 'Bobson2' 10 | s.title = 'Peaon2' 11 | end 12 | 13 | SeededModel.seed(:id) do |s| 14 | s.id = -2 15 | s.login = 'bob' 16 | s.first_name = 'Bob' 17 | s.last_name = 'Bobson' 18 | s.title = 'Peon' 19 | end 20 | 21 | bob = SeededModel.find_by_id(-2) 22 | expect(bob.first_name).to eq 'Bob' 23 | expect(bob.last_name).to eq 'Bobson' 24 | 25 | if ENV['DB'] == 'postgresql' 26 | next_id = SeededModel.connection.execute("select nextval('seeded_models_id_seq')") 27 | expect(next_id[0]['nextval'].to_i).to eq 11 28 | end 29 | end 30 | 31 | it "should create a model if one doesn't exist" do 32 | SeededModel.seed(:id) do |s| 33 | s.id = 5 34 | s.login = 'bob' 35 | s.first_name = 'Bob' 36 | s.last_name = 'Bobson' 37 | s.title = 'Peon' 38 | end 39 | 40 | bob = SeededModel.find_by_id(5) 41 | expect(bob.first_name).to eq 'Bob' 42 | expect(bob.last_name).to eq 'Bobson' 43 | end 44 | 45 | it 'should be able to handle multiple constraints' do 46 | SeededModel.seed(:title, :login) do |s| 47 | s.login = 'bob' 48 | s.title = 'Peon' 49 | s.first_name = 'Bob' 50 | end 51 | 52 | expect(SeededModel.count).to eq 1 53 | 54 | SeededModel.seed(:title, :login) do |s| 55 | s.login = 'frank' 56 | s.title = 'Peon' 57 | s.first_name = 'Frank' 58 | end 59 | 60 | expect(SeededModel.count).to eq 2 61 | 62 | expect(SeededModel.find_by_login('bob').first_name).to eq 'Bob' 63 | SeededModel.seed(:title, :login) do |s| 64 | s.login = 'bob' 65 | s.title = 'Peon' 66 | s.first_name = 'Steve' 67 | end 68 | expect(SeededModel.find_by_login('bob').first_name).to eq 'Steve' 69 | end 70 | 71 | it 'should be able to create models from an array of seed attributes' do 72 | SeededModel.seed(:title, :login, [ 73 | { login: 'bob', title: 'Peon', first_name: 'Steve' }, 74 | { login: 'frank', title: 'Peasant', first_name: 'Francis' }, 75 | { login: 'harry', title: 'Noble', first_name: 'Harry' } 76 | ]) 77 | 78 | expect(SeededModel.find_by_login('bob').first_name).to eq 'Steve' 79 | expect(SeededModel.find_by_login('frank').first_name).to eq 'Francis' 80 | expect(SeededModel.find_by_login('harry').first_name).to eq 'Harry' 81 | end 82 | 83 | it 'should be able to create models from a list of seed attribute hashes at the end of the args' do 84 | SeededModel.seed(:title, :login, 85 | { login: 'bob', title: 'Peon', first_name: 'Steve' }, 86 | { login: 'frank', title: 'Peasant', first_name: 'Francis' }, 87 | { login: 'harry', title: 'Noble', first_name: 'Harry' }) 88 | 89 | expect(SeededModel.find_by_login('bob').first_name).to eq 'Steve' 90 | expect(SeededModel.find_by_login('frank').first_name).to eq 'Francis' 91 | expect(SeededModel.find_by_login('harry').first_name).to eq 'Harry' 92 | end 93 | 94 | it 'should update, not create, if constraints are met' do 95 | SeededModel.seed(:id) do |s| 96 | s.id = 1 97 | s.login = 'bob' 98 | s.first_name = 'Bob' 99 | s.last_name = 'Bobson' 100 | s.title = 'Peon' 101 | end 102 | 103 | SeededModel.seed(:id) do |s| 104 | s.id = 1 105 | s.login = 'bob' 106 | s.first_name = 'Robert' 107 | s.last_name = 'Bobson' 108 | s.title = 'Peon' 109 | end 110 | 111 | bob = SeededModel.find_by_id(1) 112 | expect(bob.first_name).to eq 'Robert' 113 | expect(bob.last_name).to eq 'Bobson' 114 | end 115 | 116 | it 'should create but not update with seed_once' do 117 | SeededModel.seed_once(:id) do |s| 118 | s.id = 1 119 | s.login = 'bob' 120 | s.first_name = 'Bob' 121 | s.last_name = 'Bobson' 122 | s.title = 'Peon' 123 | end 124 | 125 | SeededModel.seed_once(:id) do |s| 126 | s.id = 1 127 | s.login = 'bob' 128 | s.first_name = 'Robert' 129 | s.last_name = 'Bobson' 130 | s.title = 'Peon' 131 | end 132 | 133 | bob = SeededModel.find_by_id(1) 134 | expect(bob.first_name).to eq 'Bob' 135 | expect(bob.last_name).to eq 'Bobson' 136 | end 137 | 138 | it 'should default to an id constraint' do 139 | SeededModel.seed(title: 'Bla', id: 1) 140 | SeededModel.seed(title: 'Foo', id: 1) 141 | 142 | expect(SeededModel.find(1).title).to eq 'Foo' 143 | end 144 | 145 | it 'should require that all constraints are defined' do 146 | expect { SeededModel.seed(:doesnt_exist, title: 'Bla') }.to raise_error(ArgumentError) 147 | end 148 | 149 | it 'should not perform validation' do 150 | expect { SeededModel.seed(id: 1) }.not_to raise_error 151 | end 152 | 153 | if ENV['DB'] == 'postgresql' 154 | it 'should update the primary key sequence after a records have been seeded' do 155 | id = SeededModel.connection.select_value("SELECT currval('seeded_models_id_seq')").to_i + 1 156 | SeededModel.seed(title: 'Foo', id: id) 157 | 158 | expect { SeededModel.create!(title: 'Bla') }.not_to raise_error 159 | end 160 | 161 | it 'should not raise error when there is no primary key specified' do 162 | expect { SeededModelNoPrimaryKey.seed(id: 'Id') }.not_to raise_error 163 | end 164 | 165 | it 'should not raise error when there is primary key without sequence' do 166 | expect { SeededModelNoSequence.seed(id: 'Id') }.not_to raise_error 167 | end 168 | end 169 | 170 | it 'should raise an ActiveRecord::RecordNotSaved exception if any records fail to save' do 171 | expect { SeededModel.seed(fail_to_save: true, title: 'Foo') }.to raise_error(ActiveRecord::RecordNotSaved) 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2025-12-23 00:26:17 UTC using RuboCop version 1.82.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: EnforcedStyle, AllowedGems. 11 | # SupportedStyles: Gemfile, gems.rb, gemspec 12 | Gemspec/DevelopmentDependencies: 13 | Exclude: 14 | - 'seed-do.gemspec' 15 | 16 | # Offense count: 1 17 | # This cop supports unsafe autocorrection (--autocorrect-all). 18 | Lint/UselessOr: 19 | Exclude: 20 | - 'lib/seed-do/seeder.rb' 21 | 22 | # Offense count: 1 23 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 24 | Metrics/AbcSize: 25 | Max: 19 26 | 27 | # Offense count: 2 28 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 29 | # AllowedMethods: refine 30 | Metrics/BlockLength: 31 | Max: 140 32 | 33 | # Offense count: 3 34 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 35 | Metrics/MethodLength: 36 | Max: 15 37 | 38 | # Offense count: 1 39 | # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. 40 | # CheckDefinitionPathHierarchyRoots: lib, spec, test, src 41 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 42 | Naming/FileName: 43 | Exclude: 44 | - 'Rakefile.rb' 45 | - 'lib/seed-do.rb' 46 | 47 | # Offense count: 5 48 | # Configuration parameters: ForbiddenDelimiters. 49 | # ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) 50 | Naming/HeredocDelimiterNaming: 51 | Exclude: 52 | - 'lib/seed-do/seeder.rb' 53 | - 'lib/seed-do/writer.rb' 54 | - 'lib/tasks/seed_do.rake' 55 | 56 | # Offense count: 2 57 | Security/Eval: 58 | Exclude: 59 | - 'lib/seed-do/runner.rb' 60 | 61 | # Offense count: 1 62 | Security/Open: 63 | Exclude: 64 | - 'lib/seed-do/runner.rb' 65 | 66 | # Offense count: 3 67 | Style/ClassVars: 68 | Exclude: 69 | - 'lib/seed-do.rb' 70 | - 'lib/seed-do/writer.rb' 71 | 72 | # Offense count: 1 73 | # This cop supports safe autocorrection (--autocorrect). 74 | # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. 75 | # SupportedStyles: assign_to_condition, assign_inside_condition 76 | Style/ConditionalAssignment: 77 | Exclude: 78 | - 'lib/seed-do/seeder.rb' 79 | 80 | # Offense count: 3 81 | # Configuration parameters: AllowedConstants. 82 | Style/Documentation: 83 | Exclude: 84 | - 'spec/**/*' 85 | - 'test/**/*' 86 | - 'lib/seed-do.rb' 87 | - 'lib/seed-do/active_record_extension.rb' 88 | - 'lib/seed-do/railtie.rb' 89 | 90 | # Offense count: 28 91 | # This cop supports unsafe autocorrection (--autocorrect-all). 92 | # Configuration parameters: EnforcedStyle. 93 | # SupportedStyles: always, always_true, never 94 | Style/FrozenStringLiteralComment: 95 | Enabled: false 96 | 97 | # Offense count: 1 98 | # This cop supports unsafe autocorrection (--autocorrect-all). 99 | # Configuration parameters: AllowSplatArgument. 100 | Style/HashConversion: 101 | Exclude: 102 | - 'lib/seed-do/seeder.rb' 103 | 104 | # Offense count: 1 105 | # This cop supports unsafe autocorrection (--autocorrect-all). 106 | Style/LineEndConcatenation: 107 | Exclude: 108 | - 'lib/seed-do/seeder.rb' 109 | 110 | # Offense count: 1 111 | Style/MissingRespondToMissing: 112 | Exclude: 113 | - 'lib/seed-do/block_hash.rb' 114 | 115 | # Offense count: 1 116 | # This cop supports unsafe autocorrection (--autocorrect-all). 117 | # Configuration parameters: EnforcedStyle. 118 | # SupportedStyles: literals, strict 119 | Style/MutableConstant: 120 | Exclude: 121 | - 'lib/seed-do/version.rb' 122 | 123 | # Offense count: 1 124 | # This cop supports unsafe autocorrection (--autocorrect-all). 125 | # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. 126 | # SupportedStyles: predicate, comparison 127 | Style/NumericPredicate: 128 | Exclude: 129 | - 'spec/**/*' 130 | - 'lib/seed-do/writer.rb' 131 | 132 | # Offense count: 1 133 | # This cop supports unsafe autocorrection (--autocorrect-all). 134 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. 135 | # AllowedMethods: present?, blank?, presence, try, try! 136 | Style/SafeNavigation: 137 | Exclude: 138 | - 'lib/seed-do/writer.rb' 139 | 140 | # Offense count: 1 141 | # This cop supports unsafe autocorrection (--autocorrect-all). 142 | Style/SelectByRegexp: 143 | Exclude: 144 | - 'lib/seed-do/runner.rb' 145 | 146 | # Offense count: 1 147 | # This cop supports unsafe autocorrection (--autocorrect-all). 148 | Style/SlicingWithRange: 149 | Exclude: 150 | - 'lib/seed-do/runner.rb' 151 | 152 | # Offense count: 4 153 | # This cop supports unsafe autocorrection (--autocorrect-all). 154 | # Configuration parameters: RequireEnglish. 155 | # SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names 156 | Style/SpecialGlobalVars: 157 | EnforcedStyle: use_perl_names 158 | 159 | # Offense count: 10 160 | # This cop supports unsafe autocorrection (--autocorrect-all). 161 | # Configuration parameters: Mode. 162 | Style/StringConcatenation: 163 | Exclude: 164 | - 'lib/seed-do/railtie.rb' 165 | - 'lib/seed-do/seeder.rb' 166 | - 'lib/seed-do/writer.rb' 167 | - 'lib/tasks/seed_do.rake' 168 | - 'spec/connections/sqlite3.rb' 169 | - 'spec/runner_spec.rb' 170 | - 'spec/spec_helper.rb' 171 | - 'spec/writer_spec.rb' 172 | 173 | # Offense count: 2 174 | # This cop supports safe autocorrection (--autocorrect). 175 | # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. 176 | # URISchemes: http, https 177 | Layout/LineLength: 178 | Max: 296 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seed Do 2 | 3 | This project is a fork of [seed-fu](https://github.com/mbleigh/seed-fu). 4 | 5 | Seed Do is an attempt to once and for all solve the problem of inserting and maintaining seed data in a database. It uses a variety of techniques gathered from various places around the web and combines them to create what is hopefully the most robust seed data system around. 6 | 7 | ## Installation 8 | 9 | Just add `gem 'seed-do'` to your `Gemfile` 10 | 11 | ## Documentation 12 | 13 | For detailed API documentation, please visit: https://rubydoc.info/github/willnet/seed-do 14 | 15 | ## Migrating from seed-fu 16 | 17 | If you are migrating from seed-fu to seed-do, you'll need to make the following changes: 18 | 19 | 1. Rename all occurrences of `SeedFu` to `SeedDo` in your code 20 | 2. Update rake task references from `rake db:seed_fu` to `rake db:seed_do` 21 | 22 | For example: 23 | 24 | ```ruby 25 | # Before 26 | SeedFu.seed 27 | rake db:seed_fu 28 | 29 | # After 30 | SeedDo.seed 31 | rake db:seed_do 32 | ``` 33 | 34 | Capistrano integration also needs to be updated: 35 | 36 | ```ruby 37 | # Before 38 | require 'seed-fu/capistrano' 39 | # or for Capistrano3 40 | require 'seed-fu/capistrano3' 41 | 42 | # After 43 | require 'seed-do/capistrano' 44 | # or for Capistrano3 45 | require 'seed-do/capistrano3' 46 | ``` 47 | 48 | ## Basic Example 49 | 50 | 51 | ### In `db/fixtures/users.rb` 52 | 53 | ```ruby 54 | User.seed do |s| 55 | s.id = 1 56 | s.login = "jon" 57 | s.email = "jon@example.com" 58 | s.name = "Jon" 59 | end 60 | 61 | User.seed do |s| 62 | s.id = 2 63 | s.login = "emily" 64 | s.email = "emily@example.com" 65 | s.name = "Emily" 66 | end 67 | ``` 68 | 69 | ### To load the data: 70 | 71 | ```ruby 72 | $ rake db:seed_do 73 | == Seed from /path/to/app/db/fixtures/users.rb 74 | - User {:id=>1, :login=>"jon", :email=>"jon@example.com", :name=>"Jon"} 75 | - User {:id=>2, :login=>"emily", :email=>"emily@example.com", :name=>"Emily"} 76 | ``` 77 | 78 | ## Constraints 79 | 80 | Constraints are used to identify seeds, so that they can be updated if necessary. For example: 81 | 82 | ```ruby 83 | Point.seed(:x, :y) do |s| 84 | s.x = 4 85 | s.y = 7 86 | s.name = "Home" 87 | end 88 | ``` 89 | 90 | The first time this seed is loaded, a `Point` record will be created. Now suppose the name is changed: 91 | 92 | ```ruby 93 | Point.seed(:x, :y) do |s| 94 | s.x = 4 95 | s.y = 7 96 | s.name = "Work" 97 | end 98 | ``` 99 | 100 | When this is run, Seed Do will look for a `Point` based on the `:x` and `:y` constraints provided. It will see that a matching `Point` already exists and so update its attributes rather than create a new record. 101 | 102 | If you do not want seeds to be updated after they have been created, use `seed_once`: 103 | 104 | ```ruby 105 | Point.seed_once(:x, :y) do |s| 106 | s.x = 4 107 | s.y = 7 108 | s.name = "Home" 109 | end 110 | ``` 111 | 112 | The default constraint just checks the `id` of the record. 113 | 114 | ## Where to put seed files 115 | 116 | By default, seed files are looked for in the following locations: 117 | 118 | - `#{Rails.root}/db/fixtures` and `#{Rails.root}/db/fixtures/#{Rails.env}` in a Rails app 119 | - `./db/fixtures` when loaded without Rails 120 | 121 | You can change these defaults by modifying the `SeedDo.fixture_paths` array. 122 | 123 | Seed files can be named whatever you like, and are loaded in alphabetical order. 124 | 125 | ## Terser syntax 126 | 127 | When loading lots of records, the above block-based syntax can be quite verbose. You can use the following instead: 128 | 129 | ```ruby 130 | User.seed(:id, 131 | { :id => 1, :login => "jon", :email => "jon@example.com", :name => "Jon" }, 132 | { :id => 2, :login => "emily", :email => "emily@example.com", :name => "Emily" } 133 | ) 134 | ``` 135 | 136 | ## Rake task 137 | 138 | Seed files can be run automatically using `rake db:seed_do`. There are two options which you can pass: 139 | 140 | - `rake db:seed_do FIXTURE_PATH=path/to/fixtures` -- Where to find the fixtures 141 | - `rake db:seed_do FILTER=users,articles` -- Only run seed files with a filename matching the `FILTER` 142 | 143 | You can also do a similar thing in your code by calling `SeedDo.seed(fixture_paths, filter)`. 144 | 145 | ## Disable output 146 | 147 | To disable output from Seed Do, set `SeedDo.quiet = true`. 148 | 149 | ## Handling large seed files 150 | 151 | Seed files can be huge. To handle large files (over a million rows), try these tricks: 152 | 153 | - Gzip your fixtures. Seed Do will read .rb.gz files happily. `gzip -9` gives the best compression, and with Seed Do's repetitive syntax, a 160M file can shrink to 16M. 154 | - Add lines reading `# BREAK EVAL` in your big fixtures, and Seed Do will avoid loading the whole file into memory. If you use `SeedDo::Writer`, these breaks are built into your generated fixtures. 155 | - Load a single fixture at a time with the `FILTER` environment variable 156 | - If you don't need Seed Do's ability to update seed with new data, then you may find that [activerecord-import](https://github.com/zdennis/activerecord-import) is faster 157 | 158 | ## Generating seed files 159 | 160 | If you need to programmatically generate seed files, for example to convert a CSV file into a seed file, then you can use [`SeedDo::Writer`](lib/seed-do/writer.rb). 161 | 162 | ## Capistrano deployment 163 | 164 | SeedDo has included Capistrano [deploy script](lib/seed-do/capistrano.rb), you just need require that 165 | in `config/deploy.rb`: 166 | 167 | ```ruby 168 | require 'seed-do/capistrano' 169 | 170 | # Trigger the task after update_code 171 | after 'deploy:update_code', 'db:seed_do' 172 | ``` 173 | 174 | If you use Capistrano3, you should require another file. 175 | 176 | ```ruby 177 | require 'seed-do/capistrano3' 178 | 179 | # Trigger the task before publishing 180 | before 'deploy:publishing', 'db:seed_do' 181 | ``` 182 | 183 | ## Development 184 | 185 | ### To run the specs 186 | 187 | ``` 188 | bundle install # Install the dependencies 189 | bundle exec rspec # Run the specs 190 | ``` 191 | 192 | By default an sqlite3 database is used. 193 | 194 | ### To test others 195 | 196 | ``` 197 | DB=mysql2 bundle exec rspec 198 | DB=postgresql bundle exec rspec 199 | ``` 200 | 201 | The connection paramaters for each of these are specified in spec/connections/, which you can edit if necessary (for example to change the username/password). 202 | 203 | ## Original Author 204 | 205 | [Michael Bleigh](http://www.mbleigh.com/) is the original author 206 | 207 | Copyright © 2008-2010 Michael Bleigh, released under the MIT license 208 | --------------------------------------------------------------------------------