batches_test.rb 13.3 KB
Newer Older
1
require 'cases/helper'
2
require 'models/post'
3
require 'models/subscriber'
4 5

class EachTest < ActiveRecord::TestCase
6
  fixtures :posts, :subscribers
7 8

  def setup
9
    @posts = Post.order("id asc")
10
    @total = Post.count
11
    Post.count('id') # preheat arel's table cache
12
  end
13

V
Vipul A M 已提交
14
  def test_each_should_execute_one_query_per_batch
15
    assert_queries(@total + 1) do
16
      Post.find_each(:batch_size => 1) do |post|
17 18 19 20 21
        assert_kind_of Post, post
      end
    end
  end

A
Alexander Balashov 已提交
22
  def test_each_should_not_return_query_chain_and_execute_only_one_query
23 24 25 26 27 28
    assert_queries(1) do
      result = Post.find_each(:batch_size => 100000){ }
      assert_nil result
    end
  end

29 30 31 32 33 34 35 36 37
  def test_each_should_return_an_enumerator_if_no_block_is_present
    assert_queries(1) do
      Post.find_each(:batch_size => 100000).with_index do |post, index|
        assert_kind_of Post, post
        assert_kind_of Integer, index
      end
    end
  end

38 39
  if Enumerator.method_defined? :size
    def test_each_should_return_a_sized_enumerator
40
      assert_equal 11, Post.find_each(batch_size: 1).size
41
      assert_equal 5, Post.find_each(batch_size:  2, start: 7).size
42
      assert_equal 11, Post.find_each(batch_size: 10_000).size
43 44 45
    end
  end

46 47 48 49 50 51 52 53 54
  def test_each_enumerator_should_execute_one_query_per_batch
    assert_queries(@total + 1) do
      Post.find_each(:batch_size => 1).with_index do |post, index|
        assert_kind_of Post, post
        assert_kind_of Integer, index
      end
    end
  end

55
  def test_each_should_raise_if_select_is_set_without_id
56
    assert_raise(ArgumentError) do
57 58 59
      Post.select(:title).find_each(batch_size: 1) { |post|
        flunk "should not call this block"
      }
60 61 62 63
    end
  end

  def test_each_should_execute_if_id_is_in_select
64
    assert_queries(6) do
65
      Post.select("id, title, type").find_each(:batch_size => 2) do |post|
66 67 68 69 70
        assert_kind_of Post, post
      end
    end
  end

71
  def test_warn_if_limit_scope_is_set
72 73 74
    assert_called(ActiveRecord::Base.logger, :warn) do
      Post.limit(1).find_each { |post| post }
    end
75 76 77
  end

  def test_warn_if_order_scope_is_set
78 79 80
    assert_called(ActiveRecord::Base.logger, :warn) do
      Post.order("title").find_each { |post| post }
    end
81 82
  end

83 84 85 86 87 88 89 90 91 92
  def test_logger_not_required
    previous_logger = ActiveRecord::Base.logger
    ActiveRecord::Base.logger = nil
    assert_nothing_raised do
      Post.limit(1).find_each { |post| post }
    end
  ensure
    ActiveRecord::Base.logger = previous_logger
  end

93
  def test_find_in_batches_should_return_batches
94
    assert_queries(@total + 1) do
95 96 97 98 99 100 101 102
      Post.find_in_batches(:batch_size => 1) do |batch|
        assert_kind_of Array, batch
        assert_kind_of Post, batch.first
      end
    end
  end

  def test_find_in_batches_should_start_from_the_start_option
103
    assert_queries(@total) do
104
      Post.find_in_batches(batch_size: 1, start: 2) do |batch|
105 106 107 108 109
        assert_kind_of Array, batch
        assert_kind_of Post, batch.first
      end
    end
  end
110

111
  def test_find_in_batches_should_end_at_the_finish_option
112
    assert_queries(6) do
113
      Post.find_in_batches(batch_size: 1, finish: 5) do |batch|
114 115 116 117 118 119
        assert_kind_of Array, batch
        assert_kind_of Post, batch.first
      end
    end
  end

120
  def test_find_in_batches_shouldnt_execute_query_unless_needed
121
    assert_queries(2) do
122
      Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch }
123 124 125
    end

    assert_queries(1) do
126
      Post.find_in_batches(:batch_size => @total + 1) {|batch| assert_kind_of Array, batch }
127 128
    end
  end
