seeder 0.1.3 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a5611b4210e0232e190e6a834a990a450a3f8a97
4
+ data.tar.gz: b4f69c31fe5f82988f4e8174ad6f9f5349ae07a5
5
+ SHA512:
6
+ metadata.gz: cb23ee0d4e02b6656024bad8cf34229d115e74dfe7553adccdb5424297aad92a4271c70e95e03601a4b60e7b59ac236ba466112efbf1a57ef7707300105ec92f
7
+ data.tar.gz: 39ce51f9890f2183e46d1845bb3a939a31c955dfbb4aee24b4803b3427ee870b3e2919c7382242337ceb4a6f7c215d5c65cf4bbe55a61bc6830075d8fa366da8
@@ -0,0 +1 @@
1
+ Gemfile.lock
@@ -0,0 +1,18 @@
1
+ sudo: false
2
+ cache: bundler
3
+ language: ruby
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1.2
8
+ - 2.2.3
9
+ env:
10
+ global:
11
+ - NOKOGIRI_USE_SYSTEM_LIBRARIES=true
12
+ matrix:
13
+ - AR=3.2.18
14
+ - AR=4.0.5
15
+ - AR=4.1.1
16
+ - AR=4.2.5
17
+ before_script:
18
+ - mysql -e 'create database seeder_test;'
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ # The AR environment variable lets you test against different
5
+ # versions of Rails. For example:
6
+ #
7
+ # AR=3.2.13 rm Gemfile.lock && bundle install && bundle exec rspec
8
+ # AR=4.0.0 rm Gemfile.lock && bundle install && bundle exec rspec
9
+ #
10
+ if ENV['AR']
11
+ gem 'activerecord', ENV['AR']
12
+ end
data/README.md CHANGED
@@ -1,49 +1,95 @@
1
- Description
2
- ===========
1
+ # seeder
3
2
 
4
- Seeder provides a way for your app to plant seed data in its database.
5
- It respects unique keys in your database, so that it does the equivalent of
6
- MySQL's "on duplicate key update" (but this is database agnostic)
3
+ Seeder provides a way for your app to plant seed data in its database. A
4
+ primary benefit of using this gem is that it gives you an easy way to manage
5
+ updating and deleting seed data, in addition to just creating it.
7
6
 
7
+ ## Install
8
8
 
9
- Install
10
- =======
9
+ ```
10
+ gem install seeder
11
+ ```
12
+
13
+ ## Motivation
14
+
15
+ Seed data refers to data stored in your database that is not generated
16
+ dynamically through your application. You can generate your seed data via
17
+ `rake db:seed`, which runs the contents of your `db/seeds.rb` file.
18
+
19
+ Generating seed data is easy enough. But as your application grows and
20
+ changes, you may find that you need to modify existing seed data.
21
+
22
+ You could run migrations to modify seed data, but you would need to make sure
23
+ to adjust the seeds.rb file in a consistent way. It is very easy to forget to
24
+ do this, or to make a mistake along the way.
25
+
26
+ Seeder allows you to manage all of your seeds via the seeds.rb file, without
27
+ needing to use migrations when you have to change existing seed data.
28
+
29
+ You provide a set of attributes associated with your seed data for a given
30
+ model. You also specify which attributes of the model should be treated as
31
+ "identifying attributes" - that is, which attributes should be used to
32
+ determine if an existing database row needs to be updated, or if a new row
33
+ needs to be added. Seeder then synchronizes the contents of the database table
34
+ to the attributes it is given.
35
+
36
+ ## Usage
37
+
38
+ Suppose you have a `DataField` model, with attributes `data_type`, `name`, and
39
+ `description`.
11
40
 
12
- $ gem install seeder
41
+ To seed your database with a couple data fields, you would just include the
42
+ following in your seeds.rb file:
13
43
 
14
- Usage
15
- =====
44
+ ```ruby
45
+ seeds = [
46
+ { data_type: "Oil", name: "btu", description: "Oil usage" },
47
+ { data_type: "Water", name: "gallons", description: "Water usage" }
48
+ ]
49
+
50
+ Seeder.create(seeds, [:data_type, :name], DataField)
51
+ ```
52
+
53
+ In the example above, the `data_type` and `name` attributes will be used to
54
+ identify records that already exist in the database. The first time you seed
55
+ data, there are no existing records so two new records will be created. If you
56
+ re-seed the database, Seeder will not do anything, since the records already
57
+ exist in the database, and no attributes have changed.
16
58
 
