Arel + Rails 4.2 causing problems (bindings being lost)
Asked Answered
K

1

20

We recently upgraded to Rails 4.2 from Rails 4.1 and are seeing problems with using Arel + Activerecord because we're getting this type of error:

ActiveRecord::StatementInvalid: PG::ProtocolViolation: ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 8

Here's the code that is breaking:

customers = Customer.arel_table

      ne_subquery = ImportLog.where(
        importable_type: Customer.to_s,
        importable_id: customers['id'],
        remote_type: remote_type.to_s.singularize,
        destination: 'hello'
      ).exists.not

      first  = Customer.where(ne_subquery).where(company_id: @company.id)
      second = Customer.joins(:import_logs).merge(
        ImportLog.where(
          importable_type: Customer.to_s,
          importable_id: customers['id'],
          remote_type: remote_type.to_s.singularize,
          status: 'pending',
          destination: 'hello',
          remote_id: nil
        )
      ).where(company_id: @company.id)

      Customer.from(
        customers.create_table_alias(
          first.union(second),
          Customer.table_name
        )
      )

We figured out how to solve the first part of the query (running into the same rails bug of not having bindings) by moving the exists.not to be within Customer.where like so:

ne_subquery = ImportLog.where(
       importable_type: Customer.to_s,
       importable_id: customers['id'],
       destination: 'hello'
     )

     first  = Customer.where("NOT (EXISTS (#{ne_subquery.to_sql}))").where(company_id: @company.id)

This seemed to work but we ran into the same issue with this line of code:

first.union(second)

whenever we run this part of the query, the bindings get lost. first and second are both active record objects but as soon as we "union" them, they lose the bindings are become arel objects.

We tried cycling through the query and manually replacing the bindings but couldn't seem to get it working properly. What should we do instead?

EDIT:

We also tried extracting the bind values from first and second, and then manually replacing them in the arel object like so:

union.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
  bv = bind_values[i]
  bp.replace(Customer.connection.substitute_at(bv, i))
end

However, it fails because:

NoMethodError: undefined method `replace' for #<Arel::Nodes::BindParam:0x007f8aba6cc248>

This was a solution suggested in the rails github repo.

Knocker answered 10/11, 2015 at 23:25 Comment(2)
I think that some queries could be written better (for example: second = Customer.joins(:import_logs).where(import_logs: { /* ImportLog conditions here */ }))... I don't get what you are trying to accomplish.Hex
I had the same problem. See the accepted answer to this post: #57491685 Replacing a.union(b) with Arel::Nodes::UnionAll.new(a, b) fixed the binding issue for me.Hector
T
1

I know this question is a bit old, but the error sounded familiar. I had some notes and our solution in a repository, so I thought I'd share.

The error we were receiving was:

PG::ProtocolViolation: ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1

So as you can see, our situation is a bit different. We didn't have 8 bind values. However, our single bind value was still being clobbered. I changed the naming of things to keep it general.

first_level = Blog.all_comments
second_level = Comment.where(comment_id: first_level.select(:id))
third_level = Comment.where(comment_id: second_level.select(:id))

Blog.all_comments is where we have the single bind value. That's the piece we're losing.

union = first_level.union second_level
union2 = Comment.from(
  Comment.arel_table.create_table_alias union, :comments
).union third_level

relation = Comment.from(Comment.arel_table.create_table_alias union2, :comments)

We created a union much like you except that we needed to union three different queries.

To get the lost bind values at this point, we did a simple assignment. In the end, this is a little simpler of a case than yours. However, it may be helpful.

relation.bind_values = first_level.bind_values
relation

By the way, here's the GitHub issue we found while working on this. It doesn't appear to have any updates since this question was posted though.

Tightlipped answered 28/12, 2017 at 2:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.