relation_test.rb 15.6 KB
Newer Older
1 2
# frozen_string_literal: true

3
require "cases/helper"
4 5 6 7
require "models/post"
require "models/comment"
require "models/author"
require "models/rating"
8
require "models/categorization"
9 10 11

module ActiveRecord
  class RelationTest < ActiveRecord::TestCase
12
    fixtures :posts, :comments, :authors, :author_addresses, :ratings, :categorizations
13

14
    def test_construction
15
      relation = Relation.new(FakeKlass, table: :b)
16
      assert_equal FakeKlass, relation.klass
17
      assert_equal :b, relation.table
18
      assert_not relation.loaded, "relation is not loaded"
19 20
    end

21
    def test_responds_to_model_and_returns_klass
22
      relation = Relation.new(FakeKlass)
23
      assert_equal FakeKlass, relation.model
24 25
    end

26
    def test_initialize_single_values
27
      relation = Relation.new(FakeKlass)
28
      (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
29 30
        assert_nil relation.send("#{method}_value"), method.to_s
      end
31 32 33
      value = relation.create_with_value
      assert_equal({}, value)
      assert_predicate value, :frozen?
34 35 36
    end

    def test_multi_value_initialize
37
      relation = Relation.new(FakeKlass)
38
      Relation::MULTI_VALUE_METHODS.each do |method|
39 40 41
        values = relation.send("#{method}_values")
        assert_equal [], values, method.to_s
        assert_predicate values, :frozen?, method.to_s
42 43 44 45
      end
    end

    def test_extensions
46
      relation = Relation.new(FakeKlass)
47 48
      assert_equal [], relation.extensions
    end
A
Aaron Patterson 已提交
49

50
    def test_empty_where_values_hash
51
      relation = Relation.new(FakeKlass)
A
Aaron Patterson 已提交
52 53 54
      assert_equal({}, relation.where_values_hash)
    end

55
    def test_has_values
56
      relation = Relation.new(Post)
57 58
      relation.where!(id: 10)
      assert_equal({ "id" => 10 }, relation.where_values_hash)
59 60 61
    end

    def test_values_wrong_table
62
      relation = Relation.new(Post)
63
      relation.where! Comment.arel_table[:id].eq(10)
64 65 66
      assert_equal({}, relation.where_values_hash)
    end

67
    def test_tree_is_not_traversed
68
      relation = Relation.new(Post)
69 70
      left     = relation.table[:id].eq(10)
      right    = relation.table[:id].eq(10)
71
      combine  = left.or(right)
72
      relation.where! combine
73 74 75
      assert_equal({}, relation.where_values_hash)
    end

A
Aaron Patterson 已提交
76
    def test_scope_for_create
77
      relation = Relation.new(FakeKlass)
A
Aaron Patterson 已提交
78 79
      assert_equal({}, relation.scope_for_create)
    end
80 81

    def test_create_with_value
82
      relation = Relation.new(Post)
83 84
      relation.create_with_value = { hello: "world" }
      assert_equal({ "hello" => "world" }, relation.scope_for_create)
85 86 87
    end

    def test_create_with_value_with_wheres
88
      relation = Relation.new(Post)
89 90
      assert_equal({}, relation.scope_for_create)

91
      relation.where!(id: 10)
R
Ryuta Kamizono 已提交
92
      assert_equal({ "id" => 10 }, relation.scope_for_create)
93

94
      relation.create_with_value = { hello: "world" }
R
Ryuta Kamizono 已提交
95
      assert_equal({ "hello" => "world", "id" => 10 }, relation.scope_for_create)
96
    end
A
Aaron Patterson 已提交
97

98
    def test_empty_scope
99
      relation = Relation.new(Post)
100
      assert_predicate relation, :empty_scope?
101 102

      relation.merge!(relation)
103
      assert_predicate relation, :empty_scope?
104 105 106

      assert_not_predicate NullPost.all, :empty_scope?
      assert_not_predicate FirstPost.all, :empty_scope?
107 108
    end

109 110 111 112 113 114
    def test_bad_constants_raise_errors
      assert_raises(NameError) do
        ActiveRecord::Relation::HelloWorld
      end
    end

A
Aaron Patterson 已提交
115
    def test_empty_eager_loading?
116
      relation = Relation.new(FakeKlass)
117
      assert_not_predicate relation, :eager_loading?
A
Aaron Patterson 已提交
118 119 120
    end

    def test_eager_load_values
121
      relation = Relation.new(FakeKlass)
122
      relation.eager_load! :b
123
      assert_predicate relation, :eager_loading?
A
Aaron Patterson 已提交
124
    end
125 126

    def test_references_values
127
      relation = Relation.new(FakeKlass)
128 129
      assert_equal [], relation.references_values
      relation = relation.references(:foo).references(:omg, :lol)
130
      assert_equal ["foo", "omg", "lol"], relation.references_values
131 132 133
    end

    def test_references_values_dont_duplicate
134
      relation = Relation.new(FakeKlass)
135
      relation = relation.references(:foo).references(:foo)
136
      assert_equal ["foo"], relation.references_values
137
    end
138

139
    test "merging a hash into a relation" do
140
      relation = Relation.new(Post)
141
      relation = relation.merge where: { name: :lol }, readonly: true
142

143
      assert_equal({ "name" => :lol }, relation.where_clause.to_h)
144 145 146
      assert_equal true, relation.readonly_value
    end

147
    test "merging an empty hash into a relation" do
148
      assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause
149
    end
J
Jon Leighton 已提交
150

151 152
    test "merging a hash with unknown keys raises" do
      assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: "lol") }
