matthewtodd-taps 0.2.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
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 checksum(data)
11
+ Zlib.crc32(data)
12
+ end
13
+
14
+ def valid_data?(data, crc32)
15
+ Zlib.crc32(data) == crc32.to_i
16
+ end
17
+
18
+ def gzip(data)
19
+ io = StringIO.new
20
+ gz = Zlib::GzipWriter.new(io)
21
+ gz.write data
22
+ gz.close
23
+ io.string
24
+ end
25
+
26
+ def gunzip(gzip_data)
27
+ io = StringIO.new(gzip_data)
28
+ gz = Zlib::GzipReader.new(io)
29
+ data = gz.read
30
+ gz.close
31
+ data
32
+ end
33
+
34
+ def format_data(data, string_columns)
35
+ return {} if data.size == 0
36
+ header = data[0].keys
37
+ only_data = data.collect do |row|
38
+ row = blobs_to_string(row, string_columns)
39
+ header.collect { |h| row[h] }
40
+ end
41
+ { :header => header, :data => only_data }
42
+ end
43
+
44
+ # mysql text and blobs fields are handled the same way internally
45
+ # this is not true for other databases so we must check if the field is
46
+ # actually text and manually convert it back to a string
47
+ def incorrect_blobs(db, table)
48
+ return [] unless db.class.to_s == "Sequel::MySQL::Database"
49
+
50
+ columns = []
51
+ db.schema(table).each do |data|
52
+ column, cdata = data
53
+ columns << column if cdata[:db_type] =~ /text/
54
+ end
55
+ columns
56
+ end
57
+
58
+ def blobs_to_string(row, columns)
59
+ return row if columns.size == 0
60
+ columns.each do |c|
61
+ row[c] = row[c].to_s if row[c].kind_of?(Sequel::SQL::Blob)
62
+ end
63
+ row
64
+ end
65
+
66
+ def calculate_chunksize(old_chunksize)
67
+ chunksize = old_chunksize
68
+
69
+ retries = 0
70
+ begin
71
+ t1 = Time.now
72
+ yield chunksize
73
+ rescue Errno::EPIPE
74
+ retries += 1
75
+ raise if retries > 1
76
+ # we got disconnected, the chunksize could be too large
77
+ # so we're resetting to a very small value
78
+ chunksize = 100
79
+ retry
80
+ end
81
+
82
+ t2 = Time.now
83
+
84
+ diff = t2 - t1
85
+ new_chunksize = if diff > 3.0
86
+ (chunksize / 3).ceil
87
+ elsif diff > 1.1
88
+ chunksize - 100
89
+ elsif diff < 0.8
90
+ chunksize * 2
91
+ else
92
+ chunksize + 100
93
+ end
94
+ new_chunksize = 100 if new_chunksize < 100
95
+ new_chunksize
96
+ end
97
+
98
+ def primary_key(db, table)
99
+ if db.respond_to?(:primary_key)
100
+ db.primary_key(table)
101
+ else
102
+ db.schema(table).select { |c| c[1][:primary_key] }.map { |c| c.first }.shift
103
+ end
104
+ end
105
+
106
+ def order_by(db, table)
107
+ pkey = primary_key(db, table)
108
+ if pkey
109
+ [pkey.to_sym]
110
+ else
111
+ db[table].columns
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'mocha'
4
+
5
+ class Bacon::Context
6
+ include Mocha::Standalone
7
+
8
+ alias_method :old_it, :it
9
+ def it(description)
10
+ old_it(description) do
11
+ mocha_setup
12
+ yield
13
+ mocha_verify
14
+ mocha_teardown
15
+ end
16
+ end
17
+ end
18
+
19
+ require File.dirname(__FILE__) + '/../lib/taps/config'
20
+ Taps::Config.taps_database_url = 'sqlite://test.db'
21
+ Sequel.connect(Taps::Config.taps_database_url)
@@ -0,0 +1,88 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/client_session'
3
+
4
+ describe Taps::ClientSession do
5
+ before do
6
+ @client = Taps::ClientSession.new('sqlite://my.db', 'http://example.com:3000', 1000)
7
+ @client.stubs(:session_resource).returns(mock('session resource'))
8
+ end
9
+
10
+ it "starts a session and yields the session object to the block" do
11
+ Taps::ClientSession.start('x', 'y', 1000) do |session|
12
+ session.database_url.should == 'x'
13
+ session.remote_url.should == 'y'
14
+ session.default_chunksize.should == 1000
15
+ end
16
+ end
17
+
18
+ it "opens the local db connection via sequel and the database url" do
19
+ Sequel.expects(:connect).with('sqlite://my.db').returns(:con)
20
+ @client.db.should == :con
21
+ end
22
+
23
+ it "creates a restclient resource to the remote server" do
24
+ @client.server.url.should == 'http://example.com:3000'
25
+ end
26
+
27
+ it "verifies the db version, receive the schema, data, indexes, then reset the sequences" do
28
+ @client.expects(:verify_server)
29
+ @client.expects(:cmd_receive_schema)
30
+ @client.expects(:cmd_receive_data)
31
+ @client.expects(:cmd_receive_indexes)
32
+ @client.expects(:cmd_reset_sequences)
33
+ @client.cmd_receive.should.be.nil
34
+ end
35
+
36
+ it "checks the version of the server by seeing if it has access" do
37
+ @client.stubs(:server).returns(mock('server'))
38
+ @request = mock('request')
39
+ @client.server.expects(:[]).with('/').returns(@request)
40
+ @request.expects(:get).with({:taps_version => Taps.compatible_version})
41
+
42
+ lambda { @client.verify_server }.should.not.raise
43
+ end
44
+
45
+ it "receives data from a remote taps server" do
46
+ @client.stubs(:puts)
47
+ @progressbar = mock('progressbar')
48
+ ProgressBar.stubs(:new).with('mytable', 2).returns(@progressbar)
49
+ @progressbar.stubs(:inc)
50
+ @progressbar.stubs(:finish)
51
+ @mytable = mock('mytable')
52
+ @client.expects(:fetch_remote_tables_info).returns([ { :mytable => 2 }, 2 ])
53
+ @client.stubs(:db).returns(mock('db'))
54
+ @client.db.stubs(:[]).with(:mytable).returns(@mytable)
55
+ @client.expects(:fetch_table_rows).with(:mytable, 1000, 0).returns([ 1000, { :header => [:x, :y], :data => [[1, 2], [3, 4]] } ])
56
+ @client.expects(:fetch_table_rows).with(:mytable, 1000, 2).returns([ 1000, { }])
57
+ @mytable.expects(:import).with([:x, :y], [[1, 2], [3, 4]])
58
+
59
+ lambda { @client.cmd_receive_data }.should.not.raise
60
+ end
61
+
62
+ it "fetches remote tables info from taps server" do
63
+ @marshal_data = Marshal.dump({ :mytable => 2 })
64
+ @client.session_resource.stubs(:[]).with('tables').returns(mock('tables'))
65
+ @client.session_resource['tables'].stubs(:get).with(:taps_version => Taps.compatible_version).returns(@marshal_data)
66
+ @client.fetch_remote_tables_info.should == [ { :mytable => 2 }, 2 ]
67
+ end
68
+
69
+ it "fetches table rows given a chunksize and offset from taps server" do
70
+ @data = { :header => [ :x, :y ], :data => [ [1, 2], [3, 4] ] }
71
+ @gzip_data = Taps::Utils.gzip(Marshal.dump(@data))
72
+ Taps::Utils.stubs(:calculate_chunksize).with(1000).yields(1000).returns(1000)
73
+
74
+ @response = mock('response')
75
+ @client.session_resource.stubs(:[]).with('tables/mytable/1000?offset=0').returns(mock('table resource'))
76
+ @client.session_resource['tables/mytable/1000?offset=0'].expects(:get).with(:taps_version => Taps.compatible_version).returns(@response)
77
+ @response.stubs(:to_s).returns(@gzip_data)
78
+ @response.stubs(:headers).returns({ :taps_checksum => Taps::Utils.checksum(@gzip_data) })
79
+ @client.fetch_table_rows('mytable', 1000, 0).should == [ 1000, { :header => [:x, :y], :data => [[1, 2], [3, 4]] } ]
80
+ end
81
+
82
+ it "hides the password in urls" do
83
+ @client.safe_url("postgres://postgres:password@localhost/mydb").should == "postgres://postgres:[hidden]@localhost/mydb"
84
+ @client.safe_url("postgres://postgres@localhost/mydb").should == "postgres://postgres@localhost/mydb"
85
+ @client.safe_url("http://x:y@localhost:5000").should == "http://x:[hidden]@localhost:5000"
86
+ end
87
+ end
88
+
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/schema'
3
+
4
+ describe Taps::Schema do
5
+ before do
6
+ Taps::AdapterHacks.stubs(:load)
7
+ @connection = mock("AR connection")
8
+ ActiveRecord::Base.stubs(:connection).returns(@connection)
9
+ end
10
+
11
+ it "parses a database url and returns a config hash for activerecord" do
12
+ Taps::Schema.create_config("postgres://myuser:mypass@localhost/mydb").should == {
13
+ 'adapter' => 'postgresql',
14
+ 'database' => 'mydb',
15
+ 'username' => 'myuser',
16
+ 'password' => 'mypass',
17
+ 'host' => 'localhost'
18
+ }
19
+ end
20
+
21
+ it "translates sqlite in the database url to sqlite3" do
22
+ Taps::Schema.create_config("sqlite://mydb")['adapter'].should == 'sqlite3'
23
+ end
24
+
25
+ it "translates sqlite database path" do
26
+ Taps::Schema.create_config("sqlite://pathtodb/mydb")['database'].should == 'pathtodb/mydb'
27
+ Taps::Schema.create_config("sqlite:///pathtodb/mydb")['database'].should == '/pathtodb/mydb'
28
+ Taps::Schema.create_config("sqlite:///pathtodb/mydb?encoding=utf8")['database'].should == '/pathtodb/mydb'
29
+ end
30
+
31
+ it "connects activerecord to the database" do
32
+ Taps::Schema.expects(:create_config).with("postgres://myuser:mypass@localhost/mydb").returns("db config")
33
+ ActiveRecord::Base.expects(:establish_connection).with("db config").returns(true)
34
+ Taps::Schema.connection("postgres://myuser:mypass@localhost/mydb").should == true
35
+ end
36
+
37
+ it "resets the db tables' primary keys" do
38
+ Taps::Schema.stubs(:connection)
39
+ ActiveRecord::Base.connection.expects(:respond_to?).with(:reset_pk_sequence!).returns(true)
40
+ ActiveRecord::Base.connection.stubs(:tables).returns(['table1'])
41
+ ActiveRecord::Base.connection.expects(:reset_pk_sequence!).with('table1')
42
+ should.not.raise { Taps::Schema.reset_db_sequences("postgres://myuser:mypass@localhost/mydb") }
43
+ end
44
+ end
45
+
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'sinatra'
3
+ require 'sinatra/test/bacon'
4
+
5
+ require File.dirname(__FILE__) + '/../lib/taps/server'
6
+
7
+ require 'pp'
8
+
9
+ describe Taps::Server do
10
+ before do
11
+ Taps::Config.login = 'taps'
12
+ Taps::Config.password = 'tpass'
13
+
14
+ @app = Taps::Server
15
+ @auth_header = "Basic " + ["taps:tpass"].pack("m*")
16
+ end
17
+
18
+ it "asks for http basic authentication" do
19
+ get '/'
20
+ status.should == 401
21
+ end
22
+
23
+ it "verifies the client taps version" do
24
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => Taps.compatible_version })
25
+ status.should == 200
26
+ end
27
+
28
+ it "yells loudly if the client taps version doesn't match" do
29
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => '0.0.1' })
30
+ status.should == 417
31
+ end
32
+ end
33
+
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/utils'
3
+
4
+ describe Taps::Utils do
5
+ it "gunzips a string" do
6
+ @hello_world = "\037\213\b\000R\261\207I\000\003\313H\315\311\311W(\317/\312I\001\000\205\021J\r\v\000\000\000"
7
+ Taps::Utils.gunzip(@hello_world).should == "hello world"
8
+ end
9
+
10
+ it "gzips and gunzips a string and returns the same string" do
11
+ Taps::Utils.gunzip(Taps::Utils.gzip("hello world")).should == "hello world"
12
+ end
13
+
14
+ it "generates a checksum using crc32" do
15
+ Taps::Utils.checksum("hello world").should == Zlib.crc32("hello world")
16
+ end
17
+
18
+ it "formats a data hash into one hash that contains an array of headers and an array of array of data" do
19
+ first_row = { :x => 1, :y => 1 }
20
+ first_row.stubs(:keys).returns([:x, :y])
21
+ Taps::Utils.format_data([ first_row, { :x => 2, :y => 2 } ], []).should == { :header => [ :x, :y ], :data => [ [1, 1], [2, 2] ] }
22
+ end
23
+
24
+ it "scales chunksize down slowly when the time delta of the block is just over a second" do
25
+ Time.stubs(:now).returns(10.0).returns(11.5)
26
+ Taps::Utils.calculate_chunksize(1000) { }.should == 900
27
+ end
28
+
29
+ it "scales chunksize down fast when the time delta of the block is over 3 seconds" do
30
+ Time.stubs(:now).returns(10.0).returns(15.0)
31
+ Taps::Utils.calculate_chunksize(3000) { }.should == 1000
32
+ end
33
+
34
+ it "scales up chunksize fast when the time delta of the block is under 0.8 seconds" do
35
+ Time.stubs(:now).returns(10.0).returns(10.7)
36
+ Taps::Utils.calculate_chunksize(1000) { }.should == 2000
37
+ end
38
+
39
+ it "scales up chunksize slow when the time delta of the block is between 0.8 and 1.1 seconds" do
40
+ Time.stubs(:now).returns(10.0).returns(10.8)
41
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
42
+
43
+ Time.stubs(:now).returns(10.0).returns(11.1)
44
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
45
+ end
46
+
47
+ it "will reset the chunksize to a small value if we got a broken pipe exception" do
48
+ Taps::Utils.calculate_chunksize(1000) { |c| raise Errno::EPIPE if c == 1000; c.should == 100 }.should == 200
49
+ end
50
+
51
+ it "returns a list of columns that are text fields if the database is mysql" do
52
+ @db = mock("db")
53
+ @db.class.stubs(:to_s).returns("Sequel::MySQL::Database")
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,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: matthewtodd-taps
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.19
5
+ platform: ruby
6
+ authors:
7
+ - Ricardo Chimal, Jr.
8
+ - Adam Wiggins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-11-08 00:00:00 +03:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: sinatra
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - "="
23
+ - !ruby/object:Gem::Version
24
+ version: 0.9.2
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 2.3.4
35
+ version:
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ type: :runtime
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - "="
43
+ - !ruby/object:Gem::Version
44
+ version: 0.9.9
45
+ version:
46
+ - !ruby/object:Gem::Dependency
47
+ name: rest-client
48
+ type: :runtime
49
+ version_requirement:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.1
55
+ - - <
56
+ - !ruby/object:Gem::Version
57
+ version: 1.1.0
58
+ version:
59
+ - !ruby/object:Gem::Dependency
60
+ name: sequel
61
+ type: :runtime
62
+ version_requirement:
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.0.0
68
+ - - <
69
+ - !ruby/object:Gem::Version
70
+ version: 3.1.0
71
+ version:
72
+ - !ruby/object:Gem::Dependency
73
+ name: sqlite3-ruby
74
+ type: :runtime
75
+ version_requirement:
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: 1.2.0
81
+ version:
82
+ description: A simple database agnostic import/export app to transfer data to/from a remote database.
83
+ email: ricardo@heroku.com
84
+ executables:
85
+ - taps
86
+ - schema
87
+ extensions: []
88
+
89
+ extra_rdoc_files:
90
+ - LICENSE
91
+ - README.rdoc
92
+ files:
93
+ - LICENSE
94
+ - README.rdoc
95
+ - Rakefile
96
+ - VERSION.yml
97
+ - bin/schema
98
+ - bin/schema.cmd
99
+ - bin/taps
100
+ - lib/taps/adapter_hacks.rb
101
+ - lib/taps/adapter_hacks/invalid_binary_limit.rb
102
+ - lib/taps/adapter_hacks/invalid_text_limit.rb
103
+ - lib/taps/adapter_hacks/mysql_invalid_primary_key.rb
104
+ - lib/taps/adapter_hacks/non_rails_schema_dump.rb
105
+ - lib/taps/cli.rb
106
+ - lib/taps/client_session.rb
107
+ - lib/taps/config.rb
108
+ - lib/taps/db_session.rb
109
+ - lib/taps/progress_bar.rb
110
+ - lib/taps/schema.rb
111
+ - lib/taps/server.rb
112
+ - lib/taps/utils.rb
113
+ - spec/base.rb
114
+ - spec/client_session_spec.rb
115
+ - spec/schema_spec.rb
116
+ - spec/server_spec.rb
117
+ - spec/utils_spec.rb
118
+ has_rdoc: true
119
+ homepage: http://github.com/matthewtodd/taps
120
+ licenses: []
121
+
122
+ post_install_message:
123
+ rdoc_options:
124
+ - --charset=UTF-8
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: "0"
132
+ version:
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: "0"
138
+ version:
139
+ requirements: []
140
+
141
+ rubyforge_project: taps
142
+ rubygems_version: 1.3.5
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: simple database import/export app
146
+ test_files:
147
+ - spec/base.rb
148
+ - spec/client_session_spec.rb
149
+ - spec/schema_spec.rb
150
+ - spec/server_spec.rb
151
+ - spec/utils_spec.rb