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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +122 -0
- data/Guardfile +8 -0
- data/LICENSE +202 -0
- data/README.md +221 -0
- data/Rakefile +16 -0
- data/bin/mb +6 -0
- data/lib/monkey_butler.rb +1 -0
- data/lib/monkey_butler/actions.rb +38 -0
- data/lib/monkey_butler/cli.rb +354 -0
- data/lib/monkey_butler/databases/abstract_database.rb +49 -0
- data/lib/monkey_butler/databases/cassandra_database.rb +69 -0
- data/lib/monkey_butler/databases/sqlite_database.rb +105 -0
- data/lib/monkey_butler/migrations.rb +52 -0
- data/lib/monkey_butler/project.rb +90 -0
- data/lib/monkey_butler/targets/base.rb +85 -0
- data/lib/monkey_butler/targets/cassandra/cassandra_target.rb +72 -0
- data/lib/monkey_butler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
- data/lib/monkey_butler/targets/cassandra/migration.cql.erb +0 -0
- data/lib/monkey_butler/targets/cocoapods/cocoapods_target.rb +43 -0
- data/lib/monkey_butler/targets/cocoapods/podspec.erb +12 -0
- data/lib/monkey_butler/targets/maven/maven_target.rb +60 -0
- data/lib/monkey_butler/targets/maven/project/.gitignore +6 -0
- data/lib/monkey_butler/targets/maven/project/MonkeyButler.iml +19 -0
- data/lib/monkey_butler/targets/maven/project/build.gradle +54 -0
- data/lib/monkey_butler/targets/sqlite/create_monkey_butler_tables.sql.erb +15 -0
- data/lib/monkey_butler/targets/sqlite/migration.sql.erb +11 -0
- data/lib/monkey_butler/targets/sqlite/sqlite_target.rb +91 -0
- data/lib/monkey_butler/templates/Gemfile.erb +4 -0
- data/lib/monkey_butler/templates/gitignore.erb +1 -0
- data/lib/monkey_butler/util.rb +71 -0
- data/lib/monkey_butler/version.rb +3 -0
- data/logo.jpg +0 -0
- data/monkey_butler.gemspec +33 -0
- data/spec/cli_spec.rb +700 -0
- data/spec/databases/cassandra_database_spec.rb +241 -0
- data/spec/databases/sqlite_database_spec.rb +181 -0
- data/spec/migrations_spec.rb +4 -0
- data/spec/project_spec.rb +128 -0
- data/spec/sandbox/cassandra/.gitignore +2 -0
- data/spec/sandbox/cassandra/.monkey_butler.yml +7 -0
- data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
- data/spec/sandbox/cassandra/sandbox.cql +0 -0
- data/spec/sandbox/sqlite/.gitignore +2 -0
- data/spec/sandbox/sqlite/.monkey_butler.yml +7 -0
- data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
- data/spec/sandbox/sqlite/sandbox.sql +0 -0
- data/spec/spec_helper.rb +103 -0
- data/spec/targets/cassandra_target_spec.rb +191 -0
- data/spec/targets/cocoapods_target_spec.rb +197 -0
- data/spec/targets/maven_target_spec.rb +156 -0
- data/spec/targets/sqlite_target_spec.rb +103 -0
- data/spec/util_spec.rb +13 -0
- 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
|