rails-archiver 0.1.8 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22767f51d1fefb73fd2d17b703a0ee846509bb99fc4dd2bed27c06a28fcdfc60
4
- data.tar.gz: d04f42ec798d1df81fc3ee76575d4e1658563b73e4087f1e1639553b489b52b6
3
+ metadata.gz: ddfcaefd86b70ef668e8ba941e20f68a96a62a704ad9e931fbe72343a6ef7ddb
4
+ data.tar.gz: d3283d3fbbb8abeafab718a445b7f129cef999f531b7766998af860c22de2c90
5
5
  SHA512:
6
- metadata.gz: a9b6176a12fd8fd673ebb726dcaa4dd08f2a3e561718685693870f78d0661b863e7ca03b2ea8a9da8f8e18e73f1128115c20af17f65aee08e070911584473958
7
- data.tar.gz: 176e5169d8d4756fdae4ff8fcbe5e8f5bcde4395a262a332321e7cbbdf1e61530e7373b1fd389217d5c747190d4dbb0023aa1aab48c29df5128ac7827a7a502d
6
+ metadata.gz: a69726e5dff1eb032dcb915c67804cce258f970652045561a52a727a073493844a5879d79e2bb982d6a9427c834f0d33e1873759c29c463765d65894cb19674e
7
+ data.tar.gz: 34eb78437f58aced9d64fe518e0183f8ecc5dc24e8d24e88194d7d379a5c9ecd138c00bbc46f54f23fef280ea8994acd12b8c9a86decbc0d3fa04d9014896917
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ ### Ruby template
2
+ *.gem
3
+ .idea
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## UNRELEASED
9
+
10
+ # [0.3.0] - 2025-05-22
11
+ - Added retries for delete table operations to handle SQL Timeouts and deadlocks.
12
+
13
+ # [0.2.0] - 2025-01-20
14
+ - Added support for enum columns - should be able to handle both keys and values.
15
+ - Refactored the `unarchiver` method to more easily subclass `Unarchiver`.
16
+ - For `belongs_to` associations, replace foreign key attributes with actual reference to the relevant object while archiving to avoid issues with validation.
@@ -1,4 +1,5 @@
1
1
  require 'tmpdir'
2
+ require 'rails-archiver/utils/deadlock_retry'
2
3
  # Takes a database model and:
3
4
  # 1) Visits all dependent associations
4
5
  # 2) Saves everything in one giant JSON hash
@@ -122,15 +123,26 @@ module RailsArchiver
122
123
  delete_query = <<-SQL
