prune_ar 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/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +89 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +101 -0
- data/LICENSE +7 -0
- data/README.md +240 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/prune_ar.rb +62 -0
- data/lib/prune_ar/belongs_to_association.rb +51 -0
- data/lib/prune_ar/belongs_to_association_gatherer.rb +130 -0
- data/lib/prune_ar/deleter_by_criteria.rb +63 -0
- data/lib/prune_ar/foreign_key_handler.rb +84 -0
- data/lib/prune_ar/orphaned_selection_builder.rb +53 -0
- data/lib/prune_ar/pruner.rb +158 -0
- data/lib/prune_ar/version.rb +5 -0
- data/prune_ar.gemspec +57 -0
- metadata +250 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'active_record'
|
5
|
+
require 'prune_ar/belongs_to_association_gatherer'
|
6
|
+
require 'prune_ar/orphaned_selection_builder'
|
7
|
+
require 'prune_ar/deleter_by_criteria'
|
8
|
+
require 'prune_ar/foreign_key_handler'
|
9
|
+
|
10
|
+
module PruneAr
|
11
|
+
# Core of this gem. Prunes records based on parameters given.
|
12
|
+
class Pruner
|
13
|
+
attr_reader :associations,
|
14
|
+
:deletion_criteria,
|
15
|
+
:full_delete_models,
|
16
|
+
:pre_queries_to_run,
|
17
|
+
:conjunctive_deletion_criteria,
|
18
|
+
:logger,
|
19
|
+
:foreign_key_handler,
|
20
|
+
:perform_sanity_check
|
21
|
+
|
22
|
+
def initialize(
|
23
|
+
models:,
|
24
|
+
deletion_criteria: {},
|
25
|
+
full_delete_models: [],
|
26
|
+
pre_queries_to_run: [],
|
27
|
+
conjunctive_deletion_criteria: {},
|
28
|
+
perform_sanity_check: true,
|
29
|
+
logger: Logger.new(STDOUT).tap { |l| l.level = Logger::WARN }
|
30
|
+
)
|
31
|
+
@associations = BelongsToAssociationGatherer.new(models, connection: connection).associations
|
32
|
+
@full_delete_models = full_delete_models
|
33
|
+
@pre_queries_to_run = pre_queries_to_run
|
34
|
+
@deletion_criteria = deletion_criteria
|
35
|
+
@conjunctive_deletion_criteria = conjunctive_deletion_criteria
|
36
|
+
@logger = logger
|
37
|
+
@perform_sanity_check = perform_sanity_check
|
38
|
+
@foreign_key_handler = ForeignKeyHandler.new(
|
39
|
+
models: models,
|
40
|
+
connection: connection,
|
41
|
+
logger: logger
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def prune
|
46
|
+
# Can't wrap in transaction if DDL is not supported in transactions. We use ALTER TABLE
|
47
|
+
# => statements (which are DDL)
|
48
|
+
return prune_core unless connection.supports_ddl_transactions?
|
49
|
+
|
50
|
+
# This transaction is helpful when developing/working on this code. If a SQL statement errors,
|
51
|
+
# => it leaves the database untouched.
|
52
|
+
connection.transaction do
|
53
|
+
prune_core
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def prune_core
|
60
|
+
drop_original_foreign_key_constraints
|
61
|
+
|
62
|
+
# Run any pre-queries we were told to run
|
63
|
+
pre_queries
|
64
|
+
|
65
|
+
# Delete using deletion_criteria before we sanitize anything
|
66
|
+
pre_delete
|
67
|
+
|
68
|
+
# Truncate tables we were told to wipe completely
|
69
|
+
full_delete
|
70
|
+
|
71
|
+
# Main deletion (conjunctive_deletion_criteria + orphaned records)
|
72
|
+
main_delete
|
73
|
+
|
74
|
+
# Now that there are no violations, create foreign key constraints on all :belongs_to
|
75
|
+
# => this is a sanity check that we eliminated violations
|
76
|
+
# => this ignores polymorphic relations since FKs cannot be set on polymorphic :belongs_to
|
77
|
+
# => these foreign key constraints are dropped right after since they're only for sanity
|
78
|
+
foreign_key_sanity_check if perform_sanity_check
|
79
|
+
|
80
|
+
recreate_original_foreign_key_constraints
|
81
|
+
end
|
82
|
+
|
83
|
+
def drop_original_foreign_key_constraints
|
84
|
+
logger.info('dropping existing foreign key constraints')
|
85
|
+
foreign_key_handler.drop(foreign_key_handler.original_foreign_keys)
|
86
|
+
end
|
87
|
+
|
88
|
+
def recreate_original_foreign_key_constraints
|
89
|
+
logger.info('recreating original foreign key constraints')
|
90
|
+
foreign_key_handler.create(foreign_key_handler.original_foreign_keys)
|
91
|
+
end
|
92
|
+
|
93
|
+
def pre_queries
|
94
|
+
logger.info('running pre_queries_to_run')
|
95
|
+
pre_queries_to_run.each do |sql|
|
96
|
+
logger.debug("running pre-query #{sql}")
|
97
|
+
connection.exec_query(sql)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pre_delete
|
102
|
+
logger.info('deleting via deletion_criteria')
|
103
|
+
DeleterByCriteria.new(
|
104
|
+
flatten_deletion_criteria(deletion_criteria),
|
105
|
+
connection: connection,
|
106
|
+
logger: logger
|
107
|
+
).delete
|
108
|
+
end
|
109
|
+
|
110
|
+
def full_delete
|
111
|
+
logger.info('truncating full_delete_models')
|
112
|
+
full_delete_models.each do |model|
|
113
|
+
logger.debug("truncating #{model}")
|
114
|
+
connection.exec_query("TRUNCATE #{model.table_name};")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def main_delete
|
119
|
+
logger.info('deleting via conjunctive_deletion_criteria & pruning orphaned records')
|
120
|
+
association_deletion_criteria = flat_associations_deletion_criteria(associations)
|
121
|
+
flat_conjunctive_criteria = flatten_deletion_criteria(conjunctive_deletion_criteria)
|
122
|
+
DeleterByCriteria.new(
|
123
|
+
(association_deletion_criteria + flat_conjunctive_criteria).sort,
|
124
|
+
connection: connection,
|
125
|
+
logger: logger
|
126
|
+
).delete
|
127
|
+
end
|
128
|
+
|
129
|
+
def foreign_key_sanity_check
|
130
|
+
logger.info('sanity checking via foreign key constraints')
|
131
|
+
created_foreign_keys = foreign_key_handler.create_from_belongs_to_associations(
|
132
|
+
associations.reject(&:polymorphic?)
|
133
|
+
)
|
134
|
+
|
135
|
+
foreign_key_handler.drop(created_foreign_keys)
|
136
|
+
end
|
137
|
+
|
138
|
+
def flat_associations_deletion_criteria(associations)
|
139
|
+
associations.map do |assoc|
|
140
|
+
[assoc.source_table, orphaned_selection_builder.orphaned_selection(assoc)]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def flatten_deletion_criteria(criteria)
|
145
|
+
criteria.flat_map do |model, selections|
|
146
|
+
selections.map { |s| [model.table_name, s] }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def connection
|
151
|
+
@connection ||= ActiveRecord::Base.connection
|
152
|
+
end
|
153
|
+
|
154
|
+
def orphaned_selection_builder
|
155
|
+
@orphaned_selection_builder ||= OrphanedSelectionBuilder.new
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/prune_ar.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'prune_ar/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
8
|
+
spec.name = 'prune_ar'
|
9
|
+
spec.version = PruneAr::VERSION
|
10
|
+
spec.authors = ['Anirban Mukhopadhyay']
|
11
|
+
spec.email = ['amukhopadhyay@contently.com']
|
12
|
+
|
13
|
+
spec.summary = 'Prunes database tables using ActiveRecord belongs_to relations.'
|
14
|
+
spec.description = %w[
|
15
|
+
Given an initial set of records to delete prune_ar deletes all other records (accessible via
|
16
|
+
ActiveRecord) that are now orphaned due to a belongs_to relation which is now non-existent.
|
17
|
+
This allows you to safely delete records that you want to delete without creating orphaned
|
18
|
+
records in another table (& without violating foreign key constraints if you use them). This
|
19
|
+
can be used to prune a production database (given deletion criteria for top level parent-less
|
20
|
+
independent entities) for use in a development environment without compromising customer data.
|
21
|
+
].join(' ')
|
22
|
+
spec.homepage = 'https://github.com/contently/prune_ar'
|
23
|
+
|
24
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
25
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
26
|
+
if spec.respond_to?(:metadata)
|
27
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
28
|
+
else
|
29
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
30
|
+
'public gem pushes.'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Specify which files should be added to the gem when it is released.
|
34
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
35
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
36
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
37
|
+
end
|
38
|
+
spec.files.reject! { |f| f.start_with?('.') }
|
39
|
+
spec.bindir = 'exe'
|
40
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
41
|
+
spec.require_paths = ['lib']
|
42
|
+
|
43
|
+
spec.add_dependency 'activerecord'
|
44
|
+
|
45
|
+
spec.add_development_dependency 'bundler', '~> 1.17'
|
46
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.7'
|
47
|
+
spec.add_development_dependency 'mysql2', '~> 0.5'
|
48
|
+
spec.add_development_dependency 'pg', '~>1.1'
|
49
|
+
spec.add_development_dependency 'pry', '~> 0.12'
|
50
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
51
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
52
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
|
53
|
+
spec.add_development_dependency 'rubocop', '~> 0.61'
|
54
|
+
spec.add_development_dependency 'rubocop-junit_formatter', '~> 0.2'
|
55
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
56
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: prune_ar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anirban Mukhopadhyay
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
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: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.17'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.17'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: database_cleaner
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mysql2
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.12'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.12'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '10.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.8'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.8'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec_junit_formatter
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.4'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.4'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.61'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.61'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop-junit_formatter
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.2'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.2'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: simplecov
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.12'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.12'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: sqlite3
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '1.3'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '1.3'
|
195
|
+
description: Given an initial set of records to delete prune_ar deletes all other
|
196
|
+
records (accessible via ActiveRecord) that are now orphaned due to a belongs_to
|
197
|
+
relation which is now non-existent. This allows you to safely delete records that
|
198
|
+
you want to delete without creating orphaned records in another table (& without
|
199
|
+
violating foreign key constraints if you use them). This can be used to prune a
|
200
|
+
production database (given deletion criteria for top level parent-less independent
|
201
|
+
entities) for use in a development environment without compromising customer data.
|
202
|
+
email:
|
203
|
+
- amukhopadhyay@contently.com
|
204
|
+
executables: []
|
205
|
+
extensions: []
|
206
|
+
extra_rdoc_files: []
|
207
|
+
files:
|
208
|
+
- CHANGELOG.md
|
209
|
+
- CONTRIBUTING.md
|
210
|
+
- Gemfile
|
211
|
+
- Gemfile.lock
|
212
|
+
- LICENSE
|
213
|
+
- README.md
|
214
|
+
- Rakefile
|
215
|
+
- bin/console
|
216
|
+
- bin/setup
|
217
|
+
- lib/prune_ar.rb
|
218
|
+
- lib/prune_ar/belongs_to_association.rb
|
219
|
+
- lib/prune_ar/belongs_to_association_gatherer.rb
|
220
|
+
- lib/prune_ar/deleter_by_criteria.rb
|
221
|
+
- lib/prune_ar/foreign_key_handler.rb
|
222
|
+
- lib/prune_ar/orphaned_selection_builder.rb
|
223
|
+
- lib/prune_ar/pruner.rb
|
224
|
+
- lib/prune_ar/version.rb
|
225
|
+
- prune_ar.gemspec
|
226
|
+
homepage: https://github.com/contently/prune_ar
|
227
|
+
licenses: []
|
228
|
+
metadata:
|
229
|
+
allowed_push_host: https://rubygems.org
|
230
|
+
post_install_message:
|
231
|
+
rdoc_options: []
|
232
|
+
require_paths:
|
233
|
+
- lib
|
234
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
235
|
+
requirements:
|
236
|
+
- - ">="
|
237
|
+
- !ruby/object:Gem::Version
|
238
|
+
version: '0'
|
239
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
requirements: []
|
245
|
+
rubyforge_project:
|
246
|
+
rubygems_version: 2.7.7
|
247
|
+
signing_key:
|
248
|
+
specification_version: 4
|
249
|
+
summary: Prunes database tables using ActiveRecord belongs_to relations.
|
250
|
+
test_files: []
|