J
Jon Leighton 已提交
153
    end
154

155
    test "merging nil or false raises" do
156
      relation = Relation.new(FakeKlass)
157 158 159 160 161

      e = assert_raises(ArgumentError) do
        relation = relation.merge nil
      end

162
      assert_equal "invalid argument: nil.", e.message
163 164 165 166 167

      e = assert_raises(ArgumentError) do
        relation = relation.merge false
      end

168
      assert_equal "invalid argument: false.", e.message
169 170
    end

171
    test "#values returns a dup of the values" do
172
      relation = Relation.new(Post).where!(name: :foo)
173 174 175
      values   = relation.values

      values[:where] = nil
176
      assert_not_nil relation.where_clause
177 178
    end

179
    test "relations can be created with a values hash" do
180
      relation = Relation.new(FakeKlass, values: { select: [:foo] })
S
Sean Griffin 已提交
181
      assert_equal [:foo], relation.select_values
182
    end
183

184
    test "merging a hash interpolates conditions" do
185 186
      klass = Class.new(FakeKlass) do
        def self.sanitize_sql(args)
187 188
          raise unless args == ["foo = ?", "bar"]
          "foo = bar"
189 190
        end
      end
191

192
      relation = Relation.new(klass)
193
      relation.merge!(where: ["foo = ?", "bar"])
194
      assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause
195
    end
196

197
    def test_merging_readonly_false
198
      relation = Relation.new(FakeKlass)
199 200 201 202 203 204
      readonly_false_relation = relation.readonly(false)
      # test merging in both directions
      assert_equal false, relation.merge(readonly_false_relation).readonly_value
      assert_equal false, readonly_false_relation.merge(relation).readonly_value
    end

205
    def test_relation_merging_with_merged_joins_as_symbols
206 207
      special_comments_with_ratings = SpecialComment.joins(:ratings)
      posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
