github.com/dolthub/go-mysql-server@v0.18.0/sql/analyzer/symbol_resolution.go (about) 1 // Copyright 2020-2021 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package analyzer 16 17 import ( 18 "strings" 19 20 "github.com/dolthub/go-mysql-server/sql" 21 "github.com/dolthub/go-mysql-server/sql/expression" 22 "github.com/dolthub/go-mysql-server/sql/plan" 23 "github.com/dolthub/go-mysql-server/sql/transform" 24 ) 25 26 // pruneTables removes unneeded columns from *plan.ResolvedTable nodes 27 // 28 // A preOrder walk constructs a new tree top-down. For every non-base 29 // case node encountered: 30 // 1. Collect outer tableCol dependencies for the node 31 // 2. Apply the node's dependencies to |parentCols|, |parentStars|, 32 // and |unqualifiedStar|. 33 // 3. Process the node's children with the new dependencies. 34 // 4. Rewind the dependencies, resetting |parentCols|, |parentStars|, 35 // and |unqualifiedStar| to values when we entered this node. 36 // 5. Return the node with its children to the parent. 37 // 38 // The base case prunes a *plan.ResolvedTable of parent dependencies. 39 // 40 // The dependencies considered are: 41 // - outerCols: columns used by filters or other expressions 42 // sourced from outside the node 43 // - aliasCols: a bridge between outside columns and an aliased 44 // data source. 45 // - subqueryCols: correlated subqueries have outside cols not 46 // satisfied by tablescans in the subquery 47 // - stars: a tablescan with a qualified star or cannot be pruned. An 48 // unqualified star prevents pruning every child tablescan. 49 func pruneTables(ctx *sql.Context, a *Analyzer, n sql.Node, s *plan.Scope, sel RuleSelector) (sql.Node, transform.TreeIdentity, error) { 50 // the same table can appear in multiple table scans, 51 // so we use a counter to pin references 52 parentCols := make(map[tableCol]int) 53 parentStars := make(map[string]struct{}) 54 var unqualifiedStar bool 55 56 push := func(cols []tableCol, nodeStars []string, nodeUnq bool) { 57 for _, c := range cols { 58 parentCols[c]++ 59 } 60 for _, c := range nodeStars { 61 parentStars[c] = struct{}{} 62 } 63 unqualifiedStar = unqualifiedStar || nodeUnq 64 } 65 66 pop := func(cols []tableCol, nodeStars []string, beforeUnq bool) { 67 for _, c := range cols { 68 parentCols[c]-- 69 } 70 for _, c := range nodeStars { 71 delete(parentStars, c) 72 } 73 unqualifiedStar = beforeUnq 74 } 75 76 // MATCH ... AGAINST ... prevents pruning due to its internal reliance on an expected and consistent schema in all situations 77 if ma := findMatchAgainstExpr(n); ma != nil { 78 return n, transform.SameTree, nil 79 } 80 81 var pruneWalk func(n sql.Node) (sql.Node, transform.TreeIdentity, error) 82 pruneWalk = func(n sql.Node) (sql.Node, transform.TreeIdentity, error) { 83 switch n := n.(type) { 84 case *plan.ResolvedTable: 85 return pruneTableCols(n, parentCols, parentStars, unqualifiedStar) 86 case *plan.JoinNode: 87 if n.JoinType().IsPhysical() || n.JoinType().IsUsing() { 88 return n, transform.SameTree, nil 89 } 90 // we cannot push projections past lateral joins as columns not in the projection, 91 // but are in the left subtree can be referenced by the right subtree or parent nodes 92 if sqa, ok := n.Right().(*plan.SubqueryAlias); ok && sqa.IsLateral { 93 return n, transform.SameTree, nil 94 } 95 if _, ok := n.Right().(*plan.JSONTable); ok { 96 outerCols, outerStars, outerUnq := gatherOuterCols(n.Right()) 97 aliasCols, aliasStars := gatherTableAlias(n.Right(), parentCols, parentStars, unqualifiedStar) 98 push(outerCols, outerStars, outerUnq) 99 push(aliasCols, aliasStars, false) 100 } 101 case *plan.Filter, *plan.GroupBy, *plan.Project, *plan.TableAlias, 102 *plan.Window, *plan.Sort, *plan.Limit, *plan.RecursiveCte, 103 *plan.RecursiveTable, *plan.TopN, *plan.Offset, *plan.StripRowNode: 104 default: 105 return n, transform.SameTree, nil 106 } 107 if sq := findSubqueryExpr(n); sq != nil { 108 return n, transform.SameTree, nil 109 } 110 111 beforeUnq := unqualifiedStar 112 113 //todo(max): outer and alias cols can have duplicates, as long as the pop 114 // is equal and opposite we are usually fine. In the cases we aren't, we 115 // already do not handle nested aliasing well. 116 outerCols, outerStars, outerUnq := gatherOuterCols(n) 117 aliasCols, aliasStars := gatherTableAlias(n, parentCols, parentStars, unqualifiedStar) 118 push(outerCols, outerStars, outerUnq) 119 push(aliasCols, aliasStars, false) 120 121 children := n.Children() 122 var newChildren []sql.Node 123 for i := len(children) - 1; i >= 0; i-- { 124 // TODO don't push filters too low in join? 125 // join tables scoped left -> right, prune right -> left 126 c := children[i] 127 child, same, _ := pruneWalk(c) 128 if !same { 129 if newChildren == nil { 130 newChildren = make([]sql.Node, len(children)) 131 copy(newChildren, children) 132 } 133 newChildren[i] = child 134 } 135 } 136 137 pop(outerCols, outerStars, beforeUnq) 138 pop(aliasCols, aliasStars, beforeUnq) 139 140 if len(newChildren) == 0 { 141 return n, transform.SameTree, nil 142 } 143 ret, _ := n.WithChildren(newChildren...) 144 return ret, transform.NewTree, nil 145 } 146 147 return pruneWalk(n) 148 } 149 150 // findSubqueryExpr searches for a *plan.Subquery in a single node, 151 // returning the subquery or nil 152 func findSubqueryExpr(n sql.Node) *plan.Subquery { 153 var sq *plan.Subquery 154 ne, ok := n.(sql.Expressioner) 155 if !ok { 156 return nil 157 } 158 for _, e := range ne.Expressions() { 159 found := transform.InspectExpr(e, func(e sql.Expression) bool { 160 if e, ok := e.(*plan.Subquery); ok { 161 sq = e 162 return true 163 } 164 return false 165 }) 166 if found { 167 return sq 168 } 169 } 170 return nil 171 } 172 173 // findMatchAgainstExpr searches for an *expression.MatchAgainst within the node, returning the node or nil. 174 func findMatchAgainstExpr(n sql.Node) *expression.MatchAgainst { 175 var maExpr *expression.MatchAgainst 176 transform.InspectExpressionsWithNode(n, func(n sql.Node, expr sql.Expression) bool { 177 if matchAgainstExpr, ok := expr.(*expression.MatchAgainst); ok { 178 maExpr = matchAgainstExpr 179 return false 180 } 181 return true 182 }) 183 return maExpr 184 } 185 186 // pruneTableCols uses a list of parent dependencies columns and stars 187 // to prune and return a new table node. We prune a column if no 188 // parent references the column, no parent projections this table as a 189 // qualified star, and no parent projects an unqualified star. 190 func pruneTableCols( 191 n *plan.ResolvedTable, 192 parentCols map[tableCol]int, 193 parentStars map[string]struct{}, 194 unqualifiedStar bool, 195 ) (sql.Node, transform.TreeIdentity, error) { 196 table := getTable(n) 197 ptab, ok := table.(sql.ProjectedTable) 198 if !ok || table.Name() == plan.DualTableName { 199 return n, transform.SameTree, nil 200 } 201 202 _, selectStar := parentStars[table.Name()] 203 if unqualifiedStar { 204 selectStar = true 205 } 206 207 if len(ptab.Projections()) > 0 { 208 return n, transform.SameTree, nil 209 } 210 211 // Don't prune columns if they're needed by a virtual column 212 virtualColDeps := make(map[tableCol]int) 213 if vct, ok := n.WrappedTable().(*plan.VirtualColumnTable); ok { 214 for _, projection := range vct.Projections { 215 transform.Expr(projection, func(e sql.Expression) (sql.Expression, transform.TreeIdentity, error) { 216 if cd, ok := e.(*sql.ColumnDefaultValue); ok { 217 transform.Expr(cd.Expr, func(e sql.Expression) (sql.Expression, transform.TreeIdentity, error) { 218 if gf, ok := e.(*expression.GetField); ok { 219 c := tableCol{table: strings.ToLower(gf.Table()), col: strings.ToLower(gf.Name())} 220 virtualColDeps[c] = virtualColDeps[c] + 1 221 } 222 return e, transform.SameTree, nil 223 }) 224 } 225 return e, transform.SameTree, nil 226 }) 227 } 228 } 229 230 cols := make([]string, 0) 231 source := strings.ToLower(table.Name()) 232 for _, col := range table.Schema() { 233 c := tableCol{table: strings.ToLower(source), col: strings.ToLower(col.Name)} 234 if selectStar || parentCols[c] > 0 || virtualColDeps[c] > 0 { 235 cols = append(cols, c.col) 236 } 237 } 238 239 ret, err := n.WithTable(ptab.WithProjections(cols)) 240 if err != nil { 241 return n, transform.SameTree, nil 242 } 243 244 return ret, transform.NewTree, nil 245 } 246 247 // gatherOuterCols searches a node's expressions for column 248 // references and stars. 249 func gatherOuterCols(n sql.Node) ([]tableCol, []string, bool) { 250 ne, ok := n.(sql.Expressioner) 251 if !ok { 252 return nil, nil, false 253 } 254 var cols []tableCol 255 var nodeStars []string 256 var nodeUnqualifiedStar bool 257 for _, e := range ne.Expressions() { 258 transform.InspectExpr(e, func(e sql.Expression) bool { 259 var col tableCol 260 switch e := e.(type) { 261 case *expression.Alias: 262 switch e := e.Child.(type) { 263 case *expression.GetField: 264 col = tableCol{table: strings.ToLower(e.Table()), col: strings.ToLower(e.Name())} 265 case *expression.UnresolvedColumn: 266 col = tableCol{table: strings.ToLower(e.Table()), col: strings.ToLower(e.Name())} 267 default: 268 } 269 case *expression.GetField: 270 col = tableCol{table: strings.ToLower(e.Table()), col: strings.ToLower(e.Name())} 271 case *expression.UnresolvedColumn: 272 col = tableCol{table: strings.ToLower(e.Table()), col: strings.ToLower(e.Name())} 273 case *expression.Star: 274 if len(e.Table) > 0 { 275 nodeStars = append(nodeStars, strings.ToLower(e.Table)) 276 } else { 277 nodeUnqualifiedStar = true 278 } 279 default: 280 } 281 if col.col != "" { 282 cols = append(cols, col) 283 284 } 285 return false 286 }) 287 } 288 289 return cols, nodeStars, nodeUnqualifiedStar 290 } 291 292 // gatherTableAlias bridges two scopes: the parent scope with 293 // its |parentCols|, and the child data source that is 294 // accessed through this node's alias name. We return the 295 // aliased columns qualified with the base table name, 296 // and stars if applicable. 297 // TODO: we don't have any tests with the unqualified condition 298 func gatherTableAlias( 299 n sql.Node, 300 parentCols map[tableCol]int, 301 parentStars map[string]struct{}, 302 unqualifiedStar bool, 303 ) ([]tableCol, []string) { 304 var cols []tableCol 305 var nodeStars []string 306 switch n := n.(type) { 307 case *plan.TableAlias: 308 alias := strings.ToLower(n.Name()) 309 var base string 310 if rt, ok := n.Child.(*plan.ResolvedTable); ok { 311 base = rt.Name() 312 } 313 _, starred := parentStars[alias] 314 if unqualifiedStar { 315 starred = true 316 } 317 for _, col := range n.Schema() { 318 baseCol := tableCol{table: strings.ToLower(base), col: strings.ToLower(col.Name)} 319 aliasCol := tableCol{table: strings.ToLower(alias), col: strings.ToLower(col.Name)} 320 if starred || parentCols[aliasCol] > 0 { 321 // if the outer scope requests an aliased column 322 // a table lower in the tree must provide the source 323 cols = append(cols, baseCol) 324 } 325 } 326 for t := range parentStars { 327 if t == alias { 328 nodeStars = append(nodeStars, base) 329 } 330 } 331 return cols, nodeStars 332 default: 333 } 334 return cols, nodeStars 335 }