cassandra_backup 0.0.6 → 0.1.0

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/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source :rubygems
2
+ gemspec
2
3
 
3
- gemspec
4
+ gem 'rake'
5
+ gem 'mocha'
@@ -6,16 +6,25 @@ CassandraBackup is a command line tool for dumping and restoring a Cassandra key
6
6
 
7
7
  Run cassandra_dump, specifying the keyspace:
8
8
 
9
- cassandra_dump depot_production > backup.json
9
+ cassandra_dump depot_production > backup.cql
10
10
 
11
11
  == Restoring a Keyspace
12
12
 
13
13
  Run cassandra_import, specifying the keyspace:
14
14
 
15
- cassandra_import depot_production < backup.json
15
+ cassandra_import depot_production < backup.cql
16
16
 
17
17
  == Options
18
18
 
19
- --servers:: A comma delimited list of servers. Defaults to 127.0.0.1:9160. (--servers 127.0.0.14:9169,127.0.1.19:9100)
20
- --version:: Set cassandra version. Defaults to 1.0. (--version 0.6)
21
- --columns:: The column families to dump. Defaults to all. (--columns Widgets --columns People)
19
+ --servers: A list of servers. Defaults to 127.0.0.1:9160. (--servers 127.0.0.14:9169,127.0.1.19:9100)
20
+ --version: Set cassandra version. Defaults to latest. (--version 1.0)
21
+ --tables: The tables to dump. Defaults to all. (--tables widgets,people)
22
+ --where: A condition to apply to all queries (--where="state='WA'")
23
+
24
+ == Notes on options
25
+
26
+ Options such as --servers and --tables support arrays in multiple formats. All of the following are the same:
27
+
28
+ --tables Widgets,People
29
+ --tables "Widgets People"
30
+ --tables Widgets --tables People
@@ -0,0 +1,12 @@
1
+ require 'bundler/setup'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ task default: :test
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'lib'
9
+ t.libs << 'test'
10
+ t.pattern = 'test/cassandra_backup/**/*_test.rb'
11
+ t.verbose = true
12
+ end
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.expand_path('../../lib/cassandra_backup', __FILE__)
4
- CassandraBackup::Dumper.run!
4
+ CassandraBackup::Dumper.run_command CassandraBackup::Command.new(ARGV)
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.expand_path('../../lib/cassandra_backup', __FILE__)
4
- CassandraBackup::Importer.run!
4
+ CassandraBackup::Importer.run_command CassandraBackup::Command.new(ARGV)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'cassandra_backup'
5
- s.version = '0.0.6'
5
+ s.version = '0.1.0'
6
6
  s.description = 'Cassandra Backup'
7
7
  s.summary = 'Cassandra Backup'
8
8
  s.authors = ['Infogroup']
