hummingbird 0.0.1
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.
- 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] [](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