cassandra_migrate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cassandra_migrate.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Noah Gibbs
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Cassandra Migrate
2
+
3
+ This gem is designed to allow Cassandra migrations in very roughly the
4
+ style of Rails migrations, but with a few extra features... And
5
+ without the Rails-style DSL for running the migrations. CQL is
6
+ basically fine. But we *do* want Erb!
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'cassandra_migrate'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install cassandra_migrate
21
+
22
+ You don't have to use cassandra_migrate as part of an app, but you'll
23
+ need a recent (1.9.2+) Ruby with Rubygems.
24
+
25
+ ## Usage
26
+
27
+ To run migrations from the current directory if you have them:
28
+
29
+ > cassandra_migrate latest
30
+
31
+ You can also migrate to a filename or a date string, either of which
32
+ will use the date as the important part -- you'll migrate forward or
33
+ back until the specified migration is the last one performed. You can
34
+ also roll back a single migration:
35
+
36
+ > cassandra_migrate rollback
37
+
38
+ See other options:
39
+
40
+ > cassandra_migrate --help
41
+
42
+ ## Writing Migrations
43
+
44
+ You'll need a directory for Cassandra migrations. Every migration
45
+ should have a filename of the form:
46
+
47
+ 20131023000000_create_keyspace_argus_up.cql.erb
48
+
49
+ The first fourteen digits are the date in YYYYMMDD format, followed
50
+ by six digits of your choice -- base them on time, or just make sure
51
+ they don't conflict. You can't have two migrations with exactly the
52
+ same fourteen-digit time code!
53
+
54
+ Then you can use a freeform description, like "create_keyspace_argus"
55
+ above. Then an action, like "up" or "down" (later, scripts as well),
56
+ and one or more extensions to tell Cassandra Migrate how to use the
57
+ file. Most commonly you'll want .cql or .cql.erb as the extension.
58
+ Such files will be run through Cassandra, optionally after Erubis
59
+ processing.
60
+
61
+ Here's an example that uses Erb, in this case to set the replication
62
+ factor of the keyspace via an environment variable:
63
+
64
+ ~~~
65
+ # 20131024001100_create_keyspace_cryptic_up.cql.erb
66
+ CREATE KEYSPACE "cryptic" WITH REPLICATION =
67
+ { 'class' : 'SimpleStrategy', 'replication_factor' : <%= ENV['CASS_REPLICATION'] || 1 %> };
68
+ ~~~
69
+
70
+ ## Contributing
71
+
72
+ 1. Fork it
73
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
74
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
75
+ 4. Push to the branch (`git push origin my-new-feature`)
76
+ 5. Create new Pull Request
77
+
78
+ ## Future Directions
79
+
80
+ A cassandra_migrate_rails gem could add generators for simple
81
+ Cassandra migrations from Rails.
82
+
83
+ We could add Ruby, bash or other scripts/executables to be run before
84
+ and after migrations, or as the migration itself.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (C) 2013 OL2, inc. See LICENSE.txt for details.
3
+
4
+ require "trollop"
5
+ require "cassandra_migrate"
6
+
7
+ OPTS = Trollop::options do
8
+ banner <<-BANNER
9
+ Migrate Cassandra to latest revision, based on your directory of migrations.
10
+
11
+ Usage: cassandra_migrate [options] [target_migration_date_or_file|latest|rollback]
12
+ BANNER
13
+ opt :migration_dir, "Directory of Cassandra migrations", :type => String, :default => "cassandra_migrations"
14
+ opt :host, "Cassandra host", :type => String, :default => "localhost"
15
+ opt :port, "Cassandra port", :type => Integer, :default => 9042
16
+ opt :dry_run, "Dry run, print Cassandra commands"
17
+ end
18
+
19
+ Trollop::die "Must specify no more than one argument: date, filename, latest or rollback!" if ARGV.size > 1
20
+
21
+ arg = ARGV.size > 0 ? ARGV[0] : "latest"
22
+
23
+ migrate = CassandraMigrate.new
24
+
25
+ # Pass arguments along to the migrate object
26
+ [:host, :port, :migration_dir].each do |arg|
27
+ migrate.send "#{arg}=", OPTS[arg]
28
+ end
29
+
30
+ options = {}
31
+ options[:dry_run] = OPTS[:dry_run]
32
+
33
+ case arg
34
+ when "latest"
35
+ migrate.to_latest options
36
+ when "rollback"
37
+ migrate.rollback options
38
+ when proc { arg =~ /^(\d{14})/ }
39
+ migrate.to_target $1, options
40
+ else
41
+ raise "Didn't recognize argument as a 14-digit date string, a migration pathname, 'latest' or 'rollback': #{arg.inspect}!"
42
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cassandra_migrate/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cassandra_migrate"
8
+ spec.version = CassandraMigrate::VERSION
9
+ spec.authors = ["Noah Gibbs"]
10
+ spec.email = ["noah.gibbs@onlive.com"]
11
+ spec.description = %q{Migrations for Cassandra in CQL and Erb.}
12
+ spec.summary = %q{Migrations for Cassandra in CQL and Erb.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_runtime_dependency "trollop"
24
+ spec.add_runtime_dependency "cql-rb"
25
+ spec.add_runtime_dependency "erubis"
26
+ end
@@ -0,0 +1,220 @@
1
+ # Copyright (C) 2013 OL2, inc. See LICENSE.txt for details.
2
+
3
+ require "cassandra_migrate/version"
4
+
5
+ require "cql"
6
+ require "erubis"
7
+
8
+ require "digest/sha1"
9
+
10
+ class CassandraMigrate
11
+ attr_accessor :host
12
+ attr_accessor :port
13
+ attr_accessor :migration_dir
14
+
15
+ private
16
+
17
+ def cql_client
18
+ return @cassandra_client if @cassandra_client
19
+
20
+ STDERR.puts "Connecting to Cassandra: #{host.inspect} / #{port.inspect}"
21
+ @cassandra_client = Cql::Client.connect(hosts: [host].flatten, port: port, consistency: :quorum)
22
+
23
+ @cassandra_client
24
+ end
25
+
26
+ def execute_cql(cql, options = {})
27
+ if options[:dry_run]
28
+ puts "Dry run, execute: #{cql}"
29
+ return
30
+ end
31
+
32
+ last_result = nil
33
+ # Can only execute single chunks at once
34
+ cql.split(";").map(&:strip).select {|s| s != ""}.each do |statement|
35
+ # Prep-then-execute so that a syntax error will be detectable as such
36
+ last_result = cql_client.execute statement
37
+ puts "Executing CQL: #{statement}"
38
+ end
39
+
40
+ last_result
41
+ end
42
+
43
+ def migrations_in_dir(refresh = false)
44
+ return @migrations_in_dir if @migrations_in_dir && !refresh
45
+
46
+ @migrations_in_dir = {}
47
+ Dir[File.join migration_dir, "*"].each do |file|
48
+ unless File.basename(file) =~ /^(\d{14})_/
49
+ puts "No match: #{file.inspect}"
50
+ next
51
+ end
52
+
53
+ unless /^(?<date_str>\d{14})_(?<desc>[^.]+)_(?<action>[^_.]+)(?<extensions>\..*)$/ =~ File.basename(file)
54
+ puts "No match with regexp: #{file.inspect}"
55
+ next
56
+ end
57
+
58
+ @migrations_in_dir[date_str] ||= {}
59
+ migration = @migrations_in_dir[date_str]
60
+ migration[:actions] ||= {}
61
+
62
+ if migration[:desc] && migration[:desc] != desc
63
+ raise "Only one migration name per date string! #{desc.inspect} != #{migration[:desc].inspect}"
64
+ end
65
+ migration[:desc] = desc
66
+
67
+ migration[:actions][action.to_sym] = {
68
+ file: file
69
+ }
70
+ end
71
+
72
+ raise "No migrations in directory #{migration_dir.inspect}! Did you mean to specify a migration directory?" if @migrations_in_dir.empty?
73
+
74
+ @migrations_in_dir
75
+ end
76
+
77
+ def ensure_schema_keyspace_exists(options = {})
78
+ ks = execute_cql "SELECT keyspace_name FROM system.schema_keyspaces WHERE keyspace_name = 'schema';"
79
+
80
+ if ks.empty?
81
+ raise "No schema keyspace in a dry run!" if options[:dry_run]
82
+
83
+ peers = execute_cql "SELECT peer FROM system.peers;"
84
+
85
+ @replication = [3, peers.to_a.size + 1].min
86
+ execute_cql <<-MIGRATION, options
87
+ CREATE KEYSPACE "schema" WITH REPLICATION =
88
+ { 'class' : 'SimpleStrategy', 'replication_factor' : #{@replication} };
89
+ MIGRATION
90
+ end
91
+
92
+ cf = execute_cql "SELECT columnfamily_name FROM system.schema_columnfamilies WHERE columnfamily_name = 'migrations' AND keyspace_name = 'schema';"
93
+ if cf.empty?
94
+ raise "No migration table in a dry run!" if options[:dry_run]
95
+
96
+ execute_cql <<-MIGRATION
97
+ CREATE TABLE "schema"."migrations" (
98
+ "date_string" varchar,
99
+ "up_filename" varchar,
100
+ "sha1" varchar,
101
+ PRIMARY KEY ("date_string", "up_filename"));
102
+ MIGRATION
103
+ end
104
+ end
105
+
106
+ def migrations_completed(refresh = false, options = {})
107
+ return @migrations_completed if @migrations_completed && !refresh
108
+
109
+ ensure_schema_keyspace_exists(options)
110
+
111
+ @migrations_completed = {}
112
+
113
+ migrations = execute_cql 'SELECT * FROM "schema"."migrations";'
114
+ migrations.each do |migration|
115
+ @migrations_completed[migration["date_string"]] = migration.to_hash
116
+ end
117
+
118
+ @migrations_completed
119
+ end
120
+
121
+ def sha1(path)
122
+ Digest::SHA1.hexdigest File.read path
123
+ end
124
+
125
+ def execute_migration_file(path, options)
126
+ ensure_schema_keyspace_exists(options)
127
+
128
+ STDERR.puts "Executing migration file: #{path.inspect}"
129
+
130
+ components = File.basename(path).split(".")
131
+ components.shift # Take just the extensions
132
+
133
+ content = File.read path
134
+
135
+ while components.size > 1
136
+ ext = components.pop
137
+
138
+ if ext == "erb" || ext == "erubis"
139
+ eruby = Erubis::Eruby.new content
140
+ content = eruby.result :replication => @replication
141
+ else
142
+ raise "Unknown intermediate extension in path #{path.inspect}: #{ext.inspect}!"
143
+ end
144
+ end
145
+
146
+ final_type = components.first
147
+ if ["cql", "cqlsh"].include?(final_type)
148
+ execute_cql content, options
149
+ elsif ["erb", "erubis"].include?(final_type)
150
+ raise "Can't use erb as the final extension in path #{path.inspect}!"
151
+ else
152
+ raise "Unknown extension #{final_type.inspect} in path #{path.inspect}!"
153
+ end
154
+ end
155
+
156
+ public
157
+
158
+ def up(date_str, options = {})
159
+ raise "Can't apply migration #{date_str} that already happened!" if migrations_completed(false,options)[date_str]
160
+ raise "Can't apply migration #{date_str} that has no migration files!" unless migrations_in_dir[date_str]
161
+ raise "Can't apply migration #{date_str} with no up migration!" unless migrations_in_dir[date_str][:actions][:up]
162
+
163
+ up_filename = migrations_in_dir[date_str][:actions][:up][:file]
164
+ execute_migration_file up_filename, options
165
+ execute_cql "INSERT INTO \"schema\".\"migrations\" (date_string, up_filename, sha1) VALUES ('#{date_str}', '#{up_filename}', '#{sha1 up_filename}')", options
166
+ end
167
+
168
+ def down(date_str, options = {})
169
+ raise "Can't reverse migration #{date_str} that didn't happen!" unless migrations_completed(false,options)[date_str]
170
+ raise "Can't reverse migration #{date_str} that has no migration files!" unless migrations_in_dir[date_str]
171
+ raise "Can't reverse migration #{date_str} with no down migration!" unless migrations_in_dir[date_str][:actions][:down]
172
+
173
+ execute_migration_file migrations_in_dir[date_str][:actions][:down][:file], options
174
+ execute_cql "DELETE FROM \"schema\".\"migrations\" WHERE date_string = '#{date_str}';", options
175
+ end
176
+
177
+ def up_to(date_str, options = {})
178
+ uncompleted_dates = migrations_in_dir.keys - migrations_completed(false,options).keys
179
+
180
+ STDERR.puts "Uncompleted: #{uncompleted_dates.inspect}"
181
+ migrations_to_run = uncompleted_dates.select { |d| d <= date_str }
182
+
183
+ STDERR.puts "Run #{migrations_to_run.size} migrations, update to #{date_str}."
184
+ migrations_to_run.each { |m| up(m, options) }
185
+ end
186
+
187
+ def down_to(date_str, options = {})
188
+ migrations_to_run = migrations_completed(false,options).keys.select { |d| d > date_str }
189
+
190
+ STDERR.puts "Run #{migrations_to_run.size} migrations, roll back to #{date_str}."
191
+ migrations_to_run.each { |m| down(m, options) }
192
+ end
193
+
194
+ def current_latest(options = {})
195
+ migrations_completed(false,options).keys.max
196
+ end
197
+
198
+ def latest_in_directory
199
+ migrations_in_dir.keys.max
200
+ end
201
+
202
+ def to_latest(options = {})
203
+ latest = latest_in_directory
204
+ raise "No latest migration!" unless latest
205
+ up_to latest, options
206
+ end
207
+
208
+ def to_target(date_str, options = {})
209
+ if date_str < current_latest(options)
210
+ down_to date_str, options
211
+ else
212
+ up_to date_str, options
213
+ end
214
+ end
215
+
216
+ def rollback(options = {})
217
+ down(current_latest(options), options)
218
+ end
219
+
220
+ end
@@ -0,0 +1,5 @@
1
+ # Copyright (C) 2013 OL2, inc. See LICENSE.txt for details.
2
+
3
+ class CassandraMigrate
4
+ VERSION = "0.0.1"
5
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cassandra_migrate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Noah Gibbs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: trollop
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: cql-rb
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: erubis
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Migrations for Cassandra in CQL and Erb.
95
+ email:
96
+ - noah.gibbs@onlive.com
97
+ executables:
98
+ - cassandra_migrate
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - bin/cassandra_migrate
108
+ - cassandra_migrate.gemspec
109
+ - lib/cassandra_migrate.rb
110
+ - lib/cassandra_migrate/version.rb
111
+ homepage: ''
112
+ licenses:
113
+ - MIT
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 1.8.25
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: Migrations for Cassandra in CQL and Erb.
136
+ test_files: []
137
+ has_rdoc: