db-purger 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/lib/db-purger/config.rb +17 -0
- data/lib/db-purger/metric_subscriber.rb +48 -0
- data/lib/db-purger/metrics.rb +68 -0
- data/lib/db-purger/plan.rb +56 -0
- data/lib/db-purger/plan_builder.rb +72 -0
- data/lib/db-purger/plan_validator.rb +68 -0
- data/lib/db-purger/purge_table.rb +75 -0
- data/lib/db-purger/purge_table_helper.rb +96 -0
- data/lib/db-purger/purge_table_scanner.rb +84 -0
- data/lib/db-purger/table.rb +44 -0
- data/lib/db-purger.rb +19 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b7144cdb2cda2433339283a8ca98c35ace1015c30fce5642a4c064234f07e72
|
4
|
+
data.tar.gz: 8b199fe73457afb3dabf37a2e6746cc839d42514a0ff81b99e0bd0ab62a1d2b5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3c56fb34ca9276ad3e3e258825f703631f80ab6a1711a8e703feb4fb2b2d5676672c221f2f86ca8c310712928eb23705d86cb7fbdca8e01e56e2e0b653e859a3
|
7
|
+
data.tar.gz: 7283b99020d4164eadba078b9560921339c4e87d467bad5b0870991f6ed00f42be464db212949e6f9e083cc3d8d79e8bedada03bb3450bf36c2d57cfd5776d64
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::Config keeps track of global config options for the purge process
|
5
|
+
class Config
|
6
|
+
attr_writer :explain,
|
7
|
+
:explain_file
|
8
|
+
|
9
|
+
def explain?
|
10
|
+
@explain == true
|
11
|
+
end
|
12
|
+
|
13
|
+
def explain_file
|
14
|
+
(@explain_file || $stdout)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::MetricSubscriber tracks the progress of the Plan#purge process
|
5
|
+
class MetricSubscriber < ActiveSupport::Subscriber
|
6
|
+
def self.metrics
|
7
|
+
@metrics ||= Metrics.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.reset!
|
11
|
+
metrics.reset!
|
12
|
+
end
|
13
|
+
|
14
|
+
def purge(event)
|
15
|
+
self.class.metrics.update_purge_stats(
|
16
|
+
event.payload[:table_name],
|
17
|
+
event.duration,
|
18
|
+
event.payload[:deleted]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete_records(event)
|
23
|
+
self.class.metrics.update_delete_records_stats(
|
24
|
+
event.payload[:table_name],
|
25
|
+
event.duration,
|
26
|
+
event.payload[:records_deleted],
|
27
|
+
event.payload[:num_records]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_batch(event)
|
32
|
+
self.class.metrics.update_lookup_stats(
|
33
|
+
event.payload[:table_name],
|
34
|
+
event.duration,
|
35
|
+
event.payload[:num_records]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def search_filter(event)
|
40
|
+
self.class.metrics.update_search_filter_stats(
|
41
|
+
event.payload[:table_name],
|
42
|
+
event.duration,
|
43
|
+
event.payload[:num_records],
|
44
|
+
event.payload[:num_records_selected]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::Metrics keeps track of each part of the purge process
|
5
|
+
class Metrics
|
6
|
+
attr_reader :started_at,
|
7
|
+
:finished_at,
|
8
|
+
:purge_stats,
|
9
|
+
:delete_stats,
|
10
|
+
:lookup_stats,
|
11
|
+
:filter_stats
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
reset!
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset!
|
18
|
+
@started_at = Time.now
|
19
|
+
@purge_stats = {}
|
20
|
+
@delete_stats = {}
|
21
|
+
@lookup_stats = {}
|
22
|
+
@filter_stats = {}
|
23
|
+
@finished_at = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def finished!
|
27
|
+
@finished_at = Time.now
|
28
|
+
end
|
29
|
+
|
30
|
+
def elapsed_time_in_seconds
|
31
|
+
(@finished_at || Time.now) - @started_at
|
32
|
+
end
|
33
|
+
|
34
|
+
def update_purge_stats(table_name, duration, num_records)
|
35
|
+
stats = (@purge_stats[table_name] ||= Hash.new(0))
|
36
|
+
stats[:duration] += duration
|
37
|
+
stats[:num_purges] += 1
|
38
|
+
stats[:num_records] += num_records
|
39
|
+
stats
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_delete_records_stats(table_name, duration, num_deleted, num_expected_to_delete = nil)
|
43
|
+
stats = (@delete_stats[table_name] ||= Hash.new(0))
|
44
|
+
stats[:duration] += duration
|
45
|
+
stats[:num_delete_queries] += 1
|
46
|
+
stats[:num_deleted] += num_deleted
|
47
|
+
stats[:num_expected_to_delete] += num_expected_to_delete if num_expected_to_delete
|
48
|
+
stats
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_lookup_stats(table_name, duration, records_found)
|
52
|
+
stats = (@lookup_stats[table_name] ||= Hash.new(0))
|
53
|
+
stats[:duration] += duration
|
54
|
+
stats[:num_lookups] += 1
|
55
|
+
stats[:records_found] += records_found
|
56
|
+
stats
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_search_filter_stats(table_name, duration, records_found, records_selected)
|
60
|
+
stats = (@filter_stats[table_name] ||= Hash.new(0))
|
61
|
+
stats[:duration] += duration
|
62
|
+
stats[:num_lookups] += 1
|
63
|
+
stats[:records_found] += records_found
|
64
|
+
stats[:records_selected] += records_selected
|
65
|
+
stats
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::Plan is used to describe the relationship between tables
|
5
|
+
class Plan
|
6
|
+
attr_accessor :base_table
|
7
|
+
|
8
|
+
attr_reader :parent_tables,
|
9
|
+
:child_tables,
|
10
|
+
:ignore_tables,
|
11
|
+
:search_tables
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@parent_tables = []
|
15
|
+
@child_tables = []
|
16
|
+
@ignore_tables = []
|
17
|
+
@search_tables = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def purge!(database, purge_value)
|
21
|
+
MetricSubscriber.reset!
|
22
|
+
PurgeTable.new(database, @base_table, @base_table.field, purge_value).purge!
|
23
|
+
end
|
24
|
+
|
25
|
+
def tables
|
26
|
+
all_tables = @base_table ? [@base_table] + @base_table.tables : []
|
27
|
+
all_tables += @parent_tables + @parent_tables.map(&:tables) +
|
28
|
+
@child_tables + @child_tables.map(&:tables) +
|
29
|
+
@search_tables + @search_tables.map(&:tables)
|
30
|
+
all_tables.flatten!
|
31
|
+
all_tables.compact!
|
32
|
+
all_tables
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_names
|
36
|
+
tables.map(&:name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def empty?
|
40
|
+
@base_table.nil? &&
|
41
|
+
@parent_tables.empty? &&
|
42
|
+
@child_tables.empty? &&
|
43
|
+
@search_tables.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def ignore_table?(table_name)
|
47
|
+
@ignore_tables.any? do |ignore_table_name|
|
48
|
+
if ignore_table_name.is_a?(Regexp)
|
49
|
+
ignore_table_name.match(table_name)
|
50
|
+
else
|
51
|
+
ignore_table_name.to_s == table_name
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::PlanBuilder is used to build the relationships between tables in a convenient way
|
5
|
+
class PlanBuilder
|
6
|
+
def initialize(plan)
|
7
|
+
@plan = plan
|
8
|
+
end
|
9
|
+
|
10
|
+
def base_table(table_name, field, options = {}, &block)
|
11
|
+
@plan.base_table = create_table(table_name, field, options, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def parent_table(table_name, field, options = {}, &block)
|
15
|
+
table = create_table(table_name, field, options, &block)
|
16
|
+
if @plan.base_table
|
17
|
+
@plan.base_table.nested_plan.parent_tables << table
|
18
|
+
else
|
19
|
+
@plan.parent_tables << table
|
20
|
+
end
|
21
|
+
table
|
22
|
+
end
|
23
|
+
|
24
|
+
def child_table(table_name, field, options = {}, &block)
|
25
|
+
table = create_table(table_name, field, options, &block)
|
26
|
+
if @plan.base_table
|
27
|
+
@plan.base_table.nested_plan.child_tables << table
|
28
|
+
else
|
29
|
+
@plan.child_tables << table
|
30
|
+
end
|
31
|
+
table
|
32
|
+
end
|
33
|
+
|
34
|
+
def ignore_table(table_name)
|
35
|
+
@plan.ignore_tables << table_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def purge_table_search(table_name, field, options = {}, &block)
|
39
|
+
table = create_table(table_name, field, options)
|
40
|
+
table.search_proc = block || raise('no block given for search_proc')
|
41
|
+
if @plan.base_table
|
42
|
+
@plan.base_table.nested_plan.search_tables << table
|
43
|
+
else
|
44
|
+
@plan.search_tables << table
|
45
|
+
end
|
46
|
+
table
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.build(&block)
|
50
|
+
plan = Plan.new
|
51
|
+
helper = new(plan)
|
52
|
+
helper.instance_eval(&block)
|
53
|
+
plan
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_nested_plan(table, &block)
|
57
|
+
helper = self.class.new(table.nested_plan)
|
58
|
+
helper.instance_eval(&block)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def create_table(table_name, field, options, &block)
|
64
|
+
table = Table.new(table_name, field)
|
65
|
+
table.foreign_key = options[:foreign_key]
|
66
|
+
table.batch_size = options[:batch_size]
|
67
|
+
table.conditions = options[:conditions]
|
68
|
+
build_nested_plan(table, &block) if block
|
69
|
+
table
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::PlanValidator is used to ensure that all tables in the database are part of the purge plan.
|
5
|
+
# It verifies that all table definitions are correct as well, checking that child/parent fields are part
|
6
|
+
# of the table.
|
7
|
+
class PlanValidator
|
8
|
+
include ActiveModel::Validations
|
9
|
+
|
10
|
+
validate :validate_no_missing_tables
|
11
|
+
validate :validate_no_unknown_tables
|
12
|
+
validate :validate_tables
|
13
|
+
|
14
|
+
def initialize(database, plan)
|
15
|
+
@database = database
|
16
|
+
@plan = plan
|
17
|
+
end
|
18
|
+
|
19
|
+
# tables that are part of the database but not part of the plan
|
20
|
+
def missing_tables
|
21
|
+
database_table_names - @plan.table_names.map(&:to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
# tables that are part of the plan put not part of the database
|
25
|
+
def unknown_tables
|
26
|
+
@plan.table_names.map(&:to_s) - database_table_names
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_no_missing_tables
|
32
|
+
errors.add(:missing_tables, missing_tables.sort.join(',')) unless missing_tables.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_no_unknown_tables
|
36
|
+
errors.add(:unknown_tables, unknown_tables.sort.join(',')) unless unknown_tables.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_tables
|
40
|
+
@plan.tables.each { |table| validate_table_definition(table) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_table_definition(table)
|
44
|
+
unless (model = find_model_for_table(table))
|
45
|
+
errors.add(:table, "#{table.name} has no model")
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
table.fields.each do |field|
|
50
|
+
unless model.column_names.include?(field.to_s)
|
51
|
+
errors.add(:table, "#{table.name}.#{field} is missing in the database")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_model_for_table(table)
|
57
|
+
@database.models.detect { |model| model.table_name == table.name.to_s }
|
58
|
+
end
|
59
|
+
|
60
|
+
def filter_table_names(table_names)
|
61
|
+
table_names.reject { |table_name| @plan.ignore_table?(table_name) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def database_table_names
|
65
|
+
@database_table_names ||= filter_table_names(@database.models.map(&:table_name))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::PurgeTable is used to delete table from tables in batches if possible
|
5
|
+
class PurgeTable
|
6
|
+
include PurgeTableHelper
|
7
|
+
|
8
|
+
def initialize(database, table, purge_field, purge_value)
|
9
|
+
@database = database
|
10
|
+
@table = table
|
11
|
+
@purge_field = purge_field
|
12
|
+
@purge_value = purge_value
|
13
|
+
@num_deleted = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def model
|
17
|
+
@model ||= @database.models.detect { |m| m.table_name == @table.name.to_s }
|
18
|
+
end
|
19
|
+
|
20
|
+
def purge!
|
21
|
+
ActiveSupport::Notifications.instrument('purge.db_purger',
|
22
|
+
table_name: @table.name,
|
23
|
+
purge_field: @purge_field) do |payload|
|
24
|
+
if model.primary_key
|
25
|
+
purge_in_batches!
|
26
|
+
else
|
27
|
+
purge_all!
|
28
|
+
end
|
29
|
+
purge_search_tables
|
30
|
+
payload[:deleted] = @num_deleted
|
31
|
+
end
|
32
|
+
@num_deleted
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def purge_all!
|
38
|
+
scope = model.where(@purge_field => @purge_value)
|
39
|
+
scope = scope.where(@table.conditions) if @table.conditions
|
40
|
+
delete_records_with_instrumentation(scope)
|
41
|
+
end
|
42
|
+
|
43
|
+
def purge_in_batches!
|
44
|
+
start_id = nil
|
45
|
+
until (batch = next_batch(start_id)).empty?
|
46
|
+
start_id = batch.last.send(model.primary_key)
|
47
|
+
purge_nested_tables(batch) if @table.nested_tables?
|
48
|
+
delete_records(batch)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def next_batch(start_id)
|
53
|
+
ActiveSupport::Notifications.instrument('next_batch.db_purger',
|
54
|
+
table_name: @table.name,
|
55
|
+
start_id: start_id) do |payload|
|
56
|
+
records = batch_scope(start_id).to_a
|
57
|
+
payload[:num_records] = records.size
|
58
|
+
records
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# rubocop:disable Metrics/AbcSize
|
63
|
+
def batch_scope(start_id)
|
64
|
+
scope = model
|
65
|
+
.select([model.primary_key] + @table.foreign_keys)
|
66
|
+
.where(@purge_field => @purge_value)
|
67
|
+
.order(model.primary_key)
|
68
|
+
.limit(@table.batch_size)
|
69
|
+
scope = scope.where(@table.conditions) if @table.conditions
|
70
|
+
scope = scope.where("#{model.primary_key} > #{model.connection.quote(start_id)}") if start_id
|
71
|
+
scope
|
72
|
+
end
|
73
|
+
# rubocop:enable Metrics/AbcSize
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::PurgeTableHelper keeps track of common code between purgers
|
5
|
+
module PurgeTableHelper
|
6
|
+
def model
|
7
|
+
@model ||= @database.models.detect { |m| m.table_name == @table.name.to_s }
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def purge_nested_tables(batch)
|
13
|
+
purge_child_tables(batch) unless @table.nested_plan.child_tables.empty?
|
14
|
+
purge_parent_tables
|
15
|
+
end
|
16
|
+
|
17
|
+
def purge_child_tables(batch)
|
18
|
+
ids = batch_values(batch, model.primary_key)
|
19
|
+
|
20
|
+
@table.nested_plan.child_tables.each do |table|
|
21
|
+
next if table.foreign_key
|
22
|
+
|
23
|
+
PurgeTable.new(@database, table, table.field, ids).purge!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def purge_parent_tables
|
28
|
+
@table.nested_plan.parent_tables.each do |table|
|
29
|
+
PurgeTable.new(@database, table, table.field, @purge_value).purge!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def purge_foreign_tables(batch)
|
34
|
+
@table.nested_plan.child_tables.each do |table|
|
35
|
+
next unless table.foreign_key
|
36
|
+
next if (purge_values = batch_values(batch, table.foreign_key)).empty?
|
37
|
+
|
38
|
+
PurgeTable.new(@database, table, table.field, purge_values).purge!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def batch_values(batch, field)
|
43
|
+
batch.map { |record| record.send(field) }.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def foreign_tables?
|
47
|
+
@table.nested_plan.child_tables.any?(&:foreign_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_records_with_instrumentation(scope, num_records = nil)
|
51
|
+
@num_deleted ||= 0
|
52
|
+
ActiveSupport::Notifications.instrument('delete_records.db_purger',
|
53
|
+
table_name: @table.name,
|
54
|
+
num_records: num_records) do |payload|
|
55
|
+
records_deleted =
|
56
|
+
if ::DBPurger.config.explain?
|
57
|
+
delete_sql = scope.to_sql.sub(/SELECT .*?FROM/, 'DELETE FROM')
|
58
|
+
::DBPurger.config.explain_file.puts(delete_sql)
|
59
|
+
scope.count
|
60
|
+
else
|
61
|
+
scope.delete_all
|
62
|
+
end
|
63
|
+
@num_deleted += records_deleted
|
64
|
+
payload[:records_deleted] = records_deleted
|
65
|
+
payload[:deleted] = @num_deleted
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete_records(batch)
|
70
|
+
if foreign_tables?
|
71
|
+
delete_records_and_foreign_tables(batch)
|
72
|
+
else
|
73
|
+
delete_records_by_primary_key(batch)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete_records_and_foreign_tables(batch)
|
78
|
+
model.transaction do
|
79
|
+
delete_records_by_primary_key(batch)
|
80
|
+
purge_foreign_tables(batch)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete_records_by_primary_key(batch)
|
85
|
+
ids = batch_values(batch, model.primary_key)
|
86
|
+
scope = model.where(model.primary_key => ids)
|
87
|
+
delete_records_with_instrumentation(scope, ids.size)
|
88
|
+
end
|
89
|
+
|
90
|
+
def purge_search_tables
|
91
|
+
@table.nested_plan.search_tables.each do |table|
|
92
|
+
PurgeTableScanner.new(@database, table).purge!
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::PurgeTableScanner scans the entire table in batches use the search_proc to determine what ids to delete
|
5
|
+
class PurgeTableScanner
|
6
|
+
include PurgeTableHelper
|
7
|
+
|
8
|
+
def initialize(database, table)
|
9
|
+
@database = database
|
10
|
+
@table = table
|
11
|
+
@num_deleted = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def model
|
15
|
+
@model ||= @database.models.detect { |m| m.table_name == @table.name.to_s }
|
16
|
+
end
|
17
|
+
|
18
|
+
def purge!
|
19
|
+
ActiveSupport::Notifications.instrument('purge.db_purger',
|
20
|
+
table_name: @table.name) do |payload|
|
21
|
+
purge_in_batches!
|
22
|
+
purge_search_tables
|
23
|
+
payload[:deleted] = @num_deleted
|
24
|
+
end
|
25
|
+
@num_deleted
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# rubocop:disable Metrics/AbcSize
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
32
|
+
def purge_in_batches!
|
33
|
+
scope = model
|
34
|
+
scope = scope.where(@table.conditions) if @table.conditions
|
35
|
+
|
36
|
+
instrumentation_name = 'next_batch.db_purger'
|
37
|
+
start_instrumentation(instrumentation_name)
|
38
|
+
|
39
|
+
scope.find_in_batches(batch_size: @table.batch_size) do |batch|
|
40
|
+
finish_instrumentation(
|
41
|
+
instrumentation_name,
|
42
|
+
table_name: @table.name,
|
43
|
+
num_records: batch.size
|
44
|
+
)
|
45
|
+
|
46
|
+
batch = ActiveSupport::Notifications.instrument('search_filter.db_purger',
|
47
|
+
table_name: @table.name,
|
48
|
+
num_records: batch.size) do |payload|
|
49
|
+
records_selected = @table.search_proc.call(batch)
|
50
|
+
payload[:num_records_selected] = records_selected.size
|
51
|
+
records_selected
|
52
|
+
end
|
53
|
+
|
54
|
+
if batch.empty?
|
55
|
+
start_instrumentation(instrumentation_name)
|
56
|
+
next
|
57
|
+
end
|
58
|
+
|
59
|
+
purge_nested_tables(batch) if @table.nested_tables?
|
60
|
+
delete_records(batch)
|
61
|
+
|
62
|
+
start_instrumentation(instrumentation_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
finish_instrumentation(
|
66
|
+
instrumentation_name,
|
67
|
+
table_name: @table.name,
|
68
|
+
num_records: 0,
|
69
|
+
num_selected: 0
|
70
|
+
)
|
71
|
+
end
|
72
|
+
# rubocop:enable Metrics/AbcSize
|
73
|
+
# rubocop:enable Metrics/MethodLength
|
74
|
+
|
75
|
+
def start_instrumentation(name)
|
76
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
77
|
+
@instrumenter.start(name, {})
|
78
|
+
end
|
79
|
+
|
80
|
+
def finish_instrumentation(name, payload)
|
81
|
+
@instrumenter.finish(name, payload)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DBPurger
|
4
|
+
# DBPurger::Table is an entity to keep track of basic table data
|
5
|
+
class Table
|
6
|
+
DEFAULT_BATCH_SIZE = 10_000
|
7
|
+
|
8
|
+
attr_accessor :foreign_key,
|
9
|
+
:batch_size,
|
10
|
+
:conditions,
|
11
|
+
:search_proc
|
12
|
+
|
13
|
+
attr_reader :name,
|
14
|
+
:field
|
15
|
+
|
16
|
+
def initialize(name, field)
|
17
|
+
@name = name
|
18
|
+
@field = field
|
19
|
+
@batch_size = DEFAULT_BATCH_SIZE
|
20
|
+
end
|
21
|
+
|
22
|
+
def nested_plan(&block)
|
23
|
+
@nested_plan ||= Plan.new
|
24
|
+
PlanBuilder.new(@nested_plan).build_nested_plan(self, &block) if block
|
25
|
+
@nested_plan
|
26
|
+
end
|
27
|
+
|
28
|
+
def nested_tables?
|
29
|
+
@nested_plan != nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def tables
|
33
|
+
@nested_plan ? @nested_plan.tables : []
|
34
|
+
end
|
35
|
+
|
36
|
+
def foreign_keys
|
37
|
+
@nested_plan ? @nested_plan.tables.map(&:foreign_key).compact : []
|
38
|
+
end
|
39
|
+
|
40
|
+
def fields
|
41
|
+
[@field] + foreign_keys
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/db-purger.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# DBPurger is a tool to delete data from tables based on a initial purge value
|
4
|
+
module DBPurger
|
5
|
+
autoload :Config, 'db-purger/config'
|
6
|
+
autoload :Metrics, 'db-purger/metrics'
|
7
|
+
autoload :MetricSubscriber, 'db-purger/metric_subscriber'
|
8
|
+
autoload :PurgeTable, 'db-purger/purge_table'
|
9
|
+
autoload :PurgeTableHelper, 'db-purger/purge_table_helper'
|
10
|
+
autoload :PurgeTableScanner, 'db-purger/purge_table_scanner'
|
11
|
+
autoload :Plan, 'db-purger/plan'
|
12
|
+
autoload :PlanBuilder, 'db-purger/plan_builder'
|
13
|
+
autoload :PlanValidator, 'db-purger/plan_validator'
|
14
|
+
autoload :Table, 'db-purger/table'
|
15
|
+
|
16
|
+
def self.config
|
17
|
+
@config ||= Config.new
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: db-purger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Doug Youch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dynamic-active-model
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: inheritance-helper
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Purge database tables by top level id in batches
|
42
|
+
email: dougyouch@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/db-purger.rb
|
48
|
+
- lib/db-purger/config.rb
|
49
|
+
- lib/db-purger/metric_subscriber.rb
|
50
|
+
- lib/db-purger/metrics.rb
|
51
|
+
- lib/db-purger/plan.rb
|
52
|
+
- lib/db-purger/plan_builder.rb
|
53
|
+
- lib/db-purger/plan_validator.rb
|
54
|
+
- lib/db-purger/purge_table.rb
|
55
|
+
- lib/db-purger/purge_table_helper.rb
|
56
|
+
- lib/db-purger/purge_table_scanner.rb
|
57
|
+
- lib/db-purger/table.rb
|
58
|
+
homepage: https://github.com/dougyouch/db-purger
|
59
|
+
licenses: []
|
60
|
+
metadata: {}
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.0.3
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: DB Purger by top level id
|
80
|
+
test_files: []
|