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  }