129 130 131 132 133 134 135 136 137 138

  def test_find_in_batches_should_quote_batch_order
    c = Post.connection
    assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do
      Post.find_in_batches(:batch_size => 1) do |batch|
        assert_kind_of Array, batch
        assert_kind_of Post, batch.first
      end
    end
  end
139 140 141

  def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
    not_a_post = "not a post"
142 143 144 145 146 147 148 149 150
    def not_a_post.id; end
    not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do
      assert_nothing_raised do
        Post.find_in_batches(:batch_size => 1) do |batch|
          assert_kind_of Array, batch
          assert_kind_of Post, batch.first

          batch.map! { not_a_post }
        end
151 152
      end
    end
153
  end
154

155 156 157 158 159 160 161 162 163
  def test_find_in_batches_should_ignore_the_order_default_scope
    # First post is with title scope
    first_post = PostWithDefaultScope.first
    posts = []
    PostWithDefaultScope.find_in_batches  do |batch|
      posts.concat(batch)
    end
    # posts.first will be ordered using id only. Title order scope should not apply here
    assert_not_equal first_post, posts.first
C
Cody Cutrer 已提交
164
    assert_equal posts(:welcome).id, posts.first.id
165 166 167
  end

  def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
J
Jon Leighton 已提交
168
    special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
169 170 171 172 173
    posts = []
    SpecialPostWithDefaultScope.find_in_batches do |batch|
      posts.concat(batch)
    end
    assert_equal special_posts_ids, posts.map(&:id)
174 175
  end

176 177
  def test_find_in_batches_should_not_modify_passed_options
    assert_nothing_raised do
178
      Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){}
179 180 181
    end
  end

182
  def test_find_in_batches_should_use_any_column_as_primary_key
183 184
    nick_order_subscribers = Subscriber.order('nick asc')
    start_nick = nick_order_subscribers.second.nick
185

186
    subscribers = []
187
    Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch|
188
      subscribers.concat(batch)
189 190
    end

191
    assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
192
  end
193 194 195

  def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
    assert_queries(Subscriber.count + 1) do
196 197 198
      Subscriber.find_in_batches(batch_size: 1) do |batch|
        assert_kind_of Array, batch
        assert_kind_of Subscriber, batch.first
199 200 201
      end
    end
  end
202 203 204

  def test_find_in_batches_should_return_an_enumerator
    enum = nil
205
    assert_no_queries do
206 207 208 209 210 211 212 213 214
      enum = Post.find_in_batches(:batch_size => 1)
    end
    assert_queries(4) do
      enum.first(4) do |batch|
        assert_kind_of Array, batch
        assert_kind_of Post, batch.first
      end
    end
  end
215

216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
  def test_in_batches_should_not_execute_any_query
    assert_no_queries do
      assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2)
    end
  end

  def test_in_batches_should_yield_relation_if_block_given
    assert_queries(6) do
      Post.in_batches(of: 2) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
      end
    end
  end

  def test_in_batches_should_be_enumerable_if_no_block_given
    assert_queries(6) do
      Post.in_batches(of: 2).each do |relation|
        assert_kind_of ActiveRecord::Relation, relation
      end
    end
  end

  def test_in_batches_each_record_should_yield_record_if_block_is_given
    assert_queries(6) do
      Post.in_batches(of: 2).each_record do |post|
        assert post.title.present?
        assert_kind_of Post, post
      end
    end
  end

  def test_in_batches_each_record_should_return_enumerator_if_no_block_given
    assert_queries(6) do
      Post.in_batches(of: 2).each_record.with_index do |post, i|
        assert post.title.present?
        assert_kind_of Post, post
      end
    end
  end

  def test_in_batches_each_record_should_be_ordered_by_id
    ids = Post.order('id ASC').pluck(:id)
    assert_queries(6) do
      Post.in_batches(of: 2).each_record.with_index do |post, i|
        assert_equal ids[i], post.id
      end
    end
  end

  def test_in_batches_update_all_affect_all_records
    assert_queries(6 + 6) do # 6 selects, 6 updates
      Post.in_batches(of: 2).update_all(title: "updated-title")
    end
    assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count
  end

  def test_in_batches_delete_all_should_not_delete_records_in_other_batches
    not_deleted_count = Post.where('id <= 2').count
    Post.where('id > 2').in_batches(of: 2).delete_all
    assert_equal 0, Post.where('id > 2').count
    assert_equal not_deleted_count, Post.count
  end

  def test_in_batches_should_not_be_loaded
    Post.in_batches(of: 1) do |relation|
      assert_not relation.loaded?
    end

    Post.in_batches(of: 1, load: false) do |relation|
      assert_not relation.loaded?
    end
  end

  def test_in_batches_should_be_loaded
    Post.in_batches(of: 1, load: true) do |relation|
      assert relation.loaded?
    end
  end

  def test_in_batches_if_not_loaded_executes_more_queries
    assert_queries(@total + 1) do
      Post.in_batches(of: 1, load: false) do |relation|
        assert_not relation.loaded?
      end
    end
  end

  def test_in_batches_should_return_relations
    assert_queries(@total + 1) do
      Post.in_batches(of: 1) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
      end
    end
  end

  def test_in_batches_should_start_from_the_start_option
    post = Post.order('id ASC').where('id >= ?', 2).first
    assert_queries(2) do
