matthewtodd-taps 0.2.19

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,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