hummingbird 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +16 -0
- data/.travis.yml +4 -0
- data/CONTRIBUTING.md +7 -0
- data/Gemfile +15 -0
- data/Guardfile +9 -0
- data/LICENSE +20 -0
- data/README.md +134 -0
- data/Rakefile +10 -0
- data/hummingbird.gemspec +31 -0
- data/lib/hummingbird/configuration.rb +35 -0
- data/lib/hummingbird/database.rb +31 -0
- data/lib/hummingbird/plan.rb +75 -0
- data/lib/hummingbird/plan_error.rb +11 -0
- data/lib/hummingbird/version.rb +3 -0
- data/lib/hummingbird.rb +7 -0
- data/test/fixtures/basic_config.yml +6 -0
- data/test/fixtures/no_basedir_config.yml +3 -0
- data/test/fixtures/no_basedir_user_config.yml +3 -0
- data/test/fixtures/no_connection_string_config.yml +5 -0
- data/test/fixtures/no_migrations_dir_config.yml +3 -0
- data/test/fixtures/no_migrations_dir_user_config.yml +3 -0
- data/test/fixtures/no_migrations_table_config.yml +4 -0
- data/test/fixtures/no_planfile_config.yml +3 -0
- data/test/fixtures/no_planfile_user_config.yml +3 -0
- data/test/fixtures/plan/basic.plan +4 -0
- data/test/fixtures/sql/migrations/basic/file1.sql +1 -0
- data/test/fixtures/sql/migrations/basic/file2.sql +1 -0
- data/test/fixtures/sql/migrations/basic/file3.sql +1 -0
- data/test/fixtures/sql/migrations/basic/file4.sql +1 -0
- data/test/fixtures/sql/migrations_table.sql +4 -0
- data/test/fixtures/user_config.yml +4 -0
- data/test/lib/hummingbird/configuration_test.rb +153 -0
- data/test/lib/hummingbird/database_test.rb +100 -0
- data/test/lib/hummingbird/plan_test.rb +230 -0
- data/test/lib/hummingbird/version_test.rb +7 -0
- data/test/test_helper.rb +66 -0
- data/test/test_helper_test.rb +41 -0
- metadata +237 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
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
data/hummingbird.gemspec
ADDED
@@ -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
|
data/lib/hummingbird.rb
ADDED