tapsoob 0.8.4 → 0.8.6

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: dd37614b588e363ef7ae5fefdbc98188a194045f72906d496bef4ef67df04bf7
4
+ data.tar.gz: 9270a820fdd7600f943a6343c208d8752618f40fe29d6a81c3af32051f4475ee
5
5
  SHA512:
6
- metadata.gz: baf8a2278be1d5499ad64a4ef89be355f7cc3e82f3efd759c51c254cc3e28567955fce8b6f5e4025bf451a08f8cf0fe7e236c09e536f6e09ffd71875eb276246
7
- data.tar.gz: 20ae520ae8daa165509b91f3b448268b2a3927f2285e422a2540b5eab75d3b66839aad441b90c58c0b8e83edd43004c27921c41bdcf9c80088e8745f5892d523
6
+ metadata.gz: 1ac36335eca4bc18bff412ab892b05a6e13322f34debdf6a1decf40e95128066d86b9b598edc43e150e1414291aa883d5f93f8ba10695b1b1d7fa02480d620ad
7
+ data.tar.gz: 63bbb2832e9768287cc3e9189e7cbd919cf73b9707968ed8de4f9442ea2ab833216bcedcc043ebbd761f79a2f0ccab5fc60706efabfb9fa3f57abc8fb7422738
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,15 +4,16 @@ 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
10
11
  extend self
11
12
 
12
13
  def dump(database_url, options = {})
13
- db = Sequel.connect(database_url)
14
- db.extension :schema_dumper
15
- template = ERB.new <<-END_MIG
14
+ Sequel.connect(database_url) do |db|
15
+ db.extension :schema_dumper
16
+ template = ERB.new <<-END_MIG
16
17
  Class.new(Sequel::Migration) do
17
18
  def up
18
19
  <% db.send(:sort_dumped_tables, db.tables, {}).each do |table| %>
@@ -28,7 +29,8 @@ Class.new(Sequel::Migration) do
28
29
  end
29
30
  END_MIG
30
31
 
31
- template.result(binding)
32
+ template.result(binding)
33
+ end
32
34
  end
33
35
 
34
36
  def dump_table(database_url_or_db, table, options)
@@ -67,15 +69,17 @@ END_MIG
67
69
  end
68
70
 
69
71
  def foreign_keys(database_url)
70
- db = Sequel.connect(database_url)
71
- db.extension :schema_dumper
72
- db.dump_foreign_key_migration
72
+ Sequel.connect(database_url) do |db|
73
+ db.extension :schema_dumper
74
+ db.dump_foreign_key_migration
75
+ end
73
76
  end
74
77
 
75
78
  def indexes(database_url)
76
- db = Sequel.connect(database_url)
77
- db.extension :schema_dumper
78
- db.dump_indexes_migration
79
+ Sequel.connect(database_url) do |db|
80
+ db.extension :schema_dumper
81
+ db.dump_indexes_migration
82
+ end
79
83
  end
80
84
 
81
85
  def indexes_individual(database_url)
@@ -104,6 +108,7 @@ END_MIG
104
108
  end
105
109
 
106
110
  def load(database_url_or_db, schema, options = { drop: false })
111
+ schema = rewrite_non_integer_primary_keys(schema)
107
112
  # Accept either a database URL or an existing connection object
108
113
  if database_url_or_db.is_a?(Sequel::Database)
109
114
  db = database_url_or_db
@@ -157,16 +162,38 @@ END_MIG
157
162
  end
158
163
  end
159
164
 
165
+ NON_INTEGER_PK_PATTERN = /^(\s*)primary_key\s+(:?\w+),\s*:type=>"([^"]+)"(.*)$/
166
+ INTEGER_DB_TYPES = /\A(?:int(?:eger|\d+)?|bigint|smallint|serial|bigserial|smallserial)/i
167
+
168
+ # On PG 10+, Sequel's CreateTableGenerator injects `identity: true` into
169
+ # every primary_key call via serial_primary_key_options. PG rejects IDENTITY
170
+ # on non-integer types. Rewrite `primary_key :col, :type=>"varchar..."` to
171
+ # `column :col, "varchar...", primary_key: true, null: false` which bypasses
172
+ # that code path entirely.
173
+ def rewrite_non_integer_primary_keys(schema_str)
174
+ schema_str.gsub(NON_INTEGER_PK_PATTERN) do
175
+ indent, col, db_type, rest = $1, $2, $3, $4
176
+ if db_type =~ INTEGER_DB_TYPES
177
+ "#{indent}primary_key #{col}, :type=>\"#{db_type}\"#{rest}"
178
+ else
179
+ "#{indent}column #{col}, \"#{db_type}\", primary_key: true, null: false#{rest}"
180
+ end
181
+ end
182
+ end
183
+
160
184
  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)
185
+ Sequel.connect(database_url) do |db|
186
+ db.extension :schema_dumper
187
+ next unless db.respond_to?(:reset_primary_key_sequence)
188
+ db.tables.each do |table|
189
+ pk = db.primary_key(table)
190
+ next unless pk
191
+ pk_type = db.schema(table).find { |col, _| col.to_s == pk.to_s }&.last&.dig(:db_type)
192
+ next unless pk_type&.match?(/int|serial/i)
193
+ db.reset_primary_key_sequence(table)
194
+ rescue Sequel::DatabaseError => e
195
+ Tapsoob.log.warn "Could not reset sequence for table '#{table}': #{e.message.lines.first.chomp}"
196
+ end
170
197
  end
171
198
  end
172
199
  end
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Tapsoob
3
- VERSION = "0.8.4".freeze
3
+ VERSION = "0.8.6".freeze
4
4
  end
@@ -105,12 +105,35 @@ RSpec.describe 'PostgreSQL round-trip', :integration do
105
105
 
106
106
  after(:each) do
107
107
  @src_db.run("DROP TABLE IF EXISTS varchar_pk_table")
108
+ @dst_db.run("DROP TABLE IF EXISTS varchar_pk_table")
108
109
  end
109
110
 
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
111
+ it 'skips reset without logging a warning (no sequence attached to varchar PK)' do
112
+ expect(Tapsoob.log).not_to receive(:warn)
113
+ Tapsoob::Schema.reset_db_sequences(@src_url)
114
+ end
115
+
116
+ it 'round-trips the table without identity column errors' do
117
+ dump_dir = Dir.mktmpdir
118
+ begin
119
+ pull(src_url, dump_dir)
120
+ expect { push(dst_url, dump_dir) }.not_to raise_error
121
+ expect(dst_db[:varchar_pk_table].where(id: 'abc-123').count).to eq(1)
122
+ ensure
123
+ FileUtils.rm_rf(dump_dir)
124
+ end
125
+ end
126
+ end
127
+
128
+ context 'when reset_primary_key_sequence raises a DatabaseError' do
129
+ it 'logs a warning per failing table and does not re-raise' do
130
+ allow(Sequel).to receive(:connect).and_yield(@src_db)
131
+ allow(@src_db).to receive(:reset_primary_key_sequence).and_raise(
132
+ Sequel::DatabaseError, 'ERROR: identity column type must be smallint, integer, or bigint'
133
+ )
134
+
135
+ expect(Tapsoob.log).to receive(:warn).at_least(:once)
136
+ expect { Tapsoob::Schema.reset_db_sequences(@src_url) }.not_to raise_error
114
137
  end
115
138
  end
116
139
  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.6
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