tapsoob 0.8.4 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30cfe2b3b3f3c8e9daa9283eec5d3a3cdf6e6a689a60d21d8e4ad3306740a9aa
4
- data.tar.gz: 3980451f8e14dd52c7fbf22de83cacf32ebc57de19c8c704a261575c5e92adb4
3
+ metadata.gz: 64dd145a81db1be94164524f8ec22cef232c76d7a3d455646c5289496c8e1ee2
4
+ data.tar.gz: 678fe127e48278964db8475c2d8001a085fb7a56546b4912b6fdf08a90ea2718
5
5
  SHA512:
6
- metadata.gz: baf8a2278be1d5499ad64a4ef89be355f7cc3e82f3efd759c51c254cc3e28567955fce8b6f5e4025bf451a08f8cf0fe7e236c09e536f6e09ffd71875eb276246
7
- data.tar.gz: 20ae520ae8daa165509b91f3b448268b2a3927f2285e422a2540b5eab75d3b66839aad441b90c58c0b8e83edd43004c27921c41bdcf9c80088e8745f5892d523
6
+ metadata.gz: 4263e7bdec44a5c31af5ebeba0fe1cc2b969e69f93a206a2c1b580a8141583eb94fd305626acc923d7499b20f38871294e41e1cdb9e14666fc2581e2fbbe8966
7
+ data.tar.gz: ebc49b6130cdcaa98ffe70405e8aac442012e451409a5d9e902c7c0518cf23dd7c3a158c850aac9392a9b349bae004b324efa42ad688d5dca10fd260f663fc2c
data/.gitlab-ci.yml CHANGED
@@ -22,10 +22,11 @@ stages:
22
22
  rules:
23
23
  - if: $CI_COMMIT_TAG
24
24
  when: never
25
+ - if: $CI_COMMIT_MESSAGE =~ /Bumped version/
26
+ when: never
25
27
  - when: on_success
26
28
  variables:
27
29
  INTEGRATION_TESTS: "1"
28
- SYSTEM_TESTS: "1"
29
30
  before_script:
30
31
  - apt-get update -qq && apt-get install -y -qq git
31
32
  - bundle install --quiet
@@ -49,10 +50,9 @@ test-sqlite:
49
50
  SRC_DATABASE_URL: "sqlite://tmp/tapsoob_src.db"
50
51
  DST_DATABASE_URL: "sqlite://tmp/tapsoob_dst.db"
51
52
  INTEGRATION_TESTS: "1"
52
- SYSTEM_TESTS: "1"
53
53
  script:
54
54
  - mkdir -p tmp
55
- - bundle exec rspec spec/unit spec/integration/sqlite_spec.rb spec/system/
55
+ - bundle exec rspec spec/unit spec/integration/sqlite_spec.rb
56
56
  --format progress
57
57
  --format RspecJunitFormatter --out rspec.xml
58
58
 
@@ -142,6 +142,8 @@ test-jruby:
142
142
  rules:
143
143
  - if: $CI_COMMIT_TAG
144
144
  when: never
145
+ - if: $CI_COMMIT_MESSAGE =~ /Bumped version/
146
+ when: never
145
147
  - when: on_success
146
148
  before_script:
147
149
  - apt-get update -qq && apt-get install -y -qq git libsqlite3-dev
@@ -4,6 +4,7 @@ require 'sequel/extensions/schema_dumper'
4
4
  require 'sequel/extensions/migration'
5
5
  require 'erb'
6
6
  require 'json'
7
+ require 'tapsoob/log'
7
8
 
8
9
  module Tapsoob
9
10
  module Schema
@@ -158,15 +159,18 @@ END_MIG
158
159
  end
159
160
 
160
161
  def reset_db_sequences(database_url)
161
- db = Sequel.connect(database_url)
162
- db.extension :schema_dumper
163
- return unless db.respond_to?(:reset_primary_key_sequence)
164
- db.tables.each do |table|
165
- pk = db.primary_key(table)
166
- next unless pk
167
- pk_type = db.schema(table).find { |col, _| col.to_s == pk.to_s }&.last&.dig(:db_type)
168
- next unless pk_type&.match?(/int|serial/i)
169
- db.reset_primary_key_sequence(table)
162
+ Sequel.connect(database_url) do |db|
163
+ db.extension :schema_dumper
164
+ next unless db.respond_to?(:reset_primary_key_sequence)
165
+ db.tables.each do |table|
166
+ pk = db.primary_key(table)
167
+ next unless pk
168
+ pk_type = db.schema(table).find { |col, _| col.to_s == pk.to_s }&.last&.dig(:db_type)
169
+ next unless pk_type&.match?(/int|serial/i)
170
+ db.reset_primary_key_sequence(table)
171
+ rescue Sequel::DatabaseError => e
172
+ Tapsoob.log.warn "Could not reset sequence for table '#{table}': #{e.message.lines.first.chomp}"
173
+ end
170
174
  end
