diff --git a/pkg/sql/plan/bind_update.go b/pkg/sql/plan/bind_update.go index 596ea6ad4574c..29e2f5876fe0d 100644 --- a/pkg/sql/plan/bind_update.go +++ b/pkg/sql/plan/bind_update.go @@ -85,7 +85,15 @@ func (builder *QueryBuilder) bindUpdate(stmt *tree.Update, bindCtx *BindContext) if err != nil { return 0, err } - if err = validateUpdateTargetSubqueries(builder.compCtx, stmt, dmlCtx.objRefs, dmlCtx.tableDefs); err != nil { + targetAliases := make([]string, len(dmlCtx.tableDefs)) + for i, updateCol2Expr := range dmlCtx.updateCol2Expr { + if len(updateCol2Expr) > 0 { + targetAliases[i] = dmlCtx.aliases[i] + } + } + if err = validateUpdateTargetSubqueries( + builder.compCtx, stmt, dmlCtx.objRefs, dmlCtx.tableDefs, targetAliases, + ); err != nil { return 0, err } onDuplicateAction := plan.Node_FAIL diff --git a/pkg/sql/plan/build_update.go b/pkg/sql/plan/build_update.go index f3a6bc5216679..0da6612c2b738 100644 --- a/pkg/sql/plan/build_update.go +++ b/pkg/sql/plan/build_update.go @@ -57,7 +57,11 @@ func buildTableUpdate(stmt *tree.Update, ctx CompilerContext, isPrepareStmt bool if err != nil { return nil, err } - if err = validateUpdateTargetSubqueries(ctx, stmt, tblInfo.objRef, tblInfo.tableDefs); err != nil { + targetAliases := make([]string, len(tblInfo.tableDefs)) + for alias, idx := range tblInfo.alias { + targetAliases[idx] = alias + } + if err = validateUpdateTargetSubqueries(ctx, stmt, tblInfo.objRef, tblInfo.tableDefs, targetAliases); err != nil { return nil, err } diff --git a/pkg/sql/plan/mysql_dml_compatibility.go b/pkg/sql/plan/mysql_dml_compatibility.go index e3c7dbdd7473e..70d9cda50cf9f 100644 --- a/pkg/sql/plan/mysql_dml_compatibility.go +++ b/pkg/sql/plan/mysql_dml_compatibility.go @@ -116,26 +116,36 @@ func validateUpdateTargetSubqueries( stmt *tree.Update, objRefs []*ObjectRef, tableDefs []*TableDef, + targetAliases []string, ) error { targets := makeMySQLDMLTargets(objRefs, tableDefs) visibleCTEs := mysqlCTENames(stmt.With, nil) + outerTargetQualifiers := mysqlUpdateTargetQualifiers(targets, targetAliases) for _, updateExpr := range stmt.Exprs { - if target, ok := findMySQLDMLTargetInExpr(ctx, updateExpr.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, updateExpr.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return moerr.NewUpdateTableUsed(ctx.GetContext(), target) } } if stmt.Where != nil { - if target, ok := findMySQLDMLTargetInExpr(ctx, stmt.Where.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, stmt.Where.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return moerr.NewUpdateTableUsed(ctx.GetContext(), target) } } for _, order := range stmt.OrderBy { - if target, ok := findMySQLDMLTargetInExpr(ctx, order.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, order.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return moerr.NewUpdateTableUsed(ctx.GetContext(), target) } } - if target, ok := findMySQLDMLTargetInLimit(ctx, stmt.Limit, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInLimitWithOuterTargets( + ctx, stmt.Limit, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return moerr.NewUpdateTableUsed(ctx.GetContext(), target) } for _, tableExpr := range stmt.Tables { @@ -209,14 +219,28 @@ func findMySQLDMLTargetInLimit( limit *tree.Limit, targets []mysqlDMLTarget, visibleCTEs map[string]struct{}, +) (string, bool) { + return findMySQLDMLTargetInLimitWithOuterTargets(ctx, limit, targets, visibleCTEs, nil) +} + +func findMySQLDMLTargetInLimitWithOuterTargets( + ctx CompilerContext, + limit *tree.Limit, + targets []mysqlDMLTarget, + visibleCTEs map[string]struct{}, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, ) (string, bool) { if limit == nil { return "", false } - if target, ok := findMySQLDMLTargetInExpr(ctx, limit.Count, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, limit.Count, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } - return findMySQLDMLTargetInExpr(ctx, limit.Offset, targets, visibleCTEs) + return findMySQLDMLTargetInExprWithOuterTargets( + ctx, limit.Offset, targets, visibleCTEs, outerTargetQualifiers, + ) } func findMySQLDMLTargetInExpr( @@ -224,6 +248,16 @@ func findMySQLDMLTargetInExpr( expr tree.Expr, targets []mysqlDMLTarget, visibleCTEs map[string]struct{}, +) (string, bool) { + return findMySQLDMLTargetInExprWithOuterTargets(ctx, expr, targets, visibleCTEs, nil) +} + +func findMySQLDMLTargetInExprWithOuterTargets( + ctx CompilerContext, + expr tree.Expr, + targets []mysqlDMLTarget, + visibleCTEs map[string]struct{}, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, ) (string, bool) { if expr == nil || len(targets) == 0 { return "", false @@ -237,7 +271,9 @@ func findMySQLDMLTargetInExpr( if !ok { return true } - found, _ = findMySQLDMLTargetInSelect(ctx, subquery.Select, targets, visibleCTEs) + found, _ = findMySQLDMLTargetInSelectWithOuterTargets( + ctx, subquery.Select, targets, visibleCTEs, outerTargetQualifiers, + ) // findMySQLDMLTargetInSelect owns traversal below this subquery. Do not // let the reflection walker enter it a second time. return false @@ -245,89 +281,143 @@ func findMySQLDMLTargetInExpr( return found, found != "" } -func findMySQLDMLTargetInSelect( +func findMySQLDMLTargetInSelectWithOuterTargets( ctx CompilerContext, stmt tree.SelectStatement, targets []mysqlDMLTarget, visibleCTEs map[string]struct{}, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, +) (string, bool) { + return findMySQLDMLTargetInSelectWithQueryTargets( + ctx, stmt, targets, visibleCTEs, outerTargetQualifiers, nil, + ) +} + +func findMySQLDMLTargetInSelectWithQueryTargets( + ctx CompilerContext, + stmt tree.SelectStatement, + targets []mysqlDMLTarget, + visibleCTEs map[string]struct{}, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, + queryCorrelatedTargets map[mysqlDMLTarget]struct{}, ) (string, bool) { switch selectStmt := stmt.(type) { case *tree.Select: visibleCTEs = mysqlCTENames(selectStmt.With, visibleCTEs) - if target, ok := findMySQLDMLTargetInSelect(ctx, selectStmt.Select, targets, visibleCTEs); ok { + queryCorrelatedTargets = mysqlMergeDMLTargets( + queryCorrelatedTargets, + mysqlCorrelatedUpdateTargetsInSelect(selectStmt, outerTargetQualifiers), + ) + if target, ok := findMySQLDMLTargetInSelectWithQueryTargets( + ctx, selectStmt.Select, targets, visibleCTEs, outerTargetQualifiers, queryCorrelatedTargets, + ); ok { return target, true } for _, order := range selectStmt.OrderBy { - if target, ok := findMySQLDMLTargetInExpr(ctx, order.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, order.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } - if target, ok := findMySQLDMLTargetInLimit(ctx, selectStmt.Limit, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInLimitWithOuterTargets( + ctx, selectStmt.Limit, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } if selectStmt.TimeWindow != nil { if selectStmt.TimeWindow.Interval != nil { - if target, ok := findMySQLDMLTargetInExpr(ctx, selectStmt.TimeWindow.Interval.Val, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectStmt.TimeWindow.Interval.Val, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } if selectStmt.TimeWindow.Sliding != nil { - if target, ok := findMySQLDMLTargetInExpr(ctx, selectStmt.TimeWindow.Sliding.Val, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectStmt.TimeWindow.Sliding.Val, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } if selectStmt.TimeWindow.Fill != nil { - if target, ok := findMySQLDMLTargetInExpr(ctx, selectStmt.TimeWindow.Fill.Val, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectStmt.TimeWindow.Fill.Val, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } } case *tree.ParenSelect: - return findMySQLDMLTargetInSelect(ctx, selectStmt.Select, targets, visibleCTEs) + return findMySQLDMLTargetInSelectWithQueryTargets( + ctx, selectStmt.Select, targets, visibleCTEs, outerTargetQualifiers, queryCorrelatedTargets, + ) case *tree.UnionClause: - if target, ok := findMySQLDMLTargetInSelect(ctx, selectStmt.Left, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInSelectWithQueryTargets( + ctx, selectStmt.Left, targets, visibleCTEs, outerTargetQualifiers, queryCorrelatedTargets, + ); ok { return target, true } - return findMySQLDMLTargetInSelect(ctx, selectStmt.Right, targets, visibleCTEs) + return findMySQLDMLTargetInSelectWithQueryTargets( + ctx, selectStmt.Right, targets, visibleCTEs, outerTargetQualifiers, queryCorrelatedTargets, + ) case *tree.SelectClause: + correlatedTargets := mysqlMergeDMLTargets( + queryCorrelatedTargets, + mysqlCorrelatedUpdateTargets(selectStmt, outerTargetQualifiers), + ) if selectStmt.From != nil { for _, tableExpr := range selectStmt.From.Tables { - if target, ok := findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr, targets, visibleCTEs, correlatedTargets, outerTargetQualifiers, + ); ok { return target, true } } } for _, selectExpr := range selectStmt.Exprs { - if target, ok := findMySQLDMLTargetInExpr(ctx, selectExpr.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectExpr.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } if selectStmt.Where != nil { - if target, ok := findMySQLDMLTargetInExpr(ctx, selectStmt.Where.Expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectStmt.Where.Expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } if selectStmt.GroupBy != nil { for _, exprs := range selectStmt.GroupBy.GroupByExprsList { for _, expr := range exprs { - if target, ok := findMySQLDMLTargetInExpr(ctx, expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } } for _, expr := range selectStmt.GroupBy.GroupingSet { - if target, ok := findMySQLDMLTargetInExpr(ctx, expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } } if selectStmt.Having != nil { - return findMySQLDMLTargetInExpr(ctx, selectStmt.Having.Expr, targets, visibleCTEs) + return findMySQLDMLTargetInExprWithOuterTargets( + ctx, selectStmt.Having.Expr, targets, visibleCTEs, outerTargetQualifiers, + ) } case *tree.ValuesClause: for _, row := range selectStmt.Rows { for _, expr := range row { - if target, ok := findMySQLDMLTargetInExpr(ctx, expr, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInExprWithOuterTargets( + ctx, expr, targets, visibleCTEs, outerTargetQualifiers, + ); ok { return target, true } } @@ -336,11 +426,328 @@ func findMySQLDMLTargetInSelect( return "", false } +func mysqlUpdateTargetQualifiers( + targets []mysqlDMLTarget, + targetAliases []string, +) map[mysqlDMLTarget]map[string]struct{} { + result := make(map[mysqlDMLTarget]map[string]struct{}, len(targets)) + for i, target := range targets { + if i < len(targetAliases) && targetAliases[i] != "" { + mysqlAddTargetQualifier(result, target, targetAliases[i]) + } + } + return result +} + +func mysqlAddTargetQualifier( + targets map[mysqlDMLTarget]map[string]struct{}, + target mysqlDMLTarget, + qualifier string, +) { + qualifiers := targets[target] + if qualifiers == nil { + qualifiers = make(map[string]struct{}) + targets[target] = qualifiers + } + qualifiers[strings.ToLower(qualifier)] = struct{}{} +} + +func mysqlCorrelatedUpdateTargets( + selectStmt *tree.SelectClause, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, +) map[mysqlDMLTarget]struct{} { + if len(outerTargetQualifiers) == 0 { + return nil + } + result := make(map[mysqlDMLTarget]struct{}) + for target, qualifiers := range outerTargetQualifiers { + if mysqlSelectClauseReferencesOuterQualifier(selectStmt, qualifiers, nil) { + result[target] = struct{}{} + } + } + return result +} + +func mysqlCorrelatedUpdateTargetsInSelect( + selectStmt *tree.Select, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, +) map[mysqlDMLTarget]struct{} { + if len(outerTargetQualifiers) == 0 { + return nil + } + result := make(map[mysqlDMLTarget]struct{}) + for target, qualifiers := range outerTargetQualifiers { + if mysqlSelectWrapperReferencesOuterQualifier(selectStmt, qualifiers, nil) { + result[target] = struct{}{} + } + } + return result +} + +func mysqlSelectWrapperReferencesOuterQualifier( + selectStmt *tree.Select, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + localShadowed := mysqlCloneNames(shadowed) + mysqlCollectSelectLocalQualifiers(selectStmt.Select, localShadowed) + for _, order := range selectStmt.OrderBy { + if mysqlExprReferencesOuterQualifier(order.Expr, qualifiers, localShadowed) { + return true + } + } + if mysqlLimitReferencesOuterQualifier(selectStmt.Limit, qualifiers, localShadowed) { + return true + } + if selectStmt.TimeWindow == nil { + return false + } + if selectStmt.TimeWindow.Interval != nil && mysqlExprReferencesOuterQualifier( + selectStmt.TimeWindow.Interval.Val, qualifiers, localShadowed, + ) { + return true + } + if selectStmt.TimeWindow.Sliding != nil && mysqlExprReferencesOuterQualifier( + selectStmt.TimeWindow.Sliding.Val, qualifiers, localShadowed, + ) { + return true + } + return selectStmt.TimeWindow.Fill != nil && mysqlExprReferencesOuterQualifier( + selectStmt.TimeWindow.Fill.Val, qualifiers, localShadowed, + ) +} + +func mysqlMergeDMLTargets( + first map[mysqlDMLTarget]struct{}, + second map[mysqlDMLTarget]struct{}, +) map[mysqlDMLTarget]struct{} { + if len(first) == 0 { + return second + } + if len(second) == 0 { + return first + } + result := make(map[mysqlDMLTarget]struct{}, len(first)+len(second)) + for target := range first { + result[target] = struct{}{} + } + for target := range second { + result[target] = struct{}{} + } + return result +} + +func mysqlSelectReferencesOuterQualifier( + stmt tree.SelectStatement, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + switch selectStmt := stmt.(type) { + case *tree.Select: + if mysqlSelectReferencesOuterQualifier(selectStmt.Select, qualifiers, shadowed) { + return true + } + return mysqlSelectWrapperReferencesOuterQualifier(selectStmt, qualifiers, shadowed) + case *tree.ParenSelect: + return mysqlSelectReferencesOuterQualifier(selectStmt.Select, qualifiers, shadowed) + case *tree.UnionClause: + return mysqlSelectReferencesOuterQualifier(selectStmt.Left, qualifiers, shadowed) || + mysqlSelectReferencesOuterQualifier(selectStmt.Right, qualifiers, shadowed) + case *tree.SelectClause: + return mysqlSelectClauseReferencesOuterQualifier(selectStmt, qualifiers, shadowed) + case *tree.ValuesClause: + for _, row := range selectStmt.Rows { + for _, expr := range row { + if mysqlExprReferencesOuterQualifier(expr, qualifiers, shadowed) { + return true + } + } + } + } + return false +} + +func mysqlSelectClauseReferencesOuterQualifier( + selectStmt *tree.SelectClause, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + localShadowed := mysqlCloneNames(shadowed) + if selectStmt.From != nil { + for _, tableExpr := range selectStmt.From.Tables { + mysqlCollectLocalTableQualifiers(tableExpr, localShadowed) + } + } + for _, selectExpr := range selectStmt.Exprs { + if mysqlExprReferencesOuterQualifier(selectExpr.Expr, qualifiers, localShadowed) { + return true + } + } + if selectStmt.Where != nil && mysqlExprReferencesOuterQualifier(selectStmt.Where.Expr, qualifiers, localShadowed) { + return true + } + if selectStmt.GroupBy != nil { + for _, exprs := range selectStmt.GroupBy.GroupByExprsList { + for _, expr := range exprs { + if mysqlExprReferencesOuterQualifier(expr, qualifiers, localShadowed) { + return true + } + } + } + for _, expr := range selectStmt.GroupBy.GroupingSet { + if mysqlExprReferencesOuterQualifier(expr, qualifiers, localShadowed) { + return true + } + } + } + if selectStmt.Having != nil && mysqlExprReferencesOuterQualifier(selectStmt.Having.Expr, qualifiers, localShadowed) { + return true + } + if selectStmt.From != nil { + for _, tableExpr := range selectStmt.From.Tables { + if mysqlTableExprReferencesOuterQualifier(tableExpr, qualifiers, shadowed) { + return true + } + } + } + return false +} + +func mysqlExprReferencesOuterQualifier( + expr tree.Expr, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + if expr == nil { + return false + } + found := false + walkGroupingSetOrderByExpr(expr, func(node tree.Expr) bool { + if found { + return false + } + switch typedExpr := node.(type) { + case *tree.UnresolvedName: + qualifier := strings.ToLower(typedExpr.TblName()) + _, isOuterTarget := qualifiers[qualifier] + _, isShadowed := shadowed[qualifier] + found = qualifier != "" && isOuterTarget && !isShadowed + return !found + case *tree.Subquery: + found = mysqlSelectReferencesOuterQualifier(typedExpr.Select, qualifiers, shadowed) + return false + } + return true + }) + return found +} + +func mysqlLimitReferencesOuterQualifier( + limit *tree.Limit, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + if limit == nil { + return false + } + return mysqlExprReferencesOuterQualifier(limit.Count, qualifiers, shadowed) || + mysqlExprReferencesOuterQualifier(limit.Offset, qualifiers, shadowed) +} + +func mysqlTableExprReferencesOuterQualifier( + expr tree.TableExpr, + qualifiers map[string]struct{}, + shadowed map[string]struct{}, +) bool { + switch tableExpr := expr.(type) { + case *tree.AliasedTableExpr: + return mysqlTableExprReferencesOuterQualifier(tableExpr.Expr, qualifiers, shadowed) + case *tree.ParenTableExpr: + return mysqlTableExprReferencesOuterQualifier(tableExpr.Expr, qualifiers, shadowed) + case *tree.JoinTableExpr: + if mysqlTableExprReferencesOuterQualifier(tableExpr.Left, qualifiers, shadowed) || + mysqlTableExprReferencesOuterQualifier(tableExpr.Right, qualifiers, shadowed) { + return true + } + if condition, ok := tableExpr.Cond.(*tree.OnJoinCond); ok { + onShadowed := mysqlCloneNames(shadowed) + mysqlCollectLocalTableQualifiers(tableExpr.Left, onShadowed) + mysqlCollectLocalTableQualifiers(tableExpr.Right, onShadowed) + return mysqlExprReferencesOuterQualifier(condition.Expr, qualifiers, onShadowed) + } + case *tree.ApplyTableExpr: + if mysqlTableExprReferencesOuterQualifier(tableExpr.Left, qualifiers, shadowed) { + return true + } + rightShadowed := mysqlCloneNames(shadowed) + mysqlCollectLocalTableQualifiers(tableExpr.Left, rightShadowed) + return mysqlTableExprReferencesOuterQualifier(tableExpr.Right, qualifiers, rightShadowed) + case *tree.TableFunction: + return tableExpr.Func != nil && mysqlExprReferencesOuterQualifier(tableExpr.Func, qualifiers, shadowed) + } + return false +} + +func mysqlCollectLocalTableQualifiers(expr tree.TableExpr, names map[string]struct{}) { + switch tableExpr := expr.(type) { + case *tree.TableName: + names[strings.ToLower(string(tableExpr.ObjectName))] = struct{}{} + case *tree.AliasedTableExpr: + if tableExpr.As.Alias != "" { + names[strings.ToLower(string(tableExpr.As.Alias))] = struct{}{} + return + } + mysqlCollectLocalTableQualifiers(tableExpr.Expr, names) + case *tree.ParenTableExpr: + mysqlCollectLocalTableQualifiers(tableExpr.Expr, names) + case *tree.JoinTableExpr: + mysqlCollectLocalTableQualifiers(tableExpr.Left, names) + mysqlCollectLocalTableQualifiers(tableExpr.Right, names) + case *tree.ApplyTableExpr: + mysqlCollectLocalTableQualifiers(tableExpr.Left, names) + mysqlCollectLocalTableQualifiers(tableExpr.Right, names) + } +} + +func mysqlCollectSelectLocalQualifiers(stmt tree.SelectStatement, names map[string]struct{}) { + switch selectStmt := stmt.(type) { + case *tree.Select: + mysqlCollectSelectLocalQualifiers(selectStmt.Select, names) + case *tree.ParenSelect: + mysqlCollectSelectLocalQualifiers(selectStmt.Select, names) + case *tree.SelectClause: + if selectStmt.From != nil { + for _, tableExpr := range selectStmt.From.Tables { + mysqlCollectLocalTableQualifiers(tableExpr, names) + } + } + } +} + +func mysqlCloneNames(names map[string]struct{}) map[string]struct{} { + result := make(map[string]struct{}, len(names)) + for name := range names { + result[name] = struct{}{} + } + return result +} + func findMySQLDMLTargetInDirectTableExpr( ctx CompilerContext, expr tree.TableExpr, targets []mysqlDMLTarget, visibleCTEs map[string]struct{}, +) (string, bool) { + return findMySQLDMLTargetInDirectTableExprExcept(ctx, expr, targets, visibleCTEs, nil, nil) +} + +func findMySQLDMLTargetInDirectTableExprExcept( + ctx CompilerContext, + expr tree.TableExpr, + targets []mysqlDMLTarget, + visibleCTEs map[string]struct{}, + ignoredTargets map[mysqlDMLTarget]struct{}, + outerTargetQualifiers map[mysqlDMLTarget]map[string]struct{}, ) (string, bool) { switch tableExpr := expr.(type) { case *tree.TableName: @@ -359,28 +766,45 @@ func findMySQLDMLTargetInDirectTableExpr( } for _, target := range targets { if target.matches(objRef, tableDef, dbName, string(tableExpr.ObjectName)) { + if _, ignored := ignoredTargets[target]; ignored { + return "", false + } return target.name, true } } case *tree.AliasedTableExpr: - return findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Expr, targets, visibleCTEs) + return findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Expr, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ) case *tree.ParenTableExpr: - return findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Expr, targets, visibleCTEs) + return findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Expr, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ) case *tree.JoinTableExpr: - if target, ok := findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Left, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Left, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ); ok { return target, true } - if target, ok := findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Right, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Right, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ); ok { return target, true } if condition, ok := tableExpr.Cond.(*tree.OnJoinCond); ok { - return findMySQLDMLTargetInExpr(ctx, condition.Expr, targets, visibleCTEs) + return findMySQLDMLTargetInExprWithOuterTargets( + ctx, condition.Expr, targets, visibleCTEs, outerTargetQualifiers, + ) } case *tree.ApplyTableExpr: - if target, ok := findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Left, targets, visibleCTEs); ok { + if target, ok := findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Left, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ); ok { return target, true } - return findMySQLDMLTargetInDirectTableExpr(ctx, tableExpr.Right, targets, visibleCTEs) + return findMySQLDMLTargetInDirectTableExprExcept( + ctx, tableExpr.Right, targets, visibleCTEs, ignoredTargets, outerTargetQualifiers, + ) case *tree.Subquery, *tree.StatementSource: // A FROM-subquery is a derived-table boundary. Its target-table read is // allowed because that result can be materialized before the DML write. diff --git a/pkg/sql/plan/mysql_dml_compatibility_test.go b/pkg/sql/plan/mysql_dml_compatibility_test.go index 570b20258cf49..1b25b4bafe33f 100644 --- a/pkg/sql/plan/mysql_dml_compatibility_test.go +++ b/pkg/sql/plan/mysql_dml_compatibility_test.go @@ -54,6 +54,25 @@ func requireMySQLDMLCompatibilityError(t *testing.T, sql string, code uint16, me require.Equal(t, message, moErr.Error()) } +func requireMySQLUpdateTargetSubqueryCompatible(t *testing.T, sql string) { + t.Helper() + ctx := NewMockCompilerContext(true) + stmt, err := parsers.ParseOne(ctx.GetContext(), dialect.MYSQL, sql, 1) + require.NoError(t, err) + defer stmt.Free() + updateStmt, ok := stmt.(*tree.Update) + require.True(t, ok) + tblInfo, err := getUpdateTableInfo(ctx, updateStmt) + require.NoError(t, err) + targetAliases := make([]string, len(tblInfo.tableDefs)) + for alias, idx := range tblInfo.alias { + targetAliases[idx] = alias + } + require.NoError(t, validateUpdateTargetSubqueries( + ctx, updateStmt, tblInfo.objRef, tblInfo.tableDefs, targetAliases, + )) +} + func TestMultiTableUpdateRejectsOrderByAndLimit(t *testing.T) { requireMySQLDMLCompatibilityError( t, @@ -108,6 +127,12 @@ func TestUpdateRejectsDirectTargetTableSubqueries(t *testing.T) { "UPDATE nation SET n_name = 'x' WHERE EXISTS (SELECT 1 FROM region ORDER BY (SELECT max(n_nationkey) FROM nation))", "UPDATE nation SET n_name = 'x' WHERE EXISTS (SELECT 1 FROM region GROUP BY r_regionkey HAVING EXISTS (SELECT 1 FROM nation))", "UPDATE nation SET n_name = 'x' WHERE EXISTS (SELECT 1 FROM region JOIN nation ON region.r_regionkey = nation.n_regionkey)", + "UPDATE nation AS dst SET n_name = (SELECT max(dst.n_name) FROM nation AS dst)", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src WHERE EXISTS (SELECT 1 FROM nation AS dst ORDER BY dst.n_nationkey))", + "UPDATE nation AS dst SET n_name = ((SELECT max(src.n_name) FROM nation AS src WHERE src.n_nationkey <= dst.n_nationkey) UNION ALL (SELECT max(other.n_name) FROM nation AS other))", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src JOIN nation2 AS dst ON dst.n_nationkey = src.n_nationkey)", + "UPDATE nation AS dst JOIN nation AS src ON dst.n_nationkey = src.n_nationkey SET dst.n_name = (SELECT max(inner_n.n_name) FROM nation AS inner_n WHERE inner_n.n_regionkey = src.n_regionkey)", + "UPDATE nation AS src SET n_name = (SELECT max(inner_n.n_name) FROM nation AS inner_n, region AS src CROSS APPLY generate_series(src.r_regionkey, src.r_regionkey) AS g)", } for _, sql := range tests { requireMySQLDMLCompatibilityError( @@ -154,6 +179,37 @@ func TestMySQLDMLCompatibilityHelpers(t *testing.T) { require.True(t, (mysqlDMLTarget{schema: "tpch", name: "nation"}).matches(nil, nil, "TPCH", "NATION")) require.False(t, (mysqlDMLTarget{schema: "tpch", name: "nation"}).matches(nil, nil, "tpch", "region")) + qualifiedTargets := make(map[mysqlDMLTarget]map[string]struct{}) + firstSameNameTarget := mysqlDMLTarget{objID: 1, schema: "db1", name: "same_name"} + secondSameNameTarget := mysqlDMLTarget{objID: 2, schema: "db2", name: "same_name"} + mysqlAddTargetQualifier(qualifiedTargets, firstSameNameTarget, "first_alias") + mysqlAddTargetQualifier(qualifiedTargets, secondSameNameTarget, "second_alias") + require.Equal(t, map[string]struct{}{"first_alias": {}}, qualifiedTargets[firstSameNameTarget]) + require.Equal(t, map[string]struct{}{"second_alias": {}}, qualifiedTargets[secondSameNameTarget]) + + outerQualifiers := map[string]struct{}{"dst": {}} + outerColumn := func() tree.Expr { + return tree.NewUnresolvedName(tree.NewCStr("dst", 1), tree.NewCStr("n_nationkey", 1)) + } + otherColumn := func() tree.Expr { + return tree.NewUnresolvedName(tree.NewCStr("src", 1), tree.NewCStr("n_nationkey", 1)) + } + selectWrapper := &tree.Select{Select: &tree.SelectClause{}} + require.False(t, mysqlSelectWrapperReferencesOuterQualifier(selectWrapper, outerQualifiers, nil)) + selectWrapper.TimeWindow = &tree.TimeWindow{Interval: &tree.Interval{Val: outerColumn()}} + require.True(t, mysqlSelectWrapperReferencesOuterQualifier(selectWrapper, outerQualifiers, nil)) + selectWrapper.TimeWindow = &tree.TimeWindow{ + Interval: &tree.Interval{Val: otherColumn()}, + Sliding: &tree.Sliding{Val: outerColumn()}, + } + require.True(t, mysqlSelectWrapperReferencesOuterQualifier(selectWrapper, outerQualifiers, nil)) + selectWrapper.TimeWindow = &tree.TimeWindow{ + Interval: &tree.Interval{Val: otherColumn()}, + Sliding: &tree.Sliding{Val: otherColumn()}, + Fill: &tree.Fill{Val: outerColumn()}, + } + require.True(t, mysqlSelectWrapperReferencesOuterQualifier(selectWrapper, outerQualifiers, nil)) + inherited := map[string]struct{}{"outer": {}} visibleCTEs := mysqlCTENames(&tree.With{CTEs: []*tree.CTE{ nil, @@ -196,6 +252,12 @@ func TestMySQLDMLCompatibilityAllowsLegalShapes(t *testing.T) { "UPDATE nation SET n_name = 'x' WHERE n_regionkey IN (SELECT r_regionkey FROM region)", "UPDATE nation SET n_name = 'x' WHERE n_nationkey IN (SELECT n_nationkey FROM (SELECT n_nationkey FROM nation) AS materialized_nation)", "UPDATE nation AS dst JOIN nation AS src ON dst.n_nationkey = src.n_nationkey SET dst.n_name = src.n_name", + "UPDATE nation SET n_name = (SELECT max(src.n_name) FROM nation AS src WHERE src.n_nationkey <= nation.n_nationkey)", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src WHERE src.n_nationkey <= dst.n_nationkey)", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src WHERE EXISTS (SELECT 1 FROM region WHERE src.n_regionkey = dst.n_regionkey))", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src ORDER BY dst.n_nationkey LIMIT 1)", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src JOIN region AS r ON r.r_regionkey = dst.n_regionkey, nation2 AS dst)", + "UPDATE nation AS dst SET n_name = (SELECT max(src.n_name) FROM nation AS src CROSS APPLY generate_series(dst.n_nationkey, dst.n_nationkey) AS g)", "DELETE FROM nation WHERE n_nationkey IN (SELECT n_nationkey FROM (SELECT n_nationkey FROM nation) AS materialized_nation)", } for _, sql := range tests { @@ -203,3 +265,19 @@ func TestMySQLDMLCompatibilityAllowsLegalShapes(t *testing.T) { require.NoError(t, err, sql) } } + +func TestUpdateTargetCompatibilityAllowsNestedJoinCorrelation(t *testing.T) { + requireMySQLUpdateTargetSubqueryCompatible(t, ` + UPDATE nation AS dst + SET n_name = ( + SELECT max(src.n_name) + FROM nation AS src + JOIN region AS r + ON EXISTS ( + SELECT 1 + FROM nation AS nested_src + WHERE nested_src.n_regionkey = dst.n_regionkey + ) + )`, + ) +}