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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PruneAr
4
+ VERSION = '0.1.0'
5
+ end
@@ -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: []