Skip to content

Commit efa77df

Browse files
authored
feat(mysql): translate UPDATE … FROM … syntax to UPDATE … JOIN … when generating MySQL (#6655)
* Fix multitable UPDATE for mysql * bugfix: change `from` detection in MySQL update_sql due to upstream changes, and lint * fix(mysql): improve update SQL generation to handle JOIN clauses correctly in UPDATE statements * fix(mysql): improve update SQL generation for MySql
1 parent 29cff1f commit efa77df

File tree

4 files changed

+66
-3
lines changed

4 files changed

+66
-3
lines changed

sqlglot/dialects/mysql.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,3 +1337,31 @@ def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
13371337
@unsupported_args("this")
13381338
def currentschema_sql(self, expression: exp.CurrentSchema) -> str:
13391339
return self.func("SCHEMA")
1340+
1341+
def _update_from_joins_sql(self, expression: exp.Update) -> t.Tuple[str, str]:
1342+
from_expr = expression.args.get("from_")
1343+
if not from_expr:
1344+
return ("", "")
1345+
1346+
# Qualify unqualified columns in SET clause with the target table
1347+
# MySQL requires qualified column names in multi-table UPDATE to avoid ambiguity
1348+
target_table = expression.this
1349+
if isinstance(target_table, exp.Table):
1350+
target_name = target_table.alias_or_name
1351+
for eq in expression.expressions:
1352+
col = eq.this
1353+
if isinstance(col, exp.Column) and not col.table:
1354+
col.set("table", exp.to_identifier(target_name))
1355+
1356+
table = from_expr.this
1357+
nested_joins = table.args.get("joins") or []
1358+
if nested_joins:
1359+
table.set("joins", None)
1360+
1361+
join_sql = self.sql(exp.Join(this=table, on=exp.true()))
1362+
for nested in nested_joins:
1363+
if not nested.args.get("on") and not nested.args.get("using"):
1364+
nested.set("on", exp.true())
1365+
join_sql += self.sql(nested)
1366+
1367+
return (join_sql, "")

sqlglot/generator.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2223,10 +2223,19 @@ def version_sql(self, expression: exp.Version) -> str:
22232223
def tuple_sql(self, expression: exp.Tuple) -> str:
22242224
return f"({self.expressions(expression, dynamic=True, new_line=True, skip_first=True, skip_last=True)})"
22252225

2226+
def _update_from_joins_sql(self, expression: exp.Update) -> t.Tuple[str, str]:
2227+
"""
2228+
Returns (join_sql, from_sql) for UPDATE statements.
2229+
- join_sql: placed after UPDATE table, before SET
2230+
- from_sql: placed after SET clause (standard position)
2231+
Dialects like MySQL override to convert FROM to JOIN syntax.
2232+
"""
2233+
return ("", self.sql(expression, "from_"))
2234+
22262235
def update_sql(self, expression: exp.Update) -> str:
22272236
this = self.sql(expression, "this")
2237+
join_sql, from_sql = self._update_from_joins_sql(expression)
22282238
set_sql = self.expressions(expression, flat=True)
2229-
from_sql = self.sql(expression, "from_")
22302239
where_sql = self.sql(expression, "where")
22312240
returning = self.sql(expression, "returning")
22322241
order = self.sql(expression, "order")
@@ -2237,7 +2246,7 @@ def update_sql(self, expression: exp.Update) -> str:
22372246
expression_sql = f"{returning}{from_sql}{where_sql}"
22382247
options = self.expressions(expression, key="options")
22392248
options = f" OPTION({options})" if options else ""
2240-
sql = f"UPDATE {this} SET {set_sql}{expression_sql}{order}{limit}{options}"
2249+
sql = f"UPDATE {this}{join_sql} SET {set_sql}{expression_sql}{order}{limit}{options}"
22412250
return self.prepend_ctes(expression, sql)
22422251

22432252
def values_sql(self, expression: exp.Values, values_as_table: bool = True) -> str:

tests/dialects/test_mysql.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,32 @@ def test_ddl(self):
194194
self.validate_identity("ALTER TABLE t ALTER COLUMN c SET INVISIBLE")
195195
self.validate_identity("ALTER TABLE t ALTER COLUMN c SET VISIBLE")
196196

197+
def test_update_from_to_join(self):
198+
# MySQL multi-table UPDATE requires qualified columns in SET to avoid ambiguity
199+
self.validate_all(
200+
"UPDATE foo JOIN bar ON TRUE SET foo.a = bar.a WHERE foo.id = bar.id",
201+
read={
202+
"postgres": "UPDATE foo SET a = bar.a FROM bar WHERE foo.id = bar.id",
203+
"mysql": "UPDATE foo JOIN bar ON TRUE SET foo.a = bar.a WHERE foo.id = bar.id",
204+
},
205+
)
206+
207+
# Multiple columns in SET clause
208+
self.validate_all(
209+
"UPDATE t1 JOIN t2 ON TRUE SET t1.id = t2.id, t1.name = t2.name WHERE t1.x = t2.x",
210+
read={
211+
"postgres": "UPDATE t1 SET id = t2.id, name = t2.name FROM t2 WHERE t1.x = t2.x",
212+
},
213+
)
214+
215+
# Already qualified columns in Postgres should remain qualified
216+
self.validate_all(
217+
"UPDATE t1 JOIN t2 ON TRUE SET t1.id = t2.id WHERE t1.x = t2.x",
218+
read={
219+
"postgres": "UPDATE t1 SET t1.id = t2.id FROM t2 WHERE t1.x = t2.x",
220+
},
221+
)
222+
197223
def test_identity(self):
198224
self.validate_identity("SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_CALC_FOUND_ROWS * FROM t")
199225
self.validate_identity("SELECT CAST(COALESCE(`id`, 'NULL') AS CHAR CHARACTER SET binary)")

tests/dialects/test_teradata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_update(self):
7070
"UPDATE A FROM schema.tableA AS A, (SELECT col1 FROM schema.tableA GROUP BY col1) AS B SET col2 = '' WHERE A.col1 = B.col1",
7171
write={
7272
"teradata": "UPDATE A FROM schema.tableA AS A, (SELECT col1 FROM schema.tableA GROUP BY col1) AS B SET col2 = '' WHERE A.col1 = B.col1",
73-
"mysql": "UPDATE A SET col2 = '' FROM `schema`.tableA AS A, (SELECT col1 FROM `schema`.tableA GROUP BY col1) AS B WHERE A.col1 = B.col1",
73+
"mysql": "UPDATE A JOIN `schema`.tableA AS A ON TRUE JOIN (SELECT col1 FROM `schema`.tableA GROUP BY col1) AS B ON TRUE SET A.col2 = '' WHERE A.col1 = B.col1",
7474
},
7575
)
7676

0 commit comments

Comments
 (0)