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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +2 -0
- data/bin/table_copy +10 -0
- data/config/database.yml +1 -0
- data/config/initializers/table_copy.rb.example +62 -0
- data/lib/table_copy/copier.rb +98 -0
- data/lib/table_copy/pg/destination.rb +162 -0
- data/lib/table_copy/pg/field.rb +42 -0
- data/lib/table_copy/pg/index.rb +21 -0
- data/lib/table_copy/pg/source.rb +139 -0
- data/lib/table_copy/pg.rb +9 -0
- data/lib/table_copy/version.rb +3 -0
- data/lib/table_copy.rb +54 -0
- data/spec/lib/table_copy/copier_spec.rb +126 -0
- data/spec/lib/table_copy/pg/destination_spec.rb +305 -0
- data/spec/lib/table_copy/pg/field_spec.rb +65 -0
- data/spec/lib/table_copy/pg/index_spec.rb +24 -0
- data/spec/lib/table_copy/pg/source_spec.rb +120 -0
- data/spec/lib/table_copy_spec.rb +89 -0
- data/spec/spec_helper.rb +94 -0
- data/table_copy.gemspec +27 -0
- metadata +147 -0
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
|