taps-jruby 0.3.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -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