fury_dumper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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