17
- Suppose you have a `User` model
59
+ Suppose the database has already been seeded, and you decide to change the
60
+ description of the Oil btu data field. All you need to do is update the
61
+ seeds.rb file and rerun the `db:seed` Rake task. Seeder will know to update the
62
+ _existing_ database record associated with Oil btu:
18
63
 
19
- And suppose that user model has fields "name", "age", "address", "gender"
64
+ ```ruby
65
+ seeds = [
66
+ { data_type: "Oil", name: "btu", description: "Fuel oil usage" },
67
+ { data_type: "Water", name: "gallons", description: "Water usage" }
68
+ ]
20
69
 
21
- Finally, suppose you've set up your database to have a unique key on the fields
22
- "name" and "address" (so two people can have the same name or live at the
23
- same address, but not both)
70
+ Seeder.create(seeds, [:data_type, :name], DataField)
71
+ ```
24
72
 
25
- If you wanted to seed your data with a couple users you could do the following:
73
+ If you then want to add an Oil therms DataField record to the db, you could
74
+ just add a line to your seeds.rb file:
26
75
 
27
76
  ```ruby
28
- seed_users = [
29
- {
30
- :name => "J'onn J'onzz",
31
- :age => 94,
32
- :address => 'Mars',
33
- :gender => 'Male'
34
- },
35
- {
36
- :name => "Barbara Gordon",
37
- :age => 35,
38
- :address => '14 Gotham Heights',
39
- :gender => 'Female'
40
- }
77
+ seeds = [
78
+ { data_type: "Oil", name: "btu", description: "Fuel oil usage (btu)" },
79
+ { data_type: "Oil", name: "therms", description: "Fuel oil usage (therms)" },
80
+ { data_type: "Water", name: "gallons", description: "Water usage" }
41
81
  ]
42
82
 
43
- SeedData.create {'User' => seed_users}, [:name, :address], User
83
+ Seeder.create(seeds, [:data_type, :name], DataField)
44
84
  ```
45
85
 
46
- License
47
- =======
86
+ The above code would adjust the description of the existing Oil btu database
87
+ row, and create a new one for Oil therms.
88
+
89
+ Now let's say you changed your mind and decided that you don't need an
90
+ Oil therms entry in the database. You could just delete the corresponding row
91
+ from seeds.rb, and Seeder will know to delete that entry from the database.
92
+
93
+ ## License
48
94
 
49
95
  See LICENSE.txt
data/Rakefile CHANGED
@@ -1,36 +1,3 @@
1
- require 'rake'
2
- require 'fileutils'
3
-
4
- def gemspec_name
5
- @gemspec_name ||= Dir['*.gemspec'][0]
6
- end
7
-
8
- def gemspec
9
- @gemspec ||= eval(File.read(gemspec_name), binding, gemspec_name)
10
- end
11
-
12
- desc "Build the gem"
13
- task :gem=>:gemspec do
14
- sh "gem build #{gemspec_name}"
15
- FileUtils.mkdir_p 'pkg'
16
- FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
17
- end
18
-
19
- desc "Install the gem locally"
20
- task :install => :gem do
21
- sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}}
22
- end
23
-
24
- desc "Generate the gemspec"
25
- task :generate do
26
- puts gemspec.to_ruby
27
- end
28
-
29
- desc "Validate the gemspec"
30
- task :gemspec do
31
- gemspec.validate
32
- end
33
-
34
1
  desc 'Run tests'
35
2
  task :test do |t|
36
3
  sh 'rspec spec'
@@ -1,78 +1,66 @@
1
1
  class Seeder
2
+ attr_reader :keys, :data, :model
2
3
 
3
- class << self
4
- QUOTING = {
5
- "ActiveRecord::ConnectionAdapters::MysqlAdapter" => "`",
6
- "ActiveRecord::ConnectionAdapters::Mysql2Adapter" => "`"
7
- }
4
+ def self.create(data, keys, model)
5
+ new(data, keys, model).create
6
+ end
8
7
 
9
- def existing_data_keys(ar_model)
10
- keys = @primary_keys[ar_model.name]
11
- ar_model.all.map{|item| item.attributes.symbolize_keys.values_at(*keys) }
12
- end
8
+ def initialize(data, keys, model)
9
+ @keys = keys.map(&:to_sym)
10
+ @data = data.map(&:symbolize_keys)
11
+ @model = model
12
+ end
13
13
 
14
- def seed_data_keys(ar_model)
15
- keys = @primary_keys[ar_model.name]
16
- @data[ar_model.name].collect{|attributes| attributes.values_at(*keys) }
14
+ def create
15
+ model.transaction do
16
+ delete_outdated_records
17
+ update_existing_records
18
+ create_new_records
17
19
  end
