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

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?

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.

Related

Update query based on two conditions

I have the following table based on this query:
SELECT
repName.repID, repName.Rep_Name, repName.Job_Code, GenItems.Item_Name,
repName.Entered
FROM
GenItems
INNER JOIN repName
ON GenItems.Job_Code = repName.Job_Code
ORDER BY
repName.Rep_Name
I want to add an update routine to it. I want to update the entered field if the user entry matches the rep.ID and the Item Name. and finally return the Max value for the Entered field. Can I add this to this query or is it better to write another.
I just started working with sql, so if my questions seems basic, please forgive me. I am self taught and stumbling greatly.
Thank you
I don't understand fully your question.
You are showing us a SELECT statement. It can only be used to return a table-like result. If you want to upate a table you must use an UPDATE query. For the SQL-Server (and SQL CE) the query looks like this:
UPDATE repName
SET repName.Entered = x
FROM
GenItems
INNER JOIN repName
ON GenItems.Job_Code = repName.Job_Code
WHERE
repName.repID = x AND GenItems.Item_Name = 'y'
The difficulty is that tables have to be joined in the UPDATE statement. This not supported in Oracle for instance, where you have to do it with sub-selects.

Why won't this merge statement work?

I've spent the better part of the day trying to determine why a merge statement won't work and I'm starting to think the problem must be something a bit exotic.
My database has dozens of PL/SQL procedures that use merge statements but I absolutely cannot get one in particular to work. Although it's much larger than the example shown, I've stripped it down so that it only updates a couple of columns and it still will not compile.
The error is 'ORA-00904 "alias"."column_name" invalid identifier'. This typically means that a column name was mistyped or, in the case of a merge, you are attempting to update a field that's used in a join. This is definately NOT the case. I've quadrupled-checked and the column names are right, they all exist and the format of the statement is exactly the same as what I'm using in many other place.
/**
Result: ORA-00904 "P"."SFDC_CUST_CONTACT_PK": invalid identifier
I'm certain that the table and column names are all correct.
If I join on any of the dozen or so other columns instead, I
get the exact same error.
Note: I'm NOT attempting to update the column that I join
against.
**/
merge into customer_contact c
using (select p.fax_number,
p.email
from sfdc_cust_contact_temp p
) p
on (p.sfdc_cust_contact_pk = c.sfdc_cust_contact_pk)
when matched then
update set
c.fax_number = p.fax_number,
c.email = p.email;
/***
This works fine on the same machine
**/
merge into customer_contact_legacy c
using (select ct.contact_legacy_pk,
ct.fax_number,
ct.email
from customer_contact_temp ct
) ct
on (upper(trim(ct.contact_legacy_pk)) = upper(trim(c.contact_legacy_pk)))
when matched then
update set
c.fax_number = ct.fax_number,
c.email = ct.email;
Any ideas what else could be wrong here? Could there be some type of corruption with the table?
The version is 10g.
It looks like your using clause is missing the column you're trying to join on.
Your code:
merge into customer_contact c
using (select p.fax_number,
p.email
from sfdc_cust_contact_temp p
) p
on (p.sfdc_cust_contact_pk = c.sfdc_cust_contact_pk)
Potential fix:
merge into customer_contact c
using (select p.sfdc_cust_contact_pk,
p.fax_number,
p.email
from sfdc_cust_contact_temp p
) p
on (p.sfdc_cust_contact_pk = c.sfdc_cust_contact_pk)

Multiple Query Writing in Single Query

I have two tables in Database , I need to select a field from one table and update it in another table with a condition where id is same .. Is it Possible to write in single query ???
This should work for you:
update storage
set storage.email = (select register.email
from register
where register.id = storage.id)
Yeah it is, you could do this for example:
UPDATE Origin SET DesiredColumn = NewValue
FROM Origin
JOIN NewTable ON Origin.Id = NewTable.Id
And guess the column names were like DesiredColumn in the updating table and NewValue in the table that holds the new value.
Yes, this is possible, although the syntax depends on the type of SQL you are using.
Here is an example for T-SQL (for Microsoft SQL Server)
UPDATE
S
SET
Email = R.Email
FROM
dbo.Register R
INNER JOIN dbo.Storage S
ON S.RegisterID = R.RegisterID

