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.
@@ -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