kaplan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +84 -0
- data/kaplan.gemspec +19 -0
- data/lib/kaplan.rb +172 -0
- data/lib/kaplan/railtie.rb +10 -0
- data/lib/kaplan/tasks/kaplan.rake +94 -0
- data/lib/kaplan/version.rb +3 -0
- metadata +73 -0
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Kaplan
|
2
|
+
|
3
|
+
## Summary
|
4
|
+
|
5
|
+
Kaplan provides some Rake tasks that are helpful in preparing your test database(s), such as seeding/plowing your database, or recreating it from your development database.
|
6
|
+
|
7
|
+
For instance, let's say you ran your unit tests, and somehow the database wasn't rolled back correctly. Simply run `rake kaplan:db:reset` -- that will dump the SQL schema for your development database to file and use it to replace your test database, then it will seed your test database.
|
8
|
+
|
9
|
+
A more common scenario might be, you ran your Cucumber tests and now you need to reset your integration test database back to zero before you run them again. In that case, add `Kaplan.plow_database()` to your Cucumber initialization file and all is well.
|
10
|
+
|
11
|
+
## Rationale
|
12
|
+
|
13
|
+
Doesn't Rails already provide a `db:reset` Rake task? Yes, but it runs `db:schema:dump` and `db:schema:load` behind the scenes, which isn't always a foolproof way of dumping your schema (there are a few cases, like custom `id` columns, where Rails drops the ball). `db:clone_structure` is almost always the better way to go. Also, eventually you'll reach a point in your Rails app where you need some data to be always in your development and test databases -- in other words, seed data. It would be nice if the `reset` task also seeded your test database too. Kaplan's `db:reset` task does both of these things.
|
14
|
+
|
15
|
+
Also, Rails doesn't give you a way to programmatically reset (or at least seed) your database. So if you want to do it inside of a script, you're forced to `require 'rake'` and then say `Rake::Task.invoke["db:reset"]`. That's ridiculous. Kaplan gives you methods for plowing and seeding the database.
|
16
|
+
|
17
|
+
Additionally, Kaplan improves Rails' seeding by 1) truncating the tables that will be seeded first, 2) allowing you to keep environment-specific seed files, and 2) allowing you to use YAML or text files since that's a simpler way of representing data (and you're probably used to seeing YAML for fixture data).
|
18
|
+
|
19
|
+
Finally, Kaplan was designed to be framework- and ORM-agnostic. Currently the following are supported:
|
20
|
+
|
21
|
+
* **Frameworks:**
|
22
|
+
* Rails
|
23
|
+
* Padrino
|
24
|
+
* **ORMs:**
|
25
|
+
* ActiveRecord
|
26
|
+
* Mongoid
|
27
|
+
|
28
|
+
It's easy to add support for another framework or ORM, though.
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Here are the Rake tasks.
|
33
|
+
|
34
|
+
* **kaplan:db:reset** - Resets the database corresponding to the given environment by re-creating it from the development schema and then re-seeding it. Accepts: `RAILS_ENV` (required, must not be "development").
|
35
|
+
* **kaplan:db:seed** - Seeds the database corresponding to the given environment with bootstrap data. The tables that will be seeded are truncated first so you don't have to. Accepts: `RAILS_ENV` (optional, default is "development").
|
36
|
+
* **kaplan:db:plow** - Truncates tables in the database corresponding to the given environment. By default this just truncates the seed tables, pass `ALL` to truncate everything. Accepts: `RAILS_ENV` (optional, default is "development"), `ALL` (optional).
|
37
|
+
* **kaplan:db:clone_structure** - Dumps the structure of the development database to file and copies it to the database corresponding to the given environment. The adapters between the databases must be the same. Accepts: `RAILS_ENV` (required, must not be "development").
|
38
|
+
* **kaplan:db:create** - Creates the database corresponding to the given environment. Accepts: `RAILS_ENV` (optional, default is "development").
|
39
|
+
* **kaplan:db:drop** - Drops the database corresponding to the given environment. Accepts: `RAILS_ENV` (optional, default is "development").
|
40
|
+
|
41
|
+
Seeding is a bit different than Rails's built-in seeding, so I'll talk a little bit about that. Basically, you can still keep seeds in `db/seeds.rb` like before, but if you want to break them into separate files, you can put them in `seeds/`, and Kaplan will look for them there too. In fact, the seed files don't have to be Ruby, they can also be YAML or text files. So a YAML seed file would look like:
|
42
|
+
|
43
|
+
people:
|
44
|
+
-
|
45
|
+
first_name: Joe
|
46
|
+
last_name: Bloe
|
47
|
+
likes: eating spaghetti, watching The Godfather
|
48
|
+
-
|
49
|
+
first_name: Jill
|
50
|
+
last_name: Schmoe
|
51
|
+
likes: biking, watching Family Guy
|
52
|
+
|
53
|
+
and a text file would look like (note it looks kind of like CSV, but not quite, because you can't quote fields):
|
54
|
+
|
55
|
+
# first name, last name, likes
|
56
|
+
Joe Bloe eating spaghetti, watching The Godfather
|
57
|
+
Jill Schmoe biking, watching Family Guy
|
58
|
+
|
59
|
+
In addition, you can have environment-specific files. So if I have a directory structure that looks like:
|
60
|
+
|
61
|
+
seeds/
|
62
|
+
people.txt
|
63
|
+
development/
|
64
|
+
cars.rb
|
65
|
+
venues.rb
|
66
|
+
test/
|
67
|
+
cars.rb
|
68
|
+
car_types.yml
|
69
|
+
|
70
|
+
Then, when I run `rake kaplan:db:seed` in different environments, this is which tables get populated:
|
71
|
+
|
72
|
+
+-------------+-------------------------+
|
73
|
+
| Environment | Tables |
|
74
|
+
+-------------+-------------------------+
|
75
|
+
| development | people, cars, venues |
|
76
|
+
| production | people |
|
77
|
+
| test | people, cars, car_types |
|
78
|
+
+-------------+-------------------------+
|
79
|
+
|
80
|
+
## Author/License
|
81
|
+
|
82
|
+
Kaplan is by Elliot Winkler (<elliot.winkler@gmail.com>).
|
83
|
+
|
84
|
+
There isn't a license; you may do what you like with it, as long as I'm not held responsible.
|
data/kaplan.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "kaplan/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "kaplan"
|
7
|
+
s.version = Kaplan::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Elliot Winkler"]
|
10
|
+
s.email = ["elliot.winkler@gmail.com"]
|
11
|
+
s.homepage = "http://rubygems.org/gems/kaplan"
|
12
|
+
s.summary = %q{Database- and framework-agnostic Rake tasks to prepare and seed your test database}
|
13
|
+
s.description = %q{Kaplan provides some Rake tasks that are helpful in preparing your test database(s), such as seeding/plowing your database, or recreating it from your development database.}
|
14
|
+
|
15
|
+
s.files = ["README.md", "kaplan.gemspec"] + Dir["lib/**/*"]
|
16
|
+
s.test_files = Dir["{test,spec/features}/**/*"]
|
17
|
+
s.executables = Dir["bin/**/*"].map {|f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
end
|
data/lib/kaplan.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/kaplan/railtie' if defined?(Rails)
|
2
|
+
|
3
|
+
module Kaplan
|
4
|
+
module DatabaseAdapters
|
5
|
+
module ActiveRecord
|
6
|
+
def database_settings
|
7
|
+
@database_settings ||= ::YAML.load_file("#{project_root}/config/database.yml")
|
8
|
+
end
|
9
|
+
|
10
|
+
def establish_database(env = current_environment)
|
11
|
+
::ActiveRecord::Base.establish_connection(database_settings[env.to_s])
|
12
|
+
end
|
13
|
+
|
14
|
+
def all_collections
|
15
|
+
::ActiveRecord::Base.connection.tables - ["schema_migrations"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def plow_collection(name)
|
19
|
+
::ActiveRecord::Base.connection.execute("TRUNCATE TABLE #{::ActiveRecord::Base.connection.quote_table_name(name)}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Mongoid
|
24
|
+
def database_settings
|
25
|
+
@database_settings ||= ::YAML.load_file("#{project_root}/config/database.mongo.yml")
|
26
|
+
end
|
27
|
+
|
28
|
+
def establish_database(env = current_environment)
|
29
|
+
::Mongoid.config.database = ::Mongo::Connection.new.db(database_settings[env.to_s]["database"])
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_collections
|
33
|
+
::Mongoid.database.collection_names - ["system.indexes"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def plow_collection(name)
|
37
|
+
::Mongoid.database.drop_collection(name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module WebFrameworks
|
43
|
+
module Rails
|
44
|
+
def project_root
|
45
|
+
::Rails.root
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_environment
|
49
|
+
::Rails.env.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_environment=(environment)
|
53
|
+
if ::Rails.version.split(".")[0] == "3"
|
54
|
+
::Rails.env = environment
|
55
|
+
else
|
56
|
+
silence_warnings do
|
57
|
+
::Rails.instance_variable_set("@_env", nil)
|
58
|
+
Object.const_set(:RAILS_ENV, environment)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
puts "Current environment: #{current_environment}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Padrino
|
66
|
+
def project_root
|
67
|
+
::Padrino.root
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_environment
|
71
|
+
::Padrino.env.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def current_environment=(environment)
|
75
|
+
silence_warnings do
|
76
|
+
::Padrino.instance_variable_set("@_env", nil)
|
77
|
+
Object.const_set(:PADRINO_ENV, environment)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class << self
|
84
|
+
def choose_database_adapter
|
85
|
+
if defined?(::ActiveRecord)
|
86
|
+
Kaplan.extend(Kaplan::DatabaseAdapters::ActiveRecord)
|
87
|
+
elsif defined?(::Mongoid)
|
88
|
+
Kaplan.extend(Kaplan::DatabaseAdapters::Mongoid)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def choose_web_framework
|
93
|
+
if defined?(::Rails)
|
94
|
+
Kaplan.extend(Kaplan::WebFrameworks::Rails)
|
95
|
+
elsif defined?(::Padrino)
|
96
|
+
Kaplan.extend(Kaplan::WebFrameworks::Padrino)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def seeds(env)
|
101
|
+
(Dir["#{project_root}/seeds/*"] + Dir["#{project_root}/seeds/#{env}/*"]).
|
102
|
+
reject {|filename| File.directory?(filename) }.
|
103
|
+
map do |filename|
|
104
|
+
basename = ::File.basename(filename)
|
105
|
+
basename =~ /^(.+?)\.([^.]+)$/
|
106
|
+
collection_name = $1
|
107
|
+
extension = $2.downcase
|
108
|
+
model = collection_name.classify.constantize
|
109
|
+
[filename, extension, collection_name, model]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
LEVELS = [:none, :info, :debug]
|
114
|
+
|
115
|
+
def seed_database(options={})
|
116
|
+
level = LEVELS.index(options[:level] || :debug)
|
117
|
+
options.reverse_merge!(:env => current_environment)
|
118
|
+
puts "Seeding the #{options[:env]} database..." if level > 0
|
119
|
+
establish_database(options[:env])
|
120
|
+
seeds(options[:env]).each do |filename, ext, collection_name, model|
|
121
|
+
if ext == "rb"
|
122
|
+
puts " - Adding data for #{collection_name}..." if level > 1
|
123
|
+
load filename
|
124
|
+
elsif ext == "yml" || ext == "yaml"
|
125
|
+
data = ::YAML.load_file(filename)
|
126
|
+
records = (Hash === data) ? data[data.keys.first] : data
|
127
|
+
puts " - Adding data for #{collection_name}..." if level > 1
|
128
|
+
insert_rows(records, model)
|
129
|
+
else
|
130
|
+
lines = ::File.read(filename).split(/\n/)
|
131
|
+
puts " - Adding data for #{collection_name}..." if level > 1
|
132
|
+
insert_rows_from_csv(lines, model)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def plow_database(options={})
|
138
|
+
level = LEVELS.index(options[:level] || :debug)
|
139
|
+
options.reverse_merge!(:env => current_environment)
|
140
|
+
puts "Plowing the #{options[:env]} database..." if level > 0
|
141
|
+
establish_database(options[:env])
|
142
|
+
collections = options[:all] ? all_collections : seedable_collections(options[:env])
|
143
|
+
collections.each do |coll|
|
144
|
+
plow_collection(coll)
|
145
|
+
puts " - Plowed #{coll}" if level > 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def seedable_collections(env)
|
150
|
+
# Remove collections that don't exist
|
151
|
+
seeds(env).map {|filename, ext, collection_name, model| collection_name } & all_collections
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
def insert_rows(rows, model)
|
156
|
+
rows.each {|row| model.create!(row) }
|
157
|
+
end
|
158
|
+
|
159
|
+
def insert_rows_from_csv(lines, model)
|
160
|
+
columns = lines.shift.sub(/^#[ ]*/, "").split(/,[ ]*/)
|
161
|
+
rows = lines.map do |line|
|
162
|
+
values = line.split(/\t|[ ]{2,}/).map {|v| v =~ /^null$/i ? nil : v }
|
163
|
+
zip = columns.zip(values).flatten
|
164
|
+
Hash[*zip]
|
165
|
+
end
|
166
|
+
insert_rows(rows, model)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
Kaplan.choose_database_adapter
|
172
|
+
Kaplan.choose_web_framework
|
@@ -0,0 +1,94 @@
|
|
1
|
+
namespace :kaplan do
|
2
|
+
namespace :db do
|
3
|
+
desc "Resets a database of your choice by re-creating it from the development schema and then re-seeding it"
|
4
|
+
task :reset, [:env] => :environment do |t, args|
|
5
|
+
env = args[:env] || ENV["RAILS_ENV"]
|
6
|
+
raise "Can't reset the dev database" if env == "development"
|
7
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
8
|
+
puts "Resetting #{env} database by cloning dev schema and seeding db..."
|
9
|
+
Rake::Task['kaplan:db:clone_structure'].invoke(env)
|
10
|
+
Rake::Task['kaplan:db:seed'].invoke(env)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Seeds a database of your choice (default: development) with bootstrap data.\nThe relevant tables are truncated first so you don't have to."
|
14
|
+
task :seed, [:env] => :environment do |t, args|
|
15
|
+
env = args[:env] || ENV["RAILS_ENV"] || "development"
|
16
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
17
|
+
Rake::Task['kaplan:db:plow'].invoke(env)
|
18
|
+
Kaplan.seed_database(:env => env)
|
19
|
+
Kaplan.establish_database(env)
|
20
|
+
Rake::Task['db:seed'].invoke
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Truncates tables in a database of your choice (default: development).\nBy default this just truncates the seed tables, if you want all of them pass ALL=true."
|
24
|
+
task :plow, [:env] => :environment do |t, args|
|
25
|
+
env = args[:env] || ENV["RAILS_ENV"] || "development"
|
26
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
27
|
+
all = !!ENV["ALL"]
|
28
|
+
Kaplan.plow_database(:env => env, :all => all)
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Dumps the structure of the development database to file and copies it to the database of your choice.\nAdapters must be the same."
|
32
|
+
task :clone_structure, [:env] => :environment do |t, args|
|
33
|
+
env = args[:env] || ENV["RAILS_ENV"]
|
34
|
+
raise "Can't clone the dev database to the dev database" if env == "development"
|
35
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
36
|
+
puts "Cloning dev structure to #{env} database..."
|
37
|
+
|
38
|
+
abcs = ActiveRecord::Base.configurations
|
39
|
+
adapter = abcs[env]["adapter"]
|
40
|
+
development_adapter = abcs["development"]["adapter"]
|
41
|
+
if adapter != development_adapter
|
42
|
+
raise "Development db adapter and #{env} db adapter must be the same to clone"
|
43
|
+
end
|
44
|
+
|
45
|
+
Kaplan.current_environment = env
|
46
|
+
Kaplan.establish_database
|
47
|
+
Rake::Task['db:drop'].invoke
|
48
|
+
Rake::Task['db:create'].invoke
|
49
|
+
|
50
|
+
Kaplan.current_environment = 'development'
|
51
|
+
Kaplan.establish_database
|
52
|
+
Rake::Task["db:structure:dump"].invoke
|
53
|
+
|
54
|
+
Kaplan.current_environment = env
|
55
|
+
Kaplan.establish_database
|
56
|
+
case adapter
|
57
|
+
when "mysql"
|
58
|
+
ActiveRecord::Base.establish_connection(env)
|
59
|
+
ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
|
60
|
+
IO.readlines("#{RAILS_ROOT}/db/development_structure.sql").join.split("\n\n").each do |table|
|
61
|
+
ActiveRecord::Base.connection.execute(table)
|
62
|
+
end
|
63
|
+
when "postgresql"
|
64
|
+
ENV['PGHOST'] = abcs[env]["host"] if abcs[env]["host"]
|
65
|
+
ENV['PGPORT'] = abcs[env]["port"].to_s if abcs[env]["port"]
|
66
|
+
ENV['PGPASSWORD'] = abcs[env]["password"].to_s if abcs[env]["password"]
|
67
|
+
`psql -U "#{abcs[env]["username"]}" -f #{Rails.root}/db/development_structure.sql #{abcs[env]["database"]}`
|
68
|
+
when "sqlite", "sqlite3"
|
69
|
+
dbfile = abcs[env]["database"] || abcs[env]["dbfile"]
|
70
|
+
`#{abcs[env]["adapter"]} #{dbfile} < #{RAILS_ROOT}/db/development_structure.sql`
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Creates a database of your choice (default: development)"
|
75
|
+
task :create, [:env] => :environment do |t, args|
|
76
|
+
env = args[:env] || ENV["RAILS_ENV"] || "development"
|
77
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
78
|
+
puts "Creating #{env} database..."
|
79
|
+
Kaplan.current_environment = env
|
80
|
+
Kaplan.establish_database
|
81
|
+
Rake::Task['db:create'].invoke
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "Drops the database of your choice (default: development)"
|
85
|
+
task :drop, [:env] => :environment do |t, args|
|
86
|
+
env = args[:env] || ENV["RAILS_ENV"] || "development"
|
87
|
+
#raise "No environment specified. Pass the environment to the task as an argument to specify the environment." unless env
|
88
|
+
puts "Dropping #{env} database..."
|
89
|
+
Kaplan.current_environment = env
|
90
|
+
Kaplan.establish_database
|
91
|
+
Rake::Task['db:drop'].invoke
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kaplan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Elliot Winkler
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-29 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Kaplan provides some Rake tasks that are helpful in preparing your test database(s), such as seeding/plowing your database, or recreating it from your development database.
|
23
|
+
email:
|
24
|
+
- elliot.winkler@gmail.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- README.md
|
33
|
+
- kaplan.gemspec
|
34
|
+
- lib/kaplan/railtie.rb
|
35
|
+
- lib/kaplan/tasks/kaplan.rake
|
36
|
+
- lib/kaplan/version.rb
|
37
|
+
- lib/kaplan.rb
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://rubygems.org/gems/kaplan
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
hash: 3
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.3.7
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Database- and framework-agnostic Rake tasks to prepare and seed your test database
|
72
|
+
test_files: []
|
73
|
+
|