@@ -16,7 +16,5 @@ Gem::Specification.new do |s|
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- {test}/*`.split("\n")
18
18
 
19
- s.add_runtime_dependency('mcmire-cassandra', ">= 0.12.3")
20
- s.add_runtime_dependency('yajl-ruby')
21
- s.add_development_dependency('bundler')
19
+ s.add_dependency 'cassandra-cql'
22
20
  end
@@ -1,4 +1,6 @@
1
- require 'yajl'
2
-
1
+ require 'cassandra-cql'
2
+ require 'cassandra_backup/command'
3
+ require 'cassandra_backup/config'
4
+ require 'cassandra_backup/cqlsh'
3
5
  require 'cassandra_backup/dumper'
4
6
  require 'cassandra_backup/importer'
@@ -3,9 +3,9 @@ require 'optparse'
3
3
  module CassandraBackup
4
4
  class Command
5
5
 
6
- attr_accessor :options
6
+ attr_reader :options
7
7
  def initialize(args)
8
- self.options = {}
8
+ @options = {}
9
9
 
10
10
  if args.first
11
11
  options[:keyspace] = args.first
@@ -15,48 +15,33 @@ module CassandraBackup
15
15
 
16
16
  OptionParser.new do |opts|
17
17
  opts.banner = "Usage: keyspace [options]"
18
- opts.on('-s', '--servers SERVERS', 'Set server list. Default is 127.0.0.1:9160.') do |v|
19
- options[:servers] = v.split(/\s|,/)
18
+ opts.on('-s', '--servers servers', 'Set server list. Default is 127.0.0.1:9160.') do |v|
19
+ store_array_option :servers, v
20
20
  end
21
- opts.on('-v', '--version VERSION', 'Set cassandra version. Default is 1.0.') do |v|
21
+ opts.on('-c', '--tables tables', 'Set tables. Defaults to all') do |v|
22
+ store_array_option :tables, v
23
+ end
24
+ opts.on('-v', '--version VERSION', 'Set cassandra version. Defaults to latest.') do |v|
22
25
  options[:version] = v
23
26
  end
24
- opts.on('-c', '--columns Columns', 'Set column families. Defaults to all') do |v|
25
- (options[:columns] ||= []).concat v.split(/\s|,/)
27
+ opts.on('-w', '--where=\'where_conditions\'') do |v|
28
+ options[:where] = v
26
29
  end
27
30
  opts.on('-h', 'Show this help message.') do
28
31
  $stdout.puts opts; exit
29
32
  end
30
- opts.parse!(args)
31
- end
32
-
33
- require required_version
34
- end
35
-
36
- def keyspace
37
- options[:keyspace]
38
- end
39
33
 
40
- def servers
41
- if options[:servers]
42
- options[:servers]
43
- else
44
- ['127.0.0.1:9160']
34
+ opts.parse!(args)
45
35
  end
46
36
  end
47
37
 
48
- def columns
49
- if options[:columns]
50
- options[:columns]
51
- end
38
+ def config
39
+ CassandraBackup::Config.new(options)
52
40
  end
53
41
 
54
- def required_version
55
- if options[:version]
56
- "cassandra/#{options[:version]}"
57
- else
58
- "cassandra/1.0"
42
+ private
43
+ def store_array_option(name, value)
44
+ (options[name] ||= []).concat value.split(/\s|,/)
59
45
  end
60
- end
61
46
  end
62
47
  end
@@ -0,0 +1,53 @@
1
+ module CassandraBackup
2
+ class Config
3
+ attr_reader :options
4
+ def initialize(options = {})
5
+ @options = options
6
+
7
+ require_version!
8
+ end
9
+
10
+ def servers
11
+ if options[:servers]
12
+ options[:servers]
13
+ else
14
+ ['127.0.0.1:9160']
15
+ end
16
+ end
17
+
18
+ def input
19
+ options[:input] || STDIN
20
+ end
21
+
22
+ def output
23
+ options[:output] || STDOUT
24
+ end
25
+
26
+ def require_version!
27
+ if options[:version]
28
+ require "cassandra-cql/#{options[:version]}"
29
+ end
30
+ end
31
+
32
+ def keyspace
33
+ options[:keyspace]
34
+ end
35
+
36
+ def tables
37
+ options[:tables]
38
+ end
39
+
40
+ def where
41
+ options[:where]
42
+ end
43
+
44
+ def connection
45
+ if keyspace
46
+ options = {keyspace: keyspace}
47
+ else
48
+ options = {}
49
+ end
50
+ @connection ||= CassandraCQL::Database.new(servers, options)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ module CassandraBackup
2
+ class Cqlsh
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def exec(command)
11
+ `echo "#{command};" | #{cqlsh} -k #{keyspace} #{server}`.sub(/^(.*)$/, '').strip
12
+ end
13
+
14
+ private
15
+ def cqlsh
16
+ ENV['CQLSH'] || 'cqlsh'
17
+ end
18
+
19
+ def keyspace
20
+ config.keyspace
21
+ end
22
+
23
+ def server
24
+ config.servers.first.gsub(/:.*/, '')
25
+ end
26
+ end
27
+ end
@@ -1,26 +1,79 @@
1
- require 'cassandra_backup/backup_process'
2
-
3
1
  module CassandraBackup
4
- class Dumper < BackupProcess
2
+ class Dumper
3
+ def self.run_command(command)
4
+ new(command.config).run
5
+ end
6
+
7
+ attr_reader :config
8
+ attr_reader :cqlsh
9
+ attr_reader :primary_keys
10
+ def initialize(config)
11
+ @config = config
12
+ @cqlsh = CassandraBackup::Cqlsh.new(config)
13
+ @primary_keys = {}
14
+ end
15
+
5
16
  def run
