table_copy 0.0.5

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.
data/lib/table_copy.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'logger'
2
+ require 'table_copy/copier'
3
+
4
+ module TableCopy
5
+ class << self
6
+ attr_writer :logger
7
+
8
+ def logger
9
+ @logger ||= Logger.new($stdout)
10
+ end
11
+
12
+ def links
13
+ if configured?
14
+ @links
15
+ else
16
+ configure_links
17
+ @links
18
+ end
19
+ end
20
+
21
+ def deferred_config(&block)
22
+ @deferred_config = block
23
+ end
24
+
25
+ def add_link(name, source, destination)
26
+ links_to_add[name] = TableCopy::Copier.new(source, destination)
27
+ end
28
+
29
+ private
30
+
31
+ def configure_links
32
+ synchronized do
33
+ return @links if configured?
34
+ @deferred_config.call if @deferred_config
35
+ @links = links_to_add
36
+ end
37
+ end
38
+
39
+ def configured?
40
+ @links && !@links.empty?
41
+ end
42
+
43
+ def links_to_add
44
+ @links_to_add ||= {}
45
+ end
46
+
47
+ def synchronized
48
+ @semaphore ||= Mutex.new
49
+ @semaphore.synchronize do
50
+ yield
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,126 @@
1
+ require 'table_copy/copier'
2
+ require 'table_copy/pg/source'
3
+ require 'table_copy/pg/destination'
4
+
5
+ describe TableCopy::Copier do
6
+ let(:copier) { TableCopy::Copier.new(source, destination) }
7
+ let(:source) { TableCopy::PG::Source.new({}) }
8
+ let(:destination) { TableCopy::PG::Destination.new({}) }
9
+ let(:fields_ddl) { 'fake fields ddl' }
10
+
11
+ describe '#update' do
12
+ context 'no destination table' do
13
+ it 'calls droppy' do
14
+ expect(destination).to receive(:none?).and_return(true)
15
+ expect(copier).to receive(:droppy)
16
+ copier.update
17
+ end
18
+ end
19
+
20
+ context 'destination table exists' do
21
+ before do
22
+ expect(destination).to receive(:none?).and_return(false)
23
+ end
24
+
25
+ context 'a max sequence is available' do
26
+ before do
27
+ expect(destination).to receive(:max_sequence).and_return(2345)
28
+ end
29
+
30
+ it 'updates the table with new data' do
31
+ expect(destination).to receive(:transaction).and_yield
32
+ expect(source).to receive(:fields_ddl).and_return(fields_ddl)
33
+ expect(destination).to receive(:create_temp).with(fields_ddl)
34
+ expect(destination).to receive(:copy_data_from).with(source, temp: true, update: 2345)
35
+ expect(destination).to receive(:copy_from_temp).with(except: nil)
36
+ copier.update
37
+ end
38
+ end
39
+
40
+ context 'no max sequence is available' do
41
+ before do
42
+ expect(destination).to receive(:max_sequence).and_return(nil)
43
+ end
44
+
45
+ it 'calls diffy_update' do
46
+ expect(destination).to receive(:transaction).and_yield
47
+ expect(source).to receive(:fields_ddl).and_return(fields_ddl)
48
+ expect(destination).to receive(:create_temp).with(fields_ddl)
49
+ expect(destination).to receive(:copy_data_from).with(source, temp: true)
50
+ expect(destination).to receive(:copy_from_temp)
51
+ copier.update
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'PG::UndefinedTable is raised' do
57
+ before do
58
+ expect(destination).to receive(:none?).and_raise(PG::UndefinedTable, 'Intentionally raised.').once
59
+ expect(destination).to receive(:none?).and_return(true) # handle the retry
60
+ end
61
+
62
+ it "passes the source's ddl to #create on the destination" do
63
+ expect(source).to receive(:fields_ddl).and_return(fields_ddl)
64
+ expect(destination).to receive(:create).with(fields_ddl)
65
+ expect(copier).to receive(:droppy) # handle the retry
66
+ copier.update
67
+ end
68
+ end
69
+
70
+ context 'PG::UndefinedColumn is raised' do
71
+ let(:fields_ddl) { 'fake fields ddl' }
72
+
73
+ before do
74
+ expect(destination).to receive(:none?).and_raise(PG::UndefinedColumn, 'Intentionally raised.').once
75
+ end
76
+
77
+ it 'calls droppy' do
78
+ expect(copier).to receive(:droppy)
79
+ copier.update
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'within a transaction in the destination' do
85
+ before do
86
+ expect(destination).to receive(:transaction).and_yield
87
+ end
88
+
89
+ describe '#droppy' do
90
+ it 'drops and rebuilds the destination table' do
91
+ expect(destination).to receive(:drop).with(cascade: true)
92
+ expect(source).to receive(:fields_ddl).and_return(fields_ddl)
93
+ expect(destination).to receive(:create).with(fields_ddl)
94
+ expect(destination).to receive(:copy_data_from).with(source)
95
+ expect(destination).to receive(:create_indexes)
96
+ copier.droppy
97
+ end
98
+ end
99
+
100
+ context 'after creating a temp table' do
101
+ before do
102
+ expect(source).to receive(:fields_ddl).and_return(fields_ddl)
103
+ expect(destination).to receive(:create_temp).with(fields_ddl)
104
+ end
105
+
106
+ describe '#find_deletes' do
107
+ it 'finds and removes deleted rows' do
108
+ expect(destination).to receive(:copy_data_from).with(source, temp: true, pk_only: true)
109
+ expect(destination).to receive(:delete_not_in_temp)
110
+ copier.find_deletes
111
+ end
112
+ end
113
+
114
+ describe '#diffy' do
115
+ it 'copies data form temp and finds and removes deleted rows' do
116
+ expect(destination).to receive(:copy_data_from).with(source, temp: true)
117
+ expect(destination).to receive(:copy_from_temp)
118
+ expect(destination).to receive(:delete_not_in_temp)
119
+ copier.diffy
120
+ end
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,305 @@
1
+ require 'table_copy/pg/destination'
2
+ require 'table_copy/pg/index'
3
+
4
+ describe TableCopy::PG::Destination do
5
+ let(:conn) { $pg_conn }
6
+ let(:table_name) { 'table_name' }
7
+ let(:indexes_sql) {
8
+ <<-SQL
9
+ select
10
+ i.relname as index_name,
11
+ a.attname as column_name
12
+ from
13
+ pg_class t,
14
+ pg_class i,
15
+ pg_index ix,
16
+ pg_attribute a
17
+ where
18
+ t.oid = ix.indrelid
19
+ and i.oid = ix.indexrelid
20
+ and a.attrelid = t.oid
21
+ and a.attnum = ANY(ix.indkey)
22
+ and t.relkind = 'r'
23
+ and t.relname = '#{table_name}'
24
+ order by
25
+ t.relname,
26
+ i.relname;
27
+ SQL
28
+ }
29
+
30
+ def with_conn
31
+ yield conn
32
+ end
33
+
34
+ def table_exists?(name=table_name)
35
+ conn.exec("select count(*) from pg_tables where tablename='#{name}'").first['count'] == '1'
36
+ end
37
+
38
+ def insert_data(name=table_name)
39
+ conn.exec("insert into #{name} values(1, 'foo', '{bar, baz}')")
40
+ end
41
+
42
+ def row_count(name=table_name)
43
+ conn.exec("select count(*) from #{name}").first['count'].to_i
44
+ end
45
+
46
+ def create_table(name=table_name)
47
+ conn.exec("create table #{name} (column1 integer, column2 varchar(123), column3 varchar(256)[])")
48
+ end
49
+
50
+ let(:dest) { TableCopy::PG::Destination.new(
51
+ table_name: table_name,
52
+ conn_method: method(:with_conn),
53
+ indexes: [ TableCopy::PG::Index.new(table_name, nil, ['column1']) ],
54
+ fields: [ 'column1', 'column2', 'column3' ],
55
+ primary_key: 'column1',
56
+ sequence_field: 'column1'
57
+ )}
58
+
59
+ after do
60
+ conn.exec("drop table if exists #{table_name}")
61
+ end
62
+
63
+ describe '#to_s' do
64
+ it 'returns the table name' do
65
+ expect(dest.to_s).to eq table_name
66
+ end
67
+ end
68
+
69
+ context 'a table exists' do
70
+ before do
71
+ create_table
72
+ end
73
+
74
+ describe '#none?' do
75
+ it 'indicates whether the table has any data' do
76
+ expect {
77
+ insert_data
78
+ }.to change {
79
+ dest.none?
80
+ }.from(true).to(false)
81
+ end
82
+ end
83
+
84
+ describe '#transaction' do
85
+ context 'no error is raised' do
86
+ it 'opens and commits a transaction' do
87
+ expect {
88
+ dest.transaction do
89
+ insert_data
90
+ end
91
+ }.to change {
92
+ conn.exec("select count(*) from #{table_name}").first['count'].to_i
93
+ }.by(1)
94
+ end
95
+ end
96
+
97
+ context 'an error is raised' do
98
+ it 'opens but does not commit a transaction' do
99
+ expect {
100
+ begin
101
+ dest.transaction do
102
+ insert_data
103
+ raise
104
+ end
105
+ rescue RuntimeError; end
106
+ }.not_to change {
107
+ conn.exec("select count(*) from #{table_name}").first['count'].to_i
108
+ }
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#drop' do
114
+ it 'drops a table' do
115
+ expect {
116
+ dest.drop
117
+ }.to change {
118
+ table_exists?
119
+ }.from(true).to(false)
120
+ end
121
+ end
122
+
123
+ describe '#create_indexes' do
124
+ it 'creates indexes' do
125
+ expect {
126
+ dest.create_indexes
127
+ }.to change {
128
+ conn.exec(indexes_sql).count
129
+ }.from(0).to(1)
130
+ end
131
+ end
132
+
133
+ describe '#max_sequence' do
134
+ context 'no sequence field' do
135
+ it 'returns nil' do
136
+ expect(dest.max_sequence).to be_nil
137
+ end
138
+ end
139
+
140
+ context 'sequence field specified' do
141
+ let(:dest) { TableCopy::PG::Destination.new(
142
+ table_name: table_name,
143
+ conn_method: method(:with_conn),
144
+ sequence_field: 'column1'
145
+ )}
146
+
147
+ context 'no rows' do
148
+ it 'returns nil' do
149
+ expect(dest.max_sequence).to be_nil
150
+ end
151
+ end
152
+
153
+ context 'with rows' do
154
+ before do
155
+ insert_data
156
+ end
157
+
158
+ it 'returns the max value of the sequence field' do
159
+ expect(dest.max_sequence).to eq '1'
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '#copy_data_from' do
166
+ let(:source) { TableCopy::PG::Source.new({}) }
167
+ let(:source_conn) { double }
168
+
169
+ context 'all fields and rows' do
170
+ before do
171
+ expect(source).to receive(:copy_from).with('column1, column2, column3', nil).and_yield(source_conn)
172
+ expect(source_conn).to receive(:get_copy_data).and_return("1,foo,\"{bar,baz}\"\n")
173
+ expect(source_conn).to receive(:get_copy_data).and_return(nil)
174
+ end
175
+
176
+ context 'default options' do
177
+ it 'inserts data' do
178
+ expect {
179
+ dest.copy_data_from(source)
180
+ }.to change {
181
+ row_count
182
+ }.from(0).to(1)
183
+ end
184
+ end
185
+
186
+ context 'temp is true' do
187
+ before do
188
+ create_table("temp_#{table_name}")
189
+ end
190
+
191
+ after do
192
+ conn.exec("drop table if exists temp_#{table_name}")
193
+ end
194
+
195
+ it 'inserts data into temp table' do
196
+ expect {
197
+ dest.copy_data_from(source, temp: true)
198
+ }.to change {
199
+ row_count("temp_#{table_name}")
200
+ }.from(0).to(1)
201
+ end
202
+ end
203
+ end
204
+
205
+ context 'pk_only is true' do
206
+ before do
207
+ expect(source).to receive(:copy_from).with('column1', nil).and_yield(source_conn)
208
+ expect(source_conn).to receive(:get_copy_data).and_return("1\n")
209
+ expect(source_conn).to receive(:get_copy_data).and_return(nil)
210
+ end
211
+
212
+ it 'inserts data' do
213
+ expect {
214
+ dest.copy_data_from(source, pk_only: true)
215
+ }.to change {
216
+ row_count
217
+ }.from(0).to(1)
218
+ end
219
+ end
220
+
221
+ context 'update value is given' do
222
+ before do
223
+ expect(source).to receive(:copy_from).with('column1, column2, column3', "where column1 > 'a_value'").and_yield(source_conn)
224
+ expect(source_conn).to receive(:get_copy_data).and_return("1,foo,\"{bar,baz}\"\n")
225
+ expect(source_conn).to receive(:get_copy_data).and_return(nil)
226
+ end
227
+
228
+ it 'inserts data' do
229
+ expect {
230
+ dest.copy_data_from(source, update: 'a_value')
231
+ }.to change {
232
+ row_count
233
+ }.from(0).to(1)
234
+ end
235
+ end
236
+ end
237
+
238
+ context 'with temp table' do
239
+ before do
240
+ create_table("temp_#{table_name}")
241
+ end
242
+
243
+ after do
244
+ conn.exec("drop table if exists temp_#{table_name}")
245
+ end
246
+
247
+ describe '#copy_from_temp' do
248
+ before do
249
+ insert_data("temp_#{table_name}")
250
+ end
251
+
252
+ it 'upserts from the temp table' do
253
+ expect {
254
+ dest.copy_from_temp
255
+ }.to change {
256
+ row_count
257
+ }.from(0).to(1)
258
+
259
+ expect {
260
+ dest.copy_from_temp
261
+ }.not_to change {
262
+ row_count
263
+ }.from(1)
264
+ end
265
+ end
266
+
267
+ describe '#delete_not_in_temp' do
268
+ before do
269
+ insert_data
270
+ end
271
+
272
+ it 'deletes row that are not in the temp table' do
273
+ expect {
274
+ dest.delete_not_in_temp
275
+ }.to change {
276
+ row_count
277
+ }.from(1).to(0)
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ describe '#create' do
284
+ it 'creates a table' do
285
+ expect {
286
+ dest.create('column1 integer')
287
+ }.to change {
288
+ table_exists?
289
+ }.from(false).to(true)
290
+ end
291
+ end
292
+
293
+ describe '#create_temp' do
294
+ it 'creates a temporary table' do
295
+ dest.transaction do
296
+ expect {
297
+ dest.create_temp('column1 integer')
298
+ }.to change {
299
+ table_exists?("temp_#{table_name}")
300
+ }.from(false).to(true)
301
+ end
302
+ expect(table_exists?("temp_#{table_name}")).to eq false
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,65 @@
1
+ require 'table_copy/pg/field'
2
+
3
+ describe TableCopy::PG::Field do
4
+ let(:field) { TableCopy::PG::Field.new(field_attrs) }
5
+ let(:field_attrs) { { 'column_name' => 'column_name' } }
6
+
7
+ context 'for a varchar field' do
8
+ let(:field_attrs) { {
9
+ 'column_name' => 'column_name',
10
+ 'data_type' => 'character varying',
11
+ 'character_maximum_length' => '256'
12
+ } }
13
+
14
+ describe '#ddl' do
15
+ it 'returns a correct segment of ddl' do
16
+ expect(field.ddl).to eq 'column_name character varying(256)'
17
+ end
18
+ end
19
+
20
+ describe '#auto_index?' do
21
+ it 'returns falsey' do
22
+ expect(field.auto_index?).to be_falsey
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'for an integer field' do
28
+ let(:field_attrs) { {
29
+ 'column_name' => 'column_name',
30
+ 'data_type' => 'integer'
31
+ } }
32
+
33
+ describe '#ddl' do
34
+ it 'returns a correct segment of ddl' do
35
+ expect(field.ddl).to eq 'column_name integer'
36
+ end
37
+ end
38
+
39
+ describe '#auto_index?' do
40
+ it 'returns truthy' do
41
+ expect(field.auto_index?).to be_truthy
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'for an array field' do
47
+ let(:field_attrs) { {
48
+ 'column_name' => 'column_name',
49
+ 'data_type' => 'ARRAY',
50
+ 'udt_name' => '_varchar'
51
+ } }
52
+
53
+ describe '#ddl' do
54
+ it 'returns a correct segment of ddl' do
55
+ expect(field.ddl).to eq 'column_name character varying(256)[]'
56
+ end
57
+ end
58
+
59
+ describe '#auto_index?' do
60
+ it 'returns falsey' do
61
+ expect(field.auto_index?).to be_falsey
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ require 'table_copy/pg/index'
2
+
3
+ describe TableCopy::PG::Index do
4
+ let(:columns) { [ 'column1', 'column2', 'column3' ] }
5
+ let(:table) { 'table_name' }
6
+ let(:name) { 'index_name' }
7
+ let(:index) { TableCopy::PG::Index.new(table, name, columns) }
8
+
9
+ describe '#create' do
10
+ let(:expected) { 'create index on table_name using btree (column1, column2, column3)' }
11
+
12
+ it 'returns a correct create index statement' do
13
+ expect(index.create).to eq expected
14
+ end
15
+ end
16
+
17
+ describe '#drop' do
18
+ let(:expected) { 'drop index if exists index_name' }
19
+
20
+ it 'returns a correct drop index statement' do
21
+ expect(index.drop).to eq expected
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,120 @@
1
+ require 'table_copy/pg/source'
2
+ require 'yaml'
3
+
4
+ describe TableCopy::PG::Source do
5
+ let(:conn) { $pg_conn }
6
+ let(:table_name) { 'table_name' }
7
+
8
+ def with_conn
9
+ yield conn
10
+ end
11
+
12
+ let(:source) { TableCopy::PG::Source.new(
13
+ table_name: table_name,
14
+ conn_method: method(:with_conn)
15
+ )}
16
+
17
+ after do
18
+ conn.exec("drop table if exists #{table_name}")
19
+ end
20
+
21
+ describe '#to_s' do
22
+ it 'returns the table name' do
23
+ expect(source.to_s).to eq table_name
24
+ end
25
+ end
26
+
27
+ describe '#primary_key' do
28
+ context 'primary key is defined' do
29
+ let(:pk) { 'primary_key' }
30
+ before do
31
+ conn.exec("create table #{table_name} (#{pk} integer primary key)")
32
+ end
33
+
34
+ it 'returns the name of the primary key' do
35
+ expect(source.primary_key).to eq pk
36
+ end
37
+ end
38
+
39
+ context 'pk is not defined' do
40
+ before do
41
+ conn.exec("create table #{table_name} (#{pk} integer)")
42
+ end
43
+
44
+ context 'pk inferrence proc is defined' do
45
+ let(:pk) { "#{table_name}_id" }
46
+
47
+ let(:source) { TableCopy::PG::Source.new(
48
+ table_name: table_name,
49
+ conn_method: method(:with_conn),
50
+ infer_pk_proc: Proc.new { |tn| "#{tn}_id" }
51
+ )}
52
+
53
+ it 'returns the name of the primary key' do
54
+ expect(source.primary_key).to eq pk
55
+ end
56
+ end
57
+
58
+ context 'pk inferrence proc is not defined' do
59
+ let(:pk) { "#{table_name}_id" }
60
+
61
+ it 'returns "id"' do
62
+ expect(source.primary_key).to eq 'id'
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ context 'a table exists' do
69
+ before do
70
+ conn.exec("create table #{table_name} (column1 integer, column2 varchar(123), column3 varchar(256)[])")
71
+ end
72
+
73
+ describe '#fields_ddl' do
74
+ it 'returns correct fields ddl' do
75
+ expect(source.fields_ddl.gsub("\n", '')).to eq 'column1 integer, column2 character varying(123), column3 character varying(256)[]'
76
+ end
77
+ end
78
+
79
+ describe '#fields' do
80
+ it 'returns an array of field names' do
81
+ expect(source.fields).to eq [ 'column1', 'column2', 'column3' ]
82
+ end
83
+ end
84
+
85
+ context 'indexes exist' do
86
+ before do
87
+ conn.exec("create index on #{table_name} (column1)")
88
+ end
89
+
90
+ describe '#indexes' do
91
+ it 'returns an array of indexes' do
92
+ expect(source.indexes.count).to eq 1
93
+ index = source.indexes.first
94
+ expect(index.table).to eq table_name
95
+ expect(index.columns).to eq [ 'column1' ]
96
+ end
97
+ end
98
+ end
99
+
100
+ context 'a row exists' do
101
+ before do
102
+ conn.exec("insert into #{table_name} values(1, 'foo', '{bar, baz}')")
103
+ end
104
+
105
+ it 'yields a copying connection' do
106
+ source.copy_from('column1, column2, column3') do |copy_conn|
107
+ expect(copy_conn.get_copy_data).to eq "1,foo,\"{bar,baz}\"\n"
108
+ expect(copy_conn.get_copy_data).to be_nil
109
+ end
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ # context 'debugging specs' do
116
+ # it 'works' do
117
+ # conn.exec("create table #{table_name} (column1 integer, column2 character varying(123), column3 character varying(256)[])")
118
+ # end
119
+ # end
120
+ end