MDEV-29919: Support INSERT ... AS alias ON DUPLICATE KEY UPDATE - #4544
MDEV-29919: Support INSERT ... AS alias ON DUPLICATE KEY UPDATE#4544MooSayed1 wants to merge 1 commit into
Conversation
d724e69 to
ff235ab
Compare
FooBarrior
left a comment
There was a problem hiding this comment.
Hi @MooSayed1! Thanks for your contribution.
I think this needs a better high-level detalization. What happens if a table has a field with the same name? This is a MySQL compatibility task -- so it's important to know what MySQL does in this case.
These details on the approach to name resolution will be important to be mentioned in the commit comment.
| When we are in ON DUPLICATE KEY UPDATE and the table qualifier matches | ||
| the insert_values_alias, we should resolve this as VALUES(column). | ||
| */ | ||
| if (select && |
There was a problem hiding this comment.
- No need to specify MDEV in the comments.
- It's enough to have a meaningful comment.
- In general, better not to refer to an exact sql_command value.
- We need to make sure the syntax can't be used in the unsupposed ways, like that REPLACE...AS would fail with a syntax error, and CREATE ... VALUES would.
- I'm surprised you have to check
select. Why? - What does this code placement mean? it's in
if (!field), meaning that it would apply only if field is not early-provided[?], but beforefind_field_in_tables, meaning that it would supersede a name resolution?
There was a problem hiding this comment.
first thanks for the review
then
Q1, Q2, Q3: Got it, will fix all of these.
Q4: Currently the grammar allows REPLACE ... AS alias to parse (since table_value_constructor is shared), but it's ignored at runtime because duplicates != DUP_UPDATE. but i think it would be better to get a syntax error at parse time instead of silent acceptance? and maybe adding tests for these scenarios
Q5: The select check was me being cautious - ensuring we have a valid query context. It's probably redundant.
Q6: It's placed before find_field_in_tables() so the alias takes priority. If a table and alias share the same name, new.b should resolve to the alias (inserted value), not the table. This matches MySQL behavior.
and should we continue the discussion here or move to Zulip?
There was a problem hiding this comment.
This matches MySQL behavior.
Please show the related MySQL output
There was a problem hiding this comment.
MariaDB> SELECT VERSION();
+----------------------+
| VERSION() |
+----------------------+
| 12.3.0-MariaDB-debug |
+----------------------+
MariaDB> CREATE TABLE t1 (a INT PRIMARY KEY, b INT, c INT);
MariaDB> INSERT INTO t1 VALUES (1, 10, 100);
MariaDB> SELECT * FROM t1;
+---+------+------+
| a | b | c |
+---+------+------+
| 1 | 10 | 100 |
+---+------+------+
MariaDB> INSERT INTO t1 VALUES (1, 20, 200) AS new
ON DUPLICATE KEY UPDATE b = new.b, c = new.c;
MariaDB> SELECT * FROM t1;
+---+------+------+
| a | b | c |
+---+------+------+
| 1 | 20 | 200 |
+---+------+------+
MariaDB> INSERT INTO t1 VALUES (1, 5, 50) AS new
ON DUPLICATE KEY UPDATE b = b + new.b, c = c + new.c;
MariaDB> SELECT * FROM t1;
+---+------+------+
| a | b | c |
+---+------+------+
| 1 | 25 | 250 |
+---+------+------+```There was a problem hiding this comment.
I was asking about
If a table and alias share the same name, new.b should resolve to the alias (inserted value), not the table.
There was a problem hiding this comment.
oh okay mb
MySQL 8.0:
mysql> CREATE TABLE new (a INT PRIMARY KEY, b INT);
mysql> INSERT INTO new VALUES (1, 999);
mysql> CREATE TABLE t1 (a INT PRIMARY KEY, b INT);
mysql> INSERT INTO t1 VALUES (1, 10);
mysql> INSERT INTO t1 VALUES (1, 50) AS new ON DUPLICATE KEY UPDATE b = new.b;
mysql> SELECT * FROM t1;
+---+------+
| a | b |
+---+------+
| 1 | 50 | -- Alias wins (not 999 from table)
+---+------+MariaDB the new implementation
MariaDB> -- Same test
MariaDB> INSERT INTO t1 VALUES (1, 50) AS new ON DUPLICATE KEY UPDATE b = new.b;
MariaDB> SELECT * FROM t1;
+---+------+
| a | b |
+---+------+
| 1 | 50 | -- Alias wins too Same as MySQL
+---+------+There was a problem hiding this comment.
I think new couldn't be a table reference in the same request with no/different alias anyway.
There was a problem hiding this comment.
Can you clarify this further?
From what I understand, my approach is incorrect when it comes to treating this as an alias. For example, consider the following case:
INSERT INTO t1 VALUES (1, 10);
SELECT * FROM t1;
INSERT INTO t1 VALUES (1, 50) AS t1
ON DUPLICATE KEY UPDATE b = t1.b;In this situation, I should get a syntax error. However, with my current implementation, it continues without any issues and incorrectly treats the newly inserted row as a valid table alias.
There was a problem hiding this comment.
@FooBarrior do u prefer making a new commit to fix this or u prefer doing only one commit to get the issue done?
43a115c to
4dcef93
Compare
FooBarrior
left a comment
There was a problem hiding this comment.
Please add test with CREATE..VALUES AS ident and REPLACE...VALUES AS ident producing a syntax error
| ); | ||
|
|
||
| --echo # | ||
| --echo # Test 1: Basic INSERT AS alias ON DUPLICATE KEY UPDATE |
There was a problem hiding this comment.
Let's remove the numbers: if we'll add some test in the middle, we'll have to renumerate everything.
Just:
--echo # Basic INSERT AS alias ON DUPLICATE KEY UPDATE
| ensures the alias takes priority over any existing table with | ||
| the same name, matching MySQL behavior. | ||
| */ | ||
| if (thd->lex->duplicates == DUP_UPDATE && |
There was a problem hiding this comment.
I think this line should be rather an assertion inside the if. If we've got to here with lex->insert_values_alias.str, then it should only be a valid case.
| SELECT * FROM t1; | ||
|
|
||
| --echo # | ||
| --echo # Test 4: Backward compatibility - VALUES() function still works |
There was a problem hiding this comment.
No need in this one. VALUES (a) is tested even in main.insert.
| --echo # | ||
| --echo # Test 9: Alias takes priority over existing table with same name | ||
| --echo # | ||
| DROP TABLE IF EXISTS new_values; |
| /* | ||
| Create a field reference without the alias qualifier, | ||
| then wrap it in Item_insert_value to get the inserted value (basically converting alis.column to Values(column)). | ||
| Use change_item_tree() for prepared statement compatibility (this's passing ps mode testing). |
There was a problem hiding this comment.
Remove the comment as it doesn't complement the code with any new information (or change it to do so, without stating what's written in the code).
|
|
||
| enum enum_duplicates duplicates; | ||
| /* Alias for INSERT ... To support the new syntax as MySQL */ | ||
| LEX_CSTRING insert_values_alias; |
There was a problem hiding this comment.
Make it a Lex_ident derivative as a modern alternative.
| const char *clause_that_disallows_subselect; | ||
|
|
||
| enum enum_duplicates duplicates; | ||
| /* Alias for INSERT ... To support the new syntax as MySQL */ |
There was a problem hiding this comment.
/* Represents INSERT...VALUES as <alias> */
No need to mention MySQL "new" syntax, available yet in MySQL 8.0.19 dated year 2020 (6 years ago)
| */ | ||
| opt_values_row_alias: | ||
| { | ||
| Lex->insert_values_alias= null_clex_str; // if AS..alias provided after Values() |
There was a problem hiding this comment.
remove junk (and partially false) comments
| @@ -0,0 +1,106 @@ | |||
| # | |||
| # Test for INSERT ... VALUES (...) AS alias ON DUPLICATE KEY UPDATE syntax | |||
There was a problem hiding this comment.
I think this test can be moved to main.insert (@vuvova agree?). ODKU is also covered there.
There was a problem hiding this comment.
Add MDEV and jira title with --echo #
4dcef93 to
cdc6fe5
Compare
|
@FooBarrior Done. |
FooBarrior
left a comment
There was a problem hiding this comment.
Please be more careful with making changes. MariaDB is used on millions of machines, and we pay extra attention to the code quality. I hope you made that mistake just in a hurry.
| table_name.str && | ||
| table_name.streq(thd->lex->insert_values_alias)) | ||
| { | ||
| DBUG_ASSERT(thd->lex->insert_values_alias.str); |
There was a problem hiding this comment.
Just to confirm: should I remove the condition (thd->lex->duplicates == DUP_UPDATE) and instead move it into a DBUG_ASSERT inside the if block?
| TRUNCATE TABLE t1; | ||
| INSERT INTO t1 VALUES (1, 10, 100); | ||
| --error ER_PARSE_ERROR | ||
| REPLACE INTO t1 VALUES (1, 50, 500) AS new ON DUPLICATE KEY UPDATE b = new.b; |
There was a problem hiding this comment.
ON DUPLICATE KEY UPDATE was never possible for REPLACE, so that's out of question.
There was a problem hiding this comment.
Thanks for the clarification. I realized my previous test included ON DUPLICATE KEY UPDATE with REPLACE, which caused a syntax error before even reaching the alias check.
I have updated the test to simply: REPLACE INTO t1 VALUES (...) AS new;
This should correctly verify that the alias syntax is rejected for REPLACE statements. Does this look correct to you?
|
|
||
| enum enum_duplicates duplicates; | ||
| /* Represents INSERT...VALUES as <alias> */ | ||
| Lex_ident_sys_st insert_values_alias; |
There was a problem hiding this comment.
can you plase explain the reason to choose Lex_ident_sys_st
There was a problem hiding this comment.
this's what i understand from this comment
Make it a Lex_ident derivative as a modern alternative.
when i searched i found this and i though it's the same as u mean
and it was matching what the parser's ident rule returns
|
@FooBarrior i used this |
6e19710 to
a02f27c
Compare
a2dba06 to
9a0e367
Compare
314e028 to
825d169
Compare
f809d5f to
910eabc
Compare
| { | ||
| if ($1.str) | ||
| { | ||
| if (Lex->sql_command != SQLCOM_INSERT) |
There was a problem hiding this comment.
I actually realized that common INSERT path goes the same way as CREATE, and table_value_constructor is tied deeply into create_select_query_expression, to which insert_values basically equals, so we really cannot avoid referring to Lex->sql_command, as I was requesting before. So, I admit my incorrectness.
There was a problem hiding this comment.
No problem, thanks for all the guidance along the way,I learned a lot.
Thank for reviewing too.
FooBarrior
left a comment
There was a problem hiding this comment.
The changes look good and complete in their functionality.
910eabc to
c5bbf25
Compare
gkodinov
left a comment
There was a problem hiding this comment.
Hello, this is a preliminary review.
Can you please make sure that:
- the buildbot tests are passing or failing in an unrelated way: currently your new test is failing on some platforms.
- Your commit has a commit message as mandated by the MariaDB coding standards document.
|
@gkodinov For the buildbot failure — it's the case sensitivity test in insert_update_alias. The test defines alias nEw but references new.b, expecting ER_BAD_FIELD_ERROR. This works on Linux where table_alias_charset is case-sensitive, but on Windows (lower_case_table_names=1) the comparison is case-insensitive so nEw matches new and the statement succeeds instead of error. |
c5bbf25 to
b048f86
Compare
gkodinov
left a comment
There was a problem hiding this comment.
LGTM. Let me ping Serg if he wants to review or not.
|
he does. Wait for the final review please. |
There was a problem hiding this comment.
Pull request overview
Adds MySQL 8-style row-alias support for INSERT ... VALUES ... AS alias ON DUPLICATE KEY UPDATE, enabling alias.col references as a cleaner alternative to VALUES(col).
Changes:
- Extends the
VALUEStable value constructor grammar to accept an optional alias and stores it inLEX. - Resolves
alias.colinON DUPLICATE KEY UPDATEby rewriting it to anItem_insert_value(VALUES-like) reference. - Adds MTR coverage for the new syntax and a few negative/compatibility cases.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sql/sql_yacc.yy | Parses optional row alias after VALUES and stores it in Lex->insert_values_alias. |
| sql/sql_lex.h | Adds LEX::insert_values_alias storage. |
| sql/sql_lex.cc | Initializes insert_values_alias in LEX::start(). |
| sql/sql_insert.cc | Rejects row alias equal to target table name for DUP_UPDATE to avoid ambiguity. |
| sql/item.cc | Rewrites alias.col to Item_insert_value during name resolution. |
| mysql-test/main/insert_update_alias.test | New test coverage for alias syntax and error cases. |
| mysql-test/main/insert_update_alias.result | Expected output for the new test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| opt_values_row_alias: | ||
| select_alias | ||
| { | ||
| if ($1.str) | ||
| { | ||
| if (Lex->sql_command != SQLCOM_INSERT) | ||
| my_yyabort_error((ER_SYNTAX_ERROR, MYF(0))); | ||
| Lex->insert_values_alias= Lex_ident_table($1); | ||
| } | ||
| else | ||
| Lex->insert_values_alias= Lex_ident_table(); | ||
| } | ||
| ; |
There was a problem hiding this comment.
Also, consider this case
insert t1 select * from (values (1,2,3) as new) as t2 on duplicate key update b=new.c;this works, but shouldn't. Basically detection with Lex->sql_command is incorrect.
| /* Handle INSERT ... VALUES (...) AS alias ON DUPLICATE KEY UPDATE */ | ||
| if (table_name.str && | ||
| table_name.streq(thd->lex->insert_values_alias)) | ||
| { | ||
| DBUG_ASSERT(thd->lex->duplicates == DUP_UPDATE); | ||
| Item_field *field_ref= new (thd->mem_root) | ||
| Item_field(thd, context, db_name, Lex_cstring_strlen(NULL), field_name); | ||
| if (!field_ref) |
vuvova
left a comment
There was a problem hiding this comment.
forgot to change the status. see comments above.
|
no problem okay i'll see it. |
b048f86 to
eb1d7b3
Compare
|
Thanks. I'll review. But just to set expectations — 13.1 deadline has passed, 13.2 deadline is in three months. |
|
no problem it's fully understandable take your time. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
sql/sql_yacc.yy:9191
- The comment says the syntax is strictly
... VALUES (...) AS alias, but the rule usesselect_aliaswhich also accepts an alias withoutAS. Either requireASin the grammar, or update the comment to reflect the actual accepted syntax.
Optional alias for the row(s) being inserted.
Syntax: INSERT ... VALUES (...) AS alias
Used to reference new values in ON DUPLICATE KEY UPDATE clause.
sql/item.cc:6490
alias.colis only converted toItem_insert_valuewhenthd->where == THD_WHERE::UPDATE_CLAUSE. Inside subqueries within the ON DUPLICATE KEY UPDATE expression,thd->wherewill typically be WHERE/HAVING/etc, so the alias won’t resolve even thoughVALUES(col)works in those contexts. To keep the alias syntax equivalent toVALUES(), gate this conversion by "not in VALUES clause" instead of "is UPDATE_CLAUSE".
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))
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.
eb1d7b3 to
9cf3efe
Compare
MySQL 8.0.20 introduced a new syntax for
INSERT ... ON DUPLICATE KEY UPDATEthat allows referencing the inserted row using an alias instead of theVALUES()function:The alias syntax is cleaner and more readable, especially when referencing multiple columns or using expressions like
new.a + new.b.Implementation
opt_values_row_aliasrule to acceptAS aliasafter VALUESinsert_values_aliasfield to store the aliasItem_field::fix_fields(), when we're in ON DUPLICATE KEY UPDATE and the table qualifier matches the alias, we convert the reference to anItem_insert_value(equivalent toVALUES())The traditional
VALUES()function continues to work unchanged.Tests
Added
mysql-test/main/insert_update_alias.testcovering:INSERT ... AS alias ON DUPLICATE KEY UPDATEnew.a + new.b)VALUES()function