tapsoob 0.2.7-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'sequel'
3
+ require 'sequel/extensions/schema_dumper'
4
+ require 'sequel/extensions/migration'
5
+ require 'json'
6
+
7
+ module Tapsoob
8
+ module Schema
9
+ extend self
10
+
11
+ def dump(database_url)
12
+ db = Sequel.connect(database_url)
13
+ db.extension :schema_dumper
14
+ db.dump_schema_migration(:indexes => false)
15
+ end
16
+
17
+ def dump_table(database_url, table)
18
+ table = table.to_sym
19
+ Sequel.connect(database_url) do |db|
20
+ db.extension :schema_dumper
21
+ <<END_MIG
22
+ Class.new(Sequel::Migration) do
23
+ def up
24
+ #{db.dump_table_schema(table, :indexes => false)}
25
+ end
26
+
27
+ def down
28
+ drop_table("#{table}") if @db.table_exists?("#{table}")
29
+ end
30
+ end
31
+ END_MIG
32
+ end
33
+ end
34
+
35
+ def indexes(database_url)
36
+ db = Sequel.connect(database_url)
37
+ db.extension :schema_dumper
38
+ db.dump_indexes_migration
39
+ end
40
+
41
+ def indexes_individual(database_url)
42
+ idxs = {}
43
+ Sequel.connect(database_url) do |db|
44
+ db.extension :schema_dumper
45
+
46
+ tables = db.tables
47
+ tables.each do |table|
48
+ idxs[table] = db.send(:dump_table_indexes, table, :add_index, {}).split("\n")
49
+ end
50
+ end
51
+
52
+ idxs.each do |table, indexes|
53
+ idxs[table] = indexes.map do |idx|
54
+ <<END_MIG
55
+ Class.new(Sequel::Migration) do
56
+ def up
57
+ #{idx}
58
+ end
59
+ end
60
+ END_MIG
61
+ end
62
+ end
63
+ JSON.generate(idxs)
64
+ end
65
+
66
+ def load(database_url, schema)
67
+ Sequel.connect(database_url) do |db|
68
+ db.extension :schema_dumper
69
+ klass = eval(schema)
70
+ klass.apply(db, :down)
71
+ klass.apply(db, :up)
72
+ end
73
+ end
74
+
75
+ def load_indexes(database_url, indexes)
76
+ Sequel.connect(database_url) do |db|
77
+ db.extension :schema_dumper
78
+ eval(indexes).apply(db, :up)
79
+ end
80
+ end
81
+
82
+ def reset_db_sequences(database_url)
83
+ db = Sequel.connect(database_url)
84
+ db.extension :schema_dumper
85
+ return unless db.respond_to?(:reset_primary_key_sequence)
86
+ db.tables.each do |table|
87
+ db.reset_primary_key_sequence(table)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,184 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'zlib'
3
+
4
+ require 'tapsoob/errors'
5
+ require 'tapsoob/chunksize'
6
+ require 'tapsoob/schema'
7
+
8
+ module Tapsoob
9
+ module Utils
10
+ extend self
11
+
12
+ def windows?
13
+ return @windows if defined?(@windows)
14
+ require 'rbconfig'
15
+ @windows = !!(::RbConfig::CONFIG['host_os'] =~ /mswin|mingw/)
16
+ end
17
+
18
+ def bin(cmd)
19
+ cmd = "#{cmd}.cmd" if windows?
20
+ cmd
21
+ end
22
+
23
+ def checksum(data)
24
+ Zlib.crc32(data)
25
+ end
26
+
27
+ def valid_data?(data, crc32)
28
+ Zlib.crc32(data) == crc32.to_i
29
+ end
30
+
31
+ def base64encode(data)
32
+ [data].pack("m")
33
+ end
34
+
35
+ def base64decode(data)
36
+ data.unpack("m").first
37
+ end
38
+
39
+ def format_data(data, opts = {})
40
+ return {} if data.size == 0
41
+ string_columns = opts[:string_columns] || []
42
+ schema = opts[:schema] || []
43
+ table = opts[:table]
44
+
45
+ max_lengths = schema.inject({}) do |hash, (column, meta)|
46
+ if meta[:db_type] =~ /^varchar\((\d+)\)/
47
+ hash.update(column => $1.to_i)
48
+ end
49
+ hash
50
+ end
51
+
52
+ header = data[0].keys
53
+ only_data = data.collect do |row|
54
+ row = blobs_to_string(row, string_columns)
55
+ row.each do |column, data|
56
+ if data.to_s.length > (max_lengths[column] || data.to_s.length)
57
+ raise Tapsoob::InvalidData.new(<<-ERROR)
58
+ Detected data that exceeds the length limitation of its column. This is
59
+ generally due to the fact that SQLite does not enforce length restrictions.
60
+
61
+ Table : #{table}
62
+ Column : #{column}
63
+ Type : #{schema.detect{|s| s.first == column}.last[:db_type]}
64
+ Data : #{data}
65
+ ERROR
66
+ end
67
+
68
+ # Type conversion
69
+ row[column] = data.strftime('%Y-%m-%d %H:%M:%S') if data.is_a?(Time)
70
+ end
71
+ header.collect { |h| row[h] }
72
+ end
73
+ { :header => header, :data => only_data }
74
+ end
75
+
76
+ # mysql text and blobs fields are handled the same way internally
77
+ # this is not true for other databases so we must check if the field is
78
+ # actually text and manually convert it back to a string
79
+ def incorrect_blobs(db, table)
80
+ return [] if (db.url =~ /mysql:\/\//).nil?
81
+
82
+ columns = []
83
+ db.schema(table).each do |data|
84
+ column, cdata = data
85
+ columns << column if cdata[:db_type] =~ /text/
86
+ end
87
+ columns
88
+ end
89
+
90
+ def blobs_to_string(row, columns)
91
+ return row if columns.size == 0
92
+ columns.each do |c|
93
+ row[c] = row[c].to_s if row[c].kind_of?(Sequel::SQL::Blob)
94
+ end
95
+ row
96
+ end
97
+
98
+ def calculate_chunksize(old_chunksize)
99
+ c = Tapsoob::Chunksize.new(old_chunksize)
100
+
101
+ begin
102
+ c.start_time = Time.now
103
+ c.time_in_db = yield c
104
+ rescue Errno::EPIPE
105
+ c.retries += 1
106
+ raise if c.retries > 2
107
+
108
+ # we got disconnected, the chunksize could be too large
109
+ # reset the chunksize based on the number of retries
110
+ c.reset_chunksize
111
+ retry
112
+ end
113
+
114
+ c.end_time = Time.now
115
+ c.calc_new_chunksize
116
+ end
117
+
118
+ def export_schema(dump_path, table, schema_data)
119
+ File.open(File.join(dump_path, "schemas", "#{table}.rb"), 'w') do |file|
120
+ file.write(schema_data)
121
+ end
122
+ end
123
+
124
+ def export_indexes(dump_path, table, index_data)
125
+ data = [index_data]
126
+ if File.exists?(File.join(dump_path, "indexes", "#{table}.json"))
127
+ previous_data = JSON.parse(File.read(File.join(dump_path, "indexes", "#{table}.json")))
128
+ data = data + previous_data
129
+ end
130
+
131
+ File.open(File.join(dump_path, "indexes", "#{table}.json"), 'w') do |file|
132
+ file.write(JSON.generate(data))
133
+ end
134
+ end
135
+
136
+ def export_rows(dump_path, table, row_data)
137
+ data = row_data
138
+ if File.exists?(File.join(dump_path, "data", "#{table}.json"))
139
+ previous_data = JSON.parse(File.read(File.join(dump_path, "data", "#{table}.json")))
140
+ data[:data] = previous_data["data"] + row_data[:data]
141
+ end
142
+
143
+ File.open(File.join(dump_path, "data", "#{table}.json"), 'w') do |file|
144
+ file.write(JSON.generate(data))
145
+ end
146
+ end
147
+
148
+ def load_schema(dump_path, database_url, table)
149
+ schema = File.join(dump_path, "schemas", "#{table}.rb")
150
+ schema_bin(:load, database_url, schema.to_s)
151
+ end
152
+
153
+ def load_indexes(database_url, index)
154
+ Tapsoob::Schema.load_indexes(database_url, index)
155
+ end
156
+
157
+ def schema_bin(command, *args)
158
+ require 'tapsoob/cli'
159
+ subcommand = "schema"
160
+ script = Tapsoob::CLI::Schema.new
161
+ script.invoke(command, args.map { |a| "#{a}" })
162
+ end
163
+
164
+ def primary_key(db, table)
165
+ db.schema(table).select { |c| c[1][:primary_key] }.map { |c| c[0] }
166
+ end
167
+
168
+ def single_integer_primary_key(db, table)
169
+ table = table.to_sym unless table.kind_of?(Sequel::SQL::Identifier)
170
+ keys = db.schema(table).select { |c| c[1][:primary_key] and c[1][:type] == :integer }
171
+ not keys.nil? and keys.size == 1
172
+ end
173
+
174
+ def order_by(db, table)
175
+ pkey = primary_key(db, table)
176
+ if pkey
177
+ pkey.kind_of?(Array) ? pkey : [pkey.to_sym]
178
+ else
179
+ table = table.to_sym unless table.kind_of?(Sequel::SQL::Identifier)
180
+ db[table].columns
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Tapsoob
3
+ VERSION = "0.2.7".freeze
4
+ end
data/lib/tapsoob.rb ADDED
@@ -0,0 +1,6 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'tapsoob/operation'
3
+
4
+ module Tapsoob
5
+ require 'tapsoob/railtie' if defined?(Rails)
6
+ end
@@ -0,0 +1,95 @@
1
+ namespace :tapsoob do
2
+ desc "Pulls a database to your filesystem"
3
+ task :pull => :environment do
4
+ # Default options
5
+ opts={:default_chunksize => 1000, :debug => false, :resume_filename => nil, :disable_compression => false, :indexes_first => false}
6
+
7
+ # Get the dump_path
8
+ dump_path = File.expand_path(Rails.root.join("db", Time.now.strftime("%Y%m%d%I%M%S%p"))).to_s
9
+
10
+ # Create paths
11
+ FileUtils.mkpath "#{dump_path}/schemas"
12
+ FileUtils.mkpath "#{dump_path}/data"
13
+ FileUtils.mkpath "#{dump_path}/indexes"
14
+
15
+ # Run operation
16
+ Tapsoob::Operation.factory(:pull, database_uri, dump_path, opts).run
17
+
18
+ # Invoke cleanup task
19
+ Rake::Task["tapsoob:clean"].reenable
20
+ Rake::Task["tapsoob:clean"].invoke
21
+ end
22
+
23
+ desc "Push a compatible dump on your filesystem to a database"
24
+ task :push, [:timestamp] => :environment do |t, args|
25
+ # Default options
26
+ opts={:default_chunksize => 1000, :debug => false, :resume_filename => nil, :disable_compression => false, :indexes_first => false}
27
+
28
+ # Get the dumps
29
+ dumps = Dir[Rails.root.join("db", "*/")].select { |e| e =~ /([0-9]{14})([A-Z]{2})/ }.sort
30
+
31
+ # In case a timestamp argument try to use it instead of using the last dump
32
+ dump_path = dumps.last
33
+ unless args[:timestamp].nil?
34
+ timestamps = dumps.map { |dump| File.basename(dump) }
35
+
36
+ # Check that the dump_path exists
37
+ raise Exception.new "Invalid or non existent timestamp: '#{args[:timestamp]}'" unless timestamps.include?(args[:timestamp])
38
+
39
+ # Select dump_path
40
+ dump_path = Rails.root.join("db", args[:timestamp])
41
+ end
42
+
43
+ # Run operation
44
+ Tapsoob::Operation.factory(:push, database_uri, dump_path, opts).run
45
+ end
46
+
47
+ desc "Cleanup old dumps"
48
+ task :clean, [:keep] => :environment do |t, args|
49
+ # Number of dumps to keep
50
+ keep = ((args[:keep] =~ /\A[0-9]+\z/).nil? ? 5 : args[:keep].to_i)
51
+
52
+ # Get all the dump folders
53
+ dumps = Dir[Rails.root.join("db", "*/")].select { |e| e =~ /([0-9]{14})([A-Z]{2})/ }.sort
54
+
55
+ # Delete old dumps only if there more than we want to keep
56
+ if dumps.count > keep
57
+ old_dumps = dumps - dumps.reverse[0..(keep - 1)]
58
+ old_dumps.each do |dir|
59
+ if Dir.exists?(dir)
60
+ puts "Deleting old dump directory ('#{dir}')"
61
+ FileUtils.remove_entry_secure(dir)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+ def database_uri
69
+ uri = ""
70
+ connection_config = YAML::load(ERB.new(Rails.root.join("config", "database.yml").read).result)[Rails.env]
71
+
72
+ case connection_config['adapter']
73
+ when "mysql", "mysql2"
74
+ uri = "#{connection_config['adapter']}://#{connection_config['host']}/#{connection_config['database']}?user=#{connection_config['username']}&password=#{connection_config['password']}"
75
+ when "postgresql", "postgres", "pg"
76
+ uri = "://#{connection_config['host']}/#{connection_config['database']}?user=#{connection_config['username']}&password=#{connection_config['password']}"
77
+ uri = ((RUBY_PLATFORM =~ /java/).nil? ? "postgres" : "postgresql") + uri
78
+ when "oracle_enhanced"
79
+ if (RUBY_PLATFORM =~ /java/).nil?
80
+ uri = "oracle://#{connection_config['host']}/#{connection_config['database']}?user=#{connection_config['username']}&password=#{connection_config['password']}"
81
+ else
82
+ uri = "oracle:thin:#{connection_config['username']}/#{connection_config['password']}@#{connection_config['host']}:1521:#{connection_config['database']}"
83
+ end
84
+ when "sqlite3", "sqlite"
85
+ uri = "sqlite://#{connection_config['database']}"
86
+ else
87
+ raise Exception, "Unsupported database adapter."
88
+ #uri = "#{connection_config['adapter']}://#{connection_config['host']}/#{connection_config['database']}?user=#{connection_config['username']}&password=#{connection_config['password']}"
89
+ end
90
+
91
+ uri = "jdbc:#{uri}" unless (RUBY_PLATFORM =~ /java/).nil?
92
+
93
+ uri
94
+ end
95
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'tapsoob/chunksize'
3
+
4
+ describe Tapsoob::Chunksize do
5
+ subject(:tapsoob) { Tapsoob::Chunksize.new(1) }
6
+ let(:chunksize) { double('chunksize') }
7
+ chunksize = Tapsoob::Chunksize.new(chunksize)
8
+ describe '#new' do
9
+ it 'works' do
10
+ result = Tapsoob::Chunksize.new(chunksize)
11
+ expect(result).not_to be_nil
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it { should respond_to :chunksize }
16
+ it { should respond_to :idle_secs }
17
+ it { should respond_to :retries }
18
+ end
19
+ end
20
+
21
+ describe '#to_i' do
22
+
23
+ it { expect(tapsoob.to_i).to eq(1) }
24
+ it { expect(tapsoob.to_i).to be_a(Integer) }
25
+ it 'works' do
26
+ chunksize = Tapsoob::Chunksize.new(chunksize)
27
+ result = chunksize.to_i
28
+ expect(result).not_to be_nil
29
+ end
30
+
31
+ context 'converts to type integer' do
32
+ it { expect(tapsoob.to_i).to eq(1) }
33
+ it { expect(tapsoob.to_i).to be_an(Integer) }
34
+ end
35
+ end
36
+
37
+ describe '#reset_chunksize' do
38
+
39
+ context 'retries <= 1' do
40
+ it { expect(tapsoob.retries).to eq(0) }
41
+ it { expect(tapsoob.reset_chunksize).to eq(10) }
42
+ end
43
+
44
+ it 'works' do
45
+ chunksize = Tapsoob::Chunksize.new(chunksize)
46
+ result = chunksize.reset_chunksize
47
+ expect(result).not_to be_nil
48
+ end
49
+ end
50
+
51
+
52
+ describe '#diff' do
53
+ it 'works' do
54
+ chunksize = Tapsoob::Chunksize.new(chunksize)
55
+ chunksize.start_time = 1
56
+ chunksize.end_time = 10
57
+ chunksize.time_in_db = 2
58
+ chunksize.idle_secs = 3
59
+ result = chunksize.diff
60
+ expect(result).not_to be_nil
61
+ end
62
+ end
63
+
64
+ describe '#time_in_db=' do
65
+ it 'works' do
66
+ chunksize = Tapsoob::Chunksize.new(chunksize)
67
+ result = chunksize.time_in_db = (1)
68
+ expect(result).not_to be_nil
69
+ end
70
+ end
71
+
72
+ describe '#time_delta' do
73
+ it 'works' do
74
+ chunksize = double('chunksize')
75
+ chunksize = Tapsoob::Chunksize.new(chunksize)
76
+ result = chunksize.time_delta
77
+ expect(result).not_to be_nil
78
+ end
79
+ end
80
+
81
+ describe '#calc_new_chunksize' do
82
+ it 'works' do
83
+ chunksize = Tapsoob::Chunksize.new(1)
84
+ chunksize.start_time = 1
85
+ chunksize.end_time = 10
86
+ chunksize.time_in_db = 2
87
+ chunksize.idle_secs = 3
88
+ result = chunksize.calc_new_chunksize
89
+ expect(result).not_to be_nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tapsoob::Chunksize do
4
+ it 'has a version number' do
5
+ expect(Tapsoob::Chunksize).not_to be nil
6
+ end
7
+ end
@@ -0,0 +1,91 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ # This file was generated by the `rspec --init` command. Conventionally, all
5
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
6
+ # The generated `.rspec` file contains
7
+ # `--require spec_helper` which will cause this
8
+ # file to always be loaded, without
9
+ # a need to explicitly require it in any files.
10
+ #
11
+ # Given that it is always loaded, you are encouraged to keep this file as
12
+ # light-weight as possible. Requiring heavyweight dependencies from this file
13
+ # will add to the boot time of your test suite on EVERY test run, even for an
14
+ # individual file that may not need all of that loaded. Instead, consider making
15
+ # a separate helper file that requires the additional dependencies and performs
16
+ # the additional setup,
17
+ # and require it from the spec files that actually need it.
18
+ #
19
+ # The `.rspec` file also contains a few flags that are not defaults but that
20
+ # users commonly want.
21
+ #
22
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
23
+ RSpec.configure do |config|
24
+ # rspec-expectations config goes here. You can use an alternate
25
+ # assertion/expectation library such as wrong or the stdlib/minitest
26
+ # assertions if you prefer.
27
+ config.expect_with :rspec do |expectations|
28
+ # This option will default to `true` in RSpec 4. It makes the `description`
29
+ # and `failure_message` of custom matchers include text for helper methods
30
+ # defined using `chain`, e.g.:
31
+ # be_bigger_than(2).and_smaller_than(4).description
32
+ # # => "be bigger than 2 and smaller than 4"
33
+ # ...rather than:
34
+ # # => "be bigger than 2"
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ end
37
+
38
+ # rspec-mocks config goes here. You can use an alternate test double
39
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
40
+ config.mock_with :rspec do |mocks|
41
+ # Prevents you from mocking or stubbing a method that does not exist on
42
+ # a real object. This is generally recommended, and will default to
43
+ # `true` in RSpec 4.
44
+ mocks.verify_partial_doubles = true
45
+ end
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ # # These two settings work together to allow you to limit a spec run
50
+ # # to individual examples or groups you care about by tagging them with
51
+ # # `:focus` metadata. When nothing is tagged with `:focus`, all examples
52
+ # # get run.
53
+ # config.filter_run :focus
54
+ # config.run_all_when_everything_filtered = true
55
+ #
56
+
57
+ # config.disable_monkey_patching!
58
+ #
59
+ # # This setting enables warnings. It's recommended, but in some cases may
60
+ # # be too noisy due to issues in dependencies.
61
+ # config.warnings = true
62
+ #
63
+ # # Many RSpec users commonly either run the entire suite or an individual
64
+ # # file, and it's useful to allow more verbose output when running an
65
+ # # individual spec file.
66
+ # if config.files_to_run.one?
67
+ # # Use the documentation formatter for detailed output,
68
+ # # unless a formatter has already been configured
69
+ # # (e.g. via a command-line flag).
70
+ # config.default_formatter = 'doc'
71
+ # end
72
+ #
73
+ # # Print the 10 slowest examples and example groups at the
74
+ # # end of the spec run, to help surface which specs are running
75
+ # # particularly slow.
76
+ # config.profile_examples = 10
77
+ #
78
+ # # Run specs in random order to surface order dependencies. If you find an
79
+ # # order dependency and want to debug it,
80
+ # # you can fix the order by providing
81
+ # # the seed, which is printed after each run.
82
+ # # --seed 1234
83
+ # config.order = :random
84
+ #
85
+ # # Seed global randomization in this process using the `--seed` CLI option.
86
+ # # Setting this allows you to use `--seed` to deterministically reproduce
87
+ # # test failures
88
+ # # related to randomization by passing the same `--seed` value
89
+ # # as the one that triggered the failure.
90
+ # Kernel.srand config.seed
91
+ end
data/tapsoob.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require "./lib/tapsoob/version" unless defined? Tapsoob::VERSION
3
+
4
+ Gem::Specification.new do |s|
5
+ # Metadata
6
+ s.name = "tapsoob"
7
+ s.version = Tapsoob::VERSION.dup
8
+ s.authors = ["Félix Bellanger", "Michael Chrisco"]
9
+ s.email = "felix.bellanger@faveod.com"
10
+ s.homepage = "https://github.com/Keeguon/tapsoob"
11
+ s.summary = "Simple tool to import/export databases."
12
+ s.description = "Simple tool to import/export databases inspired by taps but OOB, meaning databases are imported/exported from the filesystem."
13
+ s.license = "MIT"
14
+
15
+ # Manifest
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.bindir = 'bin'
20
+ s.require_paths = ["lib"]
21
+
22
+ # Dependencies
23
+ s.add_dependency "sequel", "~> 5.0.0"
24
+ s.add_dependency "thor", "~> 0.20.0"
25
+
26
+ if (RUBY_PLATFORM =~ /java/).nil?
27
+ s.add_development_dependency "mysql2", "~> 0.4.10"
28
+ s.add_development_dependency "pg", "~> 0.21.0"
29
+ s.add_development_dependency "sqlite3", "~> 1.3.11"
30
+ else
31
+ s.platform = 'java'
32
+
33
+ s.add_dependency "jdbc-mysql", "~> 5.1.44"
34
+ s.add_dependency "jdbc-postgres", "~> 42.1.4"
35
+ s.add_dependency "jdbc-sqlite3", "~> 3.20.1"
36
+ end
37
+ end