model_iterator 1.0.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.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :dev do
6
+ gem 'rake'
7
+ gem 'activerecord'
8
+ gem 'sqlite3'
9
+ end
10
+
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-* rick olson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # ModelIterator
2
+
3
+ Basic library for iterating through large ActiveRecord datasets. For instance,
4
+ let's say you add a new feature, and you need to backfill data for existing
5
+ records:
6
+
7
+ iter = ModelIterator.new(User, :redis => $redis)
8
+ iter.each do |user|
9
+ backfill(user)
10
+ end
11
+
12
+ ModelIterator selects the records in batches (100 by default), and loops
13
+ through the table filtering based on the ID.
14
+
15
+ SELECT * FROM users WHERE id > 0 LIMIT 100
16
+ SELECT * FROM users WHERE id > 100 LIMIT 100
17
+ SELECT * FROM users WHERE id > 200 LIMIT 100
18
+
19
+ Each record's ID is tracked in Redis immediately after being processed. If
20
+ jobs crash, you can fix code, and re-run from where you left off.
21
+
22
+ This code was ported from GitHub, where it's been frequently used for nearly
23
+ two years.
24
+
25
+ ## Note on Patches/Pull Requests
26
+
27
+ 1. Fork the project.
28
+ 2. Make your feature addition or bug fix.
29
+ 3. Add tests for it. This is important so I don't break it in a future version
30
+ unintentionally.
31
+ 4. Commit, do not mess with rakefile, version, or history. (if you want to have
32
+ your own version, that is fine but bump version in a commit by itself I can
33
+ ignore when I pull)
34
+ 5. Send me a pull request. Bonus points for topic branches.
35
+
data/Rakefile ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:test) do |test|
50
+ test.libs << 'lib' << 'test'
51
+ test.pattern = 'test/**/*_test.rb'
52
+ test.verbose = true
53
+ end
54
+
55
+ desc "Open an irb session preloaded with this library"
56
+ task :console do
57
+ sh "irb -rubygems -r ./lib/#{name}.rb"
58
+ end
59
+
60
+ #############################################################################
61
+ #
62
+ # Custom tasks (add your own tasks here)
63
+ #
64
+ #############################################################################
65
+
66
+
67
+
68
+ #############################################################################
69
+ #
70
+ # Packaging tasks
71
+ #
72
+ #############################################################################
73
+
74
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
75
+ task :release => :build do
76
+ unless `git branch` =~ /^\* master$/
77
+ puts "You must be on the master branch to release!"
78
+ exit!
79
+ end
80
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
81
+ sh "git tag v#{version}"
82
+ sh "git push origin master"
83
+ sh "git push origin v#{version}"
84
+ sh "gem push pkg/#{gem_file}"
85
+ end
86
+
87
+ desc "Build #{gem_file} into the pkg directory"
88
+ task :build => :gemspec do
89
+ sh "mkdir -p pkg"
90
+ sh "gem build #{gemspec_file}"
91
+ sh "mv #{gem_file} pkg"
92
+ end
93
+
94
+ desc "Generate #{gemspec_file}"
95
+ task :gemspec => :validate do
96
+ # read spec file and split out manifest section
97
+ spec = File.read(gemspec_file)
98
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
99
+
100
+ # replace name version and date
101
+ replace_header(head, :name)
102
+ replace_header(head, :version)
103
+ replace_header(head, :date)
104
+ #comment this out if your rubyforge_project has a different name
105
+ replace_header(head, :rubyforge_project)
106
+
107
+ # determine file list from git ls-files
108
+ files = `git ls-files`.
109
+ split("\n").
110
+ sort.
111
+ reject { |file| file =~ /^\./ }.
112
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
113
+ map { |file| " #{file}" }.
114
+ join("\n")
115
+
116
+ # piece file back together and write
117
+ manifest = " s.files = %w[\n#{files}\n ]\n"
118
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
119
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
120
+ puts "Updated #{gemspec_file}"
121
+ end
122
+
123
+ desc "Validate #{gemspec_file}"
124
+ task :validate do
125
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
126
+ unless libfiles.empty?
127
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
128
+ exit!
129
+ end
130
+ unless Dir['VERSION*'].empty?
131
+ puts "A `VERSION` file at root level violates Gem best practices."
132
+ exit!
133
+ end
134
+ end
@@ -0,0 +1,213 @@
1
+ # Iterates over large models, storing state in Redis.
2
+ class ModelIterator
3
+ VERSION = "1.0.0"
4
+
5
+ class MaxIterations < StandardError
6
+ attr_reader :iterator
7
+ def initialize(iter)
8
+ @iterator = iter
9
+ super "Hit the max (#{iter.max}), stopping at id #{iter.current_id}."
10
+ end
11
+ end
12
+
13
+ # Gets a reference to the ActiveRecord::Base class that is iterated.
14
+ #
15
+ # Returns a Class.
16
+ attr_reader :klass
17
+
18
+ # Gets or sets the number of records that are returned in each database
19
+ # query.
20
+ #
21
+ # Returns a Fixnum.
22
+ attr_accessor :limit
23
+
24
+ # Gets a String SQL Where clause fragment. Use `?` for variable
25
+ # substitution.
26
+ #
27
+ # Returns a String.
28
+ attr_reader :clause
29
+
30
+ # Gets an Array of values to be sql-escaped and joined with the clause.
31
+ #
32
+ # Returns an Array of unescaped sql values.
33
+ attr_reader :clause_args
34
+
35
+ # Gets a String used to prefix the redis keys used by this object.
36
+ attr_reader :prefix
37
+
38
+ # Gets a Fixnum value of the maximum iterations to run, or 0.
39
+ attr_reader :max
40
+
41
+ # Gets the String name of the ID field.
42
+ attr_reader :id_field
43
+
44
+ # Gets the String fully qualified ID field (with the table name).
45
+ attr_reader :id_clause
46
+
47
+ # Gets the :joins value for ActiveRecord::Base.find.
48
+ attr_reader :joins
49
+
50
+ # Gets or sets a Proc that is called with each model instance while
51
+ # iterating. This is set automatically by #each.
52
+ attr_accessor :job
53
+
54
+ # Gets or Sets the Redis client object.
55
+ attr_accessor :redis
56
+
57
+ # Initializes a ModelIterator instance.
58
+ #
59
+ # klass - ActiveRecord::Base class to iterate.
60
+ # clause - String SQL WHERE clause, with '?' placeholders for values.
61
+ # *values - Optional array of values to be added to a custom SQL WHERE
62
+ # clause.
63
+ # options - Optional Hash options.
64
+ # :redis - A Redis object for storing the state.
65
+ # :order - Symbol specifying the order to iterate. :asc or
66
+ # :desc. Default: :asc
67
+ # :id_field - String name of the ID column. Default: "id"
68
+ # :id_clause - String name of the fully qualified ID column.
69
+ # Prepends the model's table name to the front of
70
+ # the ID field. Default: "table_name.id"
71
+ # :start_id - Fixnum to start iterating from. Default: 1
72
+ # :prefix - Custom String prefix for redis keys.
73
+ # :select - Optional String of the columns to retrieve.
74
+ # :joins - Optional Symbol or Hash :joins option for
75
+ # ActiveRecord::Base.find.
76
+ # :max - Optional Fixnum of the maximum number of iterations.
77
+ # Use max * limit to process a known number of records
78
+ # at a time.
79
+ # :limit - Fixnum limit of objects to fetch from the db.
80
+ # Default: 100
81
+ #
82
+ # ModelIterator.new(Repository, :start_id => 5000)
83
+ # ModelIterator.new(Repository, 'public=?', true, :start_id => 1000)
84
+ #
85
+ def initialize(klass, *args)
86
+ @klass = klass
87
+ @options = if args.last.respond_to?(:fetch)
88
+ args.pop
89
+ else
90
+ {}
91
+ end
92
+ @redis = @options[:redis]
93
+ @id_field = @options[:id_field] || klass.primary_key
94
+ @id_clause = @options[:id_clause] || "#{klass.table_name}.#{@id_field}"
95
+ @order = @options[:order] == :desc ? :desc : :asc
96
+ op = @order == :asc ? '>' : '<'
97
+ @max = @options[:max].to_i
98
+ @joins = @options[:joins]
99
+ @clause = args.empty? ?
100
+ "#{@id_clause} #{op} ?" :
101
+ "#{@id_clause} #{op} ? AND (#{args.shift})"
102
+ @clause_args = args
103
+ @current_id = @options[:start_id]
104
+ @limit = @options[:limit] || 100
105
+ @job = @prefix = @key = nil
106
+ end
107
+
108
+ # Public: Points to the latest record that was yielded, by database ID.
109
+ #
110
+ # refresh - Boolean that determines if the instance variable cache should
111
+ # be reset first. Default: false.
112
+ #
113
+ # Returns a Fixnum.
114
+ def current_id(refresh = false)
115
+ @current_id = nil if refresh
116
+ @current_id ||= @redis.get(key).to_i
117
+ end
118
+
119
+ # Public: Sets the latest processed Integer ID.
120
+ attr_writer :current_id
121
+
122
+ # Public: Iterates through the whole dataset, yielding individual records as
123
+ # they are received. This calls #records multiple times, setting the
124
+ # #current_id after each run. If an exception is raised, the ModelIterator
125
+ # instance can safely be restarted, since all state is stored in Redis.
126
+ #
127
+ # &block - Block that gets called with each ActiveRecord::Base instance.
128
+ #
129
+ # Returns nothing.
130
+ def each
131
+ @job = block = (block_given? ? Proc.new : @job)
132
+ each_set do |records|
133
+ records.each do |record|
134
+ block.call(record)
135
+ @current_id = record.send(@id_field)
136
+ end
137
+ end
138
+ cleanup
139
+ end
140
+
141
+ # Public: Iterates through the whole dataset. This calls #records multiple
142
+ # times, but does not set the #current_id after each record.
143
+ #
144
+ # &block - Block that gets called with each ActiveRecord::Base instance.
145
+ #
146
+ # Returns nothing.
147
+ def each_set(&block)
148
+ loops = 0
149
+ while records = self.records
150
+ begin
151
+ block.call(records)
152
+ loops += 1
153
+ if @max > 0 && loops >= @max
154
+ raise MaxIterations, self
155
+ end
156
+ ensure
157
+ @redis.set(key, @current_id) if @current_id
158
+ end
159
+ end
160
+ end
161
+
162
+ # Public: Simple alias for #each with no block. Useful if the job errors,
163
+ # and you want to retry it again from where it left off.
164
+ alias run each
165
+
166
+ # Public: Cleans up any redis keys.
167
+ #
168
+ # Returns nothing.
169
+ def cleanup
170
+ @redis.del(key)
171
+ @current_id = nil
172
+ end
173
+
174
+ def prefix
175
+ @prefix = [@options[:prefix], self.class.name, @klass.name].
176
+ compact.join(":")
177
+ end
178
+
179
+ def key
180
+ @key ||= "#{prefix}:current"
181
+ end
182
+
183
+ # Public: Gets an ActiveRecord :connections value, ready for
184
+ # ActiveRecord::Base.all.
185
+ #
186
+ # Returns an Array with a String query clause, and unescaped db values.
187
+ def conditions
188
+ [@clause, current_id, *@clause_args]
189
+ end
190
+
191
+ # Public: Queries the database for the next page of records.
192
+ #
193
+ # Returns an Array of ActiveRecord::Base instances if any results are
194
+ # returned, or nil.
195
+ def records
196
+ arr = @klass.all(find_options)
197
+ arr.empty? ? nil : arr
198
+ end
199
+
200
+ # Public: Builds the ActiveRecord::Base.find options for a single query.
201
+ #
202
+ # Returns a Hash.
203
+ def find_options
204
+ opt = {:conditions => conditions, :limit => @limit, :order => "#{@id_clause} #{@order}"}
205
+ if columns = @options[:select]
206
+ opt[:select] = columns
207
+ end
208
+ opt[:joins] = @joins if @joins
209
+ opt
210
+ end
211
+ end
212
+
213
+
@@ -0,0 +1,58 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.3.5") if s.respond_to? :required_rubygems_version=
10
+
11
+ ## Leave these as is they will be modified for you by the rake gemspec task.
12
+ ## If your rubyforge_project name is different, then edit it and comment out
13
+ ## the sub! line in the Rakefile
14
+ s.name = 'model_iterator'
15
+ s.version = '1.0.0'
16
+ s.date = '2012-09-07'
17
+ s.rubyforge_project = 'model_iterator'
18
+
19
+ ## Make sure your summary is short. The description may be as long
20
+ ## as you like.
21
+ s.summary = "Iterate through large ActiveRecord datasets"
22
+ s.description = "Iterate through large ActiveRecord datasets"
23
+
24
+ ## List the primary authors. If there are a bunch of authors, it's probably
25
+ ## better to set the email to an email list or something. If you don't have
26
+ ## a custom homepage, consider using your GitHub URL or the like.
27
+ s.authors = ["Rick Olson"]
28
+ s.email = 'technoweenie@gmail.com'
29
+ s.homepage = 'http://github.com/technoweenie/model_iterator'
30
+
31
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
32
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
33
+ s.require_paths = %w[lib]
34
+
35
+ s.add_development_dependency 'rake'
36
+ s.add_development_dependency 'test-unit'
37
+
38
+ ## Leave this section as-is. It will be automatically generated from the
39
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
40
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
41
+ # = MANIFEST =
42
+ s.files = %w[
43
+ Gemfile
44
+ LICENSE.md
45
+ README.md
46
+ Rakefile
47
+ lib/model_iterator.rb
48
+ model_iterator.gemspec
49
+ test/helper.rb
50
+ test/init_test.rb
51
+ test/iterate_test.rb
52
+ ]
53
+ # = MANIFEST =
54
+
55
+ ## Test files will be grabbed from the file list. Make sure the path glob
56
+ ## matches what you actually use.
57
+ s.test_files = s.files.select { |path| path =~ %r{^test/*/.+\.rb} }
58
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_record'
4
+ require File.expand_path("../../lib/model_iterator", __FILE__)
5
+
6
+ class ModelIterator::TestCase < Test::Unit::TestCase
7
+ class Model < ActiveRecord::Base
8
+ establish_connection :adapter => 'sqlite3', :database => ':memory:'
9
+ connection.create_table table_name do |c|
10
+ c.column :name, :string
11
+ end
12
+
13
+ %w(a b c).each do |s|
14
+ create!(:name => s)
15
+ end
16
+ end
17
+
18
+ class RedisClient
19
+ def initialize(hash = nil)
20
+ @hash = hash || {}
21
+ end
22
+
23
+ def [](key)
24
+ @hash[key]
25
+ end
26
+ alias get []
27
+
28
+ def []=(key, value)
29
+ @hash[key] = value
30
+ end
31
+ alias set []=
32
+
33
+ def delete(key)
34
+ @hash.delete(key)
35
+ end
36
+
37
+ alias del delete
38
+ end
39
+ end
40
+
data/test/init_test.rb ADDED
@@ -0,0 +1,80 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class InitializationTest < ModelIterator::TestCase
4
+ def setup
5
+ @iter = ModelIterator.new(Model, :redis => RedisClient.new)
6
+ end
7
+
8
+ def test_sets_klass
9
+ assert_equal Model, @iter.klass
10
+ end
11
+
12
+ def test_sets_current_id
13
+ assert_equal 0, @iter.current_id
14
+ end
15
+
16
+ def test_sets_conditions
17
+ assert_equal ['models.id > ?', @iter.current_id], @iter.conditions
18
+ end
19
+
20
+ def test_sets_limit
21
+ assert_equal 100, @iter.limit
22
+ end
23
+
24
+ def test_sets_redis_prefix
25
+ assert_equal 'ModelIterator:ModelIterator::TestCase::Model', @iter.prefix
26
+ end
27
+ end
28
+
29
+ class InitializationTestWithOptions < ModelIterator::TestCase
30
+ def setup
31
+ @iter = ModelIterator.new Model, :redis => RedisClient.new,
32
+ :start_id => 5, :limit => 10, :prefix => 'foo'
33
+ end
34
+
35
+ def test_sets_klass
36
+ assert_equal Model, @iter.klass
37
+ end
38
+
39
+ def test_sets_current_id
40
+ assert_equal 5, @iter.current_id
41
+ end
42
+
43
+ def test_sets_conditions
44
+ assert_equal ['models.id > ?', @iter.current_id], @iter.conditions
45
+ end
46
+
47
+ def test_sets_limit
48
+ assert_equal 10, @iter.limit
49
+ end
50
+
51
+ def test_sets_redis_prefix
52
+ assert_equal 'foo:ModelIterator:ModelIterator::TestCase::Model', @iter.prefix
53
+ end
54
+ end
55
+
56
+ class InitializationTestWithCustomWhereClause < ModelIterator::TestCase
57
+ def setup
58
+ @iter = ModelIterator.new Model,
59
+ 'public = ?',
60
+ true,
61
+ :redis => RedisClient.new, :start_id => 5, :limit => 10
62
+ end
63
+
64
+ def test_sets_klass
65
+ assert_equal Model, @iter.klass
66
+ end
67
+
68
+ def test_sets_current_id
69
+ assert_equal 5, @iter.current_id
70
+ end
71
+
72
+ def test_sets_conditions
73
+ assert_equal ['models.id > ? AND (public = ?)', @iter.current_id, true], @iter.conditions
74
+ end
75
+
76
+ def test_sets_limit
77
+ assert_equal 10, @iter.limit
78
+ end
79
+ end
80
+
@@ -0,0 +1,83 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class IterateTest < ModelIterator::TestCase
4
+ def test_finds_current_iteration_of_records
5
+ iter = ModelIterator.new Model, :redis => RedisClient.new
6
+ assert_equal %w(a b c), iter.records.map(&:name)
7
+ end
8
+
9
+ def test_loops_through_all_records
10
+ names = []
11
+ iter = ModelIterator.new Model, :redis => RedisClient.new, :limit => 1
12
+ iter.each do |m|
13
+ names << m.name
14
+ end
15
+
16
+ assert_equal %w(a b c), names
17
+ end
18
+
19
+ def test_loops_through_filtered_records
20
+ names = []
21
+ iter = ModelIterator.new Model, 'name != ?', 'a',
22
+ :redis => RedisClient.new, :limit => 1
23
+ iter.each do |m|
24
+ names << m.name
25
+ end
26
+
27
+ assert_equal %w(b c), names
28
+ end
29
+
30
+ def test_loops_through_records_in_reverse
31
+ names = []
32
+ iter = ModelIterator.new Model, :redis => RedisClient.new, :limit => 1,
33
+ :start_id => 100000, :order => :desc
34
+ iter.each do |m|
35
+ names << m.name
36
+ end
37
+
38
+ assert_equal %w(c b a), names
39
+ end
40
+
41
+ def test_loops_through_known_number_of_records
42
+ names = []
43
+ iter = ModelIterator.new Model, :redis => RedisClient.new,
44
+ :limit => 1, :start_id => 0, :max => 2
45
+
46
+ assert_raises ModelIterator::MaxIterations do
47
+ iter.each do |m|
48
+ names << m.name
49
+ end
50
+ end
51
+
52
+ assert_equal %w(a b), names
53
+ end
54
+
55
+ def test_allows_restart_of_records_after_error
56
+ redis = RedisClient.new
57
+ names = []
58
+ iter = ModelIterator.new Model, :redis => redis, :start_id => 0
59
+ badjob = lambda do |m|
60
+ raise(ExpectedError) if m.id != 1
61
+ names << m.name
62
+ end
63
+
64
+ 2.times do
65
+ assert_raises ExpectedError do
66
+ iter.each(&badjob)
67
+ end
68
+ end
69
+
70
+ assert_equal badjob, iter.job
71
+ assert_equal %w(a), names
72
+
73
+ iter = ModelIterator.new Model, :redis => redis, :limit => 1
74
+ assert_equal 1, iter.current_id
75
+ iter.job = lambda { |m| names << m.name }
76
+ iter.run
77
+
78
+ assert_equal %w(a b c), names
79
+ end
80
+
81
+ class ExpectedError < StandardError; end
82
+ end
83
+
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: model_iterator
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rick Olson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: test-unit
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Iterate through large ActiveRecord datasets
47
+ email: technoweenie@gmail.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - Gemfile
53
+ - LICENSE.md
54
+ - README.md
55
+ - Rakefile
56
+ - lib/model_iterator.rb
57
+ - model_iterator.gemspec
58
+ - test/helper.rb
59
+ - test/init_test.rb
60
+ - test/iterate_test.rb
61
+ homepage: http://github.com/technoweenie/model_iterator
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ segments:
74
+ - 0
75
+ hash: 3722461387104018248
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.5
82
+ requirements: []
83
+ rubyforge_project: model_iterator
84
+ rubygems_version: 1.8.23
85
+ signing_key:
86
+ specification_version: 2
87
+ summary: Iterate through large ActiveRecord datasets
88
+ test_files:
89
+ - test/helper.rb
90
+ - test/init_test.rb
91
+ - test/iterate_test.rb