7

Whenever I instantiate a new ActiveRecord model (one that has not been persisted to the database) and attempt to access some various associations on the built model, the Rails query builder will sometimes:

  1. Add a (1=0) predicate to the where clause of the query.
  2. Add a 'distinct` clause to the select statement.

I think this only occurs when the has_many :through association is joining two or more tables.

I want to know why it adds the (1=0) predicate as well as the distinct clause. For the (1=0) predicate, it shouldn't matter if the new model has been saved to the database or not (right?). I have no idea why the distinct clause is being added.

I have a simple example below.

class Assignment < ActiveRecord::Base
  has_many :assignment_attachments
  has_many :attachments, through: :assignment_attachments
end

class AssignmentAttachment < ActiveRecord::Base
  belongs_to :assignment
  belongs_to :attachment
end

class Attachment < ActiveRecord::Base
  has_many :assignment_attachments
  has_many :assignments, through: :assignment_attachments
end

class Submission < ActiveRecord::Base
  belongs_to :assignment
  has_many :assignment_attachments, through: :assignment
  has_many :attachments, through: :assignment
end

s = Submission.new(assignment: Assignment.first)
s.assignment #=> #<Assignment ...>
s.assignment_attachments #=> [#AssignmentAttachment id: '1'>, #AssignmentAttachment assignment_id: '1', attachment_id: '1' ...>]
s.attachments #=> []

Here's the sql query for s.attachments:

SELECT DISTINCT attachments.*
FROM attachments
INNER JOIN assignment_attachments ON attachments.id = assignment_attachments.attachment_id
INNER JOIN assignments ON assignment_attachments.assignment_id = assignments.id
WHERE assignments.id = 'a0dbfdc7-0d67-4aad-ad06-6a7a5a91d2d0' AND (1=0)

Located somewhere deep in one of the subtrees of the abstract syntax tree that arel builds:

# the 'distinct' select clause
#<Arel::Nodes::SelectCore:0x007ffe43d45be0
  @groups=[],
  @having=nil,
  @projections=
   [#<struct Arel::Attributes::Attribute
     relation=
      #<Arel::Table:0x007ffe45a7be58
       @aliases=[],
       @columns=nil,
       @engine=
        Attachment(name: string, description: text, created_at: datetime, updated_at: datetime, required: boolean, id: uuid, slug: string, file_types: string),
       @name="attachments",
       @primary_key=nil,
       @table_alias=nil>,
     name="*">],
  @set_quantifier=#<Arel::Nodes::Distinct:0x007ffe43d44dd0>,
  ...

