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 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