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.
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'