Skip to content

Commit bab4993

Browse files
authored
planner: support table aliases in FOR UPDATE OF Clause (#65532)
close #63035
1 parent 65363dc commit bab4993

File tree

4 files changed

+309
-3
lines changed

4 files changed

+309
-3
lines changed

pkg/planner/core/logical_plan_builder.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4090,12 +4090,21 @@ func (b *PlanBuilder) buildSelect(ctx context.Context, sel *ast.SelectStmt) (p b
40904090
// Besides, it will only lock the metioned in `of` part.
40914091
b.ctx.GetSessionVars().StmtCtx.LockTableIDs[tNameW.TableInfo.ID] = struct{}{}
40924092
}
4093-
dbName := getLowerDB(tName.Schema, b.ctx.GetSessionVars())
4093+
// Use the already-resolved DBInfo to derive the privilege-check DB name.
4094+
// For OF-alias targets, tName.Schema is empty; falling back to currentDB via getLowerDB would
4095+
// authorize against the wrong database when the aliased table lives in a different schema.
4096+
var dbName string
4097+
if tNameW.DBInfo != nil {
4098+
dbName = tNameW.DBInfo.Name.L
4099+
} else {
4100+
dbName = getLowerDB(tName.Schema, b.ctx.GetSessionVars())
4101+
}
4102+
40944103
var authErr error
40954104
if user := b.ctx.GetSessionVars().User; user != nil {
4096-
authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT with locking clause", user.AuthUsername, user.AuthHostname, tNameW.Name.L)
4105+
authErr = plannererrors.ErrTableaccessDenied.GenWithStackByArgs("SELECT with locking clause", user.AuthUsername, user.AuthHostname, tNameW.TableInfo.Name.L)
40974106
}
4098-
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DeletePriv|mysql.UpdatePriv|mysql.LockTablesPriv, dbName, tNameW.Name.L, "", authErr)
4107+
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DeletePriv|mysql.UpdatePriv|mysql.LockTablesPriv, dbName, tNameW.TableInfo.Name.L, "", authErr)
40994108
}
41004109
p, err = b.buildSelectLock(p, l)
41014110
if err != nil {

pkg/planner/core/preprocess.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func Preprocess(ctx context.Context, sctx sessionctx.Context, node *resolve.Node
132132
sctx: sctx,
133133
tableAliasInJoin: make([]map[string]any, 0),
134134
preprocessWith: &preprocessWith{cteCanUsed: make([]string, 0), cteBeforeOffset: make([]int, 0)},
135+
lockSelectCtxStack: make([]lockSelectCtx, 0),
135136
staleReadProcessor: staleread.NewStaleReadProcessor(ctx, sctx),
136137
varsMutable: make(map[string]struct{}),
137138
varsReadonly: make(map[string]struct{}),
@@ -238,6 +239,11 @@ type preprocessor struct {
238239
tableAliasInJoin []map[string]any
239240
preprocessWith *preprocessWith
240241

242+
// lockSelectCtxStack tracks lock-clause resolution state for each SELECT. Each level keeps both
243+
// the LockInfo TableName nodes that should skip normal preprocess resolution and the FROM-clause
244+
// table references collected during traversal for later lock-target binding.
245+
lockSelectCtxStack []lockSelectCtx
246+
241247
staleReadProcessor staleread.Processor
242248

243249
varsMutable map[string]struct{}
@@ -262,6 +268,9 @@ func (p *preprocessor) Enter(in ast.Node) (out ast.Node, skipChildren bool) {
262268
p.preprocessWith.cteStack = append(p.preprocessWith.cteStack, node.With.CTEs)
263269
}
264270
p.checkSelectNoopFuncs(node)
271+
// SelectStmt.Accept visits FROM before LockInfo, so one per-SELECT context can collect FROM
272+
// table refs during traversal and later bind LockInfo targets without re-walking the FROM tree.
273+
p.pushLockSelectCtx(node)
265274
case *ast.SetOprStmt:
266275
if node.With != nil {
267276
p.preprocessWith.cteStack = append(p.preprocessWith.cteStack, node.With.CTEs)
@@ -655,7 +664,19 @@ func (p *preprocessor) Leave(in ast.Node) (out ast.Node, ok bool) {
655664
p.err = plannererrors.ErrUnknownExplainFormat.GenWithStackByArgs(x.Format)
656665
}
657666
case *ast.TableName:
667+
// Skip resolving TableName nodes that come from locking clauses; they are bound later against FROM aliases.
668+
if lockCtx := p.getLockSelectCtxStackTop(); lockCtx != nil {
669+
if _, ok := lockCtx.lockClauseTables[x]; ok {
670+
break
671+
}
672+
}
658673
p.handleTableName(x)
674+
case *ast.TableSource:
675+
if lockCtx := p.getLockSelectCtxStackTop(); lockCtx != nil {
676+
if v, ok := x.Source.(*ast.TableName); ok {
677+
lockCtx.collect(v, x.AsName)
678+
}
679+
}
659680
case *ast.Join:
660681
if len(p.tableAliasInJoin) > 0 {
661682
p.tableAliasInJoin = p.tableAliasInJoin[:len(p.tableAliasInJoin)-1]
@@ -716,6 +737,10 @@ func (p *preprocessor) Leave(in ast.Node) (out ast.Node, ok bool) {
716737
if x.With != nil {
717738
p.preprocessWith.cteStack = p.preprocessWith.cteStack[0 : len(p.preprocessWith.cteStack)-1]
718739
}
740+
if lockCtx := p.getLockSelectCtxStackTop(); lockCtx != nil {
741+
p.checkLockClauseTables(x, lockCtx)
742+
p.popLockSelectCtx()
743+
}
719744
case *ast.SetOprStmt:
720745
if x.With != nil {
721746
p.preprocessWith.cteStack = p.preprocessWith.cteStack[0 : len(p.preprocessWith.cteStack)-1]
@@ -1800,6 +1825,174 @@ func (p *preprocessor) handleTableName(tn *ast.TableName) {
18001825
})
18011826
}
18021827

1828+
// lockRef records the FROM table and its alias (if any) for lock-clause name resolution.
1829+
type lockRef struct {
1830+
table *ast.TableName
1831+
alias ast.CIStr
1832+
}
1833+
1834+
type lockSelectCtx struct {
1835+
// lockClauseTables are the TableName nodes that syntactically belong to this SELECT's locking
1836+
// clause. They skip handleTableName because they are resolved later against the FROM clause.
1837+
lockClauseTables map[*ast.TableName]struct{}
1838+
aliasMap map[string]lockRef
1839+
qualifiedMap map[string]lockRef
1840+
// orderedRefs preserves the left-to-right FROM order for unqualified OF fallback.
1841+
// For example, `FROM db1.t, db2.t` records `db1.t` before `db2.t`, so fallback resolution for `FOR UPDATE OF t` will try `db1.t` first.
1842+
// When both aliased and unaliased entries share the same base name, checkLockClauseTables first
1843+
// searches orderedRefs for an exact unaliased match, and only then falls back to aliased entries
1844+
// for backward compatibility.
1845+
orderedRefs []lockRef
1846+
}
1847+
1848+
func newLockSelectCtx(lockTables []*ast.TableName) lockSelectCtx {
1849+
lockClauseTables := make(map[*ast.TableName]struct{}, len(lockTables))
1850+
for _, tn := range lockTables {
1851+
lockClauseTables[tn] = struct{}{}
1852+
}
1853+
return lockSelectCtx{
1854+
lockClauseTables: lockClauseTables,
1855+
aliasMap: make(map[string]lockRef),
1856+
qualifiedMap: make(map[string]lockRef),
1857+
orderedRefs: make([]lockRef, 0),
1858+
}
1859+
}
1860+
1861+
func (c *lockSelectCtx) collect(tableName *ast.TableName, asName ast.CIStr) {
1862+
ref := lockRef{
1863+
table: tableName,
1864+
alias: asName,
1865+
}
1866+
if asName.L != "" {
1867+
c.aliasMap[asName.L] = ref
1868+
}
1869+
if tableName.Schema.L != "" {
1870+
qualifiedKey := tableName.Schema.L + "." + tableName.Name.L
1871+
// Prefer an exact unaliased `schema.table` entry. If only aliased references exist in FROM,
1872+
// keep one as the backward-compatibility fallback for `OF schema.table`.
1873+
if asName.L == "" || c.qualifiedMap[qualifiedKey].table == nil {
1874+
c.qualifiedMap[qualifiedKey] = ref
1875+
}
1876+
if asName.L != "" {
1877+
// Keep backward compatibility for `OF schema.table` while also supporting `OF schema.alias`.
1878+
c.qualifiedMap[tableName.Schema.L+"."+asName.L] = ref
1879+
}
1880+
}
1881+
c.orderedRefs = append(c.orderedRefs, ref)
1882+
}
1883+
1884+
func (p *preprocessor) pushLockSelectCtx(sel *ast.SelectStmt) {
1885+
var lockTables []*ast.TableName
1886+
if sel.LockInfo != nil {
1887+
lockTables = sel.LockInfo.Tables
1888+
}
1889+
p.lockSelectCtxStack = append(p.lockSelectCtxStack, newLockSelectCtx(lockTables))
1890+
}
1891+
1892+
func (p *preprocessor) getLockSelectCtxStackTop() *lockSelectCtx {
1893+
if len(p.lockSelectCtxStack) == 0 {
1894+
return nil
1895+
}
1896+
return &p.lockSelectCtxStack[len(p.lockSelectCtxStack)-1]
1897+
}
1898+
1899+
func (p *preprocessor) popLockSelectCtx() {
1900+
if len(p.lockSelectCtxStack) == 0 {
1901+
return
1902+
}
1903+
p.lockSelectCtxStack = p.lockSelectCtxStack[:len(p.lockSelectCtxStack)-1]
1904+
}
1905+
1906+
// checkLockClauseTables validates/attaches tables referenced by SELECT ... FOR UPDATE/LOCK IN SHARE MODE.
1907+
// It binds lock targets to the tables in FROM and keeps backward compatibility by accepting
1908+
// base-table names even when aliases exist, with a warning to guide users to explicit aliases.
1909+
func (p *preprocessor) checkLockClauseTables(stmt *ast.SelectStmt, lockCtx *lockSelectCtx) {
1910+
if stmt.LockInfo == nil || stmt.LockInfo.LockType == ast.SelectLockNone || len(stmt.LockInfo.Tables) == 0 {
1911+
return
1912+
}
1913+
if lockCtx == nil {
1914+
emptyCtx := newLockSelectCtx(nil)
1915+
lockCtx = &emptyCtx
1916+
}
1917+
warnedCompatRefs := make(map[string]struct{})
1918+
1919+
for _, ref := range stmt.LockInfo.Tables {
1920+
name := ref.Name.L
1921+
var matched lockRef
1922+
var matchedByAlias bool
1923+
1924+
if ref.Schema.L != "" {
1925+
name = ref.Schema.L + "." + ref.Name.L
1926+
matched = lockCtx.qualifiedMap[name]
1927+
matchedByAlias = matched.alias.L != "" && ref.Name.L == matched.alias.L
1928+
if matchedByAlias {
1929+
ref.IsAlias = true
1930+
}
1931+
} else if t, ok := lockCtx.aliasMap[name]; ok {
1932+
ref.IsAlias = true
1933+
matched = t
1934+
matchedByAlias = true
1935+
}
1936+
1937+
// If still not found and the lock clause is unqualified, first try an exact unaliased match.
1938+
// For example, `SELECT * FROM db.t AS a, db.t FOR UPDATE OF t` should resolve `t` to the
1939+
// unaliased `db.t`, not to `a`. Only if no unaliased entry exists do we fall back to aliased
1940+
// entries for backward compatibility. `SELECT * FROM db1.t, db2.t FOR UPDATE OF t` still
1941+
// resolves `t` to `db1.t` because lockCtx.orderedRefs preserves the left-to-right FROM order.
1942+
if matched.table == nil && ref.Schema.L == "" {
1943+
for _, tblRef := range lockCtx.orderedRefs {
1944+
if tblRef.alias.L == "" && tblRef.table.Name.L == name {
1945+
matched = tblRef
1946+
break
1947+
}
1948+
}
1949+
if matched.table == nil {
1950+
for _, tblRef := range lockCtx.orderedRefs {
1951+
if tblRef.alias.L != "" && tblRef.table.Name.L == name {
1952+
matched = tblRef
1953+
break
1954+
}
1955+
}
1956+
}
1957+
}
1958+
1959+
if matched.table == nil {
1960+
if ref.Schema.L != "" {
1961+
name = ref.Schema.O + "." + ref.Name.O
1962+
} else {
1963+
name = ref.Name.O
1964+
}
1965+
p.err = plannererrors.ErrUnknownTable.GenWithStackByArgs(name, "locking clause")
1966+
break
1967+
}
1968+
1969+
// Keep backward compatibility: if an aliased table is referenced by its base name
1970+
// in OF, accept it and emit a warning to guide users to the alias form.
1971+
if !matchedByAlias && matched.alias.L != "" {
1972+
warnKey := ref.Schema.L + "." + ref.Name.L + "->" + matched.alias.L
1973+
if _, seen := warnedCompatRefs[warnKey]; !seen {
1974+
p.sctx.GetSessionVars().StmtCtx.AppendWarning(errors.NewNoStackErrorf(
1975+
"FOR UPDATE OF references the base table name while the table is aliased. Use the alias '%s' in OF to make the lock target explicit.",
1976+
matched.alias.O,
1977+
))
1978+
warnedCompatRefs[warnKey] = struct{}{}
1979+
}
1980+
}
1981+
1982+
tNameW := p.resolveCtx.GetTableName(matched.table)
1983+
if tNameW == nil {
1984+
// CTE has no *model.HintedTable, we need to skip it.
1985+
continue
1986+
}
1987+
1988+
p.resolveCtx.AddTableName(&resolve.TableNameW{
1989+
TableName: ref,
1990+
DBInfo: tNameW.DBInfo,
1991+
TableInfo: tNameW.TableInfo,
1992+
})
1993+
}
1994+
}
1995+
18031996
func (p *preprocessor) checkNotInRepair(tn *ast.TableName) {
18041997
tableInfo, dbInfo := domainutil.RepairInfo.GetRepairedTableInfoByTableName(tn.Schema.L, tn.Name.L)
18051998
if dbInfo == nil {

tests/integrationtest/r/planner/core/issuetest/planner_issue.result

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,3 +1482,68 @@ select * from t1 natural join t2 order by t1.a;
14821482
a
14831483
1
14841484
drop table t1, t2;
1485+
set @old_db := database();
1486+
drop database if exists lock_db1;
1487+
drop database if exists lock_db2;
1488+
create database lock_db1;
1489+
create database lock_db2;
1490+
use lock_db1;
1491+
create table t(id int primary key, v int);
1492+
insert into t values (1, 100);
1493+
select ue1_0.id from t ue1_0 where ue1_0.id=1 for update of t;
1494+
id
1495+
1
1496+
Level Code Message
1497+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1498+
select ue1_0.id from t ue1_0 where ue1_0.id=1 for update of ue1_0;
1499+
id
1500+
1
1501+
select * from t ue1_0 for update of t;
1502+
id v
1503+
1 100
1504+
Level Code Message
1505+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1506+
select * from t ue1_0 for update of t, t, ue1_0;
1507+
id v
1508+
1 100
1509+
Level Code Message
1510+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1511+
select * from t ue1_0 for update of lock_db1.t;
1512+
id v
1513+
1 100
1514+
Level Code Message
1515+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1516+
select * from t ue1_0 for update of lock_db1.ue1_0;
1517+
id v
1518+
1 100
1519+
select * from t ue1_0, t where ue1_0.id = t.id for update of t;
1520+
id v id v
1521+
1 100 1 100
1522+
select * from t, t ue1_0 where ue1_0.id = t.id for update of lock_db1.t;
1523+
id v id v
1524+
1 100 1 100
1525+
select * from t ue1_0 for update of non_exist_table;
1526+
Error 1109 (42S02): Unknown table 'non_exist_table' in locking clause
1527+
use lock_db2;
1528+
create table t(id int primary key, v int);
1529+
insert into t values (1, 200);
1530+
select * from lock_db1.t ue1_0 for update of t;
1531+
id v
1532+
1 100
1533+
Level Code Message
1534+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1535+
select * from lock_db1.t ue1_0, lock_db2.t ue1_1 for update of t;
1536+
id v id v
1537+
1 100 1 200
1538+
Level Code Message
1539+
Warning 1105 FOR UPDATE OF references the base table name while the table is aliased. Use the alias 'ue1_0' in OF to make the lock target explicit.
1540+
with cte_t as (select * from t) select * from cte_t for update of cte_t;
1541+
id v
1542+
1 200
1543+
set @old_db = ifnull(@old_db, 'test');
1544+
set @stmt = concat('use ', @old_db);
1545+
prepare _ldbstmt from @stmt;
1546+
execute _ldbstmt;
1547+
drop database if exists lock_db1;
1548+
drop database if exists lock_db2;
1549+
deallocate prepare _ldbstmt;

tests/integrationtest/t/planner/core/issuetest/planner_issue.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,3 +914,42 @@ insert into t1 values (1);
914914
insert into t2 values (1);
915915
select * from t1 natural join t2 order by t1.a;
916916
drop table t1, t2;
917+
918+
# Issue63035
919+
set @old_db := database();
920+
drop database if exists lock_db1;
921+
drop database if exists lock_db2;
922+
create database lock_db1;
923+
create database lock_db2;
924+
925+
use lock_db1;
926+
create table t(id int primary key, v int);
927+
insert into t values (1, 100);
928+
--enable_warnings
929+
select ue1_0.id from t ue1_0 where ue1_0.id=1 for update of t;
930+
select ue1_0.id from t ue1_0 where ue1_0.id=1 for update of ue1_0;
931+
select * from t ue1_0 for update of t;
932+
select * from t ue1_0 for update of t, t, ue1_0;
933+
select * from t ue1_0 for update of lock_db1.t;
934+
select * from t ue1_0 for update of lock_db1.ue1_0;
935+
select * from t ue1_0, t where ue1_0.id = t.id for update of t;
936+
select * from t, t ue1_0 where ue1_0.id = t.id for update of lock_db1.t;
937+
938+
--error 1109
939+
select * from t ue1_0 for update of non_exist_table;
940+
941+
use lock_db2;
942+
create table t(id int primary key, v int);
943+
insert into t values (1, 200);
944+
select * from lock_db1.t ue1_0 for update of t;
945+
select * from lock_db1.t ue1_0, lock_db2.t ue1_1 for update of t;
946+
with cte_t as (select * from t) select * from cte_t for update of cte_t;
947+
--disable_warnings
948+
949+
set @old_db = ifnull(@old_db, 'test');
950+
set @stmt = concat('use ', @old_db);
951+
prepare _ldbstmt from @stmt;
952+
execute _ldbstmt;
953+
drop database if exists lock_db1;
954+
drop database if exists lock_db2;
955+
deallocate prepare _ldbstmt;

0 commit comments

Comments
 (0)