db-purger 0.1.0

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