Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parser: fix the compatibility problem of UNION statement #6335

Merged
merged 29 commits into from
May 14, 2018
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dc2868d
Parser: fix issue-6092
spongedu Apr 21, 2018
aa67355
Merge branch 'master' into fix_6092
spongedu Apr 22, 2018
b85bb2d
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu Apr 23, 2018
e6bf537
Refine SelectStmt
spongedu Apr 24, 2018
de21834
Refine SelectStmt && UnionStmt. Part II
spongedu Apr 24, 2018
84edf85
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu Apr 24, 2018
74c27c2
Merge branch 'fix_6092' of github.com:spongedu/tidb into fix_6092
spongedu Apr 24, 2018
61e2c5d
code fmt refine
spongedu Apr 24, 2018
0a5569c
Code format refine
spongedu Apr 24, 2018
5a4048c
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu Apr 25, 2018
24975c2
Merge branch 'master' into fix_6092
zz-jason Apr 25, 2018
b67a426
Fix field text
spongedu Apr 25, 2018
64fcd32
Merge branch 'fix_6092' of github.com:spongedu/tidb into fix_6092
spongedu Apr 25, 2018
5f66d0e
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu Apr 25, 2018
7c06a20
Merge branch 'master' into fix_6092
ngaut Apr 26, 2018
a0bc389
Merge branch 'master' into fix_6092
zz-jason May 2, 2018
a180997
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu May 2, 2018
b95d9db
Merge branch 'fix_6092' of github.com:spongedu/tidb into fix_6092
spongedu May 2, 2018
a374b36
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu May 5, 2018
500d82f
Merge branch 'master' into fix_6092
zz-jason May 7, 2018
d3a2dac
Merge branch 'master' into fix_6092
shenli May 8, 2018
ae6ebef
Code refine
spongedu May 8, 2018
b815e95
Merge remote-tracking branch 'upstream/master' into fix_6092
spongedu May 8, 2018
06363c3
Merge branch 'fix_6092' of github.com:spongedu/tidb into fix_6092
spongedu May 8, 2018
e9d5880
Merge branch 'master' into fix_6092
zz-jason May 9, 2018
5cc74fb
Merge branch 'master' into fix_6092
shenli May 9, 2018
5b0ed35
Merge branch 'master' into fix_6092
shenli May 11, 2018
c5129e3
Merge branch 'master' into fix_6092
spongedu May 11, 2018
4ff00f9
Merge branch 'master' into fix_6092
zz-jason May 14, 2018
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
2 changes: 2 additions & 0 deletions executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ func (s *testSuite) TestUnion(c *C) {
tk.MustExec("create table t(a int)")
tk.MustExec("insert into t value(0),(0)")
tk.MustQuery("select 1 from (select a from t union all select a from t) tmp").Check(testkit.Rows("1", "1", "1", "1"))
tk.MustQuery("select 10 as a from dual union select a from t order by a desc limit 1 ").Check(testkit.Rows("10"))
Copy link
Contributor

Choose a reason for hiding this comment

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

The bug this PR fixed is that, order by a desc limit 1 belongs to union statement, but our old code thought
it belongs to the second select statement.

Copy link
Contributor Author

@spongedu spongedu May 8, 2018

Choose a reason for hiding this comment

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

Yes, the last order by a desc limit 1 is expected to belong to the whole union statement.

tk.MustQuery("select -10 as a from dual union select a from t order by a limit 1 ").Check(testkit.Rows("-10"))
Copy link
Contributor

Choose a reason for hiding this comment

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

This test will cause a CI error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ciscoxll The test failure seems not caused by this test. I can pass test on my dev machine using make dev

tk.MustQuery("select count(1) from (select a from t union all select a from t) tmp").Check(testkit.Rows("4"))

_, err := tk.Exec("select 1 from (select a from t limit 1 union all select a from t limit 1) tmp")
Expand Down
177 changes: 115 additions & 62 deletions parser/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,9 @@ import (
SelectStmtFieldList "SELECT statement field list"
SelectStmtLimit "SELECT statement optional LIMIT clause"
SelectStmtOpts "Select statement options"
SelectStmtPart1 "SELECT statement part 1"
Copy link
Member

Choose a reason for hiding this comment

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

What's the difference between part1/2/3?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are syntax segments that abstracted from SelectStmt that can be reused in UnionStmt, each of them corresponds to a syntax branch of SelectStmt.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about changing to a better name?
SelectStmtPart1 SelectStmtPart2 SelectStmtPart3 is not informative, maybe we can use
SelectStmtBasic SelectStmtFromDual SelectStmtFromTable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tiancaiamao agree

SelectStmtPart2 "SELECT statement part 2"
SelectStmtPart3 "SELECT statement part 3"
SelectStmtGroup "SELECT statement optional GROUP BY clause"
ShowTargetFilterable "Show target that can be filtered by WHERE or LIKE"
ShowDatabaseNameOpt "Show tables/columns statement database name option"
Expand Down Expand Up @@ -4075,96 +4078,107 @@ RollbackStmt:
$$ = &ast.RollbackStmt{}
}

SelectStmt:
"SELECT" SelectStmtOpts SelectStmtFieldList OrderByOptional SelectStmtLimit SelectLockOpt
SelectStmtPart1:
"SELECT" SelectStmtOpts SelectStmtFieldList
{
st := &ast.SelectStmt {
SelectStmtOpts: $2.(*ast.SelectStmtOpts),
Distinct: $2.(*ast.SelectStmtOpts).Distinct,
Fields: $3.(*ast.FieldList),
LockTp: $6.(ast.SelectLockType),
}
$$ = st
}

SelectStmtPart2:
SelectStmtPart1 FromDual WhereClauseOptional
{
st := $1.(*ast.SelectStmt)
lastField := st.Fields.Fields[len(st.Fields.Fields)-1]
if lastField.Expr != nil && lastField.AsName.O == "" {
src := parser.src
var lastEnd int
if $4 != nil {
lastEnd = yyS[yypt-2].offset-1
} else if $5 != nil {
lastEnd = yyS[yypt-1].offset-1
} else if $6 != ast.SelectLockNone {
lastEnd = yyS[yypt].offset-1
} else {
lastEnd = len(src)
if src[lastEnd-1] == ';' {
lastEnd--
}
}
lastField.SetText(src[lastField.Offset:lastEnd])
}
if $4 != nil {
st.OrderBy = $4.(*ast.OrderByClause)
lastEnd := yyS[yypt-1].offset-1
lastField.SetText(parser.src[lastField.Offset:lastEnd])
}
if $5 != nil {
st.Limit = $5.(*ast.Limit)
if $3 != nil {
st.Where = $3.(ast.ExprNode)
}
$$ = st
}
| "SELECT" SelectStmtOpts SelectStmtFieldList FromDual WhereClauseOptional SelectStmtLimit SelectLockOpt


SelectStmtPart3:
SelectStmtPart1 "FROM"
TableRefsClause WhereClauseOptional SelectStmtGroup HavingClause
{
st := &ast.SelectStmt {
SelectStmtOpts: $2.(*ast.SelectStmtOpts),
Distinct: $2.(*ast.SelectStmtOpts).Distinct,
Fields: $3.(*ast.FieldList),
LockTp: $7.(ast.SelectLockType),
st := $1.(*ast.SelectStmt)
st.From = $3.(*ast.TableRefsClause)
if st.SelectStmtOpts.TableHints != nil {
st.TableHints = st.SelectStmtOpts.TableHints
}
lastField := st.Fields.Fields[len(st.Fields.Fields)-1]
if lastField.Expr != nil && lastField.AsName.O == "" {
lastEnd := yyS[yypt-3].offset-1
lastEnd := parser.endOffset(&yyS[yypt-4])
lastField.SetText(parser.src[lastField.Offset:lastEnd])
}
if $4 != nil {
st.Where = $4.(ast.ExprNode)
}
if $5 != nil {
st.Where = $5.(ast.ExprNode)
st.GroupBy = $5.(*ast.GroupByClause)
}
if $6 != nil {
st.Limit = $6.(*ast.Limit)
st.Having = $6.(*ast.HavingClause)
}
$$ = st
}
| "SELECT" SelectStmtOpts SelectStmtFieldList "FROM"
TableRefsClause WhereClauseOptional SelectStmtGroup HavingClause OrderByOptional
SelectStmtLimit SelectLockOpt

SelectStmt:
SelectStmtPart1 OrderByOptional SelectStmtLimit SelectLockOpt
{
opts := $2.(*ast.SelectStmtOpts)
st := &ast.SelectStmt{
SelectStmtOpts: $2.(*ast.SelectStmtOpts),
Distinct: opts.Distinct,
Fields: $3.(*ast.FieldList),
From: $5.(*ast.TableRefsClause),
LockTp: $11.(ast.SelectLockType),
}
if opts.TableHints != nil {
st.TableHints = opts.TableHints
}
st := $1.(*ast.SelectStmt)
st.LockTp = $4.(ast.SelectLockType)
lastField := st.Fields.Fields[len(st.Fields.Fields)-1]
if lastField.Expr != nil && lastField.AsName.O == "" {
lastEnd := parser.endOffset(&yyS[yypt-7])
lastField.SetText(parser.src[lastField.Offset:lastEnd])
src := parser.src
var lastEnd int
if $2 != nil {
lastEnd = yyS[yypt-2].offset-1
} else if $3 != nil {
lastEnd = yyS[yypt-1].offset-1
} else if $4 != ast.SelectLockNone {
lastEnd = yyS[yypt].offset-1
} else {
lastEnd = len(src)
if src[lastEnd-1] == ';' {
lastEnd--
}
}
lastField.SetText(src[lastField.Offset:lastEnd])
}
if $6 != nil {
st.Where = $6.(ast.ExprNode)
if $2 != nil {
st.OrderBy = $2.(*ast.OrderByClause)
}
if $7 != nil {
st.GroupBy = $7.(*ast.GroupByClause)
if $3 != nil {
st.Limit = $3.(*ast.Limit)
}
if $8 != nil {
st.Having = $8.(*ast.HavingClause)
$$ = st
}
| SelectStmtPart2 SelectStmtLimit SelectLockOpt
{
st := $1.(*ast.SelectStmt)
st.LockTp = $3.(ast.SelectLockType)
if $2 != nil {
st.Limit = $2.(*ast.Limit)
}
if $9 != nil {
st.OrderBy = $9.(*ast.OrderByClause)
$$ = st
}
| SelectStmtPart3 OrderByOptional SelectStmtLimit SelectLockOpt
{
st := $1.(*ast.SelectStmt)
st.LockTp = $4.(ast.SelectLockType)
if $2 != nil {
st.OrderBy = $2.(*ast.OrderByClause)
}
if $10 != nil {
st.Limit = $10.(*ast.Limit)
if $3 != nil {
st.Limit = $3.(*ast.Limit)
}
$$ = st
}
Expand Down Expand Up @@ -4591,14 +4605,53 @@ SelectLockOpt:

// See https://dev.mysql.com/doc/refman/5.7/en/union.html
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By mistake..

UnionStmt:
UnionClauseList "UNION" UnionOpt SelectStmt
UnionClauseList "UNION" UnionOpt SelectStmtPart1 OrderByOptional SelectStmtLimit SelectLockOpt
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we extract a common SelectStmtPart which is SelectStmtPart1 | SelectStmtPart2 | SelectStmtPart3 ?
Then we can make UnionStmt more simpler here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SelectStmtPart1, SelectStmtPart2 and SelectStmtPart3 are parts of selectStmt. combining them into a new rule SelectStmtPart may cause reduce/reduce conflicts on rule SelectStmtPart and SelectStmt, I think. Besides, SelectStmtPart2 is significant different from the other two and can't be combined.

{
st := $4.(*ast.SelectStmt)
union := $1.(*ast.UnionStmt)
union.Distinct = union.Distinct || $3.(bool)
lastSelect := union.SelectList.Selects[len(union.SelectList.Selects)-1]
endOffset := parser.endOffset(&yyS[yypt-2])
endOffset := parser.endOffset(&yyS[yypt-5])
parser.setLastSelectFieldText(lastSelect, endOffset)
union.SelectList.Selects = append(union.SelectList.Selects, $4.(*ast.SelectStmt))
union.SelectList.Selects = append(union.SelectList.Selects, st)
if $5 != nil {
union.OrderBy = $5.(*ast.OrderByClause)
}
if $6 != nil {
union.Limit = $6.(*ast.Limit)
}
$$ = union
Copy link
Contributor

Choose a reason for hiding this comment

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

$7 is not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes. Since the order byclause and limit clause belong to the union statement, the SelectLockOpt belongs to the union statement, which means nothing according to SelectLockOpt definition in https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to add SelectLockOpt on both side of the union in this case.
What do you think? @coocood

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tiancaiamao what if the left SelectStmt have already set SelectLockOpt ?

Copy link
Contributor

Choose a reason for hiding this comment

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

begin txn1
select a from t where union (xxx whatever) for update

begin txn2
update a xxx
commit txn2

commit txn1 (If for update works on union, this operation should fail in TiDB)

Copy link
Contributor

Choose a reason for hiding this comment

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

We should promote to lock mode either SelectLock exists on left SelectStmt or UnionStmt.

The select lock have something to do with transaction isolation level.
TiDB's Snapshot Isolation read operation will not block write operation, but if for update is used, read operation is more strict and you can thought it use a lock to prevent write operation.
@spongedu

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should promote to lock mode either SelectLock exists on left SelectStmt or UnionStmt.

@tiancaiamao what this mean ? Fail to get your point..... @.@

BTW, I test these cases in MySQL 5.7.
In MySQL:
First, execute this in the first tx:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t  union select * from t2 for update;
+----+----+---------+
| id | tn | type    |
+----+----+---------+
|  1 |  5 | cat     |
|  2 |  6 | dog     |
|  3 |  7 | duck    |
|  4 | 29 | chicken |
|  4 | 99 | chicken |
+----+----+---------+
5 rows in set (0.01 sec)

Then, execute this in the second tx:

mysql> update t  set tn = 31 where id = 4;                                                                                                     Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update t2  set tn = 31 where id = 4;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

the FOR UPDATE statement here affect only the second SelectStmt.

Another condition:
First, execute this in the first tx:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t  union select * from t2 limit 100  for update;                                                                          +----+-----+---------+
| id | tn  | type    |
+----+-----+---------+
|  1 |   5 | cat     |
|  2 |   6 | dog     |
|  3 |   7 | duck    |
|  4 | 187 | chicken |
+----+-----+---------+
4 rows in set (0.01 sec)

Then, execute this in the second tx:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t2 set tn = 1 where id = 1;                                                                                                      Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update t set tn = 1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Both Update works. The FOR UPDATE statement here seems work on neither SelectStmt

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, there is some subtle difference about the transaction isolation between TiDB and MySQL.
It's hard to explain here. I'll update the implementation myself after this PR is merged.

}
| UnionClauseList "UNION" UnionOpt SelectStmtPart2 SelectStmtLimit SelectLockOpt
{
st := $4.(*ast.SelectStmt)
union := $1.(*ast.UnionStmt)
union.Distinct = union.Distinct || $3.(bool)
lastSelect := union.SelectList.Selects[len(union.SelectList.Selects)-1]
endOffset := parser.endOffset(&yyS[yypt-4])
parser.setLastSelectFieldText(lastSelect, endOffset)
union.SelectList.Selects = append(union.SelectList.Selects, st)
if $5 != nil {
union.Limit = $5.(*ast.Limit)
}
$$ = union
}
| UnionClauseList "UNION" UnionOpt SelectStmtPart3 OrderByOptional
SelectStmtLimit SelectLockOpt
{
st := $4.(*ast.SelectStmt)
union := $1.(*ast.UnionStmt)
union.Distinct = union.Distinct || $3.(bool)
lastSelect := union.SelectList.Selects[len(union.SelectList.Selects)-1]
endOffset := parser.endOffset(&yyS[yypt-5])
parser.setLastSelectFieldText(lastSelect, endOffset)
union.SelectList.Selects = append(union.SelectList.Selects, st)
if $5 != nil {
union.OrderBy = $5.(*ast.OrderByClause)
}
if $6 != nil {
union.Limit = $6.(*ast.Limit)
}
$$ = union
}
| UnionClauseList "UNION" UnionOpt '(' SelectStmt ')' OrderByOptional SelectStmtLimit
Expand Down