6
- column_family_names.each do |column_family|
7
- connection.each(column_family) do |key, attributes|
8
- output_io.puts encoder.encode([column_family, key, attributes])
9
- end
17
+ table_names.each { |table_name| dump_table_schema(table_name) }
18
+ table_names.each { |table_name| dump_table_rows(table_name) }
19
+ end
20
+
21
+ def dump_table_schema(table_name)
22
+ table_description = cqlsh.exec("DESCRIBE COLUMNFAMILY #{table_name}")
23
+ capture_primary_key(table_name, table_description)
24
+ config.output.puts(table_description)
25
+ config.output.puts
26
+ end
27
+
28
+ def dump_table_rows(table_name)
29
+ primary_key = primary_key_for(table_name)
30
+ each_row(table_name) do |row|
31
+ next if row.keys.size == 1 && row.keys.first == primary_key
32
+
33
+ colummns = CassandraCQL::Statement.quote(row.keys)
34
+ values = CassandraCQL::Statement.quote(row.values.map { |v| CassandraCQL::Statement.escape(v) })
35
+
36
+ config.output.puts "INSERT INTO #{table_name} (#{colummns}) VALUES (#{values});"
10
37
  end
11
38
  end
12
39
 
40
+ def table_names
41
+ if config.tables
42
+ config.tables
43
+ else
44
+ cqlsh.exec('DESCRIBE COLUMNFAMILIES').split.sort
45
+ end
46
+ end
47
+
48
+ def execute(cql)
49
+ config.connection.execute(cql)
50
+ end
51
+
13
52
  private
14
- def column_family_names
15
- if command.columns
16
- command.columns
17
- else
18
- connection.column_families.keys - ['schema_migrations']
53
+ def primary_key_for(table_name)
54
+ primary_keys[table_name] || 'id'
55
+ end
56
+
57
+ def capture_primary_key(table_name, table_description)
58
+ if table_description =~ /^\s*(\w+).*PRIMARY KEY/
59
+ primary_keys[table_name] = $1
19
60
  end
20
61
  end
21
62
 
22
- def encoder
23
- @encoder ||= Yajl::Encoder.new
63
+ def each_row(table_name)
64
+ primary_key = primary_key_for(table_name)
65
+ cassandra_result = execute "SELECT * FROM #{table_name}"
66
+
67
+ while cassandra_result.rows > 0
68
+ last_fetch = nil
69
+ cassandra_result.fetch_hash do |hash|
70
+ yield(hash)
71
+ last_fetch = hash
72
+ end
73
+
74
+ where_cql = "'#{primary_key}' > '#{last_fetch[primary_key]}'"
75
+ cassandra_result = execute "SELECT * FROM #{table_name} WHERE #{where_cql}"
76
+ end
24
77
  end
25
78
  end
26
- end
79
+ end
@@ -1,17 +1,37 @@
1
- require 'cassandra_backup/backup_process'
2
-
3
1
  module CassandraBackup
4
- class Importer < BackupProcess
2
+ class Importer
3
+ def self.run_command(command)
4
+ new(command.config).run
5
+ end
6
+
7
+ attr_reader :config
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
5
12
  def run
6
- parser.parse(input_io) do |column_family, key, attributes|
7
- p "inserting"
8
- connection.insert column_family, key, attributes
13
+ each_full_statement(config.input) do |cql|
14
+ execute cql
9
15
  end
10
16
  end
11
17
 
12
- private
13
- def parser
14
- @parser ||= Yajl::Parser.new
18
+ def execute(cql)
19
+ config.connection.execute(cql)
20
+ end
21
+
22
+ def each_full_statement(io)
23
+ current_cql = ''
24
+
25
+ io.each_line do |line|
26
+ next unless line =~ /[^[:space:]]/
27
+
28
+ current_cql << line.rstrip
29
+
30
+ if current_cql =~ /;$/
31
+ yield(current_cql)
32
+ current_cql = ''
33
+ end
15
34
  end
35
+ end
16
36
  end
17
37
  end