208 209 210 211
      assert_equal({ 4 => 2 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
    end

    def test_relation_merging_with_merged_symbol_joins_keeps_inner_joins
Y
yuuji.yaginuma 已提交
212
      queries = capture_sql { Author.joins(:posts).merge(Post.joins(:comments)).to_a }
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227

      nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size }
      assert_equal 2, nb_inner_join, "Wrong amount of INNER JOIN in query"
      assert queries.none? { |sql| /LEFT\s+(OUTER)?\s+JOIN/i.match?(sql) }, "Shouldn't have any LEFT JOIN in query"
    end

    def test_relation_merging_with_merged_symbol_joins_has_correct_size_and_count
      # Has one entry per comment
      merged_authors_with_commented_posts_relation = Author.joins(:posts).merge(Post.joins(:comments))

      post_ids_with_author = Post.joins(:author).pluck(:id)
      manual_comments_on_post_that_have_author = Comment.where(post_id: post_ids_with_author).pluck(:id)

      assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.count
      assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size
228 229
    end

230 231 232 233 234 235 236 237
    def test_relation_merging_with_merged_symbol_joins_is_aliased
      categorizations_with_authors = Categorization.joins(:author)
      queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a }

      nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size }
      assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query"

      # using `\W` as the column separator
238
      assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Regexp.escape(Author.quoted_table_name)}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query"
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
    end

    def test_relation_with_merged_joins_aliased_works
      categorizations_with_authors = Categorization.joins(:author)
      posts_with_joins_and_merges = Post.joins(:author, :categorizations)
                                        .merge(Author.select(:id)).merge(categorizations_with_authors)

      author_with_posts = Author.joins(:posts).ids
      categorizations_with_author = Categorization.joins(:author).ids
      posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).ids

      assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.count
      assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.to_a.size
    end

254 255 256 257 258
    def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent
      post = Post.create!(title: "haha", body: "huhu")
      comment = post.comments.create!(body: "hu")
      3.times { comment.ratings.create! }

259
      relation = Post.joins(:comments).merge Comment.joins(:ratings)
260

261
      assert_equal 3, relation.where(id: post.id).pluck(:id).size
262 263
    end

264 265
    def test_merge_raises_with_invalid_argument
      assert_raises ArgumentError do
266
        relation = Relation.new(FakeKlass)
267 268 269 270
        relation.merge(true)
      end
    end

271
    def test_respond_to_for_non_selected_element
272
      post = Post.select(:title).first
D
Daniel Colson 已提交
273
      assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception"
274

275
      silence_warnings { post = Post.select("'title' as post_title").first }
D
Daniel Colson 已提交
276
      assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception"
277 278
    end

279
    def test_select_quotes_when_using_from_clause
280
      skip_if_sqlite3_version_includes_quoting_bug
281 282 283 284 285
      quoted_join = ActiveRecord::Base.connection.quote_table_name("join")
      selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join)
      assert_equal Post.pluck(:id), selected
    end

286 287 288 289 290 291 292 293 294
    def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used
      skip_if_sqlite3_version_includes_quoting_bug
      klass = Class.new(ActiveRecord::Base) do
        self.table_name = :test_with_keyword_column_name
        alias_attribute :description, :desc
      end
      klass.create!(description: "foo")

      assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
295
      assert_equal ["foo"], klass.reselect(:description).from(klass.all).map(&:desc)
296 297
    end

298 299 300 301
    def test_relation_merging_with_merged_joins_as_strings
      join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id"
      special_comments_with_ratings = SpecialComment.joins join_string
      posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
302
      assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
303
    end
304

305 306 307 308 309 310 311 312 313
    def test_relation_merging_keeps_joining_order
      authors  = Author.where(id: 1)
      posts    = Post.joins(:author).merge(authors)
      comments = Comment.joins(:post).merge(posts)
      ratings  = Rating.joins(:comment).merge(comments)

      assert_equal 3, ratings.count
    end