How write hibernate criteria for specific SQL?

i need write similar SQL by ICriteria:
Tables:
1Lvl -- 2Lvl -- 3Lvl
SQL:
SELECT * FROM 2Lvl
WHERE 2Lvl.1LvlFK in
(
SELECT 1Lvl.Id
FROM 3Lvl
JOIN 2Lvl ON 3Lvl.2LvlFK = 2Lvl.Id
JOIN 1Lvl ON 2Lvl.1LvlFK = 1Lvl.Id
WHERE 3Lvl.Id = 123
)
I'm sorry for so specific question, but I inherited project with Hibernate from exemployee and I can't still understand hibernate-criteria.
var subQuery = DetachedCriteria.For<Lvl3>("lvl3")
.CreateAlias("Lvl2", "sublvl2", JoinType.InnerJoin)
.CreateAlias("Lvl1", "lvl1", JoinType.InnerJoin)
.Add(Restrictions.EqProperty("sublvl2.Id", "lvl2.Id")
.Add(Restrictions.Eq("lvl3.Id", 123)
.SetProjection(Projections.Property("lvl1.Id"));
Session.CreateCriteria<Lvl2>("lvl2")
.Add(Subqueries.PropertyIn("Lvl1.Id", subQuery));
Should do the trick. I've made assumptions about what your entities are called since 1Lvl, etc.. are not valid C# identifiers. I've also assumed the primary key column of each table was Id. Also a note that this won't produce the exact SQL you're looking for, but it will get you the same result.

"ambiguous column name" in SQLite INNER JOIN

I have two tables in a SQLite DB, INVITEM and SHOPITEM. Their shared attribute is ItemId and I want to perform an INNER JOIN. Here's the query:
SELECT INVITEM.CharId AS CharId,
INVITEM.ItemId AS ItemId
FROM (INVITEM as INVITEM
INNER JOIN SHOPITEM AS SHOPITEM
ON SHOPITEM.ItemId = INVITEM.ItemId)
WHERE ItemId = 3;
SQLite doesn't like it :
SQL error: ambiguous column name: ItemId
The error goes away if I write WHERE INVITEM.ItemId = 3, but since the WHERE condition is more or less user-specified, I rather make it work without having to specify the table. NATURAL JOIN seems to solve the issue, but I'm not sure if the solution is general enough (ie I could use in this case, but I'm not sure if I can use in every case)
Any alternate SQL syntax that would fix the problem?
I would write this query this way:
SELECT i.CharId AS CharId, i.ItemId AS ItemId
FROM INVITEM as i INNER JOIN SHOPITEM AS s USING (ItemId)
WHERE i.ItemId = 3;
I'm using the USING (ItemId) syntax which is just a matter of taste. It's equivalent to ON (i.ItemID = s.ItemID).
But I resolved the ambiguity by qualifying i.ItemID in the WHERE clause. You would think this is unnecessary, since i.ItemID = s.ItemID. They're both equal by commutativity, so there's no semantic ambiguity. But apparently SQLite isn't smart enough to know that.
I don't like to use NATURAL JOIN. It's equivalent to an equi-join of every column that exists in both tables with the same name. I don't like to use this because I don't want it to compare columns that I don't want it to, simply because they have the same name.
I would steer clear of allowing the user to write SQL clauses directly. This is the source of SQL Injection vulnerabilities.
If you need the query to be flexible, try parsing the user's input and adding the appropriate where clause.
Here is some C# code to show the general idea
// from user input
string user_column = "ItemID";
string user_value = "3";
string sql = "SELECT INVITEM.CharId AS CharId, INVITEM.ItemId AS ItemId FROM (INVITEM as INVITEM INNER JOIN SHOPITEM AS SHOPITEM ON SHOPITEM.ItemId = INVITEM.ItemId) ";
if (user_column == "ItemID")
{
// using Int32.Parse here to prevent rubbish like "0 OR 1=1; --" being entered.
sql += string.Format("WHERE INVITEM.ItemID={0}",Int32.Parse(user_value));
}
Obviously if you're dealing with more than one clause, you'd have to substitute AND for WHERE in subsequent clauses.
Just change your column alias to something similar, but unique (such as ITEM_ID).