Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions sqlglot/dialects/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,3 +1334,53 @@ def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
@unsupported_args("this")
def currentschema_sql(self, expression: exp.CurrentSchema) -> str:
return self.func("SCHEMA")

def _update_join_clauses_sql(self, from_expression: exp.From) -> str:
def join_sql(node: exp.Expression) -> str:
if isinstance(node, exp.Join):
join = node.copy()
if not join.args.get("on") and not join.args.get("using"):
join.set("on", exp.true())
return self.sql(join)

table = node.copy()
nested_joins = list(table.args.get("joins") or [])
if nested_joins:
table.set("joins", None)

sql = [self.sql(exp.Join(this=table, on=exp.true()))]

for nested in nested_joins:
sql.append(join_sql(nested))

return "".join(sql)

joins: t.List[str] = []

if from_expression.this:
joins.append(join_sql(from_expression.this))

for extra in from_expression.expressions or []:
joins.append(join_sql(extra))

return "".join(joins)

def update_sql(self, expression: exp.Update) -> str:
this = self.sql(expression, "this")
set_sql = self.expressions(expression, flat=True)
from_expression = expression.args.get("from_") or expression.args.get("from")
join_sql = self._update_join_clauses_sql(from_expression) if from_expression else ""
where_sql = self.sql(expression, "where")
returning = self.sql(expression, "returning")
order = self.sql(expression, "order")
limit = self.sql(expression, "limit")
options = self.expressions(expression, key="options")
options = f" OPTION({options})" if options else ""

if self.RETURNING_END:
post_set_sql = f"{where_sql}{returning}"
else:
post_set_sql = f"{returning}{where_sql}"

sql = f"UPDATE {this}{join_sql} SET {set_sql}{post_set_sql}{order}{limit}{options}"
return self.prepend_ctes(expression, sql)
11 changes: 11 additions & 0 deletions tests/dialects/test_mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ def test_ddl(self):
self.validate_identity("ALTER TABLE t ALTER COLUMN c SET INVISIBLE")
self.validate_identity("ALTER TABLE t ALTER COLUMN c SET VISIBLE")

def test_update_from_to_join(self):
self.validate_all(
"UPDATE foo JOIN bar ON TRUE SET a = bar.a WHERE foo.id = bar.id",
read={
"postgres": "UPDATE foo SET a = bar.a FROM bar WHERE foo.id = bar.id",
},
write={
"mysql": "UPDATE foo JOIN bar ON TRUE SET a = bar.a WHERE foo.id = bar.id",
Copy link
Collaborator

@VaggelisD VaggelisD Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to work:

mysql> create table t1 as (select 3 as id);
Query OK, 1 row affected (0.015 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> create table t2 as (select 1 as id);
Query OK, 1 row affected (0.012 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> UPDATE t1 JOIN t2 ON TRUE SET id = t2.id;
ERROR 1052 (23000): Column 'id' in field list is ambiguous

We'd need to qualify all non-t2 columns with t1:

mysql> UPDATE t1 JOIN t2 ON TRUE SET t1.id = t2.id;
Query OK, 1 row affected (0.003 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t1;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.001 sec)

And the opposite does not work in Postgres to allow for the happy case of not having to qualify columns:

postgres> UPDATE t1 SET t1.id = t2.id FROM t2;
ERROR:  column "t1" of relation "t1" does not exist
LINE 1: UPDATE t1 SET t1.id = t2.id FROM t2;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be as simple as traversing all exp.Columns and setting col.table with from.this, given that Postgres assumes that t1's columns will all be unqualified while all other columns must be qualified.

This hypothesis will need more testing, of course

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated and tested the new generated sql against both MySQL and PG

},
Comment on lines +218 to +220
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this isn't correct Postgres it'd be better to remove the test; SQLGlot lives by the "garbage in, garbage out" rule so it's not our responsibility to cover such cases.

)

def test_identity(self):
self.validate_identity("SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_CALC_FOUND_ROWS * FROM t")
self.validate_identity("SELECT CAST(COALESCE(`id`, 'NULL') AS CHAR CHARACTER SET binary)")
Expand Down
2 changes: 1 addition & 1 deletion tests/dialects/test_teradata.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_update(self):
"UPDATE A FROM schema.tableA AS A, (SELECT col1 FROM schema.tableA GROUP BY col1) AS B SET col2 = '' WHERE A.col1 = B.col1",
write={
"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",
"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",
"mysql": "UPDATE A JOIN `schema`.tableA AS A ON TRUE JOIN (SELECT col1 FROM `schema`.tableA GROUP BY col1) AS B ON TRUE SET col2 = '' WHERE A.col1 = B.col1",
},
)

Expand Down