@@ -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+
18031996func (p * preprocessor ) checkNotInRepair (tn * ast.TableName ) {
18041997 tableInfo , dbInfo := domainutil .RepairInfo .GetRepairedTableInfoByTableName (tn .Schema .L , tn .Name .L )
18051998 if dbInfo == nil {
0 commit comments