# the (1=0) predicate
@wheres=
 [#<Arel::Nodes::And:0x007ffe43d45028
   @children=
    [#<Arel::Nodes::Equality:0x007ffe45958788
      @left=
       #<struct Arel::Attributes::Attribute
        relation=
         #<Arel::Table:0x007ffe45958e68
          @aliases=[],
          @columns=nil,
          @engine=ActiveRecord::Base,
          @name="assignments",
          @primary_key=nil,
          @table_alias=nil>,
        name="id">,
      @right=#<Arel::Nodes::BindParam:0x007ffe45958878>>,
     #<Arel::Nodes::Grouping:0x007ffe43d45050 @expr="1=0">]>]

Do you know why arel is building the distinct clause and the (1=0) predicate? I can use some workarounds to get what I want - however, I would love to be able to investigate and find out why and how this tree is built.

Thanks for any/all advice.

Kurt Mueller
  • 3,173
  • 2
  • 29
  • 50
  • The `1=0` looks like an attempt to force the query to return no rows. I don't speak rails, so I can't see why it is doing that. – Kevin Oct 13 '15 at 22:32
  • Hey Kevin. I know that `1=0` is `ActiveRecord`'s way of forcing the query to return nothing. I just don't know *why* it's doing that. – Kurt Mueller Oct 13 '15 at 22:33
  • I reproduced your setup (from what was available) and `s.attachments` fires **no queries whatsoever**. However, when poked at with `.to_sql` it does output `(1=0)`, yet there's no `DISTINCT`. What version of ActiveRecord are you using? Mine is `4.2.3`. – D-side Oct 14 '15 at 19:07
  • @D-side did you do something differently setup-wise from what I have in my answer down below? Mine fired fine. So weird. My ActiveRecord for there was 4.2.4. – Rob Wise Oct 14 '15 at 19:17
  • @RobWise actually, yes, I was operating on unsaved instances only. Pretty much replicated the associations and issued `Submission.new.attachments`. No queries, empty relation (was that `.none`?). So I appended `.to_sql` and looked through the result. No `DISTINCT`. – D-side Oct 14 '15 at 21:32

2 Answers2

1

Note this is an edit to reflect new info from OP. With the new info I have recreated the scenario in a fresh project by copy and pasting OP's code plus the following migration.

Migration:

class CreateAttachments < ActiveRecord::Migration
  def change
    create_table :attachments do |t|
      t.integer :assignment_attachment_id
      t.timestamps null: false
    end

    create_table :assignments do |t|
      t.integer :assignment_attachment_id
      t.timestamps null: false
    end

    create_table :assignment_attachments do |t|
      t.integer :assignment_id
      t.integer :attachment_id
      t.timestamps null: false
    end

    create_table :submissions do |t|
      t.integer :assignment_id
      t.timestamps null: false
    end
  end
end

Works fine when associations exist in DB

When I save the Attachment and Assignment models to which I will associate my Submission model s, even though never save s, it works fine.

irb(main):001:0> attachment = Attachment.new
=> #<Attachment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> assignment = Assignment.new
=> #<Assignment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>
irb(main):005:0> assignment.attachments << attachment
=> #<ActiveRecord::Associations::CollectionProxy [#<Attachment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>]>
irb(main):006:0> assignment.save
   (0.2ms)  begin transaction
  SQL (1.5ms)  INSERT INTO "assignments" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-10-14 17:10:34.936929"], ["updated_at", "2015-10-14 17:10:34.936929"]]
  SQL (0.6ms)  INSERT INTO "attachments" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-10-14 17:10:34.944453"], ["updated_at", "2015-10-14 17:10:34.944453"]]
  SQL (0.3ms)  INSERT INTO "assignment_attachments" ("assignment_id", "attachment_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["assignment_id", 1], ["attachment_id", 1], ["created_at", "2015-10-14 17:10:34.947481"], ["updated_at", "2015-10-14 17:10:34.947481"]]
   (0.8ms)  commit transaction
=> true
irb(main):007:0> s = Submission.new(assignment: Assignment.first)
  Assignment Load (0.2ms)  SELECT  "assignments".* FROM "assignments"  ORDER BY "assignments"."id" ASC LIMIT 1
=> #<Submission id: nil, assignment_id: 1, created_at: nil, updated_at: nil>
irb(main):008:0> s.assignment
=> #<Assignment id: 1, assignment_attachment_id: nil, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">
irb(main):009:0> s.assignment_attachments
  AssignmentAttachment Load (0.2ms)  SELECT "assignment_attachments".* FROM "assignment_attachments" INNER JOIN "assignments" ON "assignment_attachments"."assignment_id" = "assignments"."id" WHERE "assignments"."id" = ?  [["id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<AssignmentAttachment id: 1, assignment_id: 1, attachment_id: 1, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">]>
irb(main):010:0> s.attachments
  Attachment Load (0.2ms)  SELECT "attachments".* FROM "attachments" INNER JOIN "assignment_attachments" ON "attachments"."id" = "assignment_attachments"."attachment_id" INNER JOIN "assignments" ON "assignment_attachments"."assignment_id" = "assignments"."id" WHERE "assignments"."id" = ?  [["id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Attachment id: 1, assignment_attachment_id: nil, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">]>
irb(main):011:0> s.attachments.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"assignment_attachments\" ON \"attachments\".\"id\" = \"assignment_attachments\".\"attachment_id\" INNER JOIN \"assignments\" ON \"assignment_attachments\".\"assignment_id\" = \"assignments\".\"id\" WHERE \"assignments\".\"id\" = 1"

Generates "1=0" predicate when there are no associations

If I do not specify any associations to the Submission instance, however, when I ask for its attachments, the query does get the 1=0 predicate. As this post describes, the 1=0 predicate is added when you are trying to retrieve records that join on an array of ids that is actually empty. This is true here, since we made sure that there are no assignment ids we can use for joining to attachment.

irb(main):007:0> Submission.new.attachments.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"assignment_attachments\" ON \"attachments\".\"id\" = \"assignment_attachments\".\"attachment_id\" INNER JOIN \"assignments\" ON \"assignment_attachments\".\"assignment_id\" = \"assignments\".\"id\" WHERE \"assignments\".\"id\" = NULL AND (1=0)"
irb(main):008:0>

Notice how it says WHERE \"assignments\".\"id\" = NULL. It can't leave it at that because it doesn't want to assume that there aren't null ids in the assignments table that would cause it to return false positives. With the additional 1=0 predicate, you are ensured to get the correct answer: an empty result.

DISTINCT

I am unable to reproduce a scenario where DISTINCT appears.

Community
  • 1
  • 1
Rob Wise
  • 4,930
  • 3
  • 26
  • 31
  • Woops, I forgot to add `Submission :has_many :attachments, through: :assignments`. Sorry about that - I updated the code in my question. – Kurt Mueller Oct 14 '15 at 13:12
  • In the code above, I want to be able to retrieve the attachments associated with the submission's assignment. I'm know that I could retrieve these attachments via `submission.assignment.attachments` (and this is the workaround I am using). However, the question pertains to *why* arel's algebraic syntax tree is using the `distinct` clause & `(1=0)` predicate. – Kurt Mueller Oct 14 '15 at 13:20
  • However, yes, you are correct... Rails' guides specifies that `has_many :through` is meant for many-to-many associations. – Kurt Mueller Oct 14 '15 at 13:21
  • Well you can also use it as a shortcut which I think is what you're doing here (now that I have that other piece of information lol). I recreated this scenario in a fresh project by copy and pasting your code and doing the necessary underlying migrations. But, my query does not have the `DISTINCT` clause or the weird `1=0` predicate and actually returns the result correctly when calling `s.attachments`. I will edit to show my code. – Rob Wise Oct 14 '15 at 17:16
0

According to the Rails' guide, the has_many :through asssociation is used for the classic many-to-many connection.

In this context, I the (1=0) predicate makes sense. Let me explain with an example.

Consider the Assignment, AssignmentAttachment, and Attachment models from the question above:

Assignment.new.attachments.to_sql

SELECT attachments.*
FROM attachments INNER JOIN assignment_attachments ON attachments.id = assignment_attachments.attachment_id
WHERE assignment_attachments.assignment_id = NULL AND (1=0)

In code above, a new Assignment instance, not yet persisted to the database, tries to access attachments associated with it. Obviously, since the model has not been saved, there are no attachments associated with it and the query builder adds a (1=0) predicate.

I still don't know why the DISTINCT clause doesn't get added to the query for a classic many-to-many relationship but does when the has_many :through relationship goes through >= 2 tables.

Kurt Mueller
  • 3,173
  • 2
  • 29
  • 50
  • 1
    "Is used for"... including, but not limited to. – D-side Oct 14 '15 at 17:42
  • @D-side correct, and I should have made that more clear in my original answer as well (Kurt had forgotten one of the associations, so I was focusing on a different issue). If you keep reading the rails guide (linked by Kurt) it goes on to say that it can also be used as a shortcut for nested `has_many` associations. > The has_many :through association is also useful for setting up "shortcuts" through nested has_many associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. – Rob Wise Oct 14 '15 at 19:21
  • @KurtMueller if you are looking at only a single `assignment` object, how could there be more than one instance of the same `attachment` object associated with it? There couldn't be, therefore there is no need for `DISTINCT` (although it wouldn't change the outcome if you added it). If you are looking at more than one `assignment` object, it is possible that they both point to the same `attachment` object, but you only want to instantiate that `attachment` object once, so we need to call `DISTINCT` there. – Rob Wise Oct 14 '15 at 19:36