314
      relation = Post.in_batches(of: 1, start: 2).first
315 316 317 318
      assert_equal post, relation.first
    end
  end

319
  def test_in_batches_should_end_at_the_finish_option
320 321
    post = Post.order('id DESC').where('id <= ?', 5).first
    assert_queries(7) do
322
      relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
      assert_equal post, relation.last
    end
  end

  def test_in_batches_shouldnt_execute_query_unless_needed
    assert_queries(2) do
      Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation }
    end

    assert_queries(1) do
      Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation }
    end
  end

  def test_in_batches_should_quote_batch_order
    c = Post.connection
    assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do
      Post.in_batches(of: 1) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
        assert_kind_of Post, relation.first
      end
    end
  end

  def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
    not_a_post = "not a post"
    def not_a_post.id
      raise StandardError.new("not_a_post had #id called on it")
    end

    assert_nothing_raised do
      Post.in_batches(of: 1) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
        assert_kind_of Post, relation.first

        relation = [not_a_post] * relation.count
      end
    end
  end

  def test_in_batches_should_not_ignore_default_scope_without_order_statements
    special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
    posts = []
    SpecialPostWithDefaultScope.in_batches do |relation|
      posts.concat(relation)
    end
    assert_equal special_posts_ids, posts.map(&:id)
  end

  def test_in_batches_should_not_modify_passed_options
    assert_nothing_raised do
374
      Post.in_batches({ of: 42, start: 1 }.freeze){}
375 376 377 378 379 380 381 382
    end
  end

  def test_in_batches_should_use_any_column_as_primary_key
    nick_order_subscribers = Subscriber.order('nick asc')
    start_nick = nick_order_subscribers.second.nick

    subscribers = []
383
    Subscriber.in_batches(of: 1, start: start_nick) do |relation|
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
      subscribers.concat(relation)
    end

    assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
  end

  def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
    assert_queries(Subscriber.count + 1) do
      Subscriber.in_batches(of: 1, load: true) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
        assert_kind_of Subscriber, relation.first
      end
    end
  end

  def test_in_batches_should_return_an_enumerator
    enum = nil
    assert_no_queries do
      enum = Post.in_batches(of: 1)
    end
    assert_queries(4) do
      enum.first(4) do |relation|
        assert_kind_of ActiveRecord::Relation, relation
        assert_kind_of Post, relation.first
      end
    end
  end

  def test_in_batches_relations_should_not_overlap_with_each_other
    seen_posts = []
    Post.in_batches(of: 2, load: true) do |relation|
      relation.to_a.each do |post|
        assert_not seen_posts.include?(post)
        seen_posts << post
      end
    end
  end

  def test_in_batches_relations_with_condition_should_not_overlap_with_each_other
    seen_posts = []
    author_id = Post.first.author_id
    posts_by_author = Post.where(author_id: author_id)
    Post.in_batches(of: 2) do |batch|
      seen_posts += batch.where(author_id: author_id)
    end

    assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort
  end

  def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
    Post.update_all(author_id: 0)
    person = Post.last
    person.update_attributes(author_id: 1)

    Post.in_batches(of: 2) do |batch|
      batch.where('author_id >= 1').update_all('author_id = author_id + 1')
    end
    assert_equal 2, person.reload.author_id # incremented only once
  end

444 445 446 447
  if Enumerator.method_defined? :size
    def test_find_in_batches_should_return_a_sized_enumerator
      assert_equal 11, Post.find_in_batches(:batch_size => 1).size
      assert_equal 6, Post.find_in_batches(:batch_size => 2).size
448
      assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size
449 450 451 452
      assert_equal 4, Post.find_in_batches(:batch_size => 3).size
      assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size
    end
  end
453
end