sqlruby-on-railscommon-table-expressionarel

Arel UpdateManager with Join creates invalid SQL - how to rephrase?


Apparently there is an issue in Arel core, where Arel::UpdateManager, when performing a column update on a join, does not generate the table name for the update column. It results in invalid SQL.

I ran into this in a Rails 5.2 app, where I had an SQL literal UPDATE statement that I was trying to rephrase in Arel.

UPDATE observations o, names n
SET o.lifeform = n.lifeform
WHERE o.name_id = n.id
  AND o.lifeform != n.lifeform

In Arel, i wrote this:

names = Name.arel_table
obs = Observation.arel_table
join_source = Arel::Nodes::JoinSource.new(
  obs, [obs.create_join(names)]
)

Arel::UpdateManager.new.
  table(join_source).
  where(obs[:id].eq(names[:id]).
        and(obs[:lifeform].not_eq(names[:lifeform]))).
  set([[obs[:lifeform], names[:lifeform]]])

This returns:

Mysql2::Error: Column 'lifeform' in field list is ambiguous: 

The problem is at the end. The SQL generated from this does not specify the table where the column is to be set.

UPDATE `observations` 
INNER JOIN `names` 
SET `lifeform` = `names`.`lifeform` 
WHERE (`observations`.`id` = `names`.`id`) 
AND (`observations`.`lifeform` != `names`.`lifeform`)

Elsewhere, Arel-generated SQL usually qualifies columns with table names to avoid ambiguity. But the source code for update_manager.rb definitely uses Nodes::UnqualifiedColumn.new(column). (I have added my description to the Arel issue on GitHub.)

For now I'd maybe like to rephrase my Arel some other way. Is there a way to force Arel to quote the table name, similar to connection.quote_table_name?

Or would using a CTE be appropriate?


Solution

  • I guess one way to do this is with ActiveRecord's connection.update_all.

    names = Arel::Table.new(:names)
    Observation.joins(:name).
     where(names[:correct_spelling_id].not_eq(nil)).
      update_all("`observations`.`name_id` = `names`.`correct_spelling_id`")
    

    This generates the desired SQL:

    UPDATE `observations` 
    INNER JOIN `names` 
    ON (`observations`.`name_id` = `names`.`correct_spelling_id`) 
      AND (`names`.`correct_spelling_id` IS NOT NULL) 
    SET `observations`.`name_id` = `names`.`correct_spelling_id`
    

    I think this is the way to go.