evil-seed 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|