1

I'm trying to use spatial distance in my Rails app, but I keep running into the "ActiveRecord::UnknownAttributeReference: Query method called with non-attribute argument(s)" error when I try to use the order method.

Here's my code:

def nearby_locations
  third_party_location_query = ThirdPartyLocation.where(id: id).select('geom').to_sql

  third_party.organisation.locations.active.
    select("locations.*, ROUND(ST_DistanceSphere(locations.geom, (#{third_party_location_query}))::numeric, 0)::integer distance").
    order("locations.geom <-> (#{third_party_location_query})").
    limit(10).as_json(methods: [:distance])
end

I understand that the error is caused by passing a non-attribute value to the order method, but I'm not sure how to avoid it in this case. How can I use spatial distance in my query without running into this error?

Yousuf
  • 61
  • 5

1 Answers1

5

As of Rails 6.0 you cannot use a non-column reference in an order statement without passing an Arel object.

In your case options include:

 order(Arel.sql("locations.geom <-> (#{third_party_location_query})"))
# Or 
 third_party_location_query = ThirdPartyLocation.select(:geom)
    .arel.where(ThirdPartyLocation.arel_table[:id].eq(id))
 order(
  Arel::Nodes::InfixOperation.new("<->", # operator
    Locations.arel_table[:geom], # left side
    third_party_location_query)  # right side (will add parens as a subquery)  
  ))

We can even convert this part to arel but it wouldn't be pretty

# ROUND(ST_DistanceSphere(locations.geom, (#{third_party_location_query}))::numeric, 0)::integer distance
function = Arel::Nodes::NamedFunction
operation = Arel::Nodes::InfixOperation
operation.new('::',  
  function.new('ROUND',
    [operation.new('::',
      function.new('ST_DistanceSphere',[
        Location.arel_table[:geom],
        ThirdPartyLocation.select(:geom).arel.where(ThirdPartyLocation.arel_table[:id].eq(id))
      ]),
      Arel.sql('numeric')),
    0]),
  Arel.sql('integer')).as('distance')

For others that stumble across this post:

Please know the Arel#sql will not perform escaping.

If third_party_location_query needs to be escaped, because it comes from a third party and could be dangerous, using other techniques can and should be used to sanitize this data:

For instance if the parens are not needed then:

 Arel::Nodes::InfixOperation.new("<->",
    Locations.arel_table[:geom],
    Arel::Nodes.build_quoted(third_party_location_query))

should work.

If the parens are needed and the argument is singular or arguments are comma separated. Then

third_party_location_query = "hello" 
Arel::Nodes::Grouping.new([Arel::Nodes.build_quoted(third_party_location_query)]).to_sql
#=> (N'hello')
# Or 
third_party_location_query = [1,2,3]
Arel::Nodes::Grouping.new(third_party_location_query ).to_sql
#=> (1,2,3)

There are many other ways to handle the needed escaping depending on implementation.

engineersmnky
  • 25,495
  • 2
  • 36
  • 52
  • SELECT "third_party_locations"."geom" FROM "third_party_locations" WHERE "third_party_locations"."id" = 59 – Yousuf Feb 20 '23 at 21:20