named_scoping_test.rb 17.5 KB
Newer Older
1 2
# frozen_string_literal: true

3
require "cases/helper"
4 5 6 7 8 9 10
require "models/post"
require "models/topic"
require "models/comment"
require "models/reply"
require "models/author"
require "models/developer"
require "models/computer"
11

12
class NamedScopingTest < ActiveRecord::TestCase
13
  fixtures :posts, :authors, :topics, :comments, :author_addresses
14

15
  def test_implements_enumerable
J
Jon Leighton 已提交
16
    assert !Topic.all.empty?
17

J
Jon Leighton 已提交
18 19 20 21
    assert_equal Topic.all.to_a, Topic.base
    assert_equal Topic.all.to_a, Topic.base.to_a
    assert_equal Topic.first,    Topic.base.first
    assert_equal Topic.all.to_a, Topic.base.map { |i| i }
22
  end
23

24
  def test_found_items_are_cached
25
    all_posts = Topic.base
26

27
    assert_queries(1) do
28 29
      all_posts.collect { true }
      all_posts.collect { true }
30 31
    end
  end
32

33
  def test_reload_expires_cache_of_found_items
34
    all_posts = Topic.base
35
    all_posts.to_a
36

37
    new_post = Topic.create!
38 39
    assert_not_includes all_posts, new_post
    assert_includes all_posts.reload, new_post
40
  end
41

42
  def test_delegates_finds_and_calculations_to_the_base_class
J
Jon Leighton 已提交
43
    assert !Topic.all.empty?
44

J
Jon Leighton 已提交
45 46 47
    assert_equal Topic.all.to_a,                Topic.base.to_a
    assert_equal Topic.first,                   Topic.base.first
    assert_equal Topic.count,                   Topic.base.count
48
    assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
49
  end
50

51 52 53 54 55 56 57
  def test_calling_merge_at_first_in_scope
    Topic.class_eval do
      scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) }
    end
    assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a
  end

58 59 60
  def test_method_missing_priority_when_delegating
    klazz = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"
61 62
      scope :since, Proc.new { where("written_on >= ?", Time.now - 1.day) }
      scope :to,    Proc.new { where("written_on <= ?", Time.now) }
63
    end
64
    assert_equal klazz.to.since.to_a, klazz.since.to.to_a
65 66
  end

67
  def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
68
    assert Topic.approved.respond_to?(:limit)
69 70 71 72
    assert Topic.approved.respond_to?(:count)
    assert Topic.approved.respond_to?(:length)
  end

73
  def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
74
    assert !Topic.all.merge!(where: { approved: true }).to_a.empty?
75

76
    assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved
77
    assert_equal Topic.where(approved: true).count, Topic.approved.count
78
  end
79

80 81 82 83 84 85
  def test_scopes_with_string_name_can_be_composed
    # NOTE that scopes defined with a string as a name worked on their own
    # but when called on another scope the other scope was completely replaced
    assert_equal Topic.replied.approved, Topic.replied.approved_as_string
  end

86
  def test_scopes_are_composable
87
    assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
88
    assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
89
    assert !(approved == replied)
90
    assert !(approved & replied).empty?
91

92 93 94 95
    assert_equal approved & replied, Topic.approved.replied
  end

  def test_procedural_scopes
96 97
    topics_written_before_the_third = Topic.where("written_on < ?", topics(:third).written_on)
    topics_written_before_the_second = Topic.where("written_on < ?", topics(:second).written_on)
98
    assert_not_equal topics_written_before_the_second, topics_written_before_the_third
99

100 101 102
    assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on)
    assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
  end
103

104
  def test_procedural_scopes_returning_nil
J
Jon Leighton 已提交
105
    all_topics = Topic.all
106 107 108

    assert_equal all_topics, Topic.written_before(nil)
  end
109

110 111 112
  def test_scope_with_object
    objects = Topic.with_object
    assert_operator objects.length, :>, 0
113
    assert objects.all?(&:approved?), "all objects should be approved"
114 115
  end

116
  def test_has_many_associations_have_access_to_scopes
117 118
    assert_not_equal Post.containing_the_letter_a, authors(:david).posts
    assert !Post.containing_the_letter_a.empty?
119

120 121
    expected = authors(:david).posts & Post.containing_the_letter_a
    assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id)
122
  end
123

124
  def test_scope_with_STI
125 126
    assert_equal 3, Post.containing_the_letter_a.count
    assert_equal 1, SpecialPost.containing_the_letter_a.count
127 128
  end