123
124
  DELETE FROM `#{table}` WHERE `id` IN (#{group.compact.join(',')})
124
125
  SQL
125
- ActiveRecord::Base.connection.delete(delete_query)
126
+ RailsArchiver::Utils::DeadlockRetry.wrap do
127
+ ActiveRecord::Base.connection.delete(delete_query)
128
+ end
126
129
  end
127
130
 
128
131
  @logger.info("Finished deleting from #{table}")
129
132
  end
130
133
 
134
+ # @return [Class]
135
+ def unarchiver_class
136
+ RailsArchiver::Unarchiver
137
+ end
138
+
131
139
  # @return [RailsArchiver::Unarchiver]
132
- def unarchiver
133
- unarchiver = RailsArchiver::Unarchiver.new(@model, :logger => @logger)
140
+ # @param crash_on_errors [Boolean]
141
+ def unarchiver(crash_on_errors: false)
142
+ unarchiver = unarchiver_class.new(@model,
143
+ logger: @logger,
144
+ crash_on_errors: crash_on_errors
145
+ )
134
146
  unarchiver.transport = self.transport
135
147
  unarchiver
136
148
  end
@@ -11,18 +11,24 @@ module RailsArchiver
11
11
  attr_accessor :errors, :transport
12
12
 
13
13
  # @param model [ActiveRecord::Base]
14
- # @param options [Hash]
15
- # * logger [Logger]
16
- # * new_copy [Boolean] if true, create all new objects instead of
17
- # replacing existing ones.
18
- # * crash_on_errors [Boolean] if true, do not do any imports if any
19
- # models' validations failed.
20
- def initialize(model, options={})
14
+ # @param logger [Logger]
15
+ # @param new_copy [Boolean] if true, create all new objects instead of
16
+ # replacing existing ones.
17
+ # @param crash_on_errors [Boolean] if true, do not do any imports if any
18
+ # models' validations failed.
19
+ # @param transport [Symbol|RailsArchiver::Transport::Base]
20
+ def initialize(model,
21
+ transport: :in_memory,
22
+ logger: Logger.new(STDOUT),
23
+ new_copy: false, crash_on_errors: false)
21
24
  @model = model
22
- @logger = options.delete(:logger) || Logger.new(STDOUT)
23
- @options = options
25
+ @logger = logger
26
+ @options = {
27
+ new_copy: new_copy,
28
+ crash_on_errors: crash_on_errors
29
+ }
24
30
  # Transport for downloading
25
- self.transport = _get_transport(options.delete(:transport) || :in_memory)
31
+ self.transport = _get_transport(transport)
26
32
  self.errors = []
27
33
  end
28
34
 
@@ -58,7 +64,7 @@ module RailsArchiver
58
64
  end
59
65
  end
60
66
  if @options[:crash_on_errors] && self.errors.any?
61
- raise ImportError.new("Errors occurred during load - please see 'errors' method for more details")
67
+ raise ImportError.new("Errors occurred during load: #{self.errors.join("\n")}")
62
68
  end
63
69
  end
64
70
 
@@ -114,13 +120,52 @@ module RailsArchiver
114
120
  end
115
121
 
116
122
  model
123
+ rescue => e
124
+ self.errors << "Error importing class #{klass.name}: #{e.message}"
117
125
  end
118
126
 
119
127
  private
120
128
 
129
+ # @param model [ActiveRecord::Base]
130
+ # @param field [String]
131
+ # @param attrs [Hash]
132
+ def assign_enum_value(model, field, attrs)
133
+ entries = model.class.public_send(field.pluralize)
134
+ is_integer = entries.values.first.is_a?(Integer)
135
+ value = attrs[field]
136
+ valid = if entries.keys.include?(value)
137
+ true
138
+ elsif is_integer && entries.values.include?(value.to_i)
139
+ true
140
+ else
141
+ entries.values.include?(value)
142
+ end
143
+
144
+ return if valid
145
+
146
+ @logger.warn("Invalid value for #{field}: #{attrs[field]}")
147
+ new_value = model.class.columns.find { |a| a.name == field }.default
148
+ new_value = new_value.to_i if is_integer && new_value
149
+ attrs[field] = new_value
150
+ end
151
+
121
152
  # @param model [ActiveRecord::Base]
122
153
  # @param attrs [Hash]
123
154
  def _assign_attributes(model, attrs)
155
+ # Handle integer enum values that were turned into strings
156
+ if model.class.respond_to?(:attribute_types)
157
+ enums = model.class.attribute_types.select { |_, v| v.is_a?(ActiveRecord::Enum::EnumType) }
158
+ enums.keys.each do |field|
159
+ assign_enum_value(model, field, attrs)
160
+ end
161
+ end
162
+ model.class.reflect_on_all_associations(:belongs_to).each do |assoc|
163
+ if attrs[assoc.foreign_key].present?
164
+ record = assoc.klass.find_by(assoc.association_primary_key => attrs[assoc.foreign_key])
165
+ attrs[assoc.name] = record if record
166
+ end
167
+ end
168
+
124
169
  if model.method(:attributes=).arity == 1
125
170
  model.attributes = attrs
126
171
  else
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsArchiver
4
+ module Utils
5
+ # Utility module to retry a block of code in case of a deadlock.
6
+ module DeadlockRetry
7
+ class << self
8
+
9
+ RETRY_COUNT = 2
10
+
11
+ def wrap(&block)
12
+ count = RETRY_COUNT
13
+ begin
14
+ ActiveRecord::Base.transaction(&block)
15
+ rescue ActiveRecord::Deadlocked, ActiveRecord::LockWaitTimeout => e
16
+ raise if count <= 0
17
+
18
+ Rails.logger.error("Error: #{e.message}. Retrying. #{count} attempts remaining")
19
+ count -= 1
20
+
21
+ sleep(Random.rand(5.0) + 0.5)
22
+
23
+ retry
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'rails-archiver'
3
3
  s.require_paths = %w(. lib lib/rails-archiver)
4
- s.version = '0.1.8'
5
- s.date = '2024-07-03'
4
+ s.version = '0.3.0'
5
+ s.date = '2025-05-22'
6
6
  s.summary = 'Fully archive a Rails model'
7
7
  s.description = <<-EOF
8
8
  EOF
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-archiver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-03 00:00:00.000000000 Z
11
+ date: 2025-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,6 +44,8 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - ".gitignore"
48
+ - CHANGELOG.md
47
49
  - README.md
48
50
  - lib/rails-archiver.rb
49
51
  - lib/rails-archiver/archiver.rb
@@ -51,6 +53,7 @@ files:
51
53
  - lib/rails-archiver/transport/in_memory.rb
52
54
  - lib/rails-archiver/transport/s3.rb
53
55
  - lib/rails-archiver/unarchiver.rb
56
+ - lib/rails-archiver/utils/deadlock_retry.rb
54
57
  - rails-archiver.gemspec
55
58
  homepage: https://github.com/dorner/rails-archiver
56
59
  licenses: