From 9cf3efe45ceb15aadf3457190a710228864c9b8c Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 24 Jan 2026 00:45:15 +0200 Subject: [PATCH] MDEV-29919 Support INSERT ... VALUES AS alias ON DUPLICATE KEY UPDATE Implement MySQL 8.0.19 compatible row alias syntax for INSERT ... ON DUPLICATE KEY UPDATE. The alias allows referencing inserted values by name instead of VALUES(): INSERT INTO t1 VALUES (1,2) AS new ON DUPLICATE KEY UPDATE b = new.b; Parser: added opt_values_row_alias rule in sql_yacc.yy to accept AS alias after VALUES clause. The AS keyword is required and the alias must be a plain identifier, as in MySQL. The alias is accepted only when the VALUES clause is the direct value source of the INSERT, that is, when its select sits directly below the INSERT top select on the parse stack. This rejects a VALUES table value constructor used as a derived table, e.g. INSERT t1 SELECT * FROM (VALUES (1,2,3) AS new) AS t2 ON DUPLICATE KEY UPDATE b = new.c; which is a syntax error rather than silently treating new as an INSERT row alias. When no alias is present the rule leaves insert_values_alias unchanged, so a VALUES constructor in a subquery of the same INSERT does not clobber the row alias. The alias is reset once per statement in LEX::start(). LEX: added insert_values_alias to store the alias name. Name resolution: in Item_field::fix_fields(), when resolving an ON DUPLICATE KEY UPDATE value (thd->where == UPDATE_CLAUSE and duplicates == DUP_UPDATE) and the qualifier matches the row alias, the reference is converted to Item_insert_value (equivalent to VALUES()). To make this gate reliable, the ODKU update values for INSERT ... VALUES are now set up with THD_WHERE::UPDATE_CLAUSE. The alias must differ from the target table name to avoid ambiguity (ER_NONUNIQ_TABLE). Alias matching uses table_alias_charset, consistent with how table aliases are compared. The alias currently resolves only in expressions directly in the ON DUPLICATE KEY UPDATE list, not inside subqueries there; full VALUES() equivalence in subqueries is left to a follow-up task. --- mysql-test/main/insert_update_alias.result | 138 +++++++++++++++++++++ mysql-test/main/insert_update_alias.test | 136 ++++++++++++++++++++ sql/item.cc | 19 +++ sql/sql_insert.cc | 14 ++- sql/sql_lex.cc | 1 + sql/sql_lex.h | 2 + sql/sql_yacc.yy | 33 ++++- 7 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 mysql-test/main/insert_update_alias.result create mode 100644 mysql-test/main/insert_update_alias.test diff --git a/mysql-test/main/insert_update_alias.result b/mysql-test/main/insert_update_alias.result new file mode 100644 index 0000000000000..4d1cfc52dcc4d --- /dev/null +++ b/mysql-test/main/insert_update_alias.result @@ -0,0 +1,138 @@ +# +# MDEV-29919: Support INSERT ... VALUES AS alias ON DUPLICATE KEY UPDATE +# +# +# Test setup +# +CREATE TABLE t1 ( +a INT PRIMARY KEY, +b INT, +c INT +); +# +# Basic INSERT AS alias ON DUPLICATE KEY UPDATE +# +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new +ON DUPLICATE KEY UPDATE b = new.b, c = new.c; +SELECT * FROM t1; +a b c +1 20 200 +# +# Multiple rows with AS alias +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200), (2, 30, 300) AS new +ON DUPLICATE KEY UPDATE b = new.b; +SELECT * FROM t1 ORDER BY a; +a b c +1 20 100 +2 30 300 +# +# Expression using alias columns +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 5, 50) AS new +ON DUPLICATE KEY UPDATE b = new.b + new.c, c = new.a * 10; +SELECT * FROM t1; +a b c +1 55 10 +# +# Mix of alias and table column references +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new +ON DUPLICATE KEY UPDATE b = new.b, c = t1.c + new.c; +SELECT * FROM t1; +a b c +1 20 300 +# +# INSERT without ON DUPLICATE KEY (alias should be ignored) +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100) AS new; +SELECT * FROM t1; +a b c +1 10 100 +# +# Different alias names +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 99, 999) AS inserted_row +ON DUPLICATE KEY UPDATE b = inserted_row.b, c = inserted_row.c; +SELECT * FROM t1; +a b c +1 99 999 +# +# The AS keyword is required for the row alias +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) new +ON DUPLICATE KEY UPDATE b = new.b; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'new +ON DUPLICATE KEY UPDATE b = new.b' at line 1 +INSERT INTO t1 VALUES (1, 20, 200) AS 'new' + ON DUPLICATE KEY UPDATE b = new.b; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''new' + ON DUPLICATE KEY UPDATE b = new.b' at line 1 +INSERT INTO t1 VALUES (1, 20, 200) 'new' + ON DUPLICATE KEY UPDATE b = new.b; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''new' + ON DUPLICATE KEY UPDATE b = new.b' at line 1 +SELECT * FROM t1; +a b c +1 10 100 +# +# Alias cannot be the same as the target table name +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 50, 500) AS t1 +ON DUPLICATE KEY UPDATE b = t1.b; +ERROR 42000: Not unique table/alias: 't1' +# +# A VALUES alias inside a derived table is not an INSERT row alias +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 SELECT * FROM (VALUES (1, 2, 3) AS new) AS t2 +ON DUPLICATE KEY UPDATE b = new.c; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +SELECT * FROM t1; +a b c +1 10 100 +# +# A VALUES constructor in an ON DUPLICATE KEY UPDATE subquery must not +# clobber the row alias of the outer INSERT +# +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new +ON DUPLICATE KEY UPDATE +b = new.b + (SELECT COUNT(*) FROM (VALUES (5),(9)) AS dt), +c = new.c; +SELECT * FROM t1; +a b c +1 22 200 +# +# Verify that AS alias is NOT allowed in REPLACE +# +REPLACE INTO t1 VALUES (1, 50, 500) AS new; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +# +# Verify that AS alias is NOT allowed in CREATE ... VALUES +# +CREATE TABLE t2 AS VALUES (1, 10, 100) AS new; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use +# +# Cleanup +# +DROP TABLE t1; +# +# End of tests +# diff --git a/mysql-test/main/insert_update_alias.test b/mysql-test/main/insert_update_alias.test new file mode 100644 index 0000000000000..8afcbef351cca --- /dev/null +++ b/mysql-test/main/insert_update_alias.test @@ -0,0 +1,136 @@ +--echo # +--echo # MDEV-29919: Support INSERT ... VALUES AS alias ON DUPLICATE KEY UPDATE +--echo # + +# +# This test validates the row alias syntax for INSERT ON DUPLICATE KEY UPDATE +# which allows referencing inserted values using alias.column instead of VALUES(column) +# + +--echo # +--echo # Test setup +--echo # +CREATE TABLE t1 ( + a INT PRIMARY KEY, + b INT, + c INT +); + +--echo # +--echo # Basic INSERT AS alias ON DUPLICATE KEY UPDATE +--echo # +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new + ON DUPLICATE KEY UPDATE b = new.b, c = new.c; +SELECT * FROM t1; + +--echo # +--echo # Multiple rows with AS alias +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200), (2, 30, 300) AS new + ON DUPLICATE KEY UPDATE b = new.b; +SELECT * FROM t1 ORDER BY a; + +--echo # +--echo # Expression using alias columns +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 5, 50) AS new + ON DUPLICATE KEY UPDATE b = new.b + new.c, c = new.a * 10; +SELECT * FROM t1; + +--echo # +--echo # Mix of alias and table column references +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new + ON DUPLICATE KEY UPDATE b = new.b, c = t1.c + new.c; +SELECT * FROM t1; + +--echo # +--echo # INSERT without ON DUPLICATE KEY (alias should be ignored) +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100) AS new; +SELECT * FROM t1; + +--echo # +--echo # Different alias names +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 99, 999) AS inserted_row + ON DUPLICATE KEY UPDATE b = inserted_row.b, c = inserted_row.c; +SELECT * FROM t1; + +--echo # +--echo # The AS keyword is required for the row alias +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +--error ER_PARSE_ERROR +INSERT INTO t1 VALUES (1, 20, 200) new + ON DUPLICATE KEY UPDATE b = new.b; +--error ER_PARSE_ERROR +INSERT INTO t1 VALUES (1, 20, 200) AS 'new' + ON DUPLICATE KEY UPDATE b = new.b; +--error ER_PARSE_ERROR +INSERT INTO t1 VALUES (1, 20, 200) 'new' + ON DUPLICATE KEY UPDATE b = new.b; +SELECT * FROM t1; + +--echo # +--echo # Alias cannot be the same as the target table name +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +--error ER_NONUNIQ_TABLE +INSERT INTO t1 VALUES (1, 50, 500) AS t1 + ON DUPLICATE KEY UPDATE b = t1.b; + +--echo # +--echo # A VALUES alias inside a derived table is not an INSERT row alias +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +--error ER_SYNTAX_ERROR +INSERT INTO t1 SELECT * FROM (VALUES (1, 2, 3) AS new) AS t2 + ON DUPLICATE KEY UPDATE b = new.c; +SELECT * FROM t1; + +--echo # +--echo # A VALUES constructor in an ON DUPLICATE KEY UPDATE subquery must not +--echo # clobber the row alias of the outer INSERT +--echo # +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1, 10, 100); +INSERT INTO t1 VALUES (1, 20, 200) AS new + ON DUPLICATE KEY UPDATE + b = new.b + (SELECT COUNT(*) FROM (VALUES (5),(9)) AS dt), + c = new.c; +SELECT * FROM t1; + +--echo # +--echo # Verify that AS alias is NOT allowed in REPLACE +--echo # +--error ER_SYNTAX_ERROR +REPLACE INTO t1 VALUES (1, 50, 500) AS new; + +--echo # +--echo # Verify that AS alias is NOT allowed in CREATE ... VALUES +--echo # +--error ER_SYNTAX_ERROR +CREATE TABLE t2 AS VALUES (1, 10, 100) AS new; + +--echo # +--echo # Cleanup +--echo # +DROP TABLE t1; + +--echo # +--echo # End of tests +--echo # diff --git a/sql/item.cc b/sql/item.cc index e642344e29f75..277c4160082a0 100644 --- a/sql/item.cc +++ b/sql/item.cc @@ -6481,6 +6481,25 @@ bool Item_field::fix_fields(THD *thd, Item **reference) if (!field) // If field is not checked { TABLE_LIST *table_list; + + /* Handle INSERT ... VALUES (...) AS alias ON DUPLICATE KEY UPDATE */ + if (thd->where == THD_WHERE::UPDATE_CLAUSE && + thd->lex->duplicates == DUP_UPDATE && + thd->lex->insert_values_alias.str && + table_name.str && + table_name.streq(thd->lex->insert_values_alias)) + { + Item_field *field_ref= new (thd->mem_root) + Item_field(thd, context, db_name, Lex_cstring_strlen(NULL), field_name); + if (!field_ref) + return TRUE; + Item_insert_value *ins_val= new (thd->mem_root) + Item_insert_value(thd, context, field_ref); + if (!ins_val) + return TRUE; + thd->change_item_tree(reference, ins_val); + return ins_val->fix_fields(thd, reference); + } /* In case of view, find_field_in_tables() write pointer to view field expression to 'reference', i.e. it substitute that expression instead diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index ea23c19d0bf95..926a35ee18525 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -1825,6 +1825,17 @@ int mysql_prepare_insert(THD *thd, TABLE_LIST *table_list, if (duplic == DUP_UPDATE) { + /* + The row alias for INSERT ... AS alias cannot be the same as the + target table name, as this would make column references ambiguous. + */ + if (thd->lex->insert_values_alias.str && + table_list->table_name.streq(thd->lex->insert_values_alias)) + { + my_error(ER_NONUNIQ_TABLE, MYF(0), thd->lex->insert_values_alias.str); + DBUG_RETURN(1); + } + /* it should be allocated before Item::fix_fields() */ if (table_list->set_insert_values(thd->mem_root)) DBUG_RETURN(1); @@ -1862,7 +1873,8 @@ int mysql_prepare_insert(THD *thd, TABLE_LIST *table_list, if (!res) res= setup_fields(thd, Ref_ptr_array(), - update_values, MARK_COLUMNS_READ, 0, NULL, 0); + update_values, MARK_COLUMNS_READ, 0, NULL, 0, + THD_WHERE::UPDATE_CLAUSE); if (!res && duplic == DUP_UPDATE) { diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 8fedc08645837..62942b1ffe239 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -1277,6 +1277,7 @@ void LEX::start(THD *thd_arg) part_info= 0; m_sql_cmd= NULL; duplicates= DUP_ERROR; + insert_values_alias= Lex_ident_table(); spname= NULL; spcont= NULL; proc_list.first= 0; diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 438f8824ab99f..bc681a9d1081e 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -3480,6 +3480,8 @@ struct LEX: public Query_tables_list const char *clause_that_disallows_subselect; enum enum_duplicates duplicates; + /* Represents INSERT...VALUES as */ + Lex_ident_table insert_values_alias; enum enum_tx_isolation tx_isolation; enum enum_ha_read_modes ha_read_mode; union { diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 21525c7990ccf..fecdfad7b8a00 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -9178,13 +9178,44 @@ table_value_constructor: if (Lex->parsed_TVC_start()) MYSQL_YYABORT; } - values_list + values_list opt_values_row_alias { if (!($$= Lex->parsed_TVC_end())) MYSQL_YYABORT; } ; +/* + Optional alias for the row(s) being inserted. + Syntax: INSERT ... VALUES (...) AS alias + Used to reference new values in ON DUPLICATE KEY UPDATE clause. + The AS keyword is required, as in MySQL. + + The empty alternative must not touch insert_values_alias: a VALUES + table value constructor without an alias may appear in a subquery of + the same INSERT, and clearing the alias here would clobber the row + alias set earlier. It is reset in LEX::start(). +*/ +opt_values_row_alias: + /* empty */ + | AS ident + { + /* + The row alias is only valid as the direct value source of an + INSERT, i.e. INSERT ... VALUES (...) AS alias. The VALUES select + must sit directly below the INSERT's top select on the parse + stack. This excludes a VALUES table value constructor used as a + derived table, e.g. INSERT ... SELECT * FROM (VALUES (...) AS x). + */ + SELECT_LEX *parent= Lex->select_stack_top >= 2 ? + Lex->select_stack[Lex->select_stack_top - 2] : nullptr; + if (Lex->sql_command != SQLCOM_INSERT || + parent != Lex->first_select_lex()) + my_yyabort_error((ER_SYNTAX_ERROR, MYF(0))); + Lex->insert_values_alias= Lex_ident_table($2); + } + ; + opt_hint_comment: /*empty */ { $$.init(); } | HINT_COMMENT { $$= $1; }