171
175
  end
172
176
  end
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Tapsoob
3
- VERSION = "0.8.4".freeze
3
+ VERSION = "0.8.5".freeze
4
4
  end
@@ -107,10 +107,21 @@ RSpec.describe 'PostgreSQL round-trip', :integration do
107
107
  @src_db.run("DROP TABLE IF EXISTS varchar_pk_table")
108
108
  end
109
109
 
110
- it 'reset_db_sequences does not raise on a varchar primary key' do
111
- expect {
112
- Tapsoob::Schema.reset_db_sequences(@src_url)
113
- }.not_to raise_error
110
+ it 'skips reset without logging a warning (no sequence attached to varchar PK)' do
111
+ expect(Tapsoob.log).not_to receive(:warn)
112
+ Tapsoob::Schema.reset_db_sequences(@src_url)
113
+ end
114
+ end
115
+
116
+ context 'when reset_primary_key_sequence raises a DatabaseError' do
117
+ it 'logs a warning per failing table and does not re-raise' do
118
+ allow(Sequel).to receive(:connect).and_yield(@src_db)
119
+ allow(@src_db).to receive(:reset_primary_key_sequence).and_raise(
120
+ Sequel::DatabaseError, 'ERROR: identity column type must be smallint, integer, or bigint'
121
+ )
122
+
123
+ expect(Tapsoob.log).to receive(:warn).at_least(:once)
124
+ expect { Tapsoob::Schema.reset_db_sequences(@src_url) }.not_to raise_error
114
125
  end
115
126
  end
116
127
  end
data/spec/spec_helper.rb CHANGED
@@ -42,12 +42,9 @@ RSpec.configure do |config|
42
42
  config.order = :random
43
43
  Kernel.srand config.seed
44
44
 
45
- # Integration/system tests require a real DB — skip unless env vars are set.
45
+ # Integration tests require a real DB — skip unless env vars are set.
46
46
  config.filter_run_excluding :integration unless ENV['INTEGRATION_TESTS'] || ENV['SRC_DATABASE_URL']
47
- config.filter_run_excluding :system unless ENV['SYSTEM_TESTS'] || ENV['SRC_DATABASE_URL']
48
47
 
49
- config.include DbHelpers, :integration
50
- config.include DbHelpers, :system
48
+ config.include DbHelpers, :integration
51
49
  config.include RoundTripHelper, :integration
52
- config.include RoundTripHelper, :system
53
50
  end
@@ -15,7 +15,7 @@ require 'sequel'
15
15
  # null_heavy – every nullable column is NULL for half the rows
16
16
  #
17
17
  module Fixtures
18
- LARGE_TABLE_ROWS = 150_000
18
+ LARGE_TABLE_ROWS = 20_000
19
19
  DOCUMENT_ROWS = 500
20
20
  ATTACHMENT_ROWS = 200
21
21
  STANDARD_ROWS = 1_000
@@ -6,49 +6,53 @@
6
6
  # defined in DbHelpers (which delegate to the ivars set in before(:all)).
7
7
 
8
8
  RSpec.shared_examples 'a complete round-trip' do
9
+ # Pull once into a shared dir and reuse across all examples in this group.
10
+ before(:all) do
11
+ @shared_dump_dir = Dir.mktmpdir('tapsoob_shared_')
12
+ pull(src_url, @shared_dump_dir)
13
+ end
14
+
15
+ after(:all) do
16
+ FileUtils.rm_rf(@shared_dump_dir)
17
+ end
18
+
9
19
  it 'pulls without error' do
10
- expect { pull(src_url, dump_dir) }.not_to raise_error
20
+ expect(File).to exist(File.join(@shared_dump_dir, 'schemas'))
11
21
  end
12
22
 
13
23
  it 'creates schema dump files for every table' do