20
+ end
18
21
 
19
- def items_to_delete(ar_model)
20
- existing_data_keys(ar_model) - seed_data_keys(ar_model)
21
- end
22
+ def delete_outdated_records
23
+ return unless records_to_delete.present?
24
+ model.where(id: records_to_delete).delete_all
25
+ end
22
26
 
23
- def value_to_sql(value)
24
- if value.nil? then 'NULL'
25
- elsif value.is_a?(String) then "'#{value}'"
26
- elsif value == false then 0
27
- elsif value == true then 1
28
- else value
29
- end
30
- end
27
+ def update_existing_records
28
+ existing_records_keys_hash.each do |record_keys, record|
29
+ attributes = data_keys_hash[record_keys]
30
+ next unless attributes
31
31
 
32
- def quote(value)
33
- q = QUOTING[ActiveRecord::Base.connection.class.to_s] || '"'
34
- "#{q}#{value}#{q}"
32
+ record.attributes = attributes
33
+ record.save! if record.changed?
35
34
  end
35
+ end
36
36
 
37
- def add_new_or_changed_data(ar_model)
38
- keys = @primary_keys[ar_model.name]
39
- find_method = "find_by_#{keys.join('_and_')}"
40
-
41
- @data[ar_model.name].collect do |attributes|
42
- relevant_keys = attributes.keys & ar_model.column_names.map(&:to_sym)
43
- existing = ar_model.send(find_method, *attributes.values_at(*keys))
44
- values = attributes.values_at(*relevant_keys)
37
+ def create_new_records
38
+ new_keys = data_keys_hash.keys - existing_records_keys_hash.keys
45
39
 
