db-purger 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|