314 315 316 317 318 319 320 321 322 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
    def test_relation_with_annotation_includes_comment_in_to_sql
      post_with_annotation = Post.where(id: 1).annotate("foo")
      assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
    end

    def test_relation_with_annotation_includes_comment_in_sql
      post_with_annotation = Post.where(id: 1).annotate("foo")
      assert_sql(%r{/\* foo \*/}) do
        assert post_with_annotation.first, "record should be found"
      end
    end

    def test_relation_with_annotation_chains_sql_comments
      post_with_annotation = Post.where(id: 1).annotate("foo").annotate("bar")
      assert_sql(%r{/\* foo \*/ /\* bar \*/}) do
        assert post_with_annotation.first, "record should be found"
      end
    end

    def test_relation_with_annotation_filters_sql_comment_delimiters
      post_with_annotation = Post.where(id: 1).annotate("**//foo//**")
      assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
    end

    def test_relation_with_annotation_includes_comment_in_count_query
      post_with_annotation = Post.annotate("foo")
      all_count = Post.all.to_a.count
      assert_sql(%r{/\* foo \*/}) do
        assert_equal all_count, post_with_annotation.count
      end
    end

    def test_relation_without_annotation_does_not_include_an_empty_comment
      log = capture_sql do
        Post.where(id: 1).first
      end

      assert_not_predicate log, :empty?
      assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
    end

    def test_relation_with_optimizer_hints_filters_sql_comment_delimiters
      post_with_hint = Post.where(id: 1).optimizer_hints("**//BADHINT//**")
      assert_match %r{BADHINT}, post_with_hint.to_sql
      assert_no_match %r{\*/BADHINT}, post_with_hint.to_sql
      assert_no_match %r{\*//BADHINT}, post_with_hint.to_sql
      assert_no_match %r{BADHINT/\*}, post_with_hint.to_sql
      assert_no_match %r{BADHINT//\*}, post_with_hint.to_sql
      post_with_hint = Post.where(id: 1).optimizer_hints("/*+ BADHINT */")
      assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql
    end

366 367 368 369 370 371 372
    def test_does_not_duplicate_optimizer_hints_on_merge
      escaped_table = Post.connection.quote_table_name("posts")
      expected = "SELECT /*+ OMGHINT */ #{escaped_table}.* FROM #{escaped_table}"
      query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
      assert_equal expected, query
    end

373 374 375 376 377
    class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
      def type
        :string
      end

S
Sean Griffin 已提交
378 379 380 381 382
      def cast(value)
        raise value unless value == "value from user"
        "cast value"
      end

383
      def deserialize(value)
384 385 386 387
        raise value unless value == "type cast for database"
        "type cast from database"
      end

388
      def serialize(value)
S
Sean Griffin 已提交
389
        raise value unless value == "cast value"
390 391 392 393 394
        "type cast for database"
      end
    end

    class UpdateAllTestModel < ActiveRecord::Base
395
      self.table_name = "posts"
396 397 398 399 400 401 402 403 404

      attribute :body, EnsureRoundTripTypeCasting.new
    end

    def test_update_all_goes_through_normal_type_casting
      UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI

      assert_equal "type cast from database", UpdateAllTestModel.first.body
    end
405

406 407 408 409 410 411 412 413
    def test_skip_preloading_after_arel_has_been_generated
      assert_nothing_raised do
        relation = Comment.all
        relation.arel
        relation.skip_preloading!
      end
    end

414 415
    private

416 417 418
      def skip_if_sqlite3_version_includes_quoting_bug
        if sqlite3_version_includes_quoting_bug?
          skip <<-ERROR.squish
419 420 421 422
            You are using an outdated version of SQLite3 which has a bug in
            quoted column names. Please update SQLite3 and rebuild the sqlite3
            ruby gem
          ERROR
423
        end
424 425
      end

426 427 428 429 430 431 432
      def sqlite3_version_includes_quoting_bug?
        if current_adapter?(:SQLite3Adapter)
          selected_quoted_column_names = ActiveRecord::Base.connection.exec_query(
            'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery'
          ).columns
          ["join"] != selected_quoted_column_names
        end
433
      end
434 435
  end
end