46
- if existing
47
- updates = attributes.slice(*relevant_keys).map { |k,v| "#{quote(k)} = #{value_to_sql(v)}" }.join(", ")
48
- conditions = keys.map { |k| "#{quote(k)} = #{value_to_sql(existing.send(k))}" }.join(" AND ")
49
- %(UPDATE #{ar_model.table_name} SET #{updates} WHERE #{conditions})
50
- else
51
- columns = relevant_keys.map { |k| quote(k) }.join(", ")
52
- inserts = attributes.slice(*relevant_keys).values.map { |v| value_to_sql(v) }.join(", ")
53
- %(INSERT INTO #{ar_model.table_name} (#{columns}) VALUES (#{inserts}))
54
- end
55
- end.compact
40
+ data_keys_hash.values_at(*new_keys).each do |attributes|
41
+ model.create!(attributes)
56
42
  end
43
+ end
57
44
 
58
- def delete_outdated_data(ar_model)
59
- keys = @primary_keys[ar_model.name]
60
- items_to_delete(ar_model).collect do |field_to_delete|
61
- conditions = field_to_delete.to_enum.with_index.collect{|value, index| "#{keys[index]} = '#{value}'"}.join(' AND ')
62
- %{ DELETE FROM #{ar_model.table_name} WHERE #{conditions} }
63
- end.compact
45
+ private
46
+
47
+ def data_keys_hash
48
+ @data_keys_hash ||= data.inject({}) do |hash, attributes|
49
+ hash.merge!(attributes.values_at(*keys) => attributes)
64
50
  end
51
+ end
65
52
 
66
- def create(data, keys, models)
67
- @data, @primary_keys = data, keys
68
- models.each {|model|
69
- delete_outdated_data(model).each{|query| sql(query)}
70
- add_new_or_changed_data(model).each{|query| sql(query)}
71
- }
53
+ def existing_records_keys_hash
54
+ @existing_records_keys_hash ||= model.all.inject({}) do |hash, record|
55
+ record_keys = keys.map { |key| record.public_send(key) }
56
+ hash.merge!(record_keys => record)
72
57
  end
58
+ end
73
59
 
74
- def sql(sql_command)
75
- ActiveRecord::Base.connection.execute(sql_command)
60
+ def records_to_delete
61
+ @records_to_delete ||= begin
62
+ keys_to_delete = existing_records_keys_hash.keys - data_keys_hash.keys
63
+ existing_records_keys_hash.values_at(*keys_to_delete)
76
64
  end
77
65
  end
78
66
  end
@@ -1,3 +1,3 @@
1
1
  class Seeder
2
- VERSION = '0.1.3'
2
+ VERSION = '1.0'
3
3
  end
@@ -3,16 +3,20 @@ require 'rubygems' unless defined? Gem
3
3
  require File.dirname(__FILE__) + "/lib/seeder/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "seeder"
7
- s.version = Seeder::VERSION
8
- s.authors = ["Barun Singh", "Gabriel Horner"]
9
- s.email = "bsingh@wegowise.com"
10
- s.homepage = "http://github.com/wegowise/seeder"
11
- s.summary = "Seed your data"
12
- s.description = "Keep your app's seed data in one file and update it easily, while respecting key constraints"
6
+ s.name = "seeder"
7
+ s.version = Seeder::VERSION
8
+ s.authors = ["Barun Singh"]
9
+ s.email = "bsingh@wegowise.com"
10
+ s.homepage = "http://github.com/wegowise/seeder"
11
+ s.summary = "Manage seed data for your Rails app"
12
+ s.description = "Keep your app's seed data in one file and update it easily"
13
13
  s.required_rubygems_version = ">= 1.3.6"
14
- s.files = Dir.glob(%w[{lib,spec}/**/*.rb [A-Z]*.{txt,rdoc,md} *.gemspec]) + %w{Rakefile}
14
+ s.files = `git ls-files`.split("\n")
15
15
  s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
16
16
  s.license = 'MIT'
17
- s.add_development_dependency 'rspec', '~> 2.6.0'
17
+
18
+ s.add_dependency('activerecord', '>= 3.2', '< 5.0')
19
+
20
+ s.add_development_dependency('mysql2', '~> 0.3.10')
21
+ s.add_development_dependency('rspec-rails', '~> 3.0')
18
22
  end
@@ -0,0 +1,108 @@
1
+ require 'active_record'
2
+ require 'support/setup'
3
+ require 'seeder'
4
+
5
+ describe Seeder do
6
+ before(:each) { Grade.delete_all }
7
+
8
+ let(:seeder) do
9
+ Seeder.new(
10
+ [{ 'student_id' => 1, 'course_id' => 1, 'grade' => 90 },
11
+ { 'student_id' => 1, 'course_id' => 2, 'grade' => 80 }],
12
+ %w[student_id course_id],
13
+ Grade
14
+ )
15
+ end
16
+
17
+ describe '.create' do
18
+ it 'should call create on a new instance' do
19
+ seeder = double
20
+ expect(Seeder).to receive(:new).with(:data, :keys, :model)
21
+ .and_return(seeder)
22
+ expect(seeder).to receive(:create)
23
+
24
+ Seeder.create(:data, :keys, :model)
25
+ end
26
+ end
27
+
28
+ describe '#new' do
29
+ specify { expect(seeder.model).to eq Grade }
30
+ specify { expect(seeder.keys).to eq [:student_id, :course_id] }
31
+ specify do
32
+ expect(seeder.data).to eq(
33
+ [{ student_id: 1, course_id: 1, grade: 90 },
34
+ { student_id: 1, course_id: 2, grade: 80 }]
35
+ )
36
+ end
37
+ end
38
+
39
+ describe '#delete_outdated_records' do
40
+ before do
41
+ Grade.create!(student_id: 1, course_id: 3)
42
+ Grade.create!(student_id: 1, course_id: 1)
43
+ end
44
+
45
+ it 'should delete outdated records' do
46
+ expect { seeder.delete_outdated_records }.to change { Grade.count }.to(1)
47
+ expect(Grade.first.course_id).to eq 1
48
+ end
49
+ end
50
+
51
+ describe '#update_existing_records' do
52
+ let!(:existing_grade) { Grade.create!(student_id: 1, course_id: 1) }
53
+
54
+ it 'should delete outdated records' do
55
+ expect { seeder.update_existing_records }
56
+ .to change { existing_grade.reload.grade }.to(90)
57
+ end
58
+ end
59
+
60
+ describe '#create_new_records' do
61
+ let!(:existing_grade) { Grade.create!(student_id: 1, course_id: 1) }
62
+
63
+ it 'should create new records when there are no existing records with
64
+ matching keys' do
65
+ expect { seeder.create_new_records }.to change { Grade.count }.to(2)
66
+ expect(Grade.last.course_id).to eq 2
67
+ end
68
+ end
69
+
70
+ describe '#create' do
71
+ it 'calls the delete, update and create methods in order' do
72
+ expect(seeder).to receive(:delete_outdated_records).ordered
73
+ expect(seeder).to receive(:update_existing_records).ordered
74
+ expect(seeder).to receive(:create_new_records).ordered
75
+ seeder.create
76
+ end
77
+
78
+ it 'aborts when an exception is raised' do
79
+ expect(seeder).to receive(:create_new_records).and_raise
80
+ initial_attributes = Grade.all.map(&:attributes)
81
+
82
+ expect { seeder.create }.to raise_error
83
+
84
+ expect(Grade.all.map(&:attributes)).to eq(initial_attributes)
85
+ end
86
+
87
+ it 'produces the appropriate results' do
88
+ grade1 = Grade.create!(student_id: 1, course_id: 3)
89
+ grade2 = Grade.create!(student_id: 1, course_id: 1)
90
+
91
+ seeder.create
92
+
93
+ expect(Grade.count).to eq(2)
94
+ expect(Grade.exists?(grade1)).to eq(false)
95
+
96
+ grade2.reload
97
+ expect(grade2.student_id).to eq(1)
98
+ expect(grade2.course_id).to eq(1)
99
+ expect(grade2.grade).to eq(90)
100
+
101
+ grade3 = Grade.last
102
+ expect(grade3.student_id).to eq(1)
103
+ expect(grade3.course_id).to eq(2)
104
+ expect(grade3.grade).to eq(80)
105
+ end
106
+ end
107
+
108
+ end
@@ -0,0 +1,20 @@
1
+ ActiveRecord::Base.establish_connection({
2
+ adapter: 'mysql2',
3
+ username: 'travis',
4
+ database: 'seeder_test'
5
+ })
6
+
7
+ ActiveRecord::Base.connection.tables.each do |table|
8
+ ActiveRecord::Base.connection.drop_table table
9
+ end
10
+
11
+ ActiveRecord::Schema.define do
12
+ create_table :grades do |t|
13
+ t.integer :student_id
14
+ t.integer :course_id
15
+ t.integer :grade
16
+ end
17
+ end
18
+
19
+ class Grade < ActiveRecord::Base
20
+ end
metadata CHANGED
@@ -1,30 +1,64 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seeder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
5
- prerelease:
4
+ version: '1.0'
6
5
  platform: ruby
7
6
  authors:
8
7
  - Barun Singh
9
- - Gabriel Horner
10
8
  autorequire:
11
9
  bindir: bin
12
10
  cert_chain: []
13
- date: 2011-09-20 00:00:00.000000000Z
11
+ date: 2016-01-21 00:00:00.000000000 Z
14
12
  dependencies:
15
13
  - !ruby/object:Gem::Dependency
16
- name: rspec
17
- requirement: &2165419640 !ruby/object:Gem::Requirement
18
- none: false
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
19
16
  requirements:
20
- - - ~>
17
+ - - ">="
21
18
  - !ruby/object:Gem::Version
22
- version: 2.6.0
19
+ version: '3.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: mysql2
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.3.10
23
40
  type: :development
24
41
  prerelease: false
25
- version_requirements: *2165419640
26
- description: Keep your app's seed data in one file and update it easily, while respecting
27
- key constraints
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.3.10
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ description: Keep your app's seed data in one file and update it easily
28
62
  email: bsingh@wegowise.com
29
63
  executables: []
30
64
  extensions: []
@@ -32,36 +66,40 @@ extra_rdoc_files:
32
66
  - README.md
33
67
  - LICENSE.txt
34
68
  files:
35
- - lib/seeder/version.rb
36
- - lib/seeder.rb
69
+ - ".gitignore"
70
+ - ".travis.yml"
37
71
  - CHANGELOG.txt
72
+ - Gemfile
38
73
  - LICENSE.txt
39
74
  - README.md
40
- - seeder.gemspec
41
75
  - Rakefile
76
+ - lib/seeder.rb
77
+ - lib/seeder/version.rb
78
+ - seeder.gemspec
79
+ - spec/seeder_spec.rb
80
+ - spec/support/setup.rb
42
81
  homepage: http://github.com/wegowise/seeder
43
82
  licenses:
44
83
  - MIT
84
+ metadata: {}
45
85
  post_install_message:
46
86
  rdoc_options: []
47
87
  require_paths:
48
88
  - lib
49
89
  required_ruby_version: !ruby/object:Gem::Requirement
50
- none: false
51
90
  requirements:
52
- - - ! '>='
91
+ - - ">="
53
92
  - !ruby/object:Gem::Version
54
93
  version: '0'
55
94
  required_rubygems_version: !ruby/object:Gem::Requirement
56
- none: false
57
95
  requirements:
58
- - - ! '>='
96
+ - - ">="
59
97
  - !ruby/object:Gem::Version
60
98
  version: 1.3.6
61
99
  requirements: []
62
100
  rubyforge_project:
63
- rubygems_version: 1.8.10
101
+ rubygems_version: 2.4.5.1
64
102
  signing_key:
65
- specification_version: 3
66
- summary: Seed your data
103
+ specification_version: 4
104
+ summary: Manage seed data for your Rails app
67
105
  test_files: []