evil-seed 0.1.0 → 0.1.1
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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +19 -15
- data/Rakefile +12 -0
- data/gemfiles/activerecord_4_2.gemfile +1 -1
- data/gemfiles/activerecord_5_0.gemfile +1 -1
- data/lib/evil_seed/refinements/in_batches.rb +223 -0
- data/lib/evil_seed/relation_dumper.rb +7 -0
- data/lib/evil_seed/root_dumper.rb +3 -1
- data/lib/evil_seed/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 411501048ad3322c19a3db3c781b9e7e19d48725
|
4
|
+
data.tar.gz: 8587a602745f2d08375c9fb6636412c974df963d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf912a9462601ae9b0b47bf987e1031789a46012a60138db5a5ba9d354739a6044d54839935c3c8366e0fce0daf04f4005270b16bdde4612ebb7798e8f7b721b
|
7
|
+
data.tar.gz: 45219968ddf66748d6d0f40c8a39f86b635b5564d1047266711e14d94ec30679d9a9bd9c02743490ca30de9b7867f83dc7e62cd69018df90910de13458abc395
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,21 +1,6 @@
|
|
1
1
|
cache: bundler
|
2
2
|
sudo: false
|
3
3
|
language: ruby
|
4
|
-
rvm:
|
5
|
-
- 2.4.1
|
6
|
-
- 2.3.4
|
7
|
-
- 2.2.7
|
8
|
-
gemfile:
|
9
|
-
- gemfiles/activerecord-5-0.gemfile
|
10
|
-
- gemfiles/activerecord-4-2.gemfile
|
11
|
-
env:
|
12
|
-
- DB=postgresql
|
13
|
-
- DB=sqlite
|
14
|
-
- DB=mysql
|
15
|
-
before_install:
|
16
|
-
- gem install bundler -v 1.14.6
|
17
|
-
- bundle install
|
18
|
-
- appraisal install
|
19
4
|
|
20
5
|
addons:
|
21
6
|
apt:
|
@@ -23,3 +8,22 @@ addons:
|
|
23
8
|
- travis-ci/sqlite3
|
24
9
|
packages:
|
25
10
|
- sqlite3
|
11
|
+
|
12
|
+
matrix:
|
13
|
+
include:
|
14
|
+
- rvm: 2.4.1
|
15
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
16
|
+
env: "DB=sqlite"
|
17
|
+
- rvm: 2.4.1
|
18
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
19
|
+
env: "DB=postgresql"
|
20
|
+
- rvm: 2.4.1
|
21
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
22
|
+
env: "DB=mysql"
|
23
|
+
- rvm: 2.3.4
|
24
|
+
gemfile: gemfiles/activerecord_4_2.gemfile
|
25
|
+
env: "DB=postgresql"
|
26
|
+
- rvm: 2.3.4
|
27
|
+
gemfile: gemfiles/activerecord_4_2.gemfile
|
28
|
+
env: "DB=sqlite"
|
29
|
+
# Please note that gem can't be tested against MySQL on ActiveRecord 4.2 (Dump and restore test doesn't work)!
|
data/Rakefile
CHANGED
@@ -10,4 +10,16 @@ Rake::TestTask.new(:test) do |t|
|
|
10
10
|
t.test_files = FileList['test/**/*_test.rb']
|
11
11
|
end
|
12
12
|
|
13
|
+
ADAPTERS = %w[postgresql sqlite mysql].freeze
|
14
|
+
|
15
|
+
namespace :test do
|
16
|
+
ADAPTERS.each do |adapter|
|
17
|
+
task adapter => ["#{adapter}:env", :test]
|
18
|
+
|
19
|
+
namespace adapter do
|
20
|
+
task(:env) { ENV['DB'] = adapter }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
13
25
|
task default: :test
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'active_record/relation/batches'
|
2
|
+
|
3
|
+
module EvilSeed
|
4
|
+
module Refinements
|
5
|
+
# This backports ActiveRecord::Relation#in_batches method for ActiveRecord 4.2
|
6
|
+
# This module contains this method and +BatchEnumerator+ class picked from Ruby on Rails codebase at 2017-05-14
|
7
|
+
# See https://github.com/rails/rails/commit/25cee1f0373aa3b1d893413a959375480e0ac684
|
8
|
+
# The ActiveRecord MIT license is obviously compatible with our license (MIT also)
|
9
|
+
module InBatches
|
10
|
+
refine ActiveRecord::Relation do
|
11
|
+
|
12
|
+
# This is from active_record/core
|
13
|
+
def arel_attribute(name, table = klass.arel_table) # :nodoc:
|
14
|
+
name = klass.attribute_alias(name) if klass.attribute_alias?(name)
|
15
|
+
table[name]
|
16
|
+
end
|
17
|
+
|
18
|
+
class BatchEnumerator
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc:
|
22
|
+
@of = of
|
23
|
+
@relation = relation
|
24
|
+
@start = start
|
25
|
+
@finish = finish
|
26
|
+
end
|
27
|
+
|
28
|
+
# Looping through a collection of records from the database (using the
|
29
|
+
# +all+ method, for example) is very inefficient since it will try to
|
30
|
+
# instantiate all the objects at once.
|
31
|
+
#
|
32
|
+
# In that case, batch processing methods allow you to work with the
|
33
|
+
# records in batches, thereby greatly reducing memory consumption.
|
34
|
+
#
|
35
|
+
# Person.in_batches.each_record do |person|
|
36
|
+
# person.do_awesome_stuff
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Person.where("age > 21").in_batches(of: 10).each_record do |person|
|
40
|
+
# person.party_all_night!
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# If you do not provide a block to #each_record, it will return an Enumerator
|
44
|
+
# for chaining with other methods:
|
45
|
+
#
|
46
|
+
# Person.in_batches.each_record.with_index do |person, index|
|
47
|
+
# person.award_trophy(index + 1)
|
48
|
+
# end
|
49
|
+
def each_record
|
50
|
+
return to_enum(:each_record) unless block_given?
|
51
|
+
|
52
|
+
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation|
|
53
|
+
relation.records.each { |record| yield record }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Delegates #delete_all, #update_all, #destroy_all methods to each batch.
|
58
|
+
#
|
59
|
+
# People.in_batches.delete_all
|
60
|
+
# People.where('age < 10').in_batches.destroy_all
|
61
|
+
# People.in_batches.update_all('age = age + 1')
|
62
|
+
[:delete_all, :update_all, :destroy_all].each do |method|
|
63
|
+
define_method(method) do |*args, &block|
|
64
|
+
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation|
|
65
|
+
relation.send(method, *args, &block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Yields an ActiveRecord::Relation object for each batch of records.
|
71
|
+
#
|
72
|
+
# Person.in_batches.each do |relation|
|
73
|
+
# relation.update_all(awesome: true)
|
74
|
+
# end
|
75
|
+
def each
|
76
|
+
enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false)
|
77
|
+
return enum.each { |relation| yield relation } if block_given?
|
78
|
+
enum
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Yields ActiveRecord::Relation objects to work with a batch of records.
|
83
|
+
#
|
84
|
+
# Person.where("age > 21").in_batches do |relation|
|
85
|
+
# relation.delete_all
|
86
|
+
# sleep(10) # Throttle the delete queries
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# If you do not provide a block to #in_batches, it will return a
|
90
|
+
# BatchEnumerator which is enumerable.
|
91
|
+
#
|
92
|
+
# Person.in_batches.with_index do |relation, batch_index|
|
93
|
+
# puts "Processing relation ##{batch_index}"
|
94
|
+
# relation.each { |relation| relation.delete_all }
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# Examples of calling methods on the returned BatchEnumerator object:
|
98
|
+
#
|
99
|
+
# Person.in_batches.delete_all
|
100
|
+
# Person.in_batches.update_all(awesome: true)
|
101
|
+
# Person.in_batches.each_record(&:party_all_night!)
|
102
|
+
#
|
103
|
+
# ==== Options
|
104
|
+
# * <tt>:of</tt> - Specifies the size of the batch. Default to 1000.
|
105
|
+
# * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false.
|
106
|
+
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
|
107
|
+
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
|
108
|
+
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
|
109
|
+
# an order is present in the relation.
|
110
|
+
#
|
111
|
+
# Limits are honored, and if present there is no requirement for the batch
|
112
|
+
# size, it can be less than, equal, or greater than the limit.
|
113
|
+
#
|
114
|
+
# The options +start+ and +finish+ are especially useful if you want
|
115
|
+
# multiple workers dealing with the same processing queue. You can make
|
116
|
+
# worker 1 handle all the records between id 1 and 9999 and worker 2
|
117
|
+
# handle from 10000 and beyond by setting the +:start+ and +:finish+
|
118
|
+
# option on each worker.
|
119
|
+
#
|
120
|
+
# # Let's process from record 10_000 on.
|
121
|
+
# Person.in_batches(start: 10_000).update_all(awesome: true)
|
122
|
+
#
|
123
|
+
# An example of calling where query method on the relation:
|
124
|
+
#
|
125
|
+
# Person.in_batches.each do |relation|
|
126
|
+
# relation.update_all('age = age + 1')
|
127
|
+
# relation.where('age > 21').update_all(should_party: true)
|
128
|
+
# relation.where('age <= 21').delete_all
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# NOTE: If you are going to iterate through each record, you should call
|
132
|
+
# #each_record on the yielded BatchEnumerator:
|
133
|
+
#
|
134
|
+
# Person.in_batches.each_record(&:party_all_night!)
|
135
|
+
#
|
136
|
+
# NOTE: It's not possible to set the order. That is automatically set to
|
137
|
+
# ascending on the primary key ("id ASC") to make the batch ordering
|
138
|
+
# consistent. Therefore the primary key must be orderable, e.g an integer
|
139
|
+
# or a string.
|
140
|
+
#
|
141
|
+
# NOTE: By its nature, batch processing is subject to race conditions if
|
142
|
+
# other processes are modifying the database.
|
143
|
+
def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
|
144
|
+
relation = self
|
145
|
+
unless block_given?
|
146
|
+
return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
|
147
|
+
end
|
148
|
+
|
149
|
+
if arel.orders.present?
|
150
|
+
act_on_ignored_order(error_on_ignore)
|
151
|
+
end
|
152
|
+
|
153
|
+
batch_limit = of
|
154
|
+
if limit_value
|
155
|
+
remaining = limit_value
|
156
|
+
batch_limit = remaining if remaining < batch_limit
|
157
|
+
end
|
158
|
+
|
159
|
+
relation = relation.reorder(batch_order).limit(batch_limit)
|
160
|
+
relation = apply_limits(relation, start, finish)
|
161
|
+
batch_relation = relation
|
162
|
+
|
163
|
+
loop do
|
164
|
+
if load
|
165
|
+
records = batch_relation.records
|
166
|
+
ids = records.map(&:id)
|
167
|
+
yielded_relation = where(primary_key => ids)
|
168
|
+
yielded_relation.load_records(records)
|
169
|
+
else
|
170
|
+
ids = batch_relation.pluck(primary_key)
|
171
|
+
yielded_relation = where(primary_key => ids)
|
172
|
+
end
|
173
|
+
|
174
|
+
break if ids.empty?
|
175
|
+
|
176
|
+
primary_key_offset = ids.last
|
177
|
+
raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
|
178
|
+
|
179
|
+
yield yielded_relation
|
180
|
+
|
181
|
+
break if ids.length < batch_limit
|
182
|
+
|
183
|
+
if limit_value
|
184
|
+
remaining -= ids.length
|
185
|
+
|
186
|
+
if remaining == 0
|
187
|
+
# Saves a useless iteration when the limit is a multiple of the
|
188
|
+
# batch size.
|
189
|
+
break
|
190
|
+
elsif remaining < batch_limit
|
191
|
+
relation = relation.limit(remaining)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset))
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def apply_limits(relation, start, finish)
|
202
|
+
relation = relation.where(arel_attribute(primary_key).gteq(start)) if start
|
203
|
+
relation = relation.where(arel_attribute(primary_key).lteq(finish)) if finish
|
204
|
+
relation
|
205
|
+
end
|
206
|
+
|
207
|
+
def batch_order
|
208
|
+
"#{quoted_table_name}.#{quoted_primary_key} ASC"
|
209
|
+
end
|
210
|
+
|
211
|
+
def act_on_ignored_order(error_on_ignore)
|
212
|
+
raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore)
|
213
|
+
|
214
|
+
if raise_error
|
215
|
+
raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
|
216
|
+
elsif logger
|
217
|
+
logger.warn(ORDER_IGNORE_MESSAGE)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -1,5 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# As method ActiveRecord::Relation#in_batches is available only since ActiveRecord 5.0
|
4
|
+
# we will backport it only for us via refinements for ActiveRecord 4.2 compatibility.
|
5
|
+
unless ActiveRecord::Batches.instance_methods(false).include?(:in_batches)
|
6
|
+
require_relative 'refinements/in_batches'
|
7
|
+
using EvilSeed::Refinements::InBatches
|
8
|
+
end
|
9
|
+
|
3
10
|
module EvilSeed
|
4
11
|
# This class performs actual dump generation for single relation and all its not yet loaded dependencies
|
5
12
|
#
|
@@ -23,7 +23,9 @@ module EvilSeed
|
|
23
23
|
# @param output [IO] Stream to write SQL dump into
|
24
24
|
def call
|
25
25
|
association_path = model_class.model_name.singular
|
26
|
-
|
26
|
+
relation = model_class.all
|
27
|
+
relation = relation.where(*root.constraints) if root.constraints.any? # without arguments returns not a relation
|
28
|
+
RelationDumper.new(relation, self, association_path).call
|
27
29
|
end
|
28
30
|
|
29
31
|
# @return [Boolean] +true+ if limits are NOT reached and +false+ otherwise
|
data/lib/evil_seed/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: evil-seed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrey Novikov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-05-
|
12
|
+
date: 2017-05-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -152,6 +152,7 @@ files:
|
|
152
152
|
- lib/evil_seed/configuration/root.rb
|
153
153
|
- lib/evil_seed/dumper.rb
|
154
154
|
- lib/evil_seed/record_dumper.rb
|
155
|
+
- lib/evil_seed/refinements/in_batches.rb
|
155
156
|
- lib/evil_seed/relation_dumper.rb
|
156
157
|
- lib/evil_seed/root_dumper.rb
|
157
158
|
- lib/evil_seed/version.rb
|