monkey_butler 1.2.2

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +122 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE +202 -0
  10. data/README.md +221 -0
  11. data/Rakefile +16 -0
  12. data/bin/mb +6 -0
  13. data/lib/monkey_butler.rb +1 -0
  14. data/lib/monkey_butler/actions.rb +38 -0
  15. data/lib/monkey_butler/cli.rb +354 -0
  16. data/lib/monkey_butler/databases/abstract_database.rb +49 -0
  17. data/lib/monkey_butler/databases/cassandra_database.rb +69 -0
  18. data/lib/monkey_butler/databases/sqlite_database.rb +105 -0
  19. data/lib/monkey_butler/migrations.rb +52 -0
  20. data/lib/monkey_butler/project.rb +90 -0
  21. data/lib/monkey_butler/targets/base.rb +85 -0
  22. data/lib/monkey_butler/targets/cassandra/cassandra_target.rb +72 -0
  23. data/lib/monkey_butler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
  24. data/lib/monkey_butler/targets/cassandra/migration.cql.erb +0 -0
  25. data/lib/monkey_butler/targets/cocoapods/cocoapods_target.rb +43 -0
  26. data/lib/monkey_butler/targets/cocoapods/podspec.erb +12 -0
  27. data/lib/monkey_butler/targets/maven/maven_target.rb +60 -0
  28. data/lib/monkey_butler/targets/maven/project/.gitignore +6 -0
  29. data/lib/monkey_butler/targets/maven/project/MonkeyButler.iml +19 -0
  30. data/lib/monkey_butler/targets/maven/project/build.gradle +54 -0
  31. data/lib/monkey_butler/targets/sqlite/create_monkey_butler_tables.sql.erb +15 -0
  32. data/lib/monkey_butler/targets/sqlite/migration.sql.erb +11 -0
  33. data/lib/monkey_butler/targets/sqlite/sqlite_target.rb +91 -0
  34. data/lib/monkey_butler/templates/Gemfile.erb +4 -0
  35. data/lib/monkey_butler/templates/gitignore.erb +1 -0
  36. data/lib/monkey_butler/util.rb +71 -0
  37. data/lib/monkey_butler/version.rb +3 -0
  38. data/logo.jpg +0 -0
  39. data/monkey_butler.gemspec +33 -0
  40. data/spec/cli_spec.rb +700 -0
  41. data/spec/databases/cassandra_database_spec.rb +241 -0
  42. data/spec/databases/sqlite_database_spec.rb +181 -0
  43. data/spec/migrations_spec.rb +4 -0
  44. data/spec/project_spec.rb +128 -0
  45. data/spec/sandbox/cassandra/.gitignore +2 -0
  46. data/spec/sandbox/cassandra/.monkey_butler.yml +7 -0
  47. data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
  48. data/spec/sandbox/cassandra/sandbox.cql +0 -0
  49. data/spec/sandbox/sqlite/.gitignore +2 -0
  50. data/spec/sandbox/sqlite/.monkey_butler.yml +7 -0
  51. data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
  52. data/spec/sandbox/sqlite/sandbox.sql +0 -0
  53. data/spec/spec_helper.rb +103 -0
  54. data/spec/targets/cassandra_target_spec.rb +191 -0
  55. data/spec/targets/cocoapods_target_spec.rb +197 -0
  56. data/spec/targets/maven_target_spec.rb +156 -0
  57. data/spec/targets/sqlite_target_spec.rb +103 -0
  58. data/spec/util_spec.rb +13 -0
  59. metadata +260 -0
@@ -0,0 +1,69 @@
1
+ require 'uri'
2
+ require 'cql'
3
+
4
+ module MonkeyButler
5
+ module Databases
6
+ class CassandraDatabase < AbstractDatabase
7
+ attr_reader :client, :keyspace
8
+
9
+ class << self
10
+ def migration_ext
11
+ ".cql"
12
+ end
13
+
14
+ def exception_class
15
+ Cql::CqlError
16
+ end
17
+ end
18
+
19
+ def initialize(url)
20
+ super(url)
21
+ options = { host: url.host, port: (url.port || 9042) }
22
+ @client = Cql::Client.connect(options)
23
+ @keyspace = url.path[1..-1] # Drop leading slash
24
+ end
25
+
26
+ def migrations_table?
27
+ client.use('system')
28
+ rows = client.execute "SELECT columnfamily_name FROM schema_columnfamilies WHERE keyspace_name='#{keyspace}' AND columnfamily_name='schema_migrations'"
29
+ !rows.empty?
30
+ end
31
+
32
+ def origin_version
33
+ client.use(keyspace)
34
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version ASC LIMIT 1")
35
+ rows.empty? ? nil : rows.each.first['version']
36
+ end
37
+
38
+ def current_version
39
+ client.use(keyspace)
40
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version DESC LIMIT 1")
41
+ rows.empty? ? nil : rows.each.first['version']
42
+ end
43
+
44
+ def all_versions
45
+ client.use(keyspace)
46
+ rows = client.execute("SELECT version FROM schema_migrations WHERE partition_key = 0 ORDER BY version ASC")
47
+ rows.each.map { |row| row['version'] }
48
+ end
49
+
50
+ def insert_version(version)
51
+ client.use(keyspace)
52
+ client.execute "INSERT INTO schema_migrations (partition_key, version) VALUES (0, ?)", version
53
+ end
54
+
55
+ def execute_migration(cql)
56
+ cql.split(';').each { |statement| client.execute(statement) unless statement.strip.empty? }
57
+ end
58
+
59
+ def drop(keyspaces = [keyspace])
60
+ keyspaces.each { |keyspace| client.execute "DROP KEYSPACE IF EXISTS #{keyspace}" }
61
+ end
62
+
63
+ def create_migrations_table
64
+ client.execute "CREATE KEYSPACE IF NOT EXISTS #{keyspace} WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
65
+ client.execute "CREATE TABLE IF NOT EXISTS #{keyspace}.schema_migrations (partition_key INT, version VARINT, PRIMARY KEY (partition_key, version));"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,105 @@
1
+ require 'sqlite3'
2
+ require 'uri'
3
+
4
+ module MonkeyButler
5
+ module Databases
6
+ class SqliteDatabase < AbstractDatabase
7
+ attr_reader :db, :path
8
+
9
+ class << self
10
+ def create_schema_migrations_sql
11
+ MonkeyButler::Util.strip_leading_whitespace <<-SQL
12
+ CREATE TABLE schema_migrations(
13
+ version INTEGER UNIQUE NOT NULL
14
+ );
15
+ SQL
16
+ end
17
+
18
+ def create(url)
19
+ new(url) do |database|
20
+ database.db.execute(create_schema_migrations_sql)
21
+ end
22
+ end
23
+
24
+ def migration_ext
25
+ ".sql"
26
+ end
27
+
28
+ def exception_class
29
+ SQLite3::Exception
30
+ end
31
+ end
32
+
33
+ def initialize(url)
34
+ super(url)
35
+ raise ArgumentError, "Must initialize with a URI" unless url.kind_of?(URI)
36
+ raise ArgumentError, "Must initialize with a sqlite URI" unless url.scheme.nil? || url.scheme == 'sqlite'
37
+ path = url.path || url.opaque
38
+ raise ArgumentError, "Must initialize with a sqlite URI that has a path component" unless path
39
+ @path = path
40
+ @db = SQLite3::Database.new(path)
41
+ yield self if block_given?
42
+ end
43
+
44
+ def migrations_table?
45
+ has_table?('schema_migrations')
46
+ end
47
+
48
+ def origin_version
49
+ db.get_first_value('SELECT MIN(version) FROM schema_migrations')
50
+ end
51
+
52
+ def current_version
53
+ db.get_first_value('SELECT MAX(version) FROM schema_migrations')
54
+ end
55
+
56
+ def all_versions
57
+ db.execute('SELECT version FROM schema_migrations ORDER BY version ASC').map { |row| row[0] }
58
+ end
59
+
60
+ def insert_version(version)
61
+ db.execute("INSERT INTO schema_migrations(version) VALUES (?)", version)
62
+ end
63
+
64
+ def execute_migration(sql)
65
+ db.transaction do |db|
66
+ db.execute_batch(sql)
67
+ end
68
+ end
69
+
70
+ def drop
71
+ File.truncate(path, 0) if File.size?(path)
72
+ end
73
+
74
+ def to_s
75
+ path
76
+ end
77
+
78
+ ####
79
+ # Outside of abstract interface...
80
+
81
+ def dump_to_schema(type, schema_path)
82
+ sql = MonkeyButler::Util.strip_leading_whitespace <<-SQL
83
+ SELECT name, sql
84
+ FROM sqlite_master
85
+ WHERE sql NOT NULL AND type = '#{type}'
86
+ ORDER BY name ASC
87
+ SQL
88
+ File.open(schema_path, 'a') do |f|
89
+ db.execute(sql) do |row|
90
+ name, sql = row
91
+ next if name =~ /^sqlite/
92
+ f << "#{sql};\n\n"
93
+ yield name if block_given?
94
+ end
95
+ f.puts
96
+ end
97
+ end
98
+
99
+ private
100
+ def has_table?(table)
101
+ db.get_first_value("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,52 @@
1
+ module MonkeyButler
2
+ class Migrations
3
+ attr_reader :path, :database
4
+
5
+ def initialize(path, database)
6
+ @path = path
7
+ @database = database
8
+ migration_paths = Dir.glob(File.join(path, "*#{database.class.migration_ext}"))
9
+ @paths_by_version = MonkeyButler::Util.migrations_by_version(migration_paths)
10
+ end
11
+
12
+ def current_version
13
+ database.migrations_table? ? database.current_version : nil
14
+ end
15
+
16
+ def all_versions
17
+ @paths_by_version.keys
18
+ end
19
+
20
+ def latest_version
21
+ all_versions.max
22
+ end
23
+
24
+ def applied_versions
25
+ database.migrations_table? ? database.all_versions : []
26
+ end
27
+
28
+ def pending_versions
29
+ (all_versions - applied_versions).sort
30
+ end
31
+
32
+ def pending(&block)
33
+ pending_versions.inject({}) { |hash, v| hash[v] = self[v]; hash }.tap do |hash|
34
+ hash.each(&block) if block_given?
35
+ end
36
+ end
37
+
38
+ def all(&block)
39
+ all_versions.inject({}) { |hash, v| hash[v] = self[v]; hash }.tap do |hash|
40
+ hash.each(&block) if block_given?
41
+ end
42
+ end
43
+
44
+ def up_to_date?
45
+ pending_versions.empty?
46
+ end
47
+
48
+ def [](version)
49
+ @paths_by_version[version]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,90 @@
1
+ require 'yaml'
2
+ require 'uri'
3
+
4
+ module MonkeyButler
5
+ class Project
6
+ class << self
7
+ def load(path = Dir.pwd)
8
+ @project ||= proc do
9
+ project_path = File.join(path, '.monkey_butler.yml')
10
+ raise "fatal: Not a monkey_butler repository: no .monkey_butler.yml" unless File.exists?(project_path)
11
+ options = YAML.load(File.read(project_path))
12
+ new(options)
13
+ end.call
14
+ end
15
+
16
+ def set(options)
17
+ @project = new(options)
18
+ end
19
+
20
+ def clear
21
+ @project = nil
22
+ end
23
+ end
24
+
25
+ attr_accessor :name, :config, :database_url, :targets
26
+
27
+ def initialize(options = {})
28
+ options.each { |k,v| send("#{k}=", v) }
29
+ end
30
+
31
+ def database_url=(database_url)
32
+ @database_url = database_url ? URI(database_url) : nil
33
+ end
34
+
35
+ def database
36
+ database_url.scheme || 'sqlite'
37
+ end
38
+
39
+ def schema_path
40
+ "#{name}" + database_class.migration_ext
41
+ end
42
+
43
+ def migrations_path
44
+ "migrations"
45
+ end
46
+
47
+ def git_url
48
+ `git config remote.origin.url`.chomp
49
+ end
50
+
51
+ def git_latest_tag
52
+ git_tag_for_version(nil)
53
+ end
54
+
55
+ def git_current_branch
56
+ `git symbolic-ref --short HEAD`.chomp
57
+ end
58
+
59
+ def git_tag_for_version(version)
60
+ pattern = version && "#{version}*"
61
+ tag = `git tag -l --sort=-v:refname #{pattern} | head -n 1`.chomp
62
+ tag.empty? ? nil : tag
63
+ end
64
+
65
+ def git_user_email
66
+ `git config user.email`.chomp
67
+ end
68
+
69
+ def git_user_name
70
+ `git config user.name`.chomp
71
+ end
72
+
73
+ def save!(path)
74
+ project_path = File.join(path, '.monkey_butler.yml')
75
+ File.open(project_path, 'w') { |f| f << YAML.dump(self.to_hash) }
76
+ end
77
+
78
+ def database_class
79
+ MonkeyButler::Util.database_named(database)
80
+ end
81
+
82
+ def database_target_class
83
+ MonkeyButler::Util.target_classes_named(database)[0]
84
+ end
85
+
86
+ def to_hash
87
+ { "name" => name, "config" => config, "database_url" => database_url.to_s, "targets" => targets }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,85 @@
1
+ module MonkeyButler
2
+ module Targets
3
+ class Base < Thor
4
+ include Thor::Actions
5
+ include MonkeyButler::Actions
6
+ add_runtime_options!
7
+
8
+ class << self
9
+ def source_root
10
+ File.join File.dirname(__FILE__), name
11
+ end
12
+
13
+ def name
14
+ "#{self}".split('::').last.gsub(/Target$/, '').downcase
15
+ end
16
+
17
+ def register_with_cli(cli)
18
+ # Allows targets a chance to configure the CLI
19
+ # This is an ideal place to register any options, tweak description, etc.
20
+ end
21
+ end
22
+
23
+ attr_reader :project, :database, :migrations
24
+
25
+ # Target Command
26
+
27
+ desc "init", "Initializes the target."
28
+ def init
29
+ # Default implementation does nothing
30
+ end
31
+
32
+ desc "new NAME", "Create a new migration"
33
+ def new(path)
34
+ # Default implementation does nothing
35
+ end
36
+
37
+ desc "generate", "Generates a platform specific package."
38
+ def generate
39
+ # Default implementation does nothing
40
+ end
41
+
42
+ desc "push", "Pushes a built package."
43
+ def push
44
+ # Default implementation does nothing
45
+ end
46
+
47
+ desc "push", "Validates the environment."
48
+ def validate
49
+ # Default implementation does nothing
50
+ end
51
+
52
+ desc "dump", "Dumps the schema"
53
+ def dump
54
+ # Default implementation does nothing
55
+ end
56
+
57
+ desc "load", "Loads the schema"
58
+ def load
59
+ # Default implementation does nothing
60
+ end
61
+
62
+ desc "drop", "Drops all tables and records"
63
+ def drop
64
+ # Default implementation does nothing
65
+ end
66
+
67
+ protected
68
+ def project
69
+ @project ||= MonkeyButler::Project.load(destination_root)
70
+ end
71
+
72
+ def database
73
+ @database ||= project.database_class.new(database_url)
74
+ end
75
+
76
+ def database_url
77
+ (options[:database] && URI(options[:database])) || project.database_url
78
+ end
79
+
80
+ def migrations
81
+ @migrations ||= MonkeyButler::Migrations.new(project.migrations_path, database)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,72 @@
1
+ require 'monkey_butler/databases/cassandra_database'
2
+
3
+ module MonkeyButler
4
+ module Targets
5
+ class CassandraTarget < Base
6
+ def init
7
+ migration_path = "migrations/" + MonkeyButler::Util.migration_named('create_' + options[:name]) + '.cql'
8
+ template('create_schema_migrations.cql.erb', migration_path)
9
+ git_add migration_path
10
+ end
11
+
12
+ def new(name)
13
+ migration_path = "migrations/" + MonkeyButler::Util.migration_named(name) + '.cql'
14
+ template('migration.cql.erb', migration_path)
15
+ git_add migration_path
16
+ end
17
+
18
+ def dump
19
+ database_url = (options[:database] && URI(options[:database])) || project.database_url
20
+ @database = MonkeyButler::Databases::CassandraDatabase.new(database_url)
21
+ fail Error, "Cannot dump database: the database at '#{database_url}' does not have a `schema_migrations` table." unless database.migrations_table?
22
+ say "Dumping schema from database '#{database_url}'"
23
+
24
+ say "Dumping keyspaces '#{keyspaces.join(', ')}'..."
25
+ describe_statements = keyspaces.map { |keyspace| "describe keyspace #{keyspace};" }
26
+ run "cqlsh -e '#{describe_statements.join(' ')}' #{database_url.host} > #{project.schema_path}"
27
+
28
+ say "Dumping rows from 'schema_migrations'..."
29
+ with_padding do
30
+ File.open(project.schema_path, 'a') do |f|
31
+ f.puts "USE #{keyspace};"
32
+ database.all_versions.each do |version|
33
+ f.puts "INSERT INTO schema_migrations(partition_key, version) VALUES (0, #{version});"
34
+ say "wrote version: #{version}", :green
35
+ end
36
+ end
37
+ end
38
+ say
39
+
40
+ say "Dump complete. Schema written to #{project.schema_path}."
41
+ end
42
+
43
+ def load
44
+ project = MonkeyButler::Project.load
45
+ unless File.size?(project.schema_path)
46
+ raise Error, "Cannot load database: empty schema found at #{project.schema_path}. Maybe you need to `mb migrate`?"
47
+ end
48
+
49
+ @database = MonkeyButler::Databases::CassandraDatabase.new(database_url)
50
+ drop
51
+ run "cqlsh #{database_url.host} -f #{project.schema_path}"
52
+
53
+ say "Loaded schema at version #{database.current_version}"
54
+ end
55
+
56
+ def drop
57
+ say_status :drop, database_url, :yellow
58
+ database.drop(keyspaces)
59
+ end
60
+
61
+ private
62
+ def keyspace
63
+ database_url.path[1..-1]
64
+ end
65
+
66
+ def keyspaces
67
+ keyspaces = (project.config['cassandra.keyspaces'] || []).dup
68
+ keyspaces.unshift(keyspace)
69
+ end
70
+ end
71
+ end
72
+ end