14
- pull(src_url, dump_dir)
15
24
  src_db.tables.each do |table|
16
- expect(File).to exist(File.join(dump_dir, 'schemas', "#{table}.rb"))
25
+ expect(File).to exist(File.join(@shared_dump_dir, 'schemas', "#{table}.rb"))
17
26
  end
18
27
  end
19
28
 
20
29
  it 'creates data dump files for every seeded table' do
21
- pull(src_url, dump_dir)
22
30
  %i[users orders products documents attachments events large_table null_heavy].each do |table|
23
- expect(File).to exist(File.join(dump_dir, 'data', "#{table}.json"))
31
+ expect(File).to exist(File.join(@shared_dump_dir, 'data', "#{table}.json"))
24
32
  end
25
33
  end
26
34
 
27
35
  it 'pushes without error' do
28
- pull(src_url, dump_dir)
29
- expect { push(dst_url, dump_dir) }.not_to raise_error
36
+ expect { push(dst_url, @shared_dump_dir) }.not_to raise_error
30
37
  end
31
38
 
32
39
  it 'preserves row counts for all tables' do
33
- round_trip(src_url, dst_url, dump_dir)
40
+ push(dst_url, @shared_dump_dir)
34
41
  expect_same_counts(src_db, dst_db)
35
42
  end
36
43
 
37
44
  it 'preserves NULL values in null_heavy' do
38
- round_trip(src_url, dst_url, dump_dir)
39
- null_rows = dst_db[:null_heavy].where(maybe_name: nil).count
40
- expect(null_rows).to be > 0
45
+ push(dst_url, @shared_dump_dir)
46
+ expect(dst_db[:null_heavy].where(maybe_name: nil).count).to be > 0
41
47
  end
42
48
 
43
49
  it 'preserves string content in users.email' do
44
- round_trip(src_url, dst_url, dump_dir)
45
- src_emails = src_db[:users].select_map(:email).sort
46
- dst_emails = dst_db[:users].select_map(:email).sort
47
- expect(dst_emails).to eq(src_emails)
50
+ push(dst_url, @shared_dump_dir)
51
+ expect(dst_db[:users].select_map(:email).sort).to eq(src_db[:users].select_map(:email).sort)
48
52
  end
49
53
 
50
54
  it 'preserves BLOB payloads in attachments' do
51
- round_trip(src_url, dst_url, dump_dir)
55
+ push(dst_url, @shared_dump_dir)
52
56
  src_db[:attachments].order(:id).each do |src_row|
53
57
  dst_row = dst_db[:attachments][id: src_row[:id]]
54
58
  expect(dst_row).not_to be_nil
@@ -57,7 +61,7 @@ RSpec.shared_examples 'a complete round-trip' do
57
61
  end
58
62
 
59
63
  it 'preserves large TEXT bodies in documents' do
60
- round_trip(src_url, dst_url, dump_dir)
64
+ push(dst_url, @shared_dump_dir)
61
65
  src_db[:documents].order(:id).each do |src_row|
62
66
  dst_row = dst_db[:documents][id: src_row[:id]]
63
67
  expect(dst_row[:body]).to eq(src_row[:body])
@@ -65,19 +69,29 @@ RSpec.shared_examples 'a complete round-trip' do
65
69
  end
66
70
 
67
71
  it 'handles the no-PK events table' do
68
- round_trip(src_url, dst_url, dump_dir)
72
+ push(dst_url, @shared_dump_dir)
69
73
  expect(dst_db[:events].count).to eq(src_db[:events].count)
70
74
  end
71
75
  end
72
76
 
73
77
  RSpec.shared_examples 'a parallel round-trip' do |workers:|
78
+ # Pull once, push with parallel workers.
79
+ before(:all) do
80
+ @parallel_dump_dir = Dir.mktmpdir('tapsoob_parallel_')
81
+ pull(src_url, @parallel_dump_dir, parallel: workers)
82
+ end
83
+
84
+ after(:all) do
85
+ FileUtils.rm_rf(@parallel_dump_dir)
86
+ end
87
+
74
88
  it "preserves row counts with #{workers} parallel workers" do
75
- round_trip(src_url, dst_url, dump_dir, parallel: workers)
89
+ push(dst_url, @parallel_dump_dir, parallel: workers)
76
90
  expect_same_counts(src_db, dst_db)
77
91
  end
