hummingbird 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +16 -0
  2. data/.travis.yml +4 -0
  3. data/CONTRIBUTING.md +7 -0
  4. data/Gemfile +15 -0
  5. data/Guardfile +9 -0
  6. data/LICENSE +20 -0
  7. data/README.md +134 -0
  8. data/Rakefile +10 -0
  9. data/hummingbird.gemspec +31 -0
  10. data/lib/hummingbird/configuration.rb +35 -0
  11. data/lib/hummingbird/database.rb +31 -0
  12. data/lib/hummingbird/plan.rb +75 -0
  13. data/lib/hummingbird/plan_error.rb +11 -0
  14. data/lib/hummingbird/version.rb +3 -0
  15. data/lib/hummingbird.rb +7 -0
  16. data/test/fixtures/basic_config.yml +6 -0
  17. data/test/fixtures/no_basedir_config.yml +3 -0
  18. data/test/fixtures/no_basedir_user_config.yml +3 -0
  19. data/test/fixtures/no_connection_string_config.yml +5 -0
  20. data/test/fixtures/no_migrations_dir_config.yml +3 -0
  21. data/test/fixtures/no_migrations_dir_user_config.yml +3 -0
  22. data/test/fixtures/no_migrations_table_config.yml +4 -0
  23. data/test/fixtures/no_planfile_config.yml +3 -0
  24. data/test/fixtures/no_planfile_user_config.yml +3 -0
  25. data/test/fixtures/plan/basic.plan +4 -0
  26. data/test/fixtures/sql/migrations/basic/file1.sql +1 -0
  27. data/test/fixtures/sql/migrations/basic/file2.sql +1 -0
  28. data/test/fixtures/sql/migrations/basic/file3.sql +1 -0
  29. data/test/fixtures/sql/migrations/basic/file4.sql +1 -0
  30. data/test/fixtures/sql/migrations_table.sql +4 -0
  31. data/test/fixtures/user_config.yml +4 -0
  32. data/test/lib/hummingbird/configuration_test.rb +153 -0
  33. data/test/lib/hummingbird/database_test.rb +100 -0
  34. data/test/lib/hummingbird/plan_test.rb +230 -0
  35. data/test/lib/hummingbird/version_test.rb +7 -0
  36. data/test/test_helper.rb +66 -0
  37. data/test/test_helper_test.rb +41 -0
  38. metadata +237 -0
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ # Packaged Gem
2
+ /pkg/
3
+
4
+ # Developer specific files & editor cruft
5
+ Gemfile.lock
6
+ .rvmrc
7
+ .*.sw?
8
+ \#*\#
9
+ .\#*
10
+ *~
11
+
12
+ # SimpleCov
13
+ /coverage/
14
+
15
+ # OS X
16
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,7 @@
1
+ # Want to help?
2
+
3
+ We'd love to have your help! Found a bug? Have a documentation fix?
4
+ Fixed a bug? Great! Everything helps. Please feel free to file
5
+ issues in the [issue tracker][issues], and open pull requests.
6
+
7
+ [issues]: https://github.com/jhelwig/hummingbird/issues "GitHub issue tracker"
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rbconfig'
2
+
3
+ source :rubygems
4
+ gemspec
5
+
6
+ group :development do
7
+ platform = RbConfig::CONFIG['host_os'] rescue Config::CONFIG['host_os']
8
+
9
+ case platform
10
+ when /darwin/
11
+ gem 'rb-fsevent', :require => false
12
+ when /linux/
13
+ gem 'rb-inotify', :require => false
14
+ end
15
+ end
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # More info at https://github.com/guard/guard#readme
2
+ ignore /^coverage\//
3
+
4
+ guard 'minitest' do
5
+ # with Minitest::Unit
6
+ watch(/^test\/(.*)\/?(.*)_test\.rb/)
7
+ watch(/^(lib\/.*)([^\/]+)\.rb/) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
8
+ watch(/^test\/test_helper\.rb/) { "test" }
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Jacob Helwig <jacob@technosorcery.net>
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.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # What is it?
2
+
3
+ Hummingbird is a way to write SQL migrations in SQL, and provide
4
+ some minimal tooling around running these migrations against a DB.
5
+
6
+ # Why is it?
7
+
8
+ DSLs that abstract away the differences in databases can be nice to
9
+ work with, but they often cater to something resembling the lowest
10
+ common denominator amongst those databases.
11
+
12
+ # How to use it
13
+
14
+ ## Configuration
15
+
16
+ Hummingbird will look for a `hummingbird.yml` in the directory passed
17
+ to `Hummingbird::Configuration.new`. Hummingbird will also look for a
18
+ user-specific `.hummingbird.yml` in the current working directory.
19
+ The user-specific configuration file will take precidence over the
20
+ configuration in the `hummingbird.yml`.
21
+
22
+ Both of these configuration files are YAML files with the following
23
+ format:
24
+
25
+ ```YAML
26
+ ---
27
+ basedir: 'sql'
28
+ planfile: 'application.plan'
29
+ migrations_dir: 'migrations'
30
+ migrations_table: 'application_migrations'
31
+ connection_string: 'sqlite://db/database.db'
32
+ ```
33
+
34
+ * `basedir`: The `planfile`, and `migrations_dir` settings are relative
35
+ to this directory, which is relative to the current working
36
+ directory.
37
+ * `planfile`: Described more below. This determines the order in which
38
+ migration files are run.
39
+ * `migrations_dir`: This is the name of the directory that will
40
+ contain all of the migrations. All files in this directory are
41
+ considered to be migration files, and Hummingbird will recurse into
42
+ any and all subdirectories starting here.
43
+ * `connection_string`: This is a [Sequel][] compatible connection
44
+ string for connecting to the database.
45
+
46
+ The above example configures Hummingbird to look in the file
47
+ `sql/application.plan` for the list of migrations to run, in the
48
+ directory `sql/migrations` for all of the migration files, use the
49
+ table named `application_migrations` within the database, and to
50
+ connect to the database using the [Sequel][] connection string `'sqlite://db/database.db'`.
51
+
52
+ ## Boot-strapping the Database
53
+
54
+ Hummingbird is capable of bootstrapping itself into the database to be
55
+ managed as long as the first migration creates the table named by the
56
+ `migrations_table` configuration option. This table will need to have
57
+ a `migration_name` column of type `TEXT` (or similar data type to
58
+ handle the maximum file path relative to `migrations_dir`), and a
59
+ `run_on` column of type `INTEGER`.
60
+
61
+ The following is an example for defining the `migrations_table` for
62
+ SQLite3:
63
+
64
+ ```sql
65
+ CREATE TABLE hummingbird_migrations (
66
+ migration_name TEXT PRIMARY KEY,
67
+ run_on INTEGER
68
+ );
69
+
70
+ ```
71
+
72
+ ## Plan file
73
+
74
+ The plan file contains the names of the migration files to be run, one
75
+ per line, in the order that they should be run.
76
+
77
+ Given the following plan file and the example configuration from above:
78
+
79
+ ```
80
+ bootstrap.sql
81
+ stored_procedures/foo.sql
82
+ tables/bar.sql
83
+ ```
84
+
85
+ Hummingbird would attempt to run the files
86
+ `sql/migrations/bootstrap.sql`,
87
+ `sql/migrations/stored_procedures/foo.sql`,
88
+ `sql/migrations/tables/bar.sql` in exactly this order.
89
+
90
+ ## Running migrations
91
+
92
+ Right now, only just enough is written to enable someone to write
93
+ their own rake task, or other glue code to actually migrate their
94
+ database using Hummingbird.
95
+
96
+ This isn't really a great way to go about it, but you can do the
97
+ following in your `Rakefile` at least until the rest of the glue is
98
+ included in Hummingbird itself:
99
+
100
+ ```Ruby
101
+ desc 'Migrate the database'
102
+ task "migrate" do
103
+ require 'hummingbird'
104
+
105
+ config = Hummingbird::Configuration.new(Dir.getwd)
106
+ plan = Hummingbird::Plan.new(config.planfile, config.migrations_dir)
107
+ db = Hummingbird::Database.new(config.connection_string, config.migrations_table)
108
+
109
+ unplanned_files = plan.files_missing_from_plan
110
+ fail "Found migration files not listed in #{config.planfile}: #{unplanned_files.join(', ')}" unless unplanned_files.empty?
111
+
112
+ missing_files = plan.files_missing_from_migration_dir
113
+ fail "Found planned migration files not in migrations directory: #{missing_files.join(', ')}" unless missing_files.empty?
114
+
115
+ migrations_already_run = db.already_run_migrations
116
+ migrations_to_run = plan.migrations_to_be_run(migrations_already_run)
117
+
118
+ puts "#{plan.planned_files.count} migrations planned; #{migrations_already_run.count} already run; #{migrations_to_run.count} to run"
119
+
120
+ migrations_to_run.each do |migration|
121
+ puts "Running migration: #{migration[:migration_name]}"
122
+ db.run_migration(migration[:migration_name], migration[:sql])
123
+ end
124
+ end
125
+ ```
126
+
127
+ # Resources
128
+
129
+ * [Travis CI][travis-ci] [![Build Status](https://secure.travis-ci.org/jhelwig/hummingbird.png?branch=master)](http://travis-ci.org/jhelwig/hummingbird)
130
+ * [Issues][issues]
131
+
132
+ [travis-ci]: http://travis-ci.org "Travis CI"
133
+ [issues]: https://github.com/jhelwig/hummingbird/issues "GitHub issues"
134
+ [Sequel]: http://sequel.rubyforge.org/ "The Database Toolkit for Ruby"
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList[File.join('test','lib','hummingbird','**','*_test.rb')]
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), File.join('lib', 'hummingbird', 'version'))
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'hummingbird'
6
+ gem.version = Hummingbird::VERSION
7
+ gem.license = 'MIT'
8
+ gem.authors = ['Jacob Helwig']
9
+ gem.email = ['jacob@technosorcery.net']
10
+ gem.description = %q{Write your DB migrations in SQL and run them, hold the magic.}
11
+ gem.summary = <<-DESC
12
+ No DSL, as little magic as possible. Write your DB migrations in SQL,
13
+ and run them in the order specified by a plan file.
14
+ DESC
15
+ gem.homepage = 'http://github.com/jhelwig/hummingbird'
16
+ gem.executables = `git ls-tree --name-only -r -z HEAD #{File.join("bin","*")}`.split("\0").map{ |f| File.basename(f) }
17
+ gem.files = `git ls-tree --name-only -r -z HEAD`.split("\0")
18
+ gem.test_files = `git ls-tree --name-only -r -z HEAD #{File.join("test","*")}`.split("\0")
19
+ gem.require_paths = ['lib']
20
+ gem.required_ruby_version = '>= 1.8.7'
21
+
22
+ gem.add_dependency 'sequel'
23
+ gem.add_dependency 'optimism'
24
+
25
+ gem.add_development_dependency 'rake'
26
+ gem.add_development_dependency 'minitest'
27
+ gem.add_development_dependency 'guard'
28
+ gem.add_development_dependency 'guard-minitest'
29
+ gem.add_development_dependency 'simplecov'
30
+ gem.add_development_dependency 'sqlite3'
31
+ end
@@ -0,0 +1,35 @@
1
+ require 'optimism'
2
+
3
+ class Hummingbird
4
+ class Configuration
5
+ CONFIG_FILE = 'hummingbird.yml'
6
+ USER_CONFIG_FILE = '.hummingbird.yml'
7
+
8
+ def initialize(config_dir)
9
+ @config = Optimism.require(
10
+ File.expand_path(File.join(config_dir, CONFIG_FILE)),
11
+ File.expand_path(File.join(FileUtils.pwd, USER_CONFIG_FILE))
12
+ )
13
+ end
14
+
15
+ def basedir
16
+ @basedir ||= @config[:basedir] || '.'
17
+ end
18
+
19
+ def planfile
20
+ @planfile ||= File.expand_path(File.join(basedir, @config[:planfile] || 'hummingbird.plan'))
21
+ end
22
+
23
+ def migrations_dir
24
+ @migrations_dir ||= File.expand_path(File.join(basedir, @config[:migrations_dir] || 'migrations'))
25
+ end
26
+
27
+ def migrations_table
28
+ @migrations_table ||= (@config[:migrations_table] || :hummingbird_migrations).to_sym
29
+ end
30
+
31
+ def connection_string
32
+ @connection_string ||= @config[:connection_string]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ require 'sequel'
2
+
3
+ class Hummingbird
4
+ class Database
5
+ def initialize(connection_string, migrations_table)
6
+ @sequel_db = Sequel.connect(connection_string)
7
+ @migrations_table_name = migrations_table
8
+ @prepared_run_migration_insert = nil
9
+ end
10
+
11
+ def initialized?
12
+ @sequel_db.tables.include?(@migrations_table_name)
13
+ end
14
+
15
+ def already_run_migrations
16
+ initialized? ? @sequel_db[@migrations_table_name].order(:run_on).to_a : []
17
+ end
18
+
19
+ def run_migration(name,sql)
20
+ @prepared_run_migration_insert ||= @sequel_db[@migrations_table_name].prepare(:insert, :record_migration, migration_name: :$name, run_on: :$date)
21
+
22
+ @sequel_db.transaction do
23
+ @sequel_db.execute(sql)
24
+
25
+ @prepared_run_migration_insert.call(name: name, date: DateTime.now.strftime('%s'))
26
+ end
27
+
28
+ true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,75 @@
1
+ require 'hummingbird/plan_error'
2
+
3
+ require 'pathname'
4
+
5
+ class Hummingbird
6
+ class Plan
7
+ attr_reader :migration_dir, :planned_files
8
+
9
+ def initialize(planfile, migration_dir)
10
+ @planned_files = parse_plan(planfile)
11
+ @migration_dir = migration_dir
12
+ end
13
+
14
+ def migration_files
15
+ @migration_files ||= get_migration_files
16
+ end
17
+
18
+ def files_missing_from_plan
19
+ migration_files - planned_files
20
+ end
21
+
22
+ def files_missing_from_migration_dir
23
+ planned_files - migration_files
24
+ end
25
+
26
+ def migrations_to_be_run(already_run_migrations)
27
+ to_be_run_migration_file_names(already_run_migrations).map do |f|
28
+ {
29
+ migration_name: f,
30
+ sql: get_migration_contents(f)
31
+ }
32
+ end
33
+ end
34
+
35
+ def to_be_run_migration_file_names(already_run_migrations)
36
+ return planned_files if already_run_migrations.empty?
37
+
38
+ unless (run_migrations_missing_from_plan = already_run_migrations.map {|a| a[:migration_name]} - planned_files).empty?
39
+ raise Hummingbird::PlanError.new("Plan is missing the following already run migrations: #{run_migrations_missing_from_plan.join(', ')}",planned_files,already_run_migrations)
40
+ end
41
+
42
+ files = planned_files.dup
43
+ already_run_migrations.each do |f|
44
+ if f[:migration_name] == files.first
45
+ files.shift
46
+ else
47
+ first_out_of_sync_run_on = DateTime.strptime(f[:run_on].to_s, '%s')
48
+
49
+ raise Hummingbird::PlanError.new("Plan has '#{files.first}' before '#{f[:migration_name]}' which was run on #{first_out_of_sync_run_on}",planned_files,already_run_migrations)
50
+ end
51
+ end
52
+
53
+ files
54
+ end
55
+
56
+ def get_migration_contents(migration_file)
57
+ File.read(File.absolute_path(migration_file, @migration_dir))
58
+ end
59
+
60
+ private
61
+
62
+ def parse_plan(planfile)
63
+ File.read(planfile).split("\n")
64
+ end
65
+
66
+ def get_migration_files
67
+ listing = Dir[File.join(migration_dir,'**','*')].select {|f| File.file? f}
68
+
69
+ migration_path = Pathname.new(migration_dir)
70
+ listing.map do |f|
71
+ Pathname.new(f).relative_path_from(migration_path).to_s
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,11 @@
1
+ class Hummingbird
2
+ class PlanError < Exception
3
+ attr_reader :already_run_migrations, :planned_files
4
+
5
+ def initialize(msg,planned_files,already_run_migrations)
6
+ super(msg)
7
+ @planned_files = planned_files
8
+ @already_run_migrations = already_run_migrations
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ class Hummingbird
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'hummingbird/version'
2
+ require 'hummingbird/configuration'
3
+ require 'hummingbird/plan'
4
+ require 'hummingbird/database'
5
+
6
+ class Hummingbird
7
+ end
@@ -0,0 +1,6 @@
1
+ ---
2
+ basedir: 'sql'
3
+ planfile: 'application.plan'
4
+ migrations_dir: 'migrations-dir'
5
+ migrations_table: 'application_migrations'
6
+ connection_string: 'sequel connection string'
@@ -0,0 +1,3 @@
1
+ ---
2
+ planfile: 'application.plan'
3
+ migrations_dir: 'migrations'