@@ -0,0 +1,33 @@
1
+ require 'helper'
2
+ require 'shellwords'
3
+
4
+ class CassandraBackup::CommandTest < MiniTest::Spec
5
+ def test_keyspace
6
+ assert_equal 'zombies', command('zombies').options[:keyspace]
7
+ end
8
+
9
+ def test_single_value_options
10
+ [:where, :version].each do |option|
11
+ assert_nil command('zombies').options[option]
12
+ assert_equal 'foo', command("zombies --#{option} foo").options[option]
13
+ end
14
+ end
15
+
16
+ def test_array_options
17
+ [:servers, :tables].each do |option|
18
+ assert_nil command('zombies').options[option]
19
+ assert_equal ['foo', 'bar'], command("zombies --#{option} \"foo bar\"").options[option]
20
+ assert_equal ['foo', 'bar'], command("zombies --#{option} foo,bar").options[option]
21
+ assert_equal ['foo', 'bar'], command("zombies --#{option} foo --#{option} bar").options[option]
22
+ end
23
+ end
24
+
25
+ def test_config
26
+ assert_kind_of CassandraBackup::Config, command('zombies').config
27
+ end
28
+
29
+ private
30
+ def command(line)
31
+ CassandraBackup::Command.new Shellwords.shellsplit(line)
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::ConfigTest < MiniTest::Spec
4
+ def test_servers
5
+ assert_equal ['127.0.0.1:9160'], config.servers
6
+ assert_equal ['foo'], config(servers: ['foo']).servers
7
+ end
8
+
9
+ def test_input
10
+ assert_equal STDIN, config.input
11
+ refute_equal STDIN, config(input: StringIO.new).input
12
+ end
13
+
14
+ def test_output
15
+ assert_equal STDOUT, config.output
16
+ refute_equal STDOUT, config(output: StringIO.new).output
17
+ end
18
+
19
+ def test_require_version
20
+ # expects(:require).with('cassandra-cql/1.0').once
21
+ # CassandraBackup::Config.new(version: '1.0')
22
+ end
23
+
24
+ private
25
+ def config(options = {})
26
+ CassandraBackup::Config.new(options)
27
+ end
28
+
29
+ end
@@ -0,0 +1,43 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::DumperTest < MiniTest::Spec
4
+ def test_table_names
5
+ assert_equal ['presidents', 'senators'], create_dumper.table_names
6
+ assert_equal ['presidents'], create_dumper(tables: ['presidents']).table_names
7
+ end
8
+
9
+ def test_dump_table_schema
10
+ output = StringIO.new
11
+ dumper = create_dumper output: output
12
+
13
+ dumper.dump_table_schema('senators')
14
+
15
+ output.rewind
16
+ lines = output.readlines
17
+ assert_match /CREATE TABLE senators/, lines.first
18
+ assert_equal "\n", lines.last
19
+ assert_equal({"senators" => "id"}, dumper.primary_keys)
20
+ end
21
+
22
+ def test_dump_table_rows
23
+ output = StringIO.new
24
+ dumper = create_dumper output: output
25
+ dumper.execute "TRUNCATE senators"
26
+ dumper.execute "INSERT INTO senators ('id', 'name', 'state') VALUES ('abc', 'K''la', 'WA')"
27
+ dumper.execute "INSERT INTO senators ('id', 'name', 'state') VALUES ('xyz', 'Bob', 'NY')"
28
+
29
+ dumper.dump_table_rows('senators')
30
+
31
+ output.rewind
32
+ lines = output.readlines
33
+
34
+ assert_equal 2, lines.size
35
+ assert lines.include?("INSERT INTO senators ('id','name','state') VALUES ('abc','K''la','WA');\n")
36
+ assert lines.include?("INSERT INTO senators ('id','name','state') VALUES ('xyz','Bob','NY');\n")
37
+ end
38
+
39
+ private
40
+ def create_dumper(options = {})
41
+ CassandraBackup::Dumper.new CassandraBackup::Config.new(options.merge(keyspace: 'cassandra_dump_test'))
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::ImporterTest < MiniTest::Spec
4
+ def test_run
5
+ input = StringIO.new("INSERT INTO presidents (id, name) VALUES ('1st', 'George Washington');")
6
+ importer = create_importer input: input
7
+ importer.execute "TRUNCATE presidents"
8
+
9
+ importer.run
10
+
11
+ assert_equal 1, importer.execute("select * from presidents").rows
12
+ end
13
+
14
+ def test_each_full_statement
15
+ input = StringIO.new(
16
+ "INSERT INTO presidents (id, name) VALUES\n" +
17
+ " ('1st', 'George Washington');\n" +
18
+ " \n" +
19
+ "UPDATE presidents SET name='Zombie' WHERE id='1st';\n" +
20
+ "\n"
21
+ )
22
+ stmts = []
23
+
24
+ create_importer.each_full_statement(input) do |cql|
25
+ stmts << cql
26
+ end
27
+
28
+ expected = [
29
+ "INSERT INTO presidents (id, name) VALUES ('1st', 'George Washington');",
30
+ "UPDATE presidents SET name='Zombie' WHERE id='1st';"
31
+ ]
32
+ assert_equal expected, stmts
33
+ end
34
+
35
+ private
36
+
37
+ def create_importer(options = {})
38
+ CassandraBackup::Importer.new CassandraBackup::Config.new(options.merge(keyspace: 'cassandra_dump_test'))
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require 'minitest/autorun'
3
+ Bundler.require
4
+
5
+ require 'cassandra_backup'
6
+ require 'setup/connection'
@@ -0,0 +1,11 @@
1
+ CassandraBackup::Config.new.tap do |config|
2
+ begin
3
+ config.connection.execute("DROP KEYSPACE cassandra_dump_test")
4
+ rescue Exception => e
5
+ end
6
+ config.connection.execute("CREATE KEYSPACE cassandra_dump_test WITH strategy_class='org.apache.cassandra.locator.SimpleStrategy' AND strategy_options:replication_factor=1")
7
+ config.connection.execute("USE cassandra_dump_test")
8
+ config.connection.execute("CREATE TABLE presidents (id varchar PRIMARY KEY)")
9
+ config.connection.execute("CREATE TABLE senators (id varchar PRIMARY KEY, state varchar)")
10
+ config.connection.execute("CREATE INDEX senators_state_index ON senators(state)")
11
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra_backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,26 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-19 00:00:00.000000000 Z
12
+ date: 2012-11-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: mcmire-cassandra
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: 0.12.3
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: 0.12.3
30
- - !ruby/object:Gem::Dependency
31
- name: yajl-ruby
15
+ name: cassandra-cql
32
16
  requirement: !ruby/object:Gem::Requirement