78
92
 
79
- it "handles the large_table (>100K rows) with #{workers} workers" do
80
- round_trip(src_url, dst_url, dump_dir, parallel: workers)
93
+ it "handles the large_table with #{workers} workers" do
94
+ push(dst_url, @parallel_dump_dir, parallel: workers)
81
95
  expect(dst_db[:large_table].count).to eq(Fixtures::LARGE_TABLE_ROWS)
82
96
  end
83
97
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapsoob
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.4
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Félix Bellanger
@@ -133,7 +133,6 @@ files:
133
133
  - spec/support/fixtures.rb
134
134
  - spec/support/round_trip_helper.rb
135
135
  - spec/support/shared_examples/round_trip.rb
136
- - spec/system/large_dataset_spec.rb
137
136
  - spec/unit/tapsoob/chunksize_spec.rb
138
137
  - spec/unit/tapsoob/data_stream_spec.rb
139
138
  - spec/unit/tapsoob/operation_base_spec.rb
@@ -1,163 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe 'Large dataset system tests', :system do
4
- before(:all) do
5
- @src_url = DbHelpers.adapt_url(ENV.fetch('SRC_DATABASE_URL', 'sqlite://tmp/tapsoob_system_src.db'))
6
- @dst_url = DbHelpers.adapt_url(ENV.fetch('DST_DATABASE_URL', 'sqlite://tmp/tapsoob_system_dst.db'))
7
-
8
- FileUtils.mkdir_p('tmp')
9
- File.delete('tmp/tapsoob_system_src.db') rescue nil
10
- File.delete('tmp/tapsoob_system_dst.db') rescue nil
11
-
12
- @src_db = DbHelpers.connect(@src_url)
13
- @dst_db = DbHelpers.connect(@dst_url)
14
-
15
- Fixtures.create_tables(@src_db)
16
- Fixtures.seed(@src_db)
17
- end
18
-
19
- before(:each) do
20
- Fixtures.drop_tables(@dst_db)
21
- end
22
-
23
- after(:all) do
24
- Fixtures.drop_tables(@src_db)
25
- Fixtures.drop_tables(@dst_db)
26
- DbHelpers.disconnect_all
27
- File.delete('tmp/tapsoob_system_src.db') rescue nil
28
- File.delete('tmp/tapsoob_system_dst.db') rescue nil
29
- end
30
-
31
- # ── large_table: intra-table parallelization threshold ───────────────────────
32
-
33
- describe 'large_table (150K rows)' do
34
- it 'transfers all rows in serial mode' do
35
- round_trip(src_url, dst_url, dump_dir)
36
- expect(dst_db[:large_table].count).to eq(Fixtures::LARGE_TABLE_ROWS)
37
- end
38
-
39
- it 'transfers all rows with parallel: 2' do
40
- round_trip(src_url, dst_url, dump_dir, parallel: 2)
41
- expect(dst_db[:large_table].count).to eq(Fixtures::LARGE_TABLE_ROWS)
42
- end
43
-
44
- it 'transfers all rows with parallel: 4' do
45
- round_trip(src_url, dst_url, dump_dir, parallel: 4)
46
- expect(dst_db[:large_table].count).to eq(Fixtures::LARGE_TABLE_ROWS)
47
- end
48
-
49
- it 'has no duplicate rows after parallel pull' do
50
- round_trip(src_url, dst_url, dump_dir, parallel: 4)
51
- total = dst_db[:large_table].count
52
- distinct = dst_db[:large_table].select(:id).distinct.count
53
- expect(distinct).to eq(total)
54
- end
55
- end
56
-
57
- # ── documents: large TEXT columns ────────────────────────────────────────────
58
-
59
- describe 'documents table (large TEXT)' do
60
- it 'preserves body content exactly' do
61
- round_trip(src_url, dst_url, dump_dir)
62
- src_db[:documents].order(:id).each do |src_row|
63
- dst_row = dst_db[:documents][id: src_row[:id]]
64
- expect(dst_row[:body]).to eq(src_row[:body]),
65
- "body mismatch for document #{src_row[:id]}: " \
66
- "src=#{src_row[:body]&.length} bytes dst=#{dst_row[:body]&.length} bytes"
67
- end
68
- end
69
-
70
- it 'handles documents with nil body' do
71
- round_trip(src_url, dst_url, dump_dir)
72
- expect(dst_db[:documents].where(body: nil).count).to eq(src_db[:documents].where(body: nil).count)
73
- end
74
- end
75
-
76
- # ── attachments: BLOB encoding/decoding ──────────────────────────────────────
77
-
78
- describe 'attachments table (binary BLOBs up to 256 KB)' do
79
- it 'preserves every byte of every payload' do
80
- round_trip(src_url, dst_url, dump_dir)
81
- mismatch_count = 0
82
- src_db[:attachments].order(:id).each do |src_row|
83
- dst_row = dst_db[:attachments][id: src_row[:id]]
84
- mismatch_count += 1 unless dst_row[:payload].to_s.bytes == src_row[:payload].to_s.bytes
85
- end
86
- expect(mismatch_count).to eq(0)
87
- end
88
-
89
- it 'preserves size_bytes metadata' do
90
- round_trip(src_url, dst_url, dump_dir)
91
- src_db[:attachments].order(:id).each do |src_row|
92
- dst_row = dst_db[:attachments][id: src_row[:id]]
93
- expect(dst_row[:size_bytes]).to eq(src_row[:size_bytes])
94
- end
95
- end
96
- end
97
-
98
- # ── null_heavy: NULL preservation ────────────────────────────────────────────
99
-
100
- describe 'null_heavy table' do
101
- it 'preserves NULLs in every nullable column' do
102
- round_trip(src_url, dst_url, dump_dir)
103
- %i[maybe_name maybe_number maybe_score maybe_date maybe_text].each do |col|
104
- src_nulls = src_db[:null_heavy].where(col => nil).count
105
- dst_nulls = dst_db[:null_heavy].where(col => nil).count
106
- expect(dst_nulls).to eq(src_nulls),
107
- "NULL count mismatch for null_heavy.#{col}: src=#{src_nulls} dst=#{dst_nulls}"
108
- end
109
- end
110
- end
111
-
112
- # ── events: table without primary key ────────────────────────────────────────
113
-
114
- describe 'events table (no primary key)' do
115
- it 'uses the Base (non-keyed) stream' do
116
- round_trip(src_url, dst_url, dump_dir)
117
- expect(dst_db[:events].count).to eq(src_db[:events].count)
118
- end
119
- end
120
-
121
- # ── adaptive chunksize: very small chunks ────────────────────────────────────
122
-
123
- describe 'adaptive chunksize under load' do
124
- it 'completes with chunksize=1 (extreme case)' do
125
- small_src = DbHelpers.adapt_url('sqlite://tmp/tapsoob_small_src.db')
126
- small_dst = DbHelpers.adapt_url('sqlite://tmp/tapsoob_small_dst.db')
127
- small_dir = Dir.mktmpdir
128
-
129
- begin
130
- sdb = DbHelpers.connect(small_src)
131
- sdb.create_table!(:small_test) { primary_key :id; String :v, size: 50 }
132
- 100.times { |i| sdb[:small_test].insert(v: "row_#{i}") }
133
-
134
- round_trip(small_src, small_dst, small_dir, default_chunksize: 1)
135
- expect(DbHelpers.connect(small_dst)[:small_test].count).to eq(100)
136
- ensure
137
- FileUtils.rm_rf(small_dir)
138
- File.delete('tmp/tapsoob_small_src.db') rescue nil
139
- File.delete('tmp/tapsoob_small_dst.db') rescue nil
140
- # Reconnect suite DBs after disconnect_all clears the pool
141
- DbHelpers.disconnect_all
142
- @src_db = DbHelpers.connect(@src_url)
143
- @dst_db = DbHelpers.connect(@dst_url)
144
- end
145
- end
146
- end
147
-
148
- # ── FK order: orders depends on users ────────────────────────────────────────
149
-
150
- describe 'foreign key dependency ordering' do
151
- it 'pushes users before orders (table_order.txt respected)' do
152
- pull(src_url, dump_dir)
153
- order_file = File.join(dump_dir, 'table_order.txt')
154
- if File.exist?(order_file)
155
- order = File.readlines(order_file).map(&:strip)
156
- users_idx = order.index('users')
157
- orders_idx = order.index('orders')
158
- expect(users_idx).to be < orders_idx if users_idx && orders_idx
159
- end
160
- expect { push(dst_url, dump_dir) }.not_to raise_error
161
- end
162
- end
163
- end