129
  def test_has_many_through_associations_have_access_to_scopes
130
    assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
131
    assert !Comment.containing_the_letter_e.empty?
132

133 134
    expected = authors(:david).comments & Comment.containing_the_letter_e
    assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id)
135
  end
136

137
  def test_scopes_honor_current_scopes_from_when_defined
138 139 140
    assert !Post.ranked_by_comments.limit_by(5).empty?
    assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
    assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
141
    assert_not_equal Post.top(5), authors(:david).posts.top(5)
142
    # Oracle sometimes sorts differently if WHERE condition is changed
143
    assert_equal authors(:david).posts.ranked_by_comments.limit_by(5).to_a.sort_by(&:id), authors(:david).posts.top(5).to_a.sort_by(&:id)
144
    assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
145 146
  end

147
  def test_scopes_body_is_a_callable
148 149
    e = assert_raises ArgumentError do
      Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") }
150
    end
151
    assert_equal "The scope body needs to be callable.", e.message
152 153
  end

154
  def test_active_records_have_scope_named__all__
J
Jon Leighton 已提交
155
    assert !Topic.all.empty?
156

J
Jon Leighton 已提交
157
    assert_equal Topic.all.to_a, Topic.base
158
  end
159

160
  def test_active_records_have_scope_named__scoped__
J
Jon Leighton 已提交
161 162
    scope = Topic.where("content LIKE '%Have%'")
    assert !scope.empty?
163

164
    assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
165
  end
166

167
  def test_first_and_last_should_allow_integers_for_limit
168
    assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2)
169
    assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
170 171 172 173
  end

  def test_first_and_last_should_not_use_query_when_results_are_loaded
    topics = Topic.base
174
    topics.load # force load
175 176 177 178 179 180
    assert_no_queries do
      topics.first
      topics.last
    end
  end

181 182 183 184
  def test_empty_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.empty?  # use count query
185
      topics.load    # force load
186 187 188
      topics.empty?  # use loaded (no query)
    end
  end
189

190 191
  def test_any_should_not_load_results
    topics = Topic.base
192 193
    assert_queries(2) do
      topics.any?    # use count query
194
      topics.load    # force load
195
      topics.any?    # use loaded (no query)
196 197 198 199 200 201
    end
  end

  def test_any_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
202 203 204
      assert_not_called(topics, :empty?) do
        topics.any? { true }
      end
205 206 207
    end
  end

208
  def test_any_should_not_fire_query_if_scope_loaded
209
    topics = Topic.base
210
    topics.load # force load
211 212 213
    assert_no_queries { assert topics.any? }
  end

214 215 216 217 218 219
  def test_model_class_should_respond_to_any
    assert Topic.any?
    Topic.delete_all
    assert !Topic.any?
  end

220 221 222 223
  def test_many_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.many?   # use count query
224
      topics.load    # force load
225 226 227 228 229 230 231
      topics.many?   # use loaded (no query)
    end
  end

  def test_many_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
232 233 234
      assert_not_called(topics, :size) do
        topics.many? { true }
      end
235 236 237
    end
  end

238
  def test_many_should_not_fire_query_if_scope_loaded
239
    topics = Topic.base
240
    topics.load # force load
241 242 243 244
    assert_no_queries { assert topics.many? }
  end

  def test_many_should_return_false_if_none_or_one
245
    topics = Topic.base.where(id: 0)
246
    assert !topics.many?
247
    topics = Topic.base.where(id: 1)
248 249 250 251 252 253 254
    assert !topics.many?
  end

  def test_many_should_return_true_if_more_than_one
    assert Topic.base.many?
  end

255 256 257 258 259 260 261 262 263
  def test_model_class_should_respond_to_many
    Topic.delete_all
    assert !Topic.many?
    Topic.create!
    assert !Topic.many?
    Topic.create!
    assert Topic.many?
  end

264
  def test_should_build_on_top_of_scope
265 266 267 268
    topic = Topic.approved.build({})
    assert topic.approved
  end

269
  def test_should_build_new_on_top_of_scope
270 271 272 273
    topic = Topic.approved.new
    assert topic.approved
  end

274
  def test_should_create_on_top_of_scope
275 276 277 278
    topic = Topic.approved.create({})
    assert topic.approved
  end

279
  def test_should_create_with_bang_on_top_of_scope
280 281 282
    topic = Topic.approved.create!({})
    assert topic.approved
  end
283

284
  def test_should_build_on_top_of_chained_scopes
285 286
    topic = Topic.approved.by_lifo.build({})
    assert topic.approved
