sq-dbsync 1.0.0

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.
Files changed (46) hide show
  1. data/HISTORY.md +5 -0
  2. data/LICENSE +14 -0
  3. data/README.md +218 -0
  4. data/lib/sq/dbsync/all_tables_plan.rb +51 -0
  5. data/lib/sq/dbsync/batch_load_action.rb +95 -0
  6. data/lib/sq/dbsync/config.rb +12 -0
  7. data/lib/sq/dbsync/consistency_verifier.rb +70 -0
  8. data/lib/sq/dbsync/database/common.rb +91 -0
  9. data/lib/sq/dbsync/database/connection.rb +23 -0
  10. data/lib/sq/dbsync/database/mysql.rb +163 -0
  11. data/lib/sq/dbsync/database/postgres.rb +77 -0
  12. data/lib/sq/dbsync/error_handler.rb +59 -0
  13. data/lib/sq/dbsync/example_record_destroyer.rb +77 -0
  14. data/lib/sq/dbsync/incremental_load_action.rb +95 -0
  15. data/lib/sq/dbsync/load_action.rb +156 -0
  16. data/lib/sq/dbsync/loggers.rb +135 -0
  17. data/lib/sq/dbsync/manager.rb +241 -0
  18. data/lib/sq/dbsync/pipeline/simple_context.rb +15 -0
  19. data/lib/sq/dbsync/pipeline/threaded_context.rb +95 -0
  20. data/lib/sq/dbsync/pipeline.rb +80 -0
  21. data/lib/sq/dbsync/refresh_recent_load_action.rb +71 -0
  22. data/lib/sq/dbsync/schema_maker.rb +87 -0
  23. data/lib/sq/dbsync/static_table_plan.rb +42 -0
  24. data/lib/sq/dbsync/table_registry.rb +75 -0
  25. data/lib/sq/dbsync/tempfile_factory.rb +41 -0
  26. data/lib/sq/dbsync/version.rb +5 -0
  27. data/lib/sq/dbsync.rb +9 -0
  28. data/spec/acceptance/loading_spec.rb +237 -0
  29. data/spec/acceptance_helper.rb +2 -0
  30. data/spec/database_helper.rb +86 -0
  31. data/spec/integration/all_tables_plan_spec.rb +36 -0
  32. data/spec/integration/batch_load_action_spec.rb +229 -0
  33. data/spec/integration/consistency_verifier_spec.rb +54 -0
  34. data/spec/integration/database_connection_spec.rb +61 -0
  35. data/spec/integration/incremental_load_action_spec.rb +196 -0
  36. data/spec/integration/manager_spec.rb +109 -0
  37. data/spec/integration/schema_maker_spec.rb +119 -0
  38. data/spec/integration_helper.rb +43 -0
  39. data/spec/spec_helper.rb +27 -0
  40. data/spec/unit/config_spec.rb +18 -0
  41. data/spec/unit/error_handler_spec.rb +52 -0
  42. data/spec/unit/pipeline_spec.rb +42 -0
  43. data/spec/unit/stream_logger_spec.rb +33 -0
  44. data/spec/unit_helper.rb +1 -0
  45. data/sq-dbsync.gemspec +32 -0
  46. metadata +188 -0