33
17
  none: false
34
18
  requirements:
@@ -43,22 +27,6 @@ dependencies:
43
27
  - - ! '>='
44
28
  - !ruby/object:Gem::Version
45
29
  version: '0'
46
- - !ruby/object:Gem::Dependency
47
- name: bundler
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
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
30
  description: Cassandra Backup
63
31
  email: gems@gotime.com
64
32
  executables:
@@ -71,15 +39,22 @@ files:
71
39
  - .gitignore
72
40
  - Gemfile
73
41
  - README.rdoc
42
+ - Rakefile
74
43
  - bin/cassandra_dump
75
44
  - bin/cassandra_import
76
45
  - cassandra_backup.gemspec
77
46
  - lib/cassandra_backup.rb
78
- - lib/cassandra_backup/backup_process.rb
79
47
  - lib/cassandra_backup/command.rb
48
+ - lib/cassandra_backup/config.rb
49
+ - lib/cassandra_backup/cqlsh.rb
80
50
  - lib/cassandra_backup/dumper.rb
81
51
  - lib/cassandra_backup/importer.rb
82
- - test/test_helper.rb
52
+ - test/cassandra_backup/command_test.rb
53
+ - test/cassandra_backup/config_test.rb
54
+ - test/cassandra_backup/dumper_test.rb
55
+ - test/cassandra_backup/importer_test.rb
56
+ - test/helper.rb
57
+ - test/setup/connection.rb
83
58
  homepage: http://github.com/data-axle/cassandra_backup
84
59
  licenses: []
85
60
  post_install_message:
@@ -1,30 +0,0 @@
1
- require 'cassandra_backup/command'
2
-
3
- module CassandraBackup
4
- class BackupProcess
5
- def self.run!
6
- new(CassandraBackup::Command.new(ARGV)).run
7
- end
8
-
9
- attr_reader :command
10
- def initialize(command)
11
- @command = command
12
- end
13
-
14
- def run
15
- raise "inheriting class must implement"
16
- end
17
-
18
- def connection
19
- @connection ||= Cassandra.new(command.keyspace, command.servers)
20
- end
21
-
22
- def input_io
23
- $stdin
24
- end
25
-
26
- def output_io
27
- $stdout
28
- end
29
- end
30
- end
@@ -1,2 +0,0 @@
1
- # require 'bundler/setup'
2
- # Bundler.require