287
    assert_equal "lifo", topic.author_name
288
  end
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 314
  def test_reserved_scope_names
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"

      scope :approved, -> { where(approved: true) }

      class << self
        public
          def pub; end

        private
          def pri; end

        protected
          def pro; end
      end
    end

    subklass = Class.new(klass)

    conflicts = [
      :create,        # public class method on AR::Base
      :relation,      # private class method on AR::Base
      :new,           # redefined class method on AR::Base
      :all,           # a default scope
J
Joe Rafaniello 已提交
315
      :public,        # some important methods on Module and Class
316
      :protected,
317 318 319 320
      :private,
      :name,
      :parent,
      :superclass
321 322 323 324 325 326 327 328 329 330 331 332
    ]

    non_conflicts = [
      :find_by_title, # dynamic finder method
      :approved,      # existing scope
      :pub,           # existing public class method
      :pri,           # existing private class method
      :pro,           # existing protected class method
      :open,          # a ::Kernel method
    ]

    conflicts.each do |name|
F
Franky W 已提交
333
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
334
        klass.class_eval { scope name, -> { where(approved: true) } }
335
      end
F
Franky W 已提交
336
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
337

F
Franky W 已提交
338
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
339
        subklass.class_eval { scope name, -> { where(approved: true) } }
340
      end
F
Franky W 已提交
341
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
342 343 344 345
    end

    non_conflicts.each do |name|
      assert_nothing_raised do
346
        silence_warnings do
347
          klass.class_eval { scope name, -> { where(approved: true) } }
348
        end
349 350 351
      end

      assert_nothing_raised do
352
        subklass.class_eval { scope name, -> { where(approved: true) } }
353 354 355 356
      end
    end
  end

357 358 359 360 361 362 363
  # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
  # has been done by evaluating a string with a plain def statement. For scope
  # names which contain spaces this approach doesn't work.
  def test_spaces_in_scope_names
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"
      scope :"title containing space", -> { where("title LIKE '% %'") }
364
      scope :approved, -> { where(approved: true) }
365 366 367 368 369
    end
    assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'")
    assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'")
  end

370
  def test_find_all_should_behave_like_select
371
    assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
372
  end
373

374
  def test_rand_should_select_a_random_object_from_proxy
375
    assert_kind_of Topic, Topic.approved.sample
376 377
  end

378
  def test_should_use_where_in_query_for_scope
379
    assert_equal Developer.where(name: "Jamis").to_set, Developer.where(id: Developer.jamises).to_set
380
  end
381 382 383 384 385 386 387 388 389 390

  def test_size_should_use_count_when_results_are_not_loaded
    topics = Topic.base
    assert_queries(1) do
      assert_sql(/COUNT/i) { topics.size }
    end
  end

  def test_size_should_use_length_when_results_are_loaded
    topics = Topic.base
391
    topics.load # force load
392 393 394 395
    assert_no_queries do
      topics.size # use loaded (no query)
    end
  end
396

397
  def test_should_not_duplicates_where_values
398 399
    relation = Topic.where("1=1")
    assert_equal relation.where_clause, relation.scope_with_lambda.where_clause
400 401
  end

402 403 404
  def test_chaining_with_duplicate_joins
    join = "INNER JOIN comments ON comments.post_id = posts.id"
    post = Post.find(1)
J
Jon Leighton 已提交
405
    assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
406
  end
407

408
  def test_chaining_applies_last_conditions_when_creating
409 410 411 412 413
    post = Topic.rejected.new
    assert !post.approved?

    post = Topic.rejected.approved.new
    assert post.approved?
414

415 416 417 418 419
    post = Topic.approved.rejected.new
    assert !post.approved?

    post = Topic.approved.rejected.approved.new
    assert post.approved?
420 421
  end

422
  def test_chaining_combines_conditions_when_searching
423
    # Normal hash conditions
424 425
    assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a
    assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a
426 427

    # Nested hash conditions with same keys
428
    assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
429 430

    # Nested hash conditions with different keys
431
    assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
432
  end
433

434
  def test_scopes_batch_finders
435
    assert_equal 4, Topic.approved.count
436

437
    assert_queries(5) do
438
      Topic.approved.find_each(batch_size: 1) { |t| assert t.approved? }
439 440
    end

441
    assert_queries(3) do
442
      Topic.approved.find_in_batches(batch_size: 2) do |group|
443
        group.each { |t| assert t.approved? }
444 445 446
      end
    end
  end
