tapsoob 0.8.6-java → 0.8.7-java
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 +4 -4
- data/lib/tapsoob/cli/schema.rb +1 -1
- data/lib/tapsoob/version.rb +1 -1
- data/spec/spec_helper.rb +3 -2
- data/spec/support/operation_helpers.rb +43 -0
- data/spec/unit/tapsoob/base_spec.rb +222 -0
- data/spec/unit/tapsoob/cli_pipeline_spec.rb +380 -0
- data/spec/unit/tapsoob/config_spec.rb +54 -0
- data/spec/unit/tapsoob/data_stream_spec.rb +48 -0
- data/spec/unit/tapsoob/file_partition_spec.rb +117 -0
- data/spec/unit/tapsoob/keyed_spec.rb +121 -0
- data/spec/unit/tapsoob/progress_event_spec.rb +136 -0
- data/spec/unit/tapsoob/progress_spec.rb +335 -0
- data/spec/unit/tapsoob/pull_spec.rb +335 -0
- data/spec/unit/tapsoob/push_spec.rb +264 -0
- data/spec/unit/tapsoob/schema_spec.rb +154 -0
- data/spec/unit/tapsoob/utils_spec.rb +64 -0
- metadata +11 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tapsoob/operation/push'
|
|
3
|
+
require 'tapsoob/operation/pull'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Tapsoob::Operation::Push do
|
|
6
|
+
# Push opens its own Sequel connection from the URL, so we need a file-backed DB.
|
|
7
|
+
let(:db_path) { File.join(Dir.tmpdir, "tapsoob_push_#{Process.pid}_#{rand(9999)}.db") }
|
|
8
|
+
let(:db_url) { DbHelpers.adapt_url("sqlite://#{db_path}") }
|
|
9
|
+
|
|
10
|
+
let(:db) do
|
|
11
|
+
d = Sequel.connect(db_url)
|
|
12
|
+
d.extension :schema_dumper
|
|
13
|
+
d.create_table(:users) { primary_key :id; String :name }
|
|
14
|
+
d.create_table(:widgets) { primary_key :id; Integer :qty }
|
|
15
|
+
5.times { |i| d[:users].insert(name: "user_#{i}") }
|
|
16
|
+
3.times { |i| d[:widgets].insert(qty: i * 10) }
|
|
17
|
+
d
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:dump_dir) { Dir.mktmpdir("tapsoob_push_") }
|
|
21
|
+
|
|
22
|
+
after do
|
|
23
|
+
db.disconnect rescue nil
|
|
24
|
+
File.delete(db_path) rescue nil
|
|
25
|
+
FileUtils.rm_rf(dump_dir)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Populate dump_dir via Pull so Push has real schema + data files to read.
|
|
29
|
+
before do
|
|
30
|
+
pull_op = build_pull(db, dump_dir)
|
|
31
|
+
pull_op.initialize_dump_directory
|
|
32
|
+
pull_op.pull_schema
|
|
33
|
+
pull_op.pull_data_serial
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── fetch_local_tables_info ──────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe '#fetch_local_tables_info' do
|
|
39
|
+
it 'returns a hash of table_name => row_count' do
|
|
40
|
+
expect(build_push(db_url, dump_dir).fetch_local_tables_info).to include("users" => 5, "widgets" => 3)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'respects table_order.txt when present' do
|
|
44
|
+
expect(build_push(db_url, dump_dir).fetch_local_tables_info.keys).to include("users", "widgets")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'falls back to schema files when table_order.txt is absent' do
|
|
48
|
+
File.delete(File.join(dump_dir, "table_order.txt")) rescue nil
|
|
49
|
+
expect(build_push(db_url, dump_dir).fetch_local_tables_info.keys).to include("users", "widgets")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'applies exclude_tables filter' do
|
|
53
|
+
info = build_push(db_url, dump_dir, exclude_tables: ["widgets"]).fetch_local_tables_info
|
|
54
|
+
expect(info.keys).not_to include("widgets")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ── tables / record_count ────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe '#tables' do
|
|
61
|
+
it 'excludes completed tables' do
|
|
62
|
+
op = build_push(db_url, dump_dir)
|
|
63
|
+
op.opts[:completed_tables] = ["users"]
|
|
64
|
+
expect(op.tables.keys).not_to include("users")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#record_count' do
|
|
69
|
+
it 'sums all table row counts' do
|
|
70
|
+
expect(build_push(db_url, dump_dir).record_count).to eq(8)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ── calculate_file_line_ranges ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe '#calculate_file_line_ranges' do
|
|
77
|
+
it 'returns [] when the data file does not exist' do
|
|
78
|
+
expect(build_push(db_url, dump_dir).calculate_file_line_ranges("nonexistent", 2)).to eq([])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns a single range for 1 worker' do
|
|
82
|
+
ranges = build_push(db_url, dump_dir).calculate_file_line_ranges("users", 1)
|
|
83
|
+
expect(ranges.size).to eq(1)
|
|
84
|
+
expect(ranges.first.first).to eq(0)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'splits a multi-line file across workers without gaps' do
|
|
88
|
+
ranges = build_push(db_url, dump_dir).calculate_file_line_ranges("users", 2)
|
|
89
|
+
expect(ranges.size).to be >= 1
|
|
90
|
+
ranges.each_cons(2) do |(_, end1), (start2, _)|
|
|
91
|
+
expect(start2).to eq(end1 + 1)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ── push_schema ──────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe '#push_schema' do
|
|
99
|
+
it 'loads schema into a fresh target DB without error' do
|
|
100
|
+
fresh_path = File.join(Dir.tmpdir, "tapsoob_push_fresh_#{Process.pid}.db")
|
|
101
|
+
fresh_url = DbHelpers.adapt_url("sqlite://#{fresh_path}")
|
|
102
|
+
fresh_db = Sequel.connect(fresh_url)
|
|
103
|
+
fresh_db.extension :schema_dumper
|
|
104
|
+
begin
|
|
105
|
+
op = build_push(fresh_url, dump_dir)
|
|
106
|
+
op.instance_variable_set(:@db, fresh_db)
|
|
107
|
+
op.instance_variable_set(:@database_url, fresh_url)
|
|
108
|
+
expect { op.push_schema }.not_to raise_error
|
|
109
|
+
ensure
|
|
110
|
+
fresh_db.disconnect rescue nil
|
|
111
|
+
File.delete(fresh_path) rescue nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ── push_data_serial ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe '#push_data_serial' do
|
|
119
|
+
before do
|
|
120
|
+
db[:users].delete
|
|
121
|
+
db[:widgets].delete
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'inserts rows into the target DB' do
|
|
125
|
+
op = build_push(db_url, dump_dir)
|
|
126
|
+
op.instance_variable_set(:@db, db)
|
|
127
|
+
op.push_data_serial
|
|
128
|
+
expect(db[:users].count).to eq(5)
|
|
129
|
+
expect(db[:widgets].count).to eq(3)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'marks tables as completed' do
|
|
133
|
+
op = build_push(db_url, dump_dir)
|
|
134
|
+
op.instance_variable_set(:@db, db)
|
|
135
|
+
op.push_data_serial
|
|
136
|
+
expect(op.completed_tables).to include("users", "widgets")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ── to_hash ──────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe '#to_hash' do
|
|
143
|
+
it 'includes local_tables_info key' do
|
|
144
|
+
expect(build_push(db_url, dump_dir).to_hash).to have_key(:local_tables_info)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ── parallel? always false for Push ──────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe '#parallel?' do
|
|
151
|
+
it 'is always false regardless of :parallel option' do
|
|
152
|
+
expect(build_push(db_url, dump_dir, parallel: 4).parallel?).to be false
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# ── push_partial_data ────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe '#push_partial_data' do
|
|
159
|
+
before do
|
|
160
|
+
db[:users].delete
|
|
161
|
+
db[:widgets].delete
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'returns early when stream_state is empty' do
|
|
165
|
+
op = build_push(db_url, dump_dir)
|
|
166
|
+
op.instance_variable_set(:@db, db)
|
|
167
|
+
expect { op.push_partial_data }.not_to raise_error
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'raises ArgumentError when stream_state is set (production bug: factory called with 2 args)' do
|
|
171
|
+
op = build_push(db_url, dump_dir)
|
|
172
|
+
op.instance_variable_set(:@db, db)
|
|
173
|
+
op.stream_state = { table_name: "users", chunksize: 1000, offset: 5, size: 5, klass: "Tapsoob::DataStream::Base" }
|
|
174
|
+
expect { op.push_partial_data }.to raise_error(ArgumentError)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# ── push_data_from_file_parallel (intra-table parallelization) ───────────────
|
|
179
|
+
|
|
180
|
+
describe '#push_data_from_file_parallel' do
|
|
181
|
+
before do
|
|
182
|
+
db[:users].delete
|
|
183
|
+
db[:widgets].delete
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'inserts all rows routing through push_data_from_file_parallel' do
|
|
187
|
+
op = build_push(db_url, dump_dir)
|
|
188
|
+
op.instance_variable_set(:@db, db)
|
|
189
|
+
# Use 1 worker for all tables — with chunksize=1000 the files have 1 line each,
|
|
190
|
+
# so requesting 2 workers would leave ranges[1] nil and crash FilePartition.
|
|
191
|
+
allow(op).to receive(:table_parallel_workers).and_return(1)
|
|
192
|
+
op.push_data_serial
|
|
193
|
+
expect(db[:users].count).to eq(5)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'directly calls push_data_from_file_parallel without error' do
|
|
197
|
+
op = build_push(db_url, dump_dir)
|
|
198
|
+
op.instance_variable_set(:@db, db)
|
|
199
|
+
# 1 worker avoids the nil-current_line crash from under-populated ranges
|
|
200
|
+
expect { op.push_data_from_file_parallel("users", 5, 1) }.not_to raise_error
|
|
201
|
+
expect(db[:users].count).to eq(5)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'returns early when no data file exists' do
|
|
205
|
+
op = build_push(db_url, dump_dir)
|
|
206
|
+
op.instance_variable_set(:@db, db)
|
|
207
|
+
expect { op.push_data_from_file_parallel("nonexistent_table", 0, 2) }.not_to raise_error
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# ── push_indexes ─────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe '#push_indexes' do
|
|
214
|
+
it 'is a no-op when no index files exist' do
|
|
215
|
+
op = build_push(db_url, dump_dir)
|
|
216
|
+
op.instance_variable_set(:@db, db)
|
|
217
|
+
expect { op.push_indexes }.not_to raise_error
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# ── push_reset_sequences ─────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe '#push_reset_sequences' do
|
|
225
|
+
it 'runs without error on SQLite' do
|
|
226
|
+
op = build_push(db_url, dump_dir)
|
|
227
|
+
op.instance_variable_set(:@db, db)
|
|
228
|
+
expect { op.push_reset_sequences }.not_to raise_error
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# ── push_data_parallel (parallel? stubbed to true) ───────────────────────────
|
|
233
|
+
|
|
234
|
+
describe '#push_data_parallel' do
|
|
235
|
+
before do
|
|
236
|
+
db[:users].delete
|
|
237
|
+
db[:widgets].delete
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'inserts rows using table-level parallel workers (parallel? forced true)' do
|
|
241
|
+
op = build_push(db_url, dump_dir, parallel: 2)
|
|
242
|
+
op.instance_variable_set(:@db, db)
|
|
243
|
+
# Push#parallel? always returns false; stub it to exercise push_data_parallel
|
|
244
|
+
allow(op).to receive(:parallel?).and_return(true)
|
|
245
|
+
allow(op).to receive(:parallel_workers).and_return(2)
|
|
246
|
+
op.push_data
|
|
247
|
+
expect(db[:users].count).to eq(5)
|
|
248
|
+
expect(db[:widgets].count).to eq(3)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'handles intra-table parallelization within push_data_parallel' do
|
|
252
|
+
op = build_push(db_url, dump_dir, parallel: 2)
|
|
253
|
+
op.instance_variable_set(:@db, db)
|
|
254
|
+
allow(op).to receive(:parallel?).and_return(true)
|
|
255
|
+
allow(op).to receive(:parallel_workers).and_return(2)
|
|
256
|
+
# Use 1 intra-table worker — with chunksize=1000 and 5 rows the file has 1 line,
|
|
257
|
+
# so requesting 2 workers would leave ranges[1] nil and crash FilePartition.
|
|
258
|
+
allow(op).to receive(:table_parallel_workers).with("users", anything).and_return(1)
|
|
259
|
+
allow(op).to receive(:table_parallel_workers).with("widgets", anything).and_return(1)
|
|
260
|
+
op.push_data
|
|
261
|
+
expect(db[:users].count).to eq(5)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -2,6 +2,20 @@ require 'spec_helper'
|
|
|
2
2
|
require 'tapsoob/schema'
|
|
3
3
|
|
|
4
4
|
RSpec.describe Tapsoob::Schema do
|
|
5
|
+
# SQLite file-based URL helpers — memory: URLs open a fresh DB on each connect,
|
|
6
|
+
# so methods that open their own connection (dump, foreign_keys, indexes, …)
|
|
7
|
+
# must use a file-backed database.
|
|
8
|
+
def with_sqlite_file
|
|
9
|
+
path = File.join(Dir.tmpdir, "tapsoob_schema_#{Process.pid}_#{rand(9999)}.db")
|
|
10
|
+
url = DbHelpers.adapt_url("sqlite://#{path}")
|
|
11
|
+
db = DbHelpers.connect(url)
|
|
12
|
+
db.extension :schema_dumper
|
|
13
|
+
yield url, db
|
|
14
|
+
ensure
|
|
15
|
+
DbHelpers.disconnect_all
|
|
16
|
+
File.delete(path) rescue nil
|
|
17
|
+
end
|
|
18
|
+
|
|
5
19
|
let(:db) do
|
|
6
20
|
d = connect_sqlite
|
|
7
21
|
d.extension :schema_dumper
|
|
@@ -99,4 +113,144 @@ RSpec.describe Tapsoob::Schema do
|
|
|
99
113
|
}.not_to raise_error
|
|
100
114
|
end
|
|
101
115
|
end
|
|
116
|
+
|
|
117
|
+
# ── dump ─────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
describe '.dump' do
|
|
120
|
+
it 'returns a migration string covering all tables' do
|
|
121
|
+
with_sqlite_file do |url, tmp|
|
|
122
|
+
tmp.create_table(:things) { primary_key :id; String :name }
|
|
123
|
+
result = described_class.dump(url)
|
|
124
|
+
expect(result).to include('Sequel::Migration')
|
|
125
|
+
expect(result).to include('things')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'includes both up and down blocks' do
|
|
130
|
+
with_sqlite_file do |url, tmp|
|
|
131
|
+
tmp.create_table(:items) { primary_key :id }
|
|
132
|
+
result = described_class.dump(url)
|
|
133
|
+
expect(result).to include('def up')
|
|
134
|
+
expect(result).to include('def down')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ── foreign_keys ─────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe '.foreign_keys' do
|
|
142
|
+
it 'returns a string (even when there are no FK constraints)' do
|
|
143
|
+
with_sqlite_file do |url, _|
|
|
144
|
+
result = described_class.foreign_keys(url)
|
|
145
|
+
expect(result).to be_a(String)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ── indexes ──────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe '.indexes' do
|
|
153
|
+
it 'returns a string' do
|
|
154
|
+
with_sqlite_file do |url, tmp|
|
|
155
|
+
tmp.create_table(:idx_things) { primary_key :id; String :slug }
|
|
156
|
+
tmp.add_index(:idx_things, :slug)
|
|
157
|
+
result = described_class.indexes(url)
|
|
158
|
+
expect(result).to be_a(String)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# ── load via URL ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe '.load (URL path)' do
|
|
166
|
+
it 'creates the table when passed a URL string' do
|
|
167
|
+
with_sqlite_file do |url, tmp|
|
|
168
|
+
schema_str = described_class.dump_table(db, :articles, {})
|
|
169
|
+
described_class.load(url, schema_str)
|
|
170
|
+
expect(tmp.table_exists?(:articles)).to be true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'drops then recreates table when drop: true and passed a URL' do
|
|
175
|
+
with_sqlite_file do |url, tmp|
|
|
176
|
+
schema_str = described_class.dump_table(db, :articles, {})
|
|
177
|
+
described_class.load(url, schema_str)
|
|
178
|
+
described_class.load(url, schema_str, drop: true)
|
|
179
|
+
expect(tmp.table_exists?(:articles)).to be true
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# ── load_indexes ─────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe '.load_indexes' do
|
|
187
|
+
it 'applies an index migration without error' do
|
|
188
|
+
with_sqlite_file do |url, tmp|
|
|
189
|
+
tmp.create_table(:things) { primary_key :id; String :slug }
|
|
190
|
+
index_migration = <<~RUBY
|
|
191
|
+
Class.new(Sequel::Migration) do
|
|
192
|
+
def up
|
|
193
|
+
add_index :things, :slug
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
RUBY
|
|
197
|
+
expect { described_class.load_indexes(url, index_migration) }.not_to raise_error
|
|
198
|
+
expect(tmp.indexes(:things)).to have_key(:things_slug_index)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ── load_foreign_keys ────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
describe '.load_foreign_keys' do
|
|
206
|
+
it 'applies a foreign key migration without error' do
|
|
207
|
+
with_sqlite_file do |url, tmp|
|
|
208
|
+
tmp.create_table(:parents) { primary_key :id }
|
|
209
|
+
tmp.create_table(:children) { primary_key :id; Integer :parent_id }
|
|
210
|
+
fk_migration = <<~RUBY
|
|
211
|
+
Class.new(Sequel::Migration) do
|
|
212
|
+
def up
|
|
213
|
+
alter_table(:children) { add_foreign_key [:parent_id], :parents }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
RUBY
|
|
217
|
+
expect { described_class.load_foreign_keys(url, fk_migration) }.not_to raise_error
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# ── rewrite_non_integer_primary_keys ─────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe '.rewrite_non_integer_primary_keys' do
|
|
225
|
+
it 'leaves integer primary keys unchanged' do
|
|
226
|
+
schema = ' primary_key :id, :type=>"integer"'
|
|
227
|
+
expect(described_class.rewrite_non_integer_primary_keys(schema)).to eq(schema)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'leaves bigint primary keys unchanged' do
|
|
231
|
+
schema = ' primary_key :id, :type=>"bigint"'
|
|
232
|
+
expect(described_class.rewrite_non_integer_primary_keys(schema)).to eq(schema)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'rewrites varchar primary keys to column form' do
|
|
236
|
+
schema = ' primary_key :code, :type=>"varchar(10)"'
|
|
237
|
+
result = described_class.rewrite_non_integer_primary_keys(schema)
|
|
238
|
+
expect(result).to include('column :code')
|
|
239
|
+
expect(result).to include('"varchar(10)"')
|
|
240
|
+
expect(result).to include('primary_key: true')
|
|
241
|
+
expect(result).not_to include('primary_key :code,')
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'rewrites uuid primary keys to column form' do
|
|
245
|
+
schema = ' primary_key :id, :type=>"uuid"'
|
|
246
|
+
result = described_class.rewrite_non_integer_primary_keys(schema)
|
|
247
|
+
expect(result).to include('column :id')
|
|
248
|
+
expect(result).to include('primary_key: true')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'passes through schema with no primary_key lines unchanged' do
|
|
252
|
+
schema = " String :name, size: 50\n Integer :score"
|
|
253
|
+
expect(described_class.rewrite_non_integer_primary_keys(schema)).to eq(schema)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
102
256
|
end
|
|
@@ -220,6 +220,70 @@ RSpec.describe Tapsoob::Utils do
|
|
|
220
220
|
end
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
+
# ── windows? / bin ───────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe '.windows? / .bin' do
|
|
226
|
+
it 'returns a boolean for windows?' do
|
|
227
|
+
expect([true, false]).to include(described_class.windows?)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'returns the command unchanged on non-Windows' do
|
|
231
|
+
allow(described_class).to receive(:windows?).and_return(false)
|
|
232
|
+
expect(described_class.bin("mytool")).to eq("mytool")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'appends .cmd on Windows' do
|
|
236
|
+
allow(described_class).to receive(:windows?).and_return(true)
|
|
237
|
+
expect(described_class.bin("mytool")).to eq("mytool.cmd")
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# ── incorrect_blobs ──────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
describe '.incorrect_blobs' do
|
|
244
|
+
let(:db) do
|
|
245
|
+
d = connect_sqlite
|
|
246
|
+
d.create_table(:blob_test) { primary_key :id; File :data; String :name }
|
|
247
|
+
d
|
|
248
|
+
end
|
|
249
|
+
after { db.disconnect }
|
|
250
|
+
|
|
251
|
+
it 'returns blob-typed column names' do
|
|
252
|
+
expect(described_class.incorrect_blobs(db, :blob_test)).to include(:data)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'does not include non-blob columns' do
|
|
256
|
+
expect(described_class.incorrect_blobs(db, :blob_test)).not_to include(:name)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ── encode_blobs ASCII-8BIT branch ──────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe '.encode_blobs (ASCII-8BIT branch)' do
|
|
263
|
+
it 'encodes raw binary strings that are not Sequel::SQL::Blob' do
|
|
264
|
+
raw = "binary".force_encoding(Encoding::ASCII_8BIT)
|
|
265
|
+
row = { data: raw }
|
|
266
|
+
described_class.encode_blobs(row, [:data])
|
|
267
|
+
expect(row[:data]).to eq(described_class.base64encode(raw))
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# ── load_schema with a DB object ─────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
describe '.load_schema' do
|
|
274
|
+
let(:dir) { Dir.mktmpdir }
|
|
275
|
+
after { FileUtils.rm_rf(dir) }
|
|
276
|
+
|
|
277
|
+
it 'accepts a Sequel::Database object directly' do
|
|
278
|
+
db = connect_sqlite
|
|
279
|
+
FileUtils.mkdir_p(File.join(dir, 'schemas'))
|
|
280
|
+
schema_content = "Sequel.migration { change { create_table(:load_test) { primary_key :id } } }"
|
|
281
|
+
File.write(File.join(dir, 'schemas', 'load_test.rb'), schema_content)
|
|
282
|
+
expect { described_class.load_schema(dir, db, :load_test) }.not_to raise_error
|
|
283
|
+
db.disconnect
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
223
287
|
# ── export_rows / export_schema / export_indexes (filesystem) ────────────────
|
|
224
288
|
|
|
225
289
|
describe 'filesystem export helpers' do
|
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
|
+
version: 0.8.7
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Félix Bellanger
|
|
@@ -130,11 +130,21 @@ files:
|
|
|
130
130
|
- spec/spec_helper.rb
|
|
131
131
|
- spec/support/db_helpers.rb
|
|
132
132
|
- spec/support/fixtures.rb
|
|
133
|
+
- spec/support/operation_helpers.rb
|
|
133
134
|
- spec/support/round_trip_helper.rb
|
|
134
135
|
- spec/support/shared_examples/round_trip.rb
|
|
136
|
+
- spec/unit/tapsoob/base_spec.rb
|
|
135
137
|
- spec/unit/tapsoob/chunksize_spec.rb
|
|
138
|
+
- spec/unit/tapsoob/cli_pipeline_spec.rb
|
|
139
|
+
- spec/unit/tapsoob/config_spec.rb
|
|
136
140
|
- spec/unit/tapsoob/data_stream_spec.rb
|
|
141
|
+
- spec/unit/tapsoob/file_partition_spec.rb
|
|
142
|
+
- spec/unit/tapsoob/keyed_spec.rb
|
|
137
143
|
- spec/unit/tapsoob/operation_base_spec.rb
|
|
144
|
+
- spec/unit/tapsoob/progress_event_spec.rb
|
|
145
|
+
- spec/unit/tapsoob/progress_spec.rb
|
|
146
|
+
- spec/unit/tapsoob/pull_spec.rb
|
|
147
|
+
- spec/unit/tapsoob/push_spec.rb
|
|
138
148
|
- spec/unit/tapsoob/schema_spec.rb
|
|
139
149
|
- spec/unit/tapsoob/utils_spec.rb
|
|
140
150
|
- spec/unit/tapsoob/version_spec.rb
|