sq-dbsync 1.0.7 → 1.0.8

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.
@@ -5,6 +5,17 @@ module Sq::Dbsync::Database
5
5
 
6
6
  SQD = ::Sq::Dbsync
7
7
 
8
+ def initialize(opts, source_or_target)
9
+ db = Sequel.connect(opts)
10
+ super(db)
11
+ @db, @source_or_target = db, source_or_target
12
+ @charset = opts[:charset] if opts[:charset]
13
+ end
14
+
15
+ def inspect
16
+ "#<Database::#{self.class.name} #{source_or_target} #{opts[:database]}>"
17
+ end
18
+
8
19
  def extract_to_file(table_name, columns, file_name)
9
20
  extract_sql_to_file("SELECT %s FROM %s" % [
10
21
  columns.join(', '),
@@ -34,7 +45,11 @@ module Sq::Dbsync::Database
34
45
 
35
46
  def hash_schema(plan)
36
47
  ensure_connection
37
- Hash[schema(plan.source_table_name)]
48
+ Hash[schema(source? ? plan.source_table_name : plan.table_name)]
49
+ end
50
+
51
+ def source?
52
+ source_or_target == :source
38
53
  end
39
54
 
40
55
  def name
@@ -59,6 +74,8 @@ module Sq::Dbsync::Database
59
74
 
60
75
  protected
61
76
 
77
+ attr_reader :db, :source_or_target, :charset
78
+
62
79
  def execute!(cmd)
63
80
  # psql doesn't return a non-zero error code when executing commands from
64
81
  # a file. The best way I can come up with is to raise if anything is
@@ -9,14 +9,12 @@ module Sq::Dbsync::Database
9
9
  # Factory class to abstract selection of a decorator to faciliate databases
10
10
  # other than MySQL.
11
11
  class Connection
12
- def self.create(opts)
12
+ def self.create(opts, direction)
13
13
  case opts[:brand]
14
14
  when 'mysql'
15
- db = Sq::Dbsync::Database::Mysql.new(Sequel.connect(opts))
16
- db.charset = opts[:charset] if opts[:charset]
17
- db
15
+ Sq::Dbsync::Database::Mysql.new(opts, direction)
18
16
  when 'postgresql'
19
- Sq::Dbsync::Database::Postgres.new(Sequel.connect(opts))
17
+ Sq::Dbsync::Database::Postgres.new(opts, direction)
20
18
  else
21
19
  raise "Unsupported database: #{opts.inspect}"
22
20
  end
@@ -13,28 +13,17 @@ module Sq::Dbsync::Database
13
13
  # Decorator around a Sequel database object, providing some non-standard
14
14
  # extensions required for effective ETL with MySQL.
15
15
  class Mysql < Delegator
16
+ # 2 days is chosen as an arbitrary buffer
17
+ AUX_TIME_BUFFER = 60 * 60 * 24 * 2 # 2 days
18
+ LOAD_SQL = "LOAD DATA INFILE '%s' %s INTO TABLE %s %s (%s)"
16
19
 
17
20
  include Common
18
21
 
19
- attr_accessor :charset
20
-
21
- def initialize(db)
22
- super
23
- @db = db
24
- end
25
-
26
- def inspect; "#<Database::Mysql #{opts[:database]}>"; end
27
-
28
22
  def load_from_file(table_name, columns, file_name)
29
23
  ensure_connection
30
- character_set = self.charset ? " character set #{self.charset}" : ""
31
- sql = "LOAD DATA INFILE '%s' IGNORE INTO TABLE %s %s (%s)" % [
32
- file_name,
33
- table_name,
34
- character_set,
35
- escape_columns(columns)
36
- ]
37
- db.run sql
24
+ db.run(LOAD_SQL % [
25
+ file_name, 'IGNORE', table_name, character_set, escape_columns(columns)
26
+ ])
38
27
  end
39
28
 
40
29
  def set_lock_timeout(seconds)
@@ -46,13 +35,9 @@ module Sq::Dbsync::Database
46
35
  # Very low lock wait timeout, since we don't want loads to be blocked
47
36
  # waiting for long queries.
48
37
  set_lock_timeout(10)
49
- character_set = self.charset ? " character set #{self.charset}" : ""
50
- db.run "LOAD DATA INFILE '%s' REPLACE INTO TABLE %s %s (%s)" % [
51
- file_name,
52
- table_name,
53
- character_set,
54
- escape_columns(columns)
55
- ]
38
+ db.run(LOAD_SQL % [
39
+ file_name, 'REPLACE', table_name, character_set, escape_columns(columns)
40
+ ])
56
41
  rescue Sequel::DatabaseError => e
57
42
  transient_regex =
58
43
  /Lock wait timeout exceeded|Deadlock found when trying to get lock/
@@ -64,9 +49,6 @@ module Sq::Dbsync::Database
64
49
  end
65
50
  end
66
51
 
67
- # 2 days is chosen as an arbitrary buffer
68
- AUX_TIME_BUFFER = 60 * 60 * 24 * 2 # 2 days
69
-
70
52
  # Deletes recent rows based on timestamp, but also allows filtering by an
71
53
  # auxilary timestamp column for the case where the primary one is not
72
54
  # indexed on the target (such as the DFR reports, where imported_at is not
@@ -93,21 +75,6 @@ module Sq::Dbsync::Database
93
75
  count
94
76
  end
95
77
 
96
- # Overriden because the Sequel implementation does not work with partial
97
- # permissions on a table. See:
98
- # https://github.com/jeremyevans/sequel/issues/422
99
- def table_exists?(table_name)
100
- begin
101
- !!db.schema(table_name, reload: true)
102
- rescue Sequel::DatabaseError
103
- false
104
- end
105
- end
106
-
107
- def drop_table(table_name)
108
- db.drop_table(table_name)
109
- end
110
-
111
78
  def switch_table(to_replace, new_table)
112
79
  ensure_connection
113
80
 
@@ -131,8 +98,6 @@ module Sq::Dbsync::Database
131
98
 
132
99
  protected
133
100
 
134
- attr_reader :db
135
-
136
101
  def extract_sql_to_file(sql, file_name)
137
102
  file = sql_to_file(connection_settings + sql)
138
103
  cmd = "set -o pipefail; mysql --skip-column-names"
@@ -147,7 +112,7 @@ module Sq::Dbsync::Database
147
112
  ]
148
113
  end
149
114
 
150
- cmd += " --default-character-set %s" % opts[:charset] if opts[:charset]
115
+ cmd += " --default-character-set %s" % charset if charset
151
116
 
152
117
  cmd += " %s" % opts.fetch(:database)
153
118
 
@@ -170,9 +135,10 @@ module Sq::Dbsync::Database
170
135
  lock_timeout_sql(10)
171
136
  end
172
137
 
138
+ def character_set; charset ? " character set #{charset}" : "" end
139
+
173
140
  def lock_timeout_sql(seconds)
174
141
  "SET SESSION innodb_lock_wait_timeout = %i;" % seconds
175
142
  end
176
-
177
143
  end
178
144
  end
@@ -29,13 +29,6 @@ module Sq::Dbsync::Database
29
29
 
30
30
  include Sq::Dbsync::Database::Common
31
31
 
32
- def initialize(db)
33
- super
34
- @db = db
35
- end
36
-
37
- def inspect; "#<Database::Postgres #{opts[:database]}>"; end
38
-
39
32
  def set_lock_timeout(seconds)
40
33
  # Unimplemented
41
34
  end
@@ -44,7 +37,8 @@ module Sq::Dbsync::Database
44
37
  type_casts = plan.type_casts || {}
45
38
  ensure_connection
46
39
 
47
- result = schema(plan.source_table_name).each do |col, metadata|
40
+ table_name = source? ? plan.source_table_name : plan.table_name
41
+ result = schema(table_name).each do |col, metadata|
48
42
  metadata[:source_db_type] ||= metadata[:db_type]
49
43
  metadata[:db_type] = cast_psql_to_mysql(
50
44
  metadata[:db_type], type_casts[col.to_s]
@@ -56,8 +50,6 @@ module Sq::Dbsync::Database
56
50
 
57
51
  protected
58
52
 
59
- attr_reader :db
60
-
61
53
  def cast_psql_to_mysql(db_type, cast=nil)
62
54
  CASTS.fetch(db_type, cast || db_type)
63
55
  end
@@ -105,7 +105,8 @@ class Sq::Dbsync::Manager
105
105
  end
106
106
 
107
107
  def target
108
- @target ||= Sq::Dbsync::Database::Connection.create(config[:target])
108
+ opts = config[:target]
109
+ @target ||= Sq::Dbsync::Database::Connection.create(opts, :target)
109
110
  end
110
111
 
111
112
  def tables_to_load
@@ -124,7 +125,7 @@ class Sq::Dbsync::Manager
124
125
 
125
126
  def sources
126
127
  @sources ||= Hash[config[:sources].map do |name, opts|
127
- [name, Sq::Dbsync::Database::Connection.create(opts)]
128
+ [name, Sq::Dbsync::Database::Connection.create(opts, :source)]
128
129
  end]
129
130
  end
130
131
 
@@ -217,7 +218,7 @@ class Sq::Dbsync::Manager
217
218
  end
218
219
 
219
220
  def db
220
- @db ||= Database::Connection.create(config[:target])
221
+ @db ||= Database::Connection.create(config[:target], :target)
221
222
  end
222
223
 
223
224
  def transient_exceptions
@@ -1,5 +1,5 @@
1
1
  module Sq
2
2
  module Dbsync
3
- VERSION = '1.0.7'
3
+ VERSION = '1.0.8'
4
4
  end
5
5
  end
@@ -48,12 +48,14 @@ MB4_TEST_TARGET = db_options(database: 'sq_dbsync_test_target', charset:"utf8mb4
48
48
 
49
49
  $target = nil
50
50
  def test_target
51
- $target ||= SQD::Database::Connection.create(TEST_TARGET)
51
+ $target ||= SQD::Database::Connection.create(TEST_TARGET, :target)
52
52
  end
53
53
 
54
54
  $sources = {}
55
55
  def test_source(name)
56
- $sources[name] ||= SQD::Database::Connection.create(TEST_SOURCES.fetch(name))
56
+ $sources[name] ||= SQD::Database::Connection.create(
57
+ TEST_SOURCES.fetch(name), :source
58
+ )
57
59
  end
58
60
 
59
61
  RSpec.configure do |config|
@@ -11,8 +11,9 @@ describe SQD::BatchLoadAction do
11
11
  let!(:now) { @now = Time.now.utc }
12
12
  let(:last_synced_at) { now - 10 }
13
13
  let(:target) { test_target }
14
+ let(:target_table_name) { :test_table }
14
15
  let(:table_plan) {{
15
- table_name: :test_table,
16
+ table_name: target_table_name,
16
17
  source_table_name: :test_table,
17
18
  columns: [:id, :col1, :updated_at],
18
19
  source_db: source,
@@ -20,7 +21,7 @@ describe SQD::BatchLoadAction do
20
21
  }}
21
22
  let(:index) {{
22
23
  index_on_col1: { columns: [:col1], unique: false }
23
- } }
24
+ }}
24
25
  let(:registry) { SQD::TableRegistry.new(target) }
25
26
  let(:action) { SQD::BatchLoadAction.new(
26
27
  target,
@@ -59,14 +60,18 @@ describe SQD::BatchLoadAction do
59
60
  end
60
61
  end
61
62
 
62
- it 'copies source tables to target with matching schemas' do
63
- start_time = now.to_f
63
+ describe 'when the source and destination table names differ' do
64
+ let(:target_table_name) { :target_test_table }
64
65
 
65
- action.call
66
+ it 'copies source tables to target with matching schemas' do
67
+ start_time = now.to_f
66
68
 
67
- verify_schema
68
- verify_data
69
- verify_metadata(start_time)
69
+ action.call
70
+
71
+ verify_schema
72
+ verify_data
73
+ verify_metadata(start_time)
74
+ end
70
75
  end
71
76
 
72
77
  it 'handles column that does not exist in source' do
@@ -139,13 +144,12 @@ describe SQD::BatchLoadAction do
139
144
 
140
145
  def test_tables
141
146
  {
142
- test_table: source,
147
+ test_table: [source, :target_test_table],
143
148
  }
144
149
  end
145
150
 
146
151
  def verify_schema
147
- test_tables.each do |table_name, source_db|
148
- target_table_name = table_name
152
+ test_tables.each do |table_name, (source_db, target_table_name)|
149
153
  target.tables.should include(target_table_name)
150
154
  source_test_table_schema =
151
155
  source_db.schema(table_name).map do |column, hash|
@@ -177,8 +181,8 @@ describe SQD::BatchLoadAction do
177
181
  end
178
182
 
179
183
  def verify_data
180
- test_tables.each do |table_name, _|
181
- data = target[table_name].all
184
+ test_tables.each do |table_name, (source_db, target_table_name)|
185
+ data = target[target_table_name].all
182
186
  data.count.should == 1
183
187
  data = data[0]
184
188
  data.keys.length.should == 3
@@ -189,8 +193,8 @@ describe SQD::BatchLoadAction do
189
193
  end
190
194
 
191
195
  def verify_metadata(start_time)
192
- test_tables.each do |table_name, _|
193
- meta = registry.get(table_name)
196
+ test_tables.each do |table_name, (source_db, target_table_name)|
197
+ meta = registry.get(target_table_name)
194
198
  meta[:last_synced_at].should_not be_nil
195
199
  meta[:last_batch_synced_at].should_not be_nil
196
200
  meta[:last_batch_synced_at].to_i.should == start_time.to_i
@@ -17,15 +17,13 @@ shared_examples_for 'a decorated database adapter' do
17
17
  end
18
18
 
19
19
  describe SQD::Database::Postgres do
20
- let(:source) { test_source(:postgres) }
21
- let(:db) { SQD::Database::Postgres.new(source) }
20
+ let(:db) { test_source(:postgres) }
22
21
 
23
22
  it_should_behave_like 'a decorated database adapter'
24
23
  end
25
24
 
26
25
  describe SQD::Database::Mysql do
27
- let(:source) { test_source(:source) }
28
- let(:db) { SQD::Database::Mysql.new(source) }
26
+ let(:db) { test_source(:source) }
29
27
 
30
28
  it_should_behave_like 'a decorated database adapter'
31
29
 
@@ -34,26 +32,26 @@ describe SQD::Database::Mysql do
34
32
 
35
33
  before { @file = Tempfile.new('bogus') }
36
34
 
37
- def source_with_exception(exception_message)
38
- source.stub(:run).and_raise(
35
+ def sequel_with_exception(exception_message)
36
+ db.send(:db).stub(:run).and_raise(
39
37
  Sequel::DatabaseError.new(exception_message)
40
38
  )
41
39
  end
42
40
 
43
41
  it 're-raises deadlock related exceptions as TransientError' do
44
- source_with_exception("Deadlock found when trying to get lock")
42
+ sequel_with_exception("Deadlock found when trying to get lock")
45
43
  -> { db.load_incrementally_from_file('bogus', ['bogus'], path) }.
46
44
  should raise_error(SQD::Database::TransientError)
47
45
  end
48
46
 
49
47
  it 're-raises lock wait timeout exceptions as TransientError' do
50
- source_with_exception("Lock wait timeout exceeded")
48
+ sequel_with_exception("Lock wait timeout exceeded")
51
49
  -> { db.load_incrementally_from_file('bogus', ['bogus'], path) }.
52
50
  should raise_error(SQD::Database::TransientError)
53
51
  end
54
52
 
55
53
  it 'does not translate unknown errors' do
56
- source_with_exception("Unknown")
54
+ sequel_with_exception("Unknown")
57
55
  -> { db.load_incrementally_from_file('bogus', ['bogus'], path) }.
58
56
  should raise_error(Sequel::DatabaseError)
59
57
  end
@@ -11,8 +11,9 @@ describe SQD::IncrementalLoadAction do
11
11
  let(:last_synced_at) { now - 10 }
12
12
  let(:source) { test_source(:source) }
13
13
  let(:target) { test_target }
14
+ let(:target_table_name) { :test_table }
14
15
  let(:table_plan) {{
15
- table_name: :test_table,
16
+ table_name: target_table_name,
16
17
  source_table_name: :test_table,
17
18
  columns: [:id, :col1, :updated_at],
18
19
  source_db: source,
@@ -60,6 +61,18 @@ describe SQD::IncrementalLoadAction do
60
61
  end
61
62
  end
62
63
 
64
+ describe 'when source and target are differently named' do
65
+ let(:target_table_name) { :target_test_table }
66
+
67
+ it 'copies all columns to the correctly named target' do
68
+ setup_target_table(last_synced_at, target_table_name)
69
+
70
+ action.call
71
+
72
+ target[target_table_name].map { |row| row.values_at(:id, :col1) }.
73
+ should == [[2, 'new record']]
74
+ end
75
+ end
63
76
 
64
77
  it 'copies null data to the target' do
65
78
  source[:test_table].update(col1: nil)
@@ -48,18 +48,18 @@ def create_pg_source_table_with(*rows)
48
48
  end
49
49
  end
50
50
 
51
- def setup_target_table(last_synced_at)
52
- target.create_table! :test_table do
51
+ def setup_target_table(last_synced_at, name=:test_table)
52
+ target.create_table! name do
53
53
  Integer :id
54
54
  String :col1
55
55
  DateTime :updated_at
56
56
  DateTime :created_at
57
57
  end
58
58
 
59
- target.add_index :test_table, :id, :unique => true
59
+ target.add_index name, :id, :unique => true
60
60
 
61
61
  registry.ensure_storage_exists
62
- registry.set(:test_table,
62
+ registry.set(name,
63
63
  last_synced_at: last_synced_at,
64
64
  last_row_at: last_synced_at,
65
65
  last_batch_synced_at: last_synced_at
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sq-dbsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-05-29 00:00:00.000000000 Z
13
+ date: 2013-05-31 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec