dkastner-taps 0.3.11

Sign up to get free protection for your applications and to get access to all the features.
data/lib/taps/utils.rb ADDED
@@ -0,0 +1,154 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+ require 'time'
4
+ require 'tempfile'
5
+
6
+ module Taps
7
+ module Utils
8
+ extend self
9
+
10
+ def windows?
11
+ return @windows if defined?(@windows)
12
+ require 'rbconfig'
13
+ @windows = !!(::Config::CONFIG['host_os'] =~ /mswin|mingw/)
14
+ end
15
+
16
+ def bin(cmd)
17
+ cmd = "#{cmd}.cmd" if windows?
18
+ cmd
19
+ end
20
+
21
+ def checksum(data)
22
+ Zlib.crc32(data)
23
+ end
24
+
25
+ def valid_data?(data, crc32)
26
+ Zlib.crc32(data) == crc32.to_i
27
+ end
28
+
29
+ def base64encode(data)
30
+ [data].pack("m")
31
+ end
32
+
33
+ def base64decode(data)
34
+ data.unpack("m").first
35
+ end
36
+
37
+ def format_data(data, opts={})
38
+ return {} if data.size == 0
39
+ string_columns = opts[:string_columns] || []
40
+
41
+ header = data[0].keys
42
+ only_data = data.collect do |row|
43
+ row = blobs_to_string(row, string_columns)
44
+ header.collect { |h| row[h] }
45
+ end
46
+ { :header => header, :data => only_data }
47
+ end
48
+
49
+ # mysql text and blobs fields are handled the same way internally
50
+ # this is not true for other databases so we must check if the field is
51
+ # actually text and manually convert it back to a string
52
+ def incorrect_blobs(db, table)
53
+ return [] if (db.url =~ /mysql:\/\//).nil?
54
+
55
+ columns = []
56
+ db.schema(table).each do |data|
57
+ column, cdata = data
58
+ columns << column if cdata[:db_type] =~ /text/
59
+ end
60
+ columns
61
+ end
62
+
63
+ def blobs_to_string(row, columns)
64
+ return row if columns.size == 0
65
+ columns.each do |c|
66
+ row[c] = row[c].to_s if row[c].kind_of?(Sequel::SQL::Blob)
67
+ end
68
+ row
69
+ end
70
+
71
+ def calculate_chunksize(old_chunksize)
72
+ chunksize = old_chunksize
73
+
74
+ retries = 0
75
+ time_in_db = 0
76
+ begin
77
+ t1 = Time.now
78
+ time_in_db = yield chunksize
79
+ time_in_db = time_in_db.to_f rescue 0
80
+ rescue Errno::EPIPE, RestClient::RequestFailed, RestClient::RequestTimeout
81
+ retries += 1
82
+ raise if retries > 2
83
+
84
+ # we got disconnected, the chunksize could be too large
85
+ # on first retry change to 10, on successive retries go down to 1
86
+ chunksize = (retries == 1) ? 10 : 1
87
+
88
+ retry
89
+ end
90
+
91
+ t2 = Time.now
92
+
93
+ diff = t2 - t1 - time_in_db
94
+
95
+ new_chunksize = if retries > 0
96
+ chunksize
97
+ elsif diff > 3.0
98
+ (chunksize / 3).ceil
99
+ elsif diff > 1.1
100
+ chunksize - 100
101
+ elsif diff < 0.8
102
+ chunksize * 2
103
+ else
104
+ chunksize + 100
105
+ end
106
+ new_chunksize = 1 if new_chunksize < 1
107
+ new_chunksize
108
+ end
109
+
110
+ def load_schema(database_url, schema_data)
111
+ Tempfile.open('taps') do |tmp|
112
+ File.open(tmp.path, 'w') { |f| f.write(schema_data) }
113
+ schema_bin(:load, database_url, tmp.path)
114
+ end
115
+ end
116
+
117
+ def load_indexes(database_url, index_data)
118
+ Tempfile.open('taps') do |tmp|
119
+ File.open(tmp.path, 'w') { |f| f.write(index_data) }
120
+ schema_bin(:load_indexes, database_url, tmp.path)
121
+ end
122
+ end
123
+
124
+ def schema_bin(*args)
125
+ bin_path = File.expand_path("#{File.dirname(__FILE__)}/../../bin/#{bin('schema')}")
126
+ `"#{bin_path}" #{args.map { |a| "'#{a}'" }.join(' ')}`
127
+ end
128
+
129
+ def primary_key(db, table)
130
+ table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
131
+ if db.respond_to?(:primary_key)
132
+ db.primary_key(table)
133
+ else
134
+ db.schema(table).select { |c| c[1][:primary_key] }.map { |c| c.first.to_sym }
135
+ end
136
+ end
137
+
138
+ def single_integer_primary_key(db, table)
139
+ table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
140
+ keys = db.schema(table).select { |c| c[1][:primary_key] and c[1][:type] == :integer }
141
+ not keys.nil? and keys.size == 1
142
+ end
143
+
144
+ def order_by(db, table)
145
+ pkey = primary_key(db, table)
146
+ if pkey
147
+ pkey.kind_of?(Array) ? pkey : [pkey.to_sym]
148
+ else
149
+ table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
150
+ db[table].columns
151
+ end
152
+ end
153
+ end
154
+ end
data/spec/base.rb ADDED
@@ -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 = "sqlite://#{Tempfile.new('test.db').path}"
26
+ Sequel.connect(Taps::Config.taps_database_url)
data/spec/cli_spec.rb ADDED
@@ -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", "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.compatible_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.compatible_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.compatible_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,55 @@
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 "scales chunksize down slowly when the time delta of the block is just over a second" do
16
+ Time.stubs(:now).returns(10.0).returns(11.5)
17
+ Taps::Utils.calculate_chunksize(1000) { }.should == 900
18
+ end
19
+
20
+ it "scales chunksize down fast when the time delta of the block is over 3 seconds" do
21
+ Time.stubs(:now).returns(10.0).returns(15.0)
22
+ Taps::Utils.calculate_chunksize(3000) { }.should == 1000
23
+ end
24
+
25
+ it "scales up chunksize fast when the time delta of the block is under 0.8 seconds" do
26
+ Time.stubs(:now).returns(10.0).returns(10.7)
27
+ Taps::Utils.calculate_chunksize(1000) { }.should == 2000
28
+ end
29
+
30
+ it "scales up chunksize slow when the time delta of the block is between 0.8 and 1.1 seconds" do
31
+ Time.stubs(:now).returns(10.0).returns(10.8)
32
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
33
+
34
+ Time.stubs(:now).returns(10.0).returns(11.1)
35
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
36
+ end
37
+
38
+ it "will reset the chunksize to a small value if we got a broken pipe exception" do
39
+ Taps::Utils.calculate_chunksize(1000) { |c| raise Errno::EPIPE if c == 1000; c.should == 10 }.should == 10
40
+ end
41
+
42
+ it "will reset the chunksize to a small value if we got a broken pipe exception a second time" do
43
+ Taps::Utils.calculate_chunksize(1000) { |c| raise Errno::EPIPE if c == 1000 || c == 10; c.should == 1 }.should == 1
44
+ end
45
+
46
+ it "returns a list of columns that are text fields if the database is mysql" do
47
+ @db = mock("db", :url => "mysql://localhost/mydb")
48
+ @db.stubs(:schema).with(:mytable).returns([
49
+ [:id, { :db_type => "int" }],
50
+ [:mytext, { :db_type => "text" }]
51
+ ])
52
+ Taps::Utils.incorrect_blobs(@db, :mytable).should == [:mytext]
53
+ end
54
+ end
55
+
metadata ADDED
@@ -0,0 +1,231 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dkastner-taps
3
+ version: !ruby/object:Gem::Version
4
+ hash: 5
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 11
10
+ version: 0.3.11
11
+ platform: ruby
12
+ authors:
13
+ - Ricardo Chimal, Jr.
14
+ - Derek Kastner
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-07-12 00:00:00 -04:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: json_pure
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 31
31
+ segments:
32
+ - 1
33
+ - 2
34
+ - 0
35
+ version: 1.2.0
36
+ - - <
37
+ - !ruby/object:Gem::Version
38
+ hash: 3
39
+ segments:
40
+ - 1
41
+ - 5
42
+ - 0
43
+ version: 1.5.0
44
+ type: :runtime
45
+ version_requirements: *id001
46
+ - !ruby/object:Gem::Dependency
47
+ name: sinatra
48
+ prerelease: false
49
+ requirement: &id002 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ hash: 23
55
+ segments:
56
+ - 1
57
+ - 0
58
+ - 0
59
+ version: 1.0.0
60
+ type: :runtime
61
+ version_requirements: *id002
62
+ - !ruby/object:Gem::Dependency
63
+ name: rest-client
64
+ prerelease: false
65
+ requirement: &id003 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ hash: 7
71
+ segments:
72
+ - 1
73
+ - 4
74
+ - 0
75
+ version: 1.4.0
76
+ type: :runtime
77
+ version_requirements: *id003
78
+ - !ruby/object:Gem::Dependency
79
+ name: sequel
80
+ prerelease: false
81
+ requirement: &id004 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ hash: 51
87
+ segments:
88
+ - 3
89
+ - 13
90
+ - 0
91
+ version: 3.13.0
92
+ type: :runtime
93
+ version_requirements: *id004
94
+ - !ruby/object:Gem::Dependency
95
+ name: sqlite3-ruby
96
+ prerelease: false
97
+ requirement: &id005 !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ hash: 11
103
+ segments:
104
+ - 1
105
+ - 2
106
+ version: "1.2"
107
+ type: :runtime
108
+ version_requirements: *id005
109
+ - !ruby/object:Gem::Dependency
110
+ name: rack
111
+ prerelease: false
112
+ requirement: &id006 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 21
118
+ segments:
119
+ - 1
120
+ - 0
121
+ - 1
122
+ version: 1.0.1
123
+ type: :runtime
124
+ version_requirements: *id006
125
+ - !ruby/object:Gem::Dependency
126
+ name: jeweler
127
+ prerelease: false
128
+ requirement: &id007 !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ hash: 3
134
+ segments:
135
+ - 0
136
+ version: "0"
137
+ type: :development
138
+ version_requirements: *id007
139
+ - !ruby/object:Gem::Dependency
140
+ name: bacon
141
+ prerelease: false
142
+ requirement: &id008 !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ hash: 3
148
+ segments:
149
+ - 0
150
+ version: "0"
151
+ type: :development
152
+ version_requirements: *id008
153
+ description: A simple database agnostic import/export app to transfer data to/from a remote database.
154
+ email: derek@brighterplanet.com
155
+ executables:
156
+ - taps
157
+ - schema
158
+ extensions: []
159
+
160
+ extra_rdoc_files:
161
+ - LICENSE
162
+ - README.rdoc
163
+ - TODO
164
+ files:
165
+ - LICENSE
166
+ - README.rdoc
167
+ - Rakefile
168
+ - VERSION.yml
169
+ - bin/schema
170
+ - bin/schema.cmd
171
+ - bin/taps
172
+ - lib/taps/cli.rb
173
+ - lib/taps/config.rb
174
+ - lib/taps/data_stream.rb
175
+ - lib/taps/db_session.rb
176
+ - lib/taps/log.rb
177
+ - lib/taps/monkey.rb
178
+ - lib/taps/multipart.rb
179
+ - lib/taps/operation.rb
180
+ - lib/taps/progress_bar.rb
181
+ - lib/taps/schema.rb
182
+ - lib/taps/server.rb
183
+ - lib/taps/utils.rb
184
+ - spec/base.rb
185
+ - spec/cli_spec.rb
186
+ - spec/data_stream_spec.rb
187
+ - spec/operation_spec.rb
188
+ - spec/server_spec.rb
189
+ - spec/utils_spec.rb
190
+ - TODO
191
+ has_rdoc: true
192
+ homepage: http://github.com/dkastner/taps
193
+ licenses: []
194
+
195
+ post_install_message:
196
+ rdoc_options:
197
+ - --charset=UTF-8
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ hash: 3
206
+ segments:
207
+ - 0
208
+ version: "0"
209
+ required_rubygems_version: !ruby/object:Gem::Requirement
210
+ none: false
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ hash: 3
215
+ segments:
216
+ - 0
217
+ version: "0"
218
+ requirements: []
219
+
220
+ rubyforge_project: taps
221
+ rubygems_version: 1.3.7
222
+ signing_key:
223
+ specification_version: 3
224
+ summary: simple database import/export app
225
+ test_files:
226
+ - spec/base.rb
227
+ - spec/cli_spec.rb
228
+ - spec/data_stream_spec.rb
229
+ - spec/operation_spec.rb
230
+ - spec/server_spec.rb
231
+ - spec/utils_spec.rb