tapsoob 0.2.7-java

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.
@@ -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