fury_dumper 0.1.0
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/.github/workflows/build.yaml +43 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +67 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Breadth-first.png +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Depth-first.png +0 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +202 -0
- data/README.md +383 -0
- data/README.ru.md +382 -0
- data/Rakefile +8 -0
- data/app/controllers/fury_dumper/dump_process_controller.rb +25 -0
- data/config/routes.rb +6 -0
- data/fury_dumper.gemspec +37 -0
- data/lib/fury_dumper/api.rb +82 -0
- data/lib/fury_dumper/config.rb +113 -0
- data/lib/fury_dumper/dumper.rb +632 -0
- data/lib/fury_dumper/dumpers/dump_state.rb +61 -0
- data/lib/fury_dumper/dumpers/model.rb +131 -0
- data/lib/fury_dumper/dumpers/model_queue.rb +34 -0
- data/lib/fury_dumper/dumpers/relation_items.rb +79 -0
- data/lib/fury_dumper/encrypter.rb +17 -0
- data/lib/fury_dumper/engine.rb +7 -0
- data/lib/fury_dumper/version.rb +5 -0
- data/lib/fury_dumper.rb +102 -0
- data/lib/generators/fury_dumper/config_generator.rb +21 -0
- data/rails_generators/fury_dumper_config/fury_dumper_config_generator.rb +10 -0
- data/rails_generators/fury_dumper_config/templates/fury_dumper.rb +1 -0
- data/rails_generators/fury_dumper_config/templates/fury_dumper.yml +44 -0
- metadata +181 -0
@@ -0,0 +1,632 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FuryDumper
|
4
|
+
class Dumper
|
5
|
+
attr_reader :dump_state
|
6
|
+
|
7
|
+
def initialize(password:,
|
8
|
+
host:,
|
9
|
+
port:,
|
10
|
+
user:,
|
11
|
+
database:,
|
12
|
+
model:,
|
13
|
+
debug_mode:)
|
14
|
+
|
15
|
+
@password = password
|
16
|
+
@host = host
|
17
|
+
@user = user
|
18
|
+
@database = database
|
19
|
+
@port = port
|
20
|
+
@model = model
|
21
|
+
@debug_mode = debug_mode
|
22
|
+
@dump_state = Dumpers::DumpState.new(root_source_model: @model_name)
|
23
|
+
@undump_models = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync_models
|
27
|
+
p '--- Dump models ---'
|
28
|
+
if FuryDumper::Config.mode == :wide
|
29
|
+
sync_model_in_wight(@model)
|
30
|
+
else
|
31
|
+
sync_model_in_depth(@model)
|
32
|
+
end
|
33
|
+
|
34
|
+
@dump_state.stop
|
35
|
+
print_undump_models
|
36
|
+
end
|
37
|
+
|
38
|
+
def equal_schemas?
|
39
|
+
tables_list = cur_connection.tables.map { |e| "'#{e}'" }.join(', ')
|
40
|
+
sql = 'SELECT column_name, data_type, table_name FROM INFORMATION_SCHEMA.COLUMNS ' \
|
41
|
+
"WHERE table_name IN (#{tables_list});"
|
42
|
+
cur_schema = cur_connection.exec_query(sql).to_a
|
43
|
+
remote_schema = remote_connection.exec_query(sql).to_a
|
44
|
+
difference = difference(remote_schema, cur_schema)
|
45
|
+
|
46
|
+
if difference.present?
|
47
|
+
difference.group_by { |e| e['table_name'] }.each do |table_name, diff|
|
48
|
+
p "💣 Found difference for table #{table_name}"
|
49
|
+
diff.sort_by { |e| e['column_name'] }.each do |dif|
|
50
|
+
if cur_schema.include?(dif)
|
51
|
+
p "Current DB have column: #{dif['column_name']} <#{dif['data_type']}>"
|
52
|
+
else
|
53
|
+
p "Remote DB have column: #{dif['column_name']} <#{dif['data_type']}>"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
false
|
58
|
+
else
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def sync_model_in_depth(model)
|
66
|
+
print "Start #{model.to_short_str} dump", model.iteration if full_debug_mode?
|
67
|
+
active_record_model = model.active_record_model
|
68
|
+
return if relation_already_exist?(model, @dump_state)
|
69
|
+
|
70
|
+
# Добавляем в список моделей, которые уже стащили
|
71
|
+
@dump_state.add_loaded_relation(model)
|
72
|
+
|
73
|
+
buffer = model.to_full_str
|
74
|
+
|
75
|
+
return unless dump_model(model)
|
76
|
+
|
77
|
+
send_out_ms_dump(current_model)
|
78
|
+
|
79
|
+
return if empty_active_record?(model, buffer)
|
80
|
+
|
81
|
+
print buffer, model.iteration
|
82
|
+
|
83
|
+
active_record_model.reflect_on_all_associations.each do |relation|
|
84
|
+
# Проверка на корректную связь
|
85
|
+
next unless valid_relation?(relation, model)
|
86
|
+
|
87
|
+
# Исключения
|
88
|
+
next if excluded?(model, relation)
|
89
|
+
|
90
|
+
# Игнорим through
|
91
|
+
next if through?(relation, model.iteration)
|
92
|
+
|
93
|
+
# Если связана с полиморфной сущностью ...
|
94
|
+
new_models = build_polymorphic_models(model, relation)
|
95
|
+
unless new_models.nil?
|
96
|
+
new_models.each do |new_model|
|
97
|
+
sync_model_in_depth(new_model)
|
98
|
+
end
|
99
|
+
|
100
|
+
next
|
101
|
+
end
|
102
|
+
|
103
|
+
# Игнорим связи для другой БД
|
104
|
+
next if other_db?(relation, model.iteration)
|
105
|
+
|
106
|
+
print_new_model(model, relation)
|
107
|
+
|
108
|
+
new_model = build_as_models(model, relation, @dump_state) ||
|
109
|
+
build_default_model(model, relation, @dump_state)
|
110
|
+
|
111
|
+
next if new_model.nil?
|
112
|
+
|
113
|
+
sync_model_in_depth(new_model)
|
114
|
+
end
|
115
|
+
|
116
|
+
log_model_warnings(model)
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
def sync_model_in_wight(model, model_queue = Dumpers::ModelQueue.new)
|
121
|
+
print "Start #{model.to_short_str} dump", model.iteration if full_debug_mode?
|
122
|
+
model_queue.add_element(model: model, dump_state: @dump_state)
|
123
|
+
|
124
|
+
until model_queue.empty?
|
125
|
+
current_model, dump_state = model_queue.fetch_element
|
126
|
+
if full_debug_mode?
|
127
|
+
print "Relation #{current_model.to_short_str} start dump (queue size - #{model_queue.count})",
|
128
|
+
current_model.iteration
|
129
|
+
end
|
130
|
+
|
131
|
+
active_record_model = current_model.active_record_model
|
132
|
+
next if relation_already_exist?(current_model, dump_state)
|
133
|
+
|
134
|
+
# Добавляем в список моделей, которые уже стащили
|
135
|
+
dump_state.add_loaded_relation(current_model)
|
136
|
+
|
137
|
+
buffer = current_model.to_full_str
|
138
|
+
next unless dump_model(current_model)
|
139
|
+
|
140
|
+
send_out_ms_dump(current_model)
|
141
|
+
|
142
|
+
next if empty_active_record?(current_model, buffer)
|
143
|
+
|
144
|
+
print buffer, current_model.iteration
|
145
|
+
|
146
|
+
active_record_model.reflect_on_all_associations.each do |relation|
|
147
|
+
# Проверка на корректную связь
|
148
|
+
next unless valid_relation?(relation, current_model)
|
149
|
+
|
150
|
+
# Исключения
|
151
|
+
next if excluded?(current_model, relation)
|
152
|
+
|
153
|
+
# Игнорим through
|
154
|
+
next if through?(relation, current_model.iteration)
|
155
|
+
|
156
|
+
# Если связана с полиморфной сущностью ...
|
157
|
+
new_models = build_polymorphic_models(current_model, relation)
|
158
|
+
unless new_models.nil?
|
159
|
+
new_models.each do |new_model|
|
160
|
+
next if new_model.nil?
|
161
|
+
|
162
|
+
model_queue.add_element(model: new_model, dump_state: dump_state)
|
163
|
+
end
|
164
|
+
|
165
|
+
next
|
166
|
+
end
|
167
|
+
print_new_model(current_model, relation)
|
168
|
+
|
169
|
+
# Игнорим связи для другой БД
|
170
|
+
next if other_db?(relation, current_model.iteration)
|
171
|
+
|
172
|
+
new_model = build_as_models(current_model, relation, dump_state) ||
|
173
|
+
build_default_model(current_model, relation, dump_state)
|
174
|
+
|
175
|
+
next if new_model.nil?
|
176
|
+
|
177
|
+
print "Relation #{new_model.to_short_str} add to queue", current_model.iteration if full_debug_mode?
|
178
|
+
model_queue.add_element(model: new_model, dump_state: dump_state)
|
179
|
+
end
|
180
|
+
|
181
|
+
log_model_warnings(model)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_default_model(current_model, relation, dump_state)
|
186
|
+
relation_class = relation.klass.to_s
|
187
|
+
self_field_name = self_field_name(current_model, relation)
|
188
|
+
relation_field_name = relation_field_name(relation, current_model.iteration).to_s
|
189
|
+
|
190
|
+
new_model = Dumpers::Model.new(source_model: relation_class,
|
191
|
+
relation_items: Dumpers::RelationItems.new_with_key_value(
|
192
|
+
item_key: relation_field_name,
|
193
|
+
item_values: []
|
194
|
+
),
|
195
|
+
iteration: current_model.next_iteration,
|
196
|
+
root_model: current_model.root_model)
|
197
|
+
|
198
|
+
# Связь с другой таблицей уже была
|
199
|
+
return nil if relation_already_exist?(new_model, dump_state)
|
200
|
+
|
201
|
+
relation_values = if relation.macro == :has_and_belongs_to_many
|
202
|
+
# Get association foreign values for has_and_belongs_to_many relation
|
203
|
+
active_record_has_and_belongs_to_many(current_model, relation).presence
|
204
|
+
else
|
205
|
+
attribute_values(current_model.to_active_record_relation, self_field_name)
|
206
|
+
end
|
207
|
+
|
208
|
+
return nil if relation_values.compact.empty?
|
209
|
+
|
210
|
+
items = [Dumpers::RelationItem.new(key: relation_field_name, values_for_key: relation_values)]
|
211
|
+
|
212
|
+
# Если связь со scope, получить все условия
|
213
|
+
items += fetch_scope_items(current_model, relation)
|
214
|
+
|
215
|
+
new_model.relation_items = Dumpers::RelationItems.new_with_items(items: items)
|
216
|
+
|
217
|
+
new_model
|
218
|
+
end
|
219
|
+
|
220
|
+
# Get association foreign values for has_and_belongs_to_many relation
|
221
|
+
# @return [Array of String] association foreign values
|
222
|
+
def active_record_has_and_belongs_to_many(model, relation)
|
223
|
+
return [] unless relation.macro == :has_and_belongs_to_many
|
224
|
+
|
225
|
+
dump_proxy_table(model, relation)
|
226
|
+
end
|
227
|
+
|
228
|
+
def build_polymorphic_models(current_model, relation)
|
229
|
+
return nil unless relation.options[:polymorphic]
|
230
|
+
|
231
|
+
active_record_values = current_model.to_active_record_relation
|
232
|
+
relation_foreign_key = relation.foreign_key
|
233
|
+
polymorphic_models = attribute_values(active_record_values, relation.foreign_type)
|
234
|
+
|
235
|
+
print "Found polymorphic relation #{current_model.source_model}(#{relation.name}): " \
|
236
|
+
"#{polymorphic_models.join(', ')}", current_model.iteration
|
237
|
+
|
238
|
+
polymorphic_models.map do |polymorphic_model|
|
239
|
+
next unless polymorphic_model
|
240
|
+
next unless validate_model_by_name(polymorphic_model, current_model.iteration)
|
241
|
+
|
242
|
+
polymorphic_primary_key = polymorphic_model.constantize.primary_key.to_s
|
243
|
+
type_values = { relation.foreign_type => polymorphic_model }
|
244
|
+
polymorphic_values = attribute_values(active_record_values.where(type_values), relation_foreign_key)
|
245
|
+
|
246
|
+
Dumpers::Model.new(source_model: polymorphic_model,
|
247
|
+
relation_items: Dumpers::RelationItems.new_with_key_value(item_key: polymorphic_primary_key,
|
248
|
+
item_values: polymorphic_values),
|
249
|
+
iteration: current_model.next_iteration,
|
250
|
+
root_model: current_model.root_model)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def build_as_models(current_model, relation, dump_state)
|
255
|
+
return nil unless relation.options[:as]
|
256
|
+
|
257
|
+
relation_class = relation.klass.to_s
|
258
|
+
self_field_name = self_field_name(current_model, relation)
|
259
|
+
relation_field_name = relation_field_name(relation, current_model.iteration).to_s
|
260
|
+
|
261
|
+
new_model = Dumpers::Model.new(source_model: relation_class,
|
262
|
+
relation_items: Dumpers::RelationItems.new_with_key_value(
|
263
|
+
item_key: relation_field_name,
|
264
|
+
item_values: []
|
265
|
+
),
|
266
|
+
iteration: current_model.next_iteration,
|
267
|
+
root_model: current_model.root_model)
|
268
|
+
|
269
|
+
print "Add source for #{current_model.source_model}(#{relation.name})", current_model.iteration
|
270
|
+
|
271
|
+
active_record_values = current_model.to_active_record_relation
|
272
|
+
relation_field_values = attribute_values(active_record_values, self_field_name)
|
273
|
+
|
274
|
+
items = [Dumpers::RelationItem.new(key: relation_field_name, values_for_key: relation_field_values),
|
275
|
+
Dumpers::RelationItem.new(key: relation.type, values_for_key: [current_model.source_model],
|
276
|
+
additional: true)]
|
277
|
+
|
278
|
+
new_model.relation_items = Dumpers::RelationItems.new_with_items(items: items)
|
279
|
+
new_model.root_model = current_model
|
280
|
+
|
281
|
+
# Связь с другой таблицей уже была
|
282
|
+
return nil if relation_already_exist?(new_model, dump_state)
|
283
|
+
|
284
|
+
new_model
|
285
|
+
end
|
286
|
+
|
287
|
+
def self_field_name(current_model, relation)
|
288
|
+
relation_foreign_key = relation.foreign_key
|
289
|
+
relation_primary_key = relation.options[:primary_key]
|
290
|
+
|
291
|
+
case relation.macro
|
292
|
+
when :belongs_to
|
293
|
+
# Связь в этой таблице - получить нужные id = вытянуть значения
|
294
|
+
relation_foreign_key
|
295
|
+
when :has_many, :has_one
|
296
|
+
# Связь с другой таблице - получить где relation_foreign_key = текущие значения
|
297
|
+
(relation_primary_key || current_model.primary_key).to_s
|
298
|
+
when :has_and_belongs_to_many
|
299
|
+
'id'
|
300
|
+
else
|
301
|
+
print "Unknown macro in relation #{relation.macro}", current_model.iteration
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def relation_field_name(relation, iteration)
|
306
|
+
relation_foreign_key = relation.foreign_key
|
307
|
+
relation_primary_key = relation.options[:primary_key]
|
308
|
+
|
309
|
+
case relation.macro
|
310
|
+
when :belongs_to
|
311
|
+
# Связь в этой таблице - получить нужные id = вытянуть значения
|
312
|
+
(relation_primary_key || relation.klass.primary_key).to_s
|
313
|
+
when :has_many, :has_one
|
314
|
+
# Связь с другой таблице - получить где relation_foreign_key = текущие значения
|
315
|
+
relation_foreign_key
|
316
|
+
when :has_and_belongs_to_many
|
317
|
+
'id'
|
318
|
+
else
|
319
|
+
print "Unknown macro in relation #{relation.macro}", iteration
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def validate_model_by_name(model_name, iteration)
|
324
|
+
model_name.constantize.primary_key.to_s
|
325
|
+
true
|
326
|
+
rescue NameError, LoadError, NoMethodError => e # rubocop:disable Lint/ShadowedException
|
327
|
+
print "CRITICAL WARNING!!! #{e}", iteration
|
328
|
+
false
|
329
|
+
end
|
330
|
+
|
331
|
+
def log_model_warnings(current_model)
|
332
|
+
current_model.warnings.each do |warning|
|
333
|
+
print "CRITICAL WARNING!!! #{warning}", current_model.iteration
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def valid_relation?(relation, current_model)
|
338
|
+
# у полиморфных связей relation.klass не иницализирован (OperationLog::Source не существует)
|
339
|
+
return true if relation.options[:polymorphic]
|
340
|
+
|
341
|
+
begin
|
342
|
+
relation.klass.connection
|
343
|
+
relation.klass.primary_key
|
344
|
+
true
|
345
|
+
rescue NameError, LoadError => e
|
346
|
+
print "CRITICAL WARNING!!! #{e}", current_model.iteration
|
347
|
+
false
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def relation_already_exist?(model, dump_state)
|
352
|
+
buffer = "Relation #{model.to_short_str} already exists?"
|
353
|
+
if dump_state.include_relation?(model)
|
354
|
+
print "#{buffer} Yes", model.iteration if full_debug_mode?
|
355
|
+
return true
|
356
|
+
end
|
357
|
+
|
358
|
+
print "#{buffer} No", model.iteration if full_debug_mode?
|
359
|
+
false
|
360
|
+
end
|
361
|
+
|
362
|
+
def excluded?(current_model, relation)
|
363
|
+
relation_name = "#{current_model.source_model}.#{relation.name}"
|
364
|
+
if FuryDumper::Config.exclude_relation?(relation_name)
|
365
|
+
print "Exclude relation: #{relation_name}", current_model.iteration
|
366
|
+
return true
|
367
|
+
end
|
368
|
+
|
369
|
+
false
|
370
|
+
end
|
371
|
+
|
372
|
+
def through?(relation, iteration)
|
373
|
+
if relation.options[:through]
|
374
|
+
print "Ignore through relation #{relation.name}", iteration
|
375
|
+
return true
|
376
|
+
end
|
377
|
+
|
378
|
+
false
|
379
|
+
end
|
380
|
+
|
381
|
+
def empty_active_record?(current_model, buffer)
|
382
|
+
unless current_model.to_active_record_relation.exists?
|
383
|
+
print "#{buffer} (empty active record)", current_model.iteration
|
384
|
+
return true
|
385
|
+
end
|
386
|
+
|
387
|
+
false
|
388
|
+
end
|
389
|
+
|
390
|
+
def other_db?(relation, iteration)
|
391
|
+
# Check this db
|
392
|
+
if relation.klass.connection.current_database != target_database
|
393
|
+
print "Ignore #{relation.klass} from other db #{relation.klass.connection.current_database}", iteration
|
394
|
+
return true
|
395
|
+
end
|
396
|
+
|
397
|
+
false
|
398
|
+
end
|
399
|
+
|
400
|
+
def narrowing_relation?(relation, current_model)
|
401
|
+
relation_class = relation.klass.to_s
|
402
|
+
|
403
|
+
if current_model.all_non_scoped_models.include?(relation_class)
|
404
|
+
if full_debug_mode?
|
405
|
+
print "Narrowing relation #{current_model.source_model}(#{relation.name})",
|
406
|
+
current_model.iteration
|
407
|
+
end
|
408
|
+
return true
|
409
|
+
end
|
410
|
+
|
411
|
+
false
|
412
|
+
end
|
413
|
+
|
414
|
+
def print_new_model(current_model, relation)
|
415
|
+
r_class = relation.klass.to_s
|
416
|
+
s_field_name = self_field_name(current_model, relation)
|
417
|
+
r_field_name = relation_field_name(relation, current_model.iteration)
|
418
|
+
|
419
|
+
print "#{current_model.source_model}[#{relation.macro}] -> #{r_class} (#{s_field_name} -> #{r_field_name})",
|
420
|
+
current_model.iteration
|
421
|
+
end
|
422
|
+
|
423
|
+
def debug_mode?
|
424
|
+
@debug_mode != :none
|
425
|
+
end
|
426
|
+
|
427
|
+
def full_debug_mode?
|
428
|
+
@debug_mode == :full
|
429
|
+
end
|
430
|
+
|
431
|
+
def print(message, indent)
|
432
|
+
p "[#{Time.now.httpdate}]#{format('%03d', indent)} #{'-' * indent}> #{message}" if debug_mode?
|
433
|
+
end
|
434
|
+
|
435
|
+
def target_database
|
436
|
+
ActiveRecord::Base.connection_config[:database]
|
437
|
+
end
|
438
|
+
|
439
|
+
def attribute_values(active_record_values, field_name)
|
440
|
+
active_record_values.map { |rel| rel.read_attribute(field_name) }.uniq
|
441
|
+
end
|
442
|
+
|
443
|
+
def fetch_scope_items(current_model, relation)
|
444
|
+
return [] unless relation.scope
|
445
|
+
|
446
|
+
# Exclude some like this has_one :citizenship, ->(d) { where(lead_id: d.lead_id) }
|
447
|
+
return [] if relation.scope.lambda?
|
448
|
+
|
449
|
+
return [] if narrowing_relation?(relation, current_model)
|
450
|
+
|
451
|
+
if full_debug_mode?
|
452
|
+
print "Scoped relation will dump #{current_model.source_model}(#{relation.name})",
|
453
|
+
current_model.iteration
|
454
|
+
end
|
455
|
+
|
456
|
+
scope_queue = relation.klass.instance_exec(&relation.scope)
|
457
|
+
connection = relation.klass.connection
|
458
|
+
visitor = connection.visitor
|
459
|
+
|
460
|
+
binds = bind_values(scope_queue, connection)
|
461
|
+
|
462
|
+
where_values(scope_queue).map do |arel|
|
463
|
+
if arel.is_a?(String)
|
464
|
+
RelationItem.new(key: arel, values_for_key: nil, complex: true)
|
465
|
+
elsif arel.is_a?(Arel::Nodes::Node)
|
466
|
+
arel_node_parse(arel, connection, visitor, binds)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def bind_values(scope_queue, connection)
|
472
|
+
if ActiveRecord.version.version.to_f < 5.0
|
473
|
+
binds = scope_queue.bind_values.dup
|
474
|
+
binds.map! { |bv| connection.quote(*bv.reverse) }
|
475
|
+
elsif ActiveRecord.version.version.to_d == 5.0.to_d
|
476
|
+
binds = scope_queue.bound_attributes.map(&:value).dup
|
477
|
+
binds.map! { |bv| connection.quote(*bv) }
|
478
|
+
else
|
479
|
+
[]
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def arel_node_parse(arel, connection, visitor, binds = [])
|
484
|
+
result = if ActiveRecord.version.version.to_f < 5.0
|
485
|
+
collect = visitor.accept(arel, Arel::Collectors::Bind.new)
|
486
|
+
result = collect.substitute_binds(binds).join
|
487
|
+
binds.delete_at(0)
|
488
|
+
result
|
489
|
+
elsif ActiveRecord.version.version.to_d == 5.0.to_d
|
490
|
+
collector = ActiveRecord::ConnectionAdapters::AbstractAdapter::BindCollector.new
|
491
|
+
collect = visitor.accept(arel, collector)
|
492
|
+
result = collect.substitute_binds(binds).join
|
493
|
+
binds.delete_at(0)
|
494
|
+
result
|
495
|
+
else
|
496
|
+
collector = Arel::Collectors::SubstituteBinds.new(
|
497
|
+
connection,
|
498
|
+
Arel::Collectors::SQLString.new
|
499
|
+
)
|
500
|
+
visitor.accept(arel, collector).value
|
501
|
+
end
|
502
|
+
|
503
|
+
Dumpers::RelationItem.new(key: result, values_for_key: nil, complex: true)
|
504
|
+
end
|
505
|
+
|
506
|
+
def where_values(scope_queue)
|
507
|
+
if ActiveRecord.version.version.to_f < 5.0
|
508
|
+
scope_queue.where_values
|
509
|
+
else
|
510
|
+
scope_queue.where_clause.send(:predicates)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def default_psql_keys
|
515
|
+
"-d #{@database} -h #{@host} -p #{@port} -U #{@user}"
|
516
|
+
end
|
517
|
+
|
518
|
+
def cur_connection
|
519
|
+
@cur_connection = ActiveRecord::Base.establish_connection(save_current_config)
|
520
|
+
@cur_connection.connection
|
521
|
+
end
|
522
|
+
|
523
|
+
def dump_by_sql(select_sql, table_name, table_primary_key, current_connection)
|
524
|
+
system "export PGPASSWORD=#{@password} && psql #{default_psql_keys} -c \"\\COPY (#{select_sql}) TO " \
|
525
|
+
"'/tmp/tmp_copy.copy' WITH (FORMAT CSV, FORCE_QUOTE *);\" >> '/dev/null'"
|
526
|
+
|
527
|
+
tmp_table_name = "tmp_#{table_name}"
|
528
|
+
# copy to tmp table
|
529
|
+
current_connection.execute "CREATE TEMP TABLE #{tmp_table_name} (LIKE #{table_name} EXCLUDING ALL);"
|
530
|
+
current_connection.execute "COPY #{tmp_table_name} FROM '/tmp/tmp_copy.copy' WITH (FORMAT CSV);"
|
531
|
+
|
532
|
+
# delete existing records
|
533
|
+
current_connection.execute "ALTER TABLE #{table_name} DISABLE TRIGGER ALL;"
|
534
|
+
current_connection.execute "DELETE FROM #{table_name} WHERE #{table_name}.#{table_primary_key} IN " \
|
535
|
+
"(SELECT #{table_primary_key} FROM #{tmp_table_name});"
|
536
|
+
|
537
|
+
# copy to target table
|
538
|
+
current_connection.execute "COPY #{table_name} FROM '/tmp/tmp_copy.copy' WITH (FORMAT CSV);"
|
539
|
+
current_connection.execute "ALTER TABLE #{table_name} ENABLE TRIGGER ALL;"
|
540
|
+
|
541
|
+
current_connection.execute "DROP TABLE #{tmp_table_name};"
|
542
|
+
end
|
543
|
+
|
544
|
+
def dump_model(model)
|
545
|
+
current_connection = cur_connection
|
546
|
+
current_connection.transaction do
|
547
|
+
select_sql = model.to_active_record_relation.to_sql
|
548
|
+
|
549
|
+
dump_by_sql(select_sql, model.table_name, model.primary_key, current_connection)
|
550
|
+
rescue ActiveRecord::ActiveRecordError => e
|
551
|
+
@undump_models << { model: model, error: e }
|
552
|
+
print "CRITICAL WARNING!!! #{e}", model.iteration
|
553
|
+
end
|
554
|
+
true
|
555
|
+
end
|
556
|
+
|
557
|
+
# @return [Array of String] association foreign values
|
558
|
+
def dump_proxy_table(model, relation)
|
559
|
+
current_connection = cur_connection
|
560
|
+
current_connection.transaction do
|
561
|
+
select_sql = model.to_active_record_relation.select(model.primary_key).to_sql
|
562
|
+
proxy_table = relation.options[:join_table].to_s
|
563
|
+
proxy_foreign_key = relation.options[:foreign_key] || relation.foreign_key
|
564
|
+
|
565
|
+
proxy_select = "SELECT * FROM #{proxy_table} WHERE #{proxy_foreign_key} IN (#{select_sql})"
|
566
|
+
dump_by_sql(proxy_select, proxy_table, 'id', current_connection)
|
567
|
+
|
568
|
+
# Get association foreign values
|
569
|
+
association_foreign_key = relation.options[:association_foreign_key].to_s
|
570
|
+
cur_connection.exec_query(proxy_select).to_a.pluck(association_foreign_key).uniq
|
571
|
+
rescue ActiveRecord::ActiveRecordError => e
|
572
|
+
@undump_models << { model: model, error: e }
|
573
|
+
print "CRITICAL WARNING!!! #{e}", model.iteration
|
574
|
+
[]
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def print_undump_models
|
579
|
+
p '⚠️ ⚠️ ⚠️ These models were not dump due to pg errors ️⚠️ ⚠️ ⚠️' if @undump_models.present?
|
580
|
+
@undump_models.each do |model|
|
581
|
+
p "🔥 #{model[:model].to_full_str}"
|
582
|
+
p "🔥 #{model[:error]}"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def humanize_hash(hash)
|
587
|
+
hash.map do |k, v|
|
588
|
+
"#{k}: #{v}"
|
589
|
+
end.join(', ')
|
590
|
+
end
|
591
|
+
|
592
|
+
def difference(this_val, other_val)
|
593
|
+
(this_val - other_val) | (other_val - this_val)
|
594
|
+
end
|
595
|
+
|
596
|
+
def save_current_config
|
597
|
+
@save_current_config ||= ActiveRecord::Base.connection_config
|
598
|
+
end
|
599
|
+
|
600
|
+
def remote_connection
|
601
|
+
save_current_config
|
602
|
+
@remote_connection = ActiveRecord::Base.establish_connection(adapter: 'postgresql',
|
603
|
+
database: @database,
|
604
|
+
host: @host,
|
605
|
+
port: @port,
|
606
|
+
username: @user,
|
607
|
+
password: @password)
|
608
|
+
@remote_connection.connection
|
609
|
+
end
|
610
|
+
|
611
|
+
def send_out_ms_dump(model)
|
612
|
+
return unless FuryDumper::Config.ms_relations?(model.table_name)
|
613
|
+
|
614
|
+
FuryDumper::Config.relative_services.each do |ms_name, ms_config|
|
615
|
+
ms_config['tables'][model.table_name]&.each do |_other_model, other_model_config|
|
616
|
+
self_field_name = other_model_config['self_field_name']
|
617
|
+
as_field_name = "#buff_#{self_field_name.gsub(/\W+/, '')}"
|
618
|
+
|
619
|
+
selected_values = attribute_values(
|
620
|
+
model.to_active_record_relation.select("#{self_field_name} AS #{as_field_name}"), as_field_name
|
621
|
+
)
|
622
|
+
|
623
|
+
next if selected_values.to_a.compact.blank?
|
624
|
+
|
625
|
+
Api.new(ms_name).send_request(other_model_config['ms_model_name'],
|
626
|
+
other_model_config['ms_field_name'],
|
627
|
+
selected_values.to_a)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FuryDumper
|
4
|
+
module Dumpers
|
5
|
+
class DumpState
|
6
|
+
attr_accessor :batch, :root_source_model, :loaded_relations, :iteration
|
7
|
+
|
8
|
+
def initialize(root_source_model:, loaded_relations: [])
|
9
|
+
@root_source_model = root_source_model
|
10
|
+
@loaded_relations = loaded_relations
|
11
|
+
@start_time = Time.current
|
12
|
+
end
|
13
|
+
|
14
|
+
def stop
|
15
|
+
@end_time = Time.current
|
16
|
+
end
|
17
|
+
|
18
|
+
def include_relation?(relation_name)
|
19
|
+
loaded_relations.include?(relation_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_loaded_relation(new_relation)
|
23
|
+
raise ArgumentError unless new_relation.is_a?(Model)
|
24
|
+
|
25
|
+
loaded_relations << new_relation unless include_relation?(new_relation)
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_statistic
|
29
|
+
p "📈 Statistic for #{@root_source_model} dump"
|
30
|
+
p "Execution time: #{duration}"
|
31
|
+
p "Loaded #{@loaded_relations.count} relations"
|
32
|
+
p "Loaded #{@loaded_relations.map(&:source_model).uniq.count} uniq models"
|
33
|
+
|
34
|
+
p 'Most repeatable models relations:'
|
35
|
+
res = @loaded_relations.group_by(&:source_model).sort_by do |_k, v|
|
36
|
+
v.count
|
37
|
+
end
|
38
|
+
res.last(10).each { |k, v| p " #{k}: #{v.count} times" }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def duration
|
44
|
+
secs = (@end_time - @start_time).to_int
|
45
|
+
mins = secs / 60
|
46
|
+
hours = mins / 60
|
47
|
+
days = hours / 24
|
48
|
+
|
49
|
+
if days.positive?
|
50
|
+
"#{days} days and #{hours % 24} hours"
|
51
|
+
elsif hours.positive?
|
52
|
+
"#{hours} hours and #{mins % 60} minutes"
|
53
|
+
elsif mins.positive?
|
54
|
+
"#{mins} minutes and #{secs % 60} seconds"
|
55
|
+
elsif secs >= 0
|
56
|
+
"#{secs} seconds"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|