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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f5cdab6653f529f88d18a2bd9edebf9ee64a4694
4
- data.tar.gz: 244bfb7f54e1c4ca8c7451b79741a132d0a77dc9
3
+ metadata.gz: 411501048ad3322c19a3db3c781b9e7e19d48725
4
+ data.tar.gz: 8587a602745f2d08375c9fb6636412c974df963d
5
5
  SHA512:
6
- metadata.gz: 24ac68a4ae6aa95bb7ebd65688686f60737e4e4de68fab3ce0d269b1b7b3fe82dbab501b623df5a463d7a2ad62283794e65426f1bfbb75b15d6179c75cbc9622
7
- data.tar.gz: f6c0e7be2b76b401f4e6ad3c6755cdcab5fb7f568fdc91316889540f20462250e1bf2a4f9b8ce97439b344ab36e3683a8d0ad64a68896f3afb041a5c5ee074bc
6
+ metadata.gz: bf912a9462601ae9b0b47bf987e1031789a46012a60138db5a5ba9d354739a6044d54839935c3c8366e0fce0daf04f4005270b16bdde4612ebb7798e8f7b721b
7
+ data.tar.gz: 45219968ddf66748d6d0f40c8a39f86b635b5564d1047266711e14d94ec30679d9a9bd9c02743490ca30de9b7867f83dc7e62cd69018df90910de13458abc395
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /gemfiles/*.lock
11
+ *.gem
@@ -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
@@ -7,4 +7,4 @@ gem "pg"
7
7
  gem "sqlite3"
8
8
  gem "activerecord", "~> 4.2.0"
9
9
 
10
- gemspec path: "../"
10
+ gemspec path: ".."
@@ -7,4 +7,4 @@ gem "pg"
7
7
  gem "sqlite3"
8
8
  gem "activerecord", "~> 5.0.0"
9
9
 
10
- gemspec path: "../"
10
+ gemspec path: ".."
@@ -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
- RelationDumper.new(model_class.where(*root.constraints), self, association_path).call
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EvilSeed
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
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.0
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-09 00:00:00.000000000 Z
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