taps-jruby 0.3.14

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,199 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+ require 'time'
4
+ require 'tempfile'
5
+
6
+ require 'taps/errors'
7
+
8
+ module Taps
9
+ module Utils
10
+ extend self
11
+
12
+ def windows?
13
+ return @windows if defined?(@windows)
14
+ require 'rbconfig'
15
+ @windows = !!(::Config::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] =~ /^\w+\((\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 Taps::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
+ end
68
+ header.collect { |h| row[h] }
69
+ end
70
+ { :header => header, :data => only_data }
71
+ end
72
+
73
+ # mysql text and blobs fields are handled the same way internally
74
+ # this is not true for other databases so we must check if the field is
75
+ # actually text and manually convert it back to a string
76
+ def incorrect_blobs(db, table)
77
+ return [] if (db.url =~ /mysql:\/\//).nil?
78
+
79
+ columns = []
80
+ db.schema(table).each do |data|
81
+ column, cdata = data
82
+ columns << column if cdata[:db_type] =~ /text/
83
+ end
84
+ columns
85
+ end
86
+
87
+ def blobs_to_string(row, columns)
88
+ return row if columns.size == 0
89
+ columns.each do |c|
90
+ row[c] = row[c].to_s if row[c].kind_of?(Sequel::SQL::Blob)
91
+ end
92
+ row
93
+ end
94
+
95
+ def calculate_chunksize(old_chunksize)
96
+ chunksize = old_chunksize
97
+
98
+ retries = 0
99
+ time_in_db = 0
100
+ begin
101
+ t1 = Time.now
102
+ time_in_db = yield chunksize
103
+ time_in_db = time_in_db.to_f rescue 0
104
+ rescue Errno::EPIPE, RestClient::RequestFailed, RestClient::RequestTimeout
105
+ retries += 1
106
+ raise if retries > 2
107
+
108
+ # we got disconnected, the chunksize could be too large
109
+ # on first retry change to 10, on successive retries go down to 1
110
+ chunksize = (retries == 1) ? 10 : 1
111
+
112
+ retry
113
+ end
114
+
115
+ t2 = Time.now
116
+
117
+ diff = t2 - t1 - time_in_db
118
+
119
+ new_chunksize = if retries > 0
120
+ chunksize
121
+ elsif diff > 3.0
122
+ (chunksize / 3).ceil
123
+ elsif diff > 1.1
124
+ chunksize - 100
125
+ elsif diff < 0.8
126
+ chunksize * 2
127
+ else
128
+ chunksize + 100
129
+ end
130
+ new_chunksize = 1 if new_chunksize < 1
131
+ new_chunksize
132
+ end
133
+
134
+ def load_schema(database_url, schema_data)
135
+ Tempfile.open('taps') do |tmp|
136
+ File.open(tmp.path, 'w') { |f| f.write(schema_data) }
137
+ schema_bin(:load, database_url, tmp.path)
138
+ end
139
+ end
140
+
141
+ def load_indexes(database_url, index_data)
142
+ Tempfile.open('taps') do |tmp|
143
+ File.open(tmp.path, 'w') { |f| f.write(index_data) }
144
+ schema_bin(:load_indexes, database_url, tmp.path)
145
+ end
146
+ end
147
+
148
+ def schema_bin(*args)
149
+ bin_path = File.expand_path("#{File.dirname(__FILE__)}/../../bin/#{bin('schema')}")
150
+ `"#{bin_path}" #{args.map { |a| "'#{a}'" }.join(' ')}`
151
+ end
152
+
153
+ def primary_key(db, table)
154
+ db.schema(table).select { |c| c[1][:primary_key] }.map { |c| c[0] }
155
+ end
156
+
157
+ def single_integer_primary_key(db, table)
158
+ table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
159
+ keys = db.schema(table).select { |c| c[1][:primary_key] and c[1][:type] == :integer }
160
+ not keys.nil? and keys.size == 1
161
+ end
162
+
163
+ def order_by(db, table)
164
+ pkey = primary_key(db, table)
165
+ if pkey
166
+ pkey.kind_of?(Array) ? pkey : [pkey.to_sym]
167
+ else
168
+ table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
169
+ db[table].columns
170
+ end
171
+ end
172
+
173
+
174
+ # try to detect server side errors to
175
+ # give the client a more useful error message
176
+ def server_error_handling(&blk)
177
+ begin
178
+ blk.call
179
+ rescue Sequel::DatabaseError => e
180
+ if e.message =~ /duplicate key value/i
181
+ raise Taps::DuplicatePrimaryKeyError, e.message
182
+ else
183
+ raise
184
+ end
185
+ end
186
+ end
187
+
188
+ def reraise_server_exception(e)
189
+ if e.kind_of?(RestClient::Exception)
190
+ if e.respond_to?(:response) && e.response.headers[:content_type] == 'application/json'
191
+ json = JSON.parse(e.response.to_s)
192
+ klass = eval(json['error_class']) rescue nil
193
+ raise klass.new(json['error_message'], :backtrace => json['error_backtrace']) if klass
194
+ end
195
+ end
196
+ raise e
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'mocha'
4
+ require 'rack/test'
5
+ require 'tempfile'
6
+
7
+ $:.unshift File.dirname(__FILE__) + "/../lib"
8
+
9
+ class Bacon::Context
10
+ include Mocha::Standalone
11
+ include Rack::Test::Methods
12
+
13
+ alias_method :old_it, :it
14
+ def it(description)
15
+ old_it(description) do
16
+ mocha_setup
17
+ yield
18
+ mocha_verify
19
+ mocha_teardown
20
+ end
21
+ end
22
+ end
23
+
24
+ require 'taps/config'
25
+ Taps::Config.taps_database_url = "jdbc:sqlite://#{Tempfile.new('test.db').path}"
26
+ Sequel.connect(Taps::Config.taps_database_url)
@@ -0,0 +1,10 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'taps/cli'
3
+
4
+ describe Taps::Cli do
5
+ it "translates a list of tables into a regex that can be used in table_filter" do
6
+ @cli = Taps::Cli.new(["-t", "mytable1,logs", "jdbc:sqlite://tmp.db", "http://x:y@localhost:5000"])
7
+ opts = @cli.clientoptparse(:pull)
8
+ opts[:table_filter].should == "(^mytable1$|^logs$)"
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'taps/data_stream'
3
+
4
+ describe Taps::DataStream do
5
+ before do
6
+ @db = mock('db')
7
+ end
8
+
9
+ it "increments the offset" do
10
+ stream = Taps::DataStream.new(@db, :table_name => 'test_table', :chunksize => 100)
11
+ stream.state[:offset].should == 0
12
+ stream.increment(100)
13
+ stream.state[:offset].should == 100
14
+ end
15
+
16
+ it "marks the stream complete if no rows are fetched" do
17
+ stream = Taps::DataStream.new(@db, :table_name => 'test_table', :chunksize => 100)
18
+ stream.stubs(:fetch_rows).returns({})
19
+ stream.complete?.should.be.false
20
+ stream.fetch
21
+ stream.complete?.should.be.true
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'taps/operation'
3
+
4
+ describe Taps::Operation do
5
+ before do
6
+ @op = Taps::Operation.new('dummy://localhost', 'http://x:y@localhost:5000')
7
+ end
8
+
9
+ it "returns an array of tables that match the regex table_filter" do
10
+ @op = Taps::Operation.new('dummy://localhost', 'http://x:y@localhost:5000', :table_filter => 'abc')
11
+ @op.apply_table_filter(['abc', 'def']).should == ['abc']
12
+ end
13
+
14
+ it "returns a hash of tables that match the regex table_filter" do
15
+ @op = Taps::Operation.new('dummy://localhost', 'http://x:y@localhost:5000', :table_filter => 'abc')
16
+ @op.apply_table_filter({ 'abc' => 1, 'def' => 2 }).should == { 'abc' => 1 }
17
+ end
18
+
19
+ it "masks a url's password" do
20
+ @op.safe_url("mysql://root:password@localhost/mydb").should == "mysql://root:[hidden]@localhost/mydb"
21
+ end
22
+
23
+ it "returns http headers with compression enabled" do
24
+ @op.http_headers.should == { :taps_version => Taps.version, :accept_encoding => "gzip, deflate" }
25
+ end
26
+
27
+ it "returns http headers with compression disabled" do
28
+ @op.stubs(:compression_disabled?).returns(true)
29
+ @op.http_headers.should == { :taps_version => Taps.version, :accept_encoding => "" }
30
+ end
31
+
32
+ end
@@ -0,0 +1,35 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ require 'taps/server'
4
+
5
+ require 'pp'
6
+
7
+ describe Taps::Server do
8
+ def app
9
+ Taps::Server.new
10
+ end
11
+
12
+ before do
13
+ Taps::Config.login = 'taps'
14
+ Taps::Config.password = 'tpass'
15
+
16
+ @app = Taps::Server
17
+ @auth_header = "Basic " + ["taps:tpass"].pack("m*")
18
+ end
19
+
20
+ it "asks for http basic authentication" do
21
+ get '/'
22
+ last_response.status.should == 401
23
+ end
24
+
25
+ it "verifies the client taps version" do
26
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => Taps.version })
27
+ last_response.status.should == 200
28
+ end
29
+
30
+ it "yells loudly if the client taps version doesn't match" do
31
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => '0.0.1' })
32
+ last_response.status.should == 417
33
+ end
34
+ end
35
+
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'taps/utils'
3
+
4
+ describe Taps::Utils do
5
+ it "generates a checksum using crc32" do
6
+ Taps::Utils.checksum("hello world").should == Zlib.crc32("hello world")
7
+ end
8
+
9
+ it "formats a data hash into one hash that contains an array of headers and an array of array of data" do
10
+ first_row = { :x => 1, :y => 1 }
11
+ first_row.stubs(:keys).returns([:x, :y])
12
+ Taps::Utils.format_data([ first_row, { :x => 2, :y => 2 } ]).should == { :header => [ :x, :y ], :data => [ [1, 1], [2, 2] ] }
13
+ end
14
+
15
+ it "enforces length limitations on columns" do
16
+ data = [ { :a => "aaabbbccc" } ]
17
+ schema = [ [ :a, { :db_type => "varchar(3)" }]]
18
+ lambda { Taps::Utils.format_data(data, :schema => schema) }.should.raise(Taps::InvalidData)
19
+ end
20
+
21
+ it "scales chunksize down slowly when the time delta of the block is just over a second" do
22
+ Time.stubs(:now).returns(10.0).returns(11.5)
23
+ Taps::Utils.calculate_chunksize(1000) { }.should == 900
24
+ end
25
+
26
+ it "scales chunksize down fast when the time delta of the block is over 3 seconds" do
27
+ Time.stubs(:now).returns(10.0).returns(15.0)
28
+ Taps::Utils.calculate_chunksize(3000) { }.should == 1000
29
+ end
30
+
31
+ it "scales up chunksize fast when the time delta of the block is under 0.8 seconds" do
32
+ Time.stubs(:now).returns(10.0).returns(10.7)
33
+ Taps::Utils.calculate_chunksize(1000) { }.should == 2000
34
+ end
35
+
36
+ it "scales up chunksize slow when the time delta of the block is between 0.8 and 1.1 seconds" do
37
+ Time.stubs(:now).returns(10.0).returns(10.8)
38
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
39
+
40
+ Time.stubs(:now).returns(10.0).returns(11.1)
41
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
42
+ end
43
+
44
+ it "will reset the chunksize to a small value if we got a broken pipe exception" do
45
+ Taps::Utils.calculate_chunksize(1000) { |c| raise Errno::EPIPE if c == 1000; c.should == 10 }.should == 10
46
+ end
47
+
48
+ it "will reset the chunksize to a small value if we got a broken pipe exception a second time" do
49
+ Taps::Utils.calculate_chunksize(1000) { |c| raise Errno::EPIPE if c == 1000 || c == 10; c.should == 1 }.should == 1
50
+ end
51
+
52
+ it "returns a list of columns that are text fields if the database is mysql" do
53
+ @db = mock("db", :url => "mysql://localhost/mydb")
54
+ @db.stubs(:schema).with(:mytable).returns([
55
+ [:id, { :db_type => "int" }],
56
+ [:mytext, { :db_type => "text" }]
57
+ ])
58
+ Taps::Utils.incorrect_blobs(@db, :mytable).should == [:mytext]
59
+ end
60
+ end
61
+
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: taps-jruby
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 14
9
+ version: 0.3.14
10
+ platform: ruby
11
+ authors:
12
+ - Ricardo Chimal, Jr.
13
+ - Rob Heittman
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-19 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json_pure
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 2
31
+ - 0
32
+ version: 1.2.0
33
+ - - <
34
+ - !ruby/object:Gem::Version
35
+ segments:
36
+ - 1
37
+ - 5
38
+ - 0
39
+ version: 1.5.0
40
+ type: :runtime
41
+ version_requirements: *id001
42
+ - !ruby/object:Gem::Dependency
43
+ name: sinatra
44
+ prerelease: false
45
+ requirement: &id002 !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 1
51
+ - 0
52
+ - 0
53
+ version: 1.0.0
54
+ type: :runtime
55
+ version_requirements: *id002
56
+ - !ruby/object:Gem::Dependency
57
+ name: rest-client
58
+ prerelease: false
59
+ requirement: &id003 !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 1
65
+ - 4
66
+ - 0
67
+ version: 1.4.0
68
+ - - <
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 1
72
+ - 7
73
+ - 0
74
+ version: 1.7.0
75
+ type: :runtime
76
+ version_requirements: *id003
77
+ - !ruby/object:Gem::Dependency
78
+ name: sequel
79
+ prerelease: false
80
+ requirement: &id004 !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 3
86
+ - 17
87
+ - 0
88
+ version: 3.17.0
89
+ type: :runtime
90
+ version_requirements: *id004
91
+ - !ruby/object:Gem::Dependency
92
+ name: activerecord-jdbcsqlite3-adapter
93
+ prerelease: false
94
+ requirement: &id005 !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 1
100
+ - 0
101
+ - 2
102
+ version: 1.0.2
103
+ type: :runtime
104
+ version_requirements: *id005
105
+ - !ruby/object:Gem::Dependency
106
+ name: rack
107
+ prerelease: false
108
+ requirement: &id006 !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 1
114
+ - 0
115
+ - 1
116
+ version: 1.0.1
117
+ type: :runtime
118
+ version_requirements: *id006
119
+ description: A simple database agnostic import/export app to transfer data to/from a remote database. (JRuby version)
120
+ email: heittman.rob@gmail.com
121
+ executables:
122
+ - taps
123
+ - schema
124
+ extensions: []
125
+
126
+ extra_rdoc_files:
127
+ - LICENSE
128
+ - README.rdoc
129
+ - TODO
130
+ files:
131
+ - LICENSE
132
+ - README.rdoc
133
+ - Rakefile
134
+ - VERSION.yml
135
+ - bin/schema
136
+ - bin/schema.cmd
137
+ - bin/taps
138
+ - lib/taps/cli.rb
139
+ - lib/taps/config.rb
140
+ - lib/taps/data_stream.rb
141
+ - lib/taps/db_session.rb
142
+ - lib/taps/errors.rb
143
+ - lib/taps/log.rb
144
+ - lib/taps/monkey.rb
145
+ - lib/taps/multipart.rb
146
+ - lib/taps/operation.rb
147
+ - lib/taps/progress_bar.rb
148
+ - lib/taps/schema.rb
149
+ - lib/taps/server.rb
150
+ - lib/taps/utils.rb
151
+ - spec/base.rb
152
+ - spec/cli_spec.rb
153
+ - spec/data_stream_spec.rb
154
+ - spec/operation_spec.rb
155
+ - spec/server_spec.rb
156
+ - spec/utils_spec.rb
157
+ - TODO
158
+ has_rdoc: true
159
+ homepage: http://github.com/rfc2616/taps-jruby
160
+ licenses: []
161
+
162
+ post_install_message:
163
+ rdoc_options: []
164
+
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ segments:
172
+ - 0
173
+ version: "0"
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ segments:
179
+ - 0
180
+ version: "0"
181
+ requirements: []
182
+
183
+ rubyforge_project:
184
+ rubygems_version: 1.3.6
185
+ signing_key:
186
+ specification_version: 3
187
+ summary: simple database import/export app - jruby version
188
+ test_files:
189
+ - spec/base.rb
190
+ - spec/cli_spec.rb
191
+ - spec/data_stream_spec.rb
192
+ - spec/operation_spec.rb
193
+ - spec/server_spec.rb
194
+ - spec/utils_spec.rb