@@ -0,0 +1,119 @@
1
+ require 'integration_helper'
2
+
3
+ require 'sq/dbsync/schema_maker'
4
+ require 'ostruct'
5
+
6
+ describe SQD::SchemaMaker do
7
+ let(:target) { test_target }
8
+
9
+ it 'creates a table with a compound index' do
10
+ index = {
11
+ index_on_col1: { columns: [:col1, :col2], unique: false }
12
+ }
13
+
14
+ plan = {
15
+ prefixed_table_name: :test_table,
16
+ table_name: :test_table,
17
+ columns: [:col1, :col2],
18
+ indexes: index,
19
+ schema: {
20
+ col1: { db_type: 'varchar(255)', primary_key: true },
21
+ col2: { db_type: 'varchar(255)', primary_key: false }
22
+ }
23
+ }
24
+
25
+ described_class.create_table(target, OpenStruct.new(plan))
26
+
27
+ target.indexes(:test_table).should == index
28
+ end
29
+
30
+ it 'defaults primary key to id' do
31
+ plan = {
32
+ prefixed_table_name: :test_table,
33
+ table_name: :test_table,
34
+ columns: [:id],
35
+ schema: { id: {db_type: 'varchar(255)', primary_key: false }}
36
+ }
37
+
38
+ described_class.create_table(target, OpenStruct.new(plan))
39
+
40
+ target.schema(:test_table)[0][1][:primary_key].should == true
41
+ end
42
+
43
+ it 'allows primary key override' do
44
+ plan = {
45
+ prefixed_table_name: :test_table,
46
+ table_name: :test_table,
47
+ columns: [:id, :col1],
48
+ primary_key: [:id, :col1],
49
+ schema: {
50
+ id: {db_type: 'varchar(255)', primary_key: false },
51
+ col1: {db_type: 'varchar(255)', primary_key: false }
52
+ }
53
+ }
54
+
55
+ described_class.create_table(target, OpenStruct.new(plan))
56
+
57
+ target.schema(:test_table)[0][1][:primary_key].should == true
58
+ target.schema(:test_table)[1][1][:primary_key].should == true
59
+ end
60
+
61
+
62
+ it 'creates a table with an enum column' do
63
+ plan = {
64
+ prefixed_table_name: :test_table,
65
+ table_name: :test_table,
66
+ columns: [:col1],
67
+ db_types: {:col1 => [:enum, %w(a b)]},
68
+ schema: { col1: { primary_key: true }}
69
+ }
70
+
71
+ described_class.create_table(target, OpenStruct.new(plan))
72
+
73
+ target.schema(:test_table)[0][1][:db_type].should == "enum('a','b')"
74
+ end
75
+
76
+ it 'creates a table with non-id primary key' do
77
+ plan = {
78
+ prefixed_table_name: :test_table,
79
+ table_name: :test_table,
80
+ columns: [:col1],
81
+ schema: { col1: { db_type: 'varchar(255)', primary_key: true }}
82
+ }
83
+
84
+ described_class.create_table(target, OpenStruct.new(plan))
85
+
86
+ target.schema(:test_table)[0][1][:primary_key].should == true
87
+ end
88
+
89
+ it 'creates a table with a not-null column' do
90
+ plan = {
91
+ prefixed_table_name: :test_table,
92
+ table_name: :test_table,
93
+ columns: [:col1],
94
+ db_types: {:col1 => ['int(1) not null']},
95
+ schema: { col1: { primary_key: true }}
96
+ }
97
+
98
+ described_class.create_table(target, OpenStruct.new(plan))
99
+
100
+ target.schema(:test_table)[0][1][:allow_null].should == false
101
+ end
102
+
103
+ it 'creates a table with a composite primary key' do
104
+ plan = {
105
+ prefixed_table_name: :test_table,
106
+ table_name: :test_table,
107
+ columns: [:a, :b],
108
+ schema: {
109
+ a: { db_type: 'int', primary_key: true },
110
+ b: { db_type: 'int', primary_key: true }
111
+ }
112
+ }
113
+
114
+ described_class.create_table(target, OpenStruct.new(plan))
115
+
116
+ target.schema(:test_table)[0][1][:primary_key].should == true
117
+ target.schema(:test_table)[1][1][:primary_key].should == true
118
+ end
119
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'database_helper'
3
+
4
+ def create_source_table_with(*rows)
5
+ # Total hack to allow source db to be passed as optional first argument.
6
+ if rows[0].is_a?(Hash)
7
+ source_db = source
8
+ else
9
+ source_db = rows.shift
10
+ end
11
+ table_name = :test_table
12
+
13
+ source_db.create_table! table_name do
14
+ primary_key :id
15
+ String :col1
16
+ String :pii
17
+ DateTime :updated_at
18
+ DateTime :created_at
19
+ DateTime :imported_at
20
+ end
21
+
22
+ rows.each do |row|
23
+ source_db[table_name].insert(row)
24
+ end
25
+ end
26
+
27
+ def setup_target_table(last_synced_at)
28
+ target.create_table! :test_table do
29
+ Integer :id
30
+ String :col1
31
+ DateTime :updated_at
32
+ DateTime :created_at
33
+ end
34
+
35
+ target.add_index :test_table, :id, :unique => true
36
+
37
+ registry.ensure_storage_exists
38
+ registry.set(:test_table,
39
+ last_synced_at: last_synced_at,
40
+ last_row_at: last_synced_at,
41
+ last_batch_synced_at: last_synced_at
42
+ )
43
+ end
@@ -0,0 +1,27 @@
1
+ ENV['APP_ENV'] = 'test'
2
+
3
+ if ENV['COVERAGE'] != "0" && RUBY_PLATFORM != 'java'
4
+ require 'simplecov'
5
+
6
+ class SimpleCov::Formatter::MergedFormatter
7
+ def format(result)
8
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
9
+ File.open("coverage/covered_percent", "w") do |f|
10
+ f.puts result.source_files.covered_percent.to_i
11
+ end
12
+ end
13
+ end
14
+
15
+ SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
16
+ SimpleCov.start do
17
+ add_filter "/config/"
18
+ add_filter "/spec/"
19
+ end
20
+ end
21
+
22
+ module Sq
23
+ module Dbsync
24
+ end
25
+ end
26
+
27
+ SQD = Sq::Dbsync
@@ -0,0 +1,18 @@
1
+ require 'unit_helper'
2
+
3
+ require 'sq/dbsync/config'
4
+
5
+ describe Sq::Dbsync::Config do
6
+ it 'provides a default error handler' do
7
+ described_class.make({})[:error_handler].should respond_to(:call)
8
+ end
9
+
10
+ it 'provides a default clock' do
11
+ described_class.make({})[:clock].().should be_instance_of(Time)
12
+ end
13
+
14
+ it 'provides a default logger' do
15
+ described_class.make({})[:logger].should \
16
+ be_a_kind_of(Sq::Dbsync::Loggers::Abstract)
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ require 'unit_helper'
2
+
3
+ require 'sq/dbsync/error_handler'
4
+
5
+ describe Sq::Dbsync::ErrorHandler do
6
+ let(:config) { {
7
+ sources: {
8
+ db_a: { password: 'redactme' },
9
+ db_b: { password: 'alsome' },
10
+ db_c: {}
11
+ },
12
+ target: { password: 'thistoo'},
13
+ }}
14
+
15
+ describe '#wrap' do
16
+ it 'redacts message' do
17
+ called = nil
18
+ config[:error_handler] = ->(ex) { called = ex }
19
+ handler = described_class.new(config)
20
+ ->{
21
+ handler.wrap do
22
+ raise "redactme alsome thistoo notthis"
23
+ end
24
+ }.should raise_error("REDACTED REDACTED REDACTED notthis")
25
+ called.message.should == "REDACTED REDACTED REDACTED notthis"
26
+ end
27
+ end
28
+
29
+ describe '#notify_error' do
30
+ it 'includes tag in exception message' do
31
+ called = nil
32
+ config[:error_handler] = ->(ex) { called = ex }
33
+ handler = described_class.new(config)
34
+
35
+ handler.notify_error(:test_table, RuntimeError.new('hello'))
36
+
37
+ called.message.should include('[test_table]')
38
+ called.message.should include('hello')
39
+ end
40
+
41
+ it 'redacts message' do
42
+ called = nil
43
+ config[:error_handler] = ->(ex) { called = ex }
44
+ handler = described_class.new(config)
45
+
46
+ handler.notify_error(:test_table,
47
+ RuntimeError.new("redactme alsome thistoo notthis"))
48
+
49
+ called.message.should include("REDACTED REDACTED REDACTED notthis")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,42 @@
1
+ require 'unit_helper'
2
+
3
+ require 'sq/dbsync/pipeline'
4
+
5
+ shared_examples_for 'a pipeline' do
6
+ it 'passes tasks through each stage' do
7
+ ret = SQD::Pipeline.new([3, 4],
8
+ ->(x) { x * x },
9
+ ->(x) { x + x }
10
+ ).run(described_class)
11
+ ret.should == [18, 32]
12
+ end
13
+
14
+ it 'returns errors' do
15
+ ret = SQD::Pipeline.new([1],
16
+ ->(x) { raise("fail") }
17
+ ).run(described_class)
18
+ ret.length.should == 1
19
+ ret = ret[0]
20
+ ret.should be_instance_of(SQD::Pipeline::Failure)
21
+ ret.wrapped_exception.should be_instance_of(RuntimeError)
22
+ ret.wrapped_exception.message.should == "fail"
23
+ ret.task.should == 1
24
+ end
25
+
26
+ it 'handles errors in the middle of a pipeline' do
27
+ ret = SQD::Pipeline.new([1, 2],
28
+ ->(x) { x == 1 ? 10 : raise("fail") },
29
+ ->(x) { x + 1 }
30
+ ).run(described_class)
31
+ ret[0].should == 11
32
+ ret[1].should be_instance_of(SQD::Pipeline::Failure)
33
+ end
34
+ end
35
+
36
+ describe SQD::Pipeline::ThreadedContext do
37
+ it_should_behave_like 'a pipeline'
38
+ end
39
+
40
+ describe SQD::Pipeline::SimpleContext do
41
+ it_should_behave_like 'a pipeline'
42
+ end
@@ -0,0 +1,33 @@
1
+ require 'unit_helper'
2
+
3
+ require 'sq/dbsync/loggers'
4
+
5
+ describe SQD::Loggers::Stream do
6
+ let(:buffer) { "" }
7
+ let(:logger) { described_class.new(StringIO.new(buffer)) }
8
+
9
+ it 'logs :finished when no exception is raised' do
10
+ logger.measure(:ok) {}
11
+ buffer.should include('finished')
12
+ end
13
+
14
+ it 'logs :failed when exception is raised' do
15
+ lambda {
16
+ logger.measure(:fail) { raise("fail") }
17
+ }.should raise_error("fail")
18
+ buffer.should include('failed')
19
+ end
20
+
21
+ it 'logs error message when exception is raised' do
22
+ lambda {
23
+ logger.measure(:fail) { raise("oh no") }
24
+ }.should raise_error("oh no")
25
+ buffer.should include('oh no')
26
+ end
27
+
28
+
29
+ it 'logs specified strings' do
30
+ logger.log('logging is good')
31
+ buffer.should include('logging is good')
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ require 'spec_helper'
data/sq-dbsync.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sq/dbsync/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Xavier Shay"]
6
+ gem.email = ["xavier@squareup.com"]
7
+ gem.description =
8
+ %q{Column based, timestamp replication of MySQL and Postgres databases.}
9
+ gem.summary = %q{
10
+ Column based, timestamp replication of MySQL and Postgres databases. Uses
11
+ Ruby for the glue code but pushes the heavy lifting on to the database.
12
+ }
13
+ gem.homepage = "http://github.com/square/sq-dbsync"
14
+
15
+ gem.executables = []
16
+ gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
17
+ README.md
18
+ HISTORY.md
19
+ LICENSE
20
+ sq-dbsync.gemspec
21
+ )
22
+ gem.test_files = Dir.glob("spec/**/*.rb")
23
+ gem.name = "sq-dbsync"
24
+ gem.require_paths = ["lib"]
25
+ gem.version = Sq::Dbsync::VERSION
26
+ gem.has_rdoc = false
27
+ gem.add_development_dependency 'rspec', '~> 2.0'
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'simplecov'
30
+ gem.add_development_dependency 'cane'
31
+ gem.add_dependency 'sequel'
32
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sq-dbsync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Xavier Shay
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: simplecov
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: cane
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: sequel
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Column based, timestamp replication of MySQL and Postgres databases.
95
+ email:
96
+ - xavier@squareup.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - spec/acceptance/loading_spec.rb
102
+ - spec/acceptance_helper.rb
103
+ - spec/database_helper.rb
104
+ - spec/integration/all_tables_plan_spec.rb
105
+ - spec/integration/batch_load_action_spec.rb
106
+ - spec/integration/consistency_verifier_spec.rb
107
+ - spec/integration/database_connection_spec.rb
108
+ - spec/integration/incremental_load_action_spec.rb
109
+ - spec/integration/manager_spec.rb
110
+ - spec/integration/schema_maker_spec.rb
111
+ - spec/integration_helper.rb
112
+ - spec/spec_helper.rb
113
+ - spec/unit/config_spec.rb
114
+ - spec/unit/error_handler_spec.rb
115
+ - spec/unit/pipeline_spec.rb
116
+ - spec/unit/stream_logger_spec.rb
117
+ - spec/unit_helper.rb
118
+ - lib/sq/dbsync/all_tables_plan.rb
119
+ - lib/sq/dbsync/batch_load_action.rb
120
+ - lib/sq/dbsync/config.rb
121
+ - lib/sq/dbsync/consistency_verifier.rb
122
+ - lib/sq/dbsync/database/common.rb
123
+ - lib/sq/dbsync/database/connection.rb
124
+ - lib/sq/dbsync/database/mysql.rb
125
+ - lib/sq/dbsync/database/postgres.rb
126
+ - lib/sq/dbsync/error_handler.rb
127
+ - lib/sq/dbsync/example_record_destroyer.rb
128
+ - lib/sq/dbsync/incremental_load_action.rb
129
+ - lib/sq/dbsync/load_action.rb
130
+ - lib/sq/dbsync/loggers.rb
131
+ - lib/sq/dbsync/manager.rb
132
+ - lib/sq/dbsync/pipeline/simple_context.rb
133
+ - lib/sq/dbsync/pipeline/threaded_context.rb
134
+ - lib/sq/dbsync/pipeline.rb
135
+ - lib/sq/dbsync/refresh_recent_load_action.rb
136
+ - lib/sq/dbsync/schema_maker.rb
137
+ - lib/sq/dbsync/static_table_plan.rb
138
+ - lib/sq/dbsync/table_registry.rb
139
+ - lib/sq/dbsync/tempfile_factory.rb
140
+ - lib/sq/dbsync/version.rb
141
+ - lib/sq/dbsync.rb
142
+ - README.md
143
+ - HISTORY.md
144
+ - LICENSE
145
+ - sq-dbsync.gemspec
146
+ homepage: http://github.com/square/sq-dbsync
147
+ licenses: []
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ none: false
160
+ requirements:
161
+ - - ! '>='
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 1.8.23
167
+ signing_key:
168
+ specification_version: 3
169
+ summary: Column based, timestamp replication of MySQL and Postgres databases. Uses
170
+ Ruby for the glue code but pushes the heavy lifting on to the database.
171
+ test_files:
172
+ - spec/acceptance/loading_spec.rb
173
+ - spec/acceptance_helper.rb
174
+ - spec/database_helper.rb
175
+ - spec/integration/all_tables_plan_spec.rb
176
+ - spec/integration/batch_load_action_spec.rb
177
+ - spec/integration/consistency_verifier_spec.rb
178
+ - spec/integration/database_connection_spec.rb
179
+ - spec/integration/incremental_load_action_spec.rb
180
+ - spec/integration/manager_spec.rb
181
+ - spec/integration/schema_maker_spec.rb
182
+ - spec/integration_helper.rb
183
+ - spec/spec_helper.rb
184
+ - spec/unit/config_spec.rb
185
+ - spec/unit/error_handler_spec.rb
186
+ - spec/unit/pipeline_spec.rb
187
+ - spec/unit/stream_logger_spec.rb
188
+ - spec/unit_helper.rb