kaplan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,10 @@
1
+ require 'kaplan'
2
+ require 'rails'
3
+
4
+ module Kaplan
5
+ class Railtie < ::Rails::Railtie
6
+ rake_tasks do
7
+ load File.dirname(__FILE__) + "/tasks/kaplan.rake"
8
+ end
9
+ end
10
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Kaplan
2
+ VERSION = "0.1.0"
3
+ 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
+