447 448 449

  def test_table_names_for_chaining_scopes_with_and_without_table_name_included
    assert_nothing_raised do
450
      Comment.for_first_post.for_first_author.to_a
451 452
    end
  end
453

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
  def test_scopes_with_reserved_names
    class << Topic
      def public_method; end
      public :public_method

      def protected_method; end
      protected :protected_method

      def private_method; end
      private :private_method
    end

    [:public_method, :protected_method, :private_method].each do |reserved_method|
      assert Topic.respond_to?(reserved_method, true)
      ActiveRecord::Base.logger.expects(:warn)
469
      silence_warnings { Topic.scope(reserved_method, -> {}) }
470 471 472
    end
  end

473
  def test_scopes_on_relations
474
    # Topic.replied
475
    approved_topics = Topic.all.approved.order(Arel.sql("id DESC"))
476
    assert_equal topics(:fifth), approved_topics.first
477 478 479 480 481

    replied_approved_topics = approved_topics.replied
    assert_equal topics(:third), replied_approved_topics.first
  end

482
  def test_index_on_scope
483
    approved = Topic.approved.order(Arel.sql("id ASC"))
484 485 486
    assert_equal topics(:second), approved[0]
    assert approved.loaded?
  end
487

488
  def test_nested_scopes_queries_size
489
    assert_queries(1) do
490
      Topic.approved.by_lifo.replied.written_before(Time.now).to_a
491 492
    end
  end
493

494 495 496
  # Note: these next two are kinda odd because they are essentially just testing that the
  # query cache works as it should, but they are here for legacy reasons as they was previously
  # a separate cache on association proxies, and these show that that is not necessary.
497
  def test_scopes_are_cached_on_associations
498 499
    post = posts(:welcome)

500
    Post.cache do
501 502
      assert_queries(1) { post.comments.containing_the_letter_e.to_a }
      assert_no_queries { post.comments.containing_the_letter_e.to_a }
503
    end
504
  end
505

506
  def test_scopes_with_arguments_are_cached_on_associations
507 508
    post = posts(:welcome)

509
    Post.cache do
510
      one = assert_queries(1) { post.comments.limit_by(1).to_a }
511
      assert_equal 1, one.size
512

513
      two = assert_queries(1) { post.comments.limit_by(2).to_a }
514
      assert_equal 2, two.size
515

516 517
      assert_no_queries { post.comments.limit_by(1).to_a }
      assert_no_queries { post.comments.limit_by(2).to_a }
518
    end
519 520
  end

A
Arun Agrawal 已提交
521 522 523
  def test_scopes_to_get_newest
    post = posts(:welcome)
    old_last_comment = post.comments.newest
524
    new_comment = post.comments.create(body: "My new comment")
A
Arun Agrawal 已提交
525 526 527 528
    assert_equal new_comment, post.comments.newest
    assert_not_equal old_last_comment, post.comments.newest
  end

529
  def test_scopes_are_reset_on_association_reload
530 531 532 533
    post = posts(:welcome)

    [:destroy_all, :reset, :delete_all].each do |method|
      before = post.comments.containing_the_letter_e
534
      post.association(:comments).send(method)
535
      assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
536 537
    end
  end
538

539
  def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
540 541 542 543
    assert_nothing_raised do
      require "models/without_table"
    end
  end
J
Jon Leighton 已提交
544

545
  def test_eager_default_scope_relations_are_remove
J
Jon Leighton 已提交
546
    klass = Class.new(ActiveRecord::Base)
547
    klass.table_name = "posts"
J
Jon Leighton 已提交
548

549
    assert_raises(ArgumentError) do
550
      klass.send(:default_scope, klass.where(id: posts(:welcome).id))
J
Jon Leighton 已提交
551 552
    end
  end
N
Neeraj Singh 已提交
553 554

  def test_subclass_merges_scopes_properly
555
    assert_equal 1, SpecialComment.where(body: "go crazy").created.count
N
Neeraj Singh 已提交
556 557
  end

558 559 560 561 562 563
  def test_model_class_should_respond_to_extending
    assert_raises OopsError do
      Comment.unscoped.oops_comments.destroy_all
    end
  end

564 565 566 567 568 569 570 571 572 573 574 575 576
  def test_model_class_should_respond_to_none
    assert !Topic.none?
    Topic.delete_all
    assert Topic.none?
  end

  def test_model_class_should_respond_to_one
    assert !Topic.one?
    Topic.delete_all
    assert !Topic.one?
    Topic.create!
    assert Topic.one?
  end
577
end