cassandra_migrate 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 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: