github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/opt/idxconstraint/index_constraints_test.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package idxconstraint_test
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"fmt"
    17  	"strconv"
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/opt"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/opt/exec/execbuilder"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/opt/idxconstraint"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/opt/norm"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/opt/optbuilder"
    28  	"github.com/cockroachdb/cockroach/pkg/sql/opt/optgen/exprgen"
    29  	"github.com/cockroachdb/cockroach/pkg/sql/parser"
    30  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    31  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    32  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    33  	"github.com/cockroachdb/datadriven"
    34  )
    35  
    36  // The test files support only one command:
    37  //
    38  //   - index-constraints [arg | arg=val | arg=(val1,val2, ...)]...
    39  //
    40  //   Takes a scalar expression, builds a memo for it, and computes index
    41  //   constraints. Arguments:
    42  //
    43  //     - vars=(<type>, ...)
    44  //
    45  //       Sets the types for the index vars in the expression.
    46  //
    47  //     - index=(@<index> [ascending|asc|descending|desc] [not null], ...)
    48  //
    49  //       Information for the index (used by index-constraints). Each column of the
    50  //       index refers to an index var.
    51  //
    52  //     - inverted-index=@<index>
    53  //
    54  //       Information about an inverted index (used by index-constraints). The
    55  //       one column of the inverted index refers to an index var. Only one of
    56  //       "index" and "inverted-index" should be used.
    57  //
    58  //     - nonormalize
    59  //
    60  //       Disable the optimizer normalization rules.
    61  //
    62  //     - semtree-normalize
    63  //
    64  //       Run TypedExpr normalization before building the memo.
    65  //
    66  func TestIndexConstraints(t *testing.T) {
    67  	defer leaktest.AfterTest(t)()
    68  
    69  	datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
    70  		semaCtx := tree.MakeSemaContext()
    71  		evalCtx := tree.MakeTestingEvalContext(cluster.MakeTestingClusterSettings())
    72  
    73  		datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
    74  			var varTypes []*types.T
    75  			var indexCols []opt.OrderingColumn
    76  			var notNullCols opt.ColSet
    77  			var iVarHelper tree.IndexedVarHelper
    78  			var invertedIndex bool
    79  			var err error
    80  
    81  			var f norm.Factory
    82  			f.Init(&evalCtx, nil /* catalog */)
    83  			md := f.Metadata()
    84  
    85  			for _, arg := range d.CmdArgs {
    86  				key, vals := arg.Key, arg.Vals
    87  				switch key {
    88  				case "vars":
    89  					varTypes, err = exprgen.ParseTypes(vals)
    90  					if err != nil {
    91  						d.Fatalf(t, "%v", err)
    92  					}
    93  
    94  					iVarHelper = tree.MakeTypesOnlyIndexedVarHelper(varTypes)
    95  					// Set up the columns in the metadata.
    96  					for i, typ := range varTypes {
    97  						md.AddColumn(fmt.Sprintf("@%d", i+1), typ)
    98  					}
    99  
   100  				case "index", "inverted-index":
   101  					if varTypes == nil {
   102  						d.Fatalf(t, "vars must precede index")
   103  					}
   104  					indexCols, notNullCols = parseIndexColumns(t, md, vals)
   105  					if key == "inverted-index" {
   106  						if len(indexCols) > 1 {
   107  							d.Fatalf(t, "inverted index must be on a single column")
   108  						}
   109  						invertedIndex = true
   110  					}
   111  
   112  				case "nonormalize":
   113  					f.DisableOptimizations()
   114  
   115  				default:
   116  					d.Fatalf(t, "unknown argument: %s", key)
   117  				}
   118  			}
   119  
   120  			switch d.Cmd {
   121  			case "index-constraints":
   122  				// Allow specifying optional filters using the "optional:" delimiter.
   123  				var filters, optionalFilters memo.FiltersExpr
   124  				if idx := strings.Index(d.Input, "optional:"); idx >= 0 {
   125  					optional := d.Input[idx+len("optional:"):]
   126  					optionalFilters, err = buildFilters(optional, &semaCtx, &evalCtx, &f)
   127  					if err != nil {
   128  						d.Fatalf(t, "%v", err)
   129  					}
   130  					d.Input = d.Input[:idx]
   131  				}
   132  				if filters, err = buildFilters(d.Input, &semaCtx, &evalCtx, &f); err != nil {
   133  					d.Fatalf(t, "%v", err)
   134  				}
   135  
   136  				var ic idxconstraint.Instance
   137  				ic.Init(filters, optionalFilters, indexCols, notNullCols, invertedIndex, &evalCtx, &f)
   138  				result := ic.Constraint()
   139  				var buf bytes.Buffer
   140  				for i := 0; i < result.Spans.Count(); i++ {
   141  					fmt.Fprintf(&buf, "%s\n", result.Spans.Get(i))
   142  				}
   143  				remainingFilter := ic.RemainingFilters()
   144  				if !remainingFilter.IsTrue() {
   145  					execBld := execbuilder.New(nil /* execFactory */, f.Memo(), nil /* catalog */, &remainingFilter, &evalCtx)
   146  					expr, err := execBld.BuildScalar(&iVarHelper)
   147  					if err != nil {
   148  						return fmt.Sprintf("error: %v\n", err)
   149  					}
   150  					fmt.Fprintf(&buf, "Remaining filter: %s\n", expr)
   151  				}
   152  				return buf.String()
   153  
   154  			default:
   155  				d.Fatalf(t, "unsupported command: %s", d.Cmd)
   156  				return ""
   157  			}
   158  		})
   159  	})
   160  }
   161  
   162  func BenchmarkIndexConstraints(b *testing.B) {
   163  	type testCase struct {
   164  		name, varTypes, indexInfo, expr string
   165  	}
   166  	testCases := []testCase{
   167  		{
   168  			name:      "point-lookup",
   169  			varTypes:  "int",
   170  			indexInfo: "@1",
   171  			expr:      "@1 = 1",
   172  		},
   173  		{
   174  			name:      "no-constraints",
   175  			varTypes:  "int, int",
   176  			indexInfo: "@2",
   177  			expr:      "@1 = 1",
   178  		},
   179  		{
   180  			name:      "range",
   181  			varTypes:  "int",
   182  			indexInfo: "@1",
   183  			expr:      "@1 >= 1 AND @1 <= 10",
   184  		},
   185  		{
   186  			name:      "range-2d",
   187  			varTypes:  "int, int",
   188  			indexInfo: "@1, @2",
   189  			expr:      "@1 >= 1 AND @1 <= 10 AND @2 >= 1 AND @2 <= 10",
   190  		},
   191  		{
   192  			name:      "many-columns",
   193  			varTypes:  "int, int, int, int, int",
   194  			indexInfo: "@1, @2, @3, @4, @5",
   195  			expr:      "@1 = 1 AND @2 >= 2 AND @2 <= 4 AND (@3, @4, @5) IN ((3, 4, 5), (6, 7, 8))",
   196  		},
   197  	}
   198  	// Generate a few testcases with many columns with single value constraint.
   199  	// This characterizes scaling w.r.t the number of columns.
   200  	for _, n := range []int{10, 100} {
   201  		var tc testCase
   202  		tc.name = fmt.Sprintf("single-jumbo-span-%d", n)
   203  		for i := 1; i <= n; i++ {
   204  			if i > 1 {
   205  				tc.varTypes += ", "
   206  				tc.indexInfo += ", "
   207  				tc.expr += " AND "
   208  			}
   209  			tc.varTypes += "int"
   210  			tc.indexInfo += fmt.Sprintf("@%d", i)
   211  			tc.expr += fmt.Sprintf("@%d=%d", i, i)
   212  		}
   213  		testCases = append(testCases, tc)
   214  	}
   215  
   216  	semaCtx := tree.MakeSemaContext()
   217  	evalCtx := tree.MakeTestingEvalContext(cluster.MakeTestingClusterSettings())
   218  
   219  	for _, tc := range testCases {
   220  		b.Run(tc.name, func(b *testing.B) {
   221  			varTypes, err := exprgen.ParseTypes(strings.Split(tc.varTypes, ", "))
   222  			if err != nil {
   223  				b.Fatal(err)
   224  			}
   225  			var f norm.Factory
   226  			f.Init(&evalCtx, nil /* catalog */)
   227  			md := f.Metadata()
   228  			for i, typ := range varTypes {
   229  				md.AddColumn(fmt.Sprintf("@%d", i+1), typ)
   230  			}
   231  			indexCols, notNullCols := parseIndexColumns(b, md, strings.Split(tc.indexInfo, ", "))
   232  
   233  			filters, err := buildFilters(tc.expr, &semaCtx, &evalCtx, &f)
   234  			if err != nil {
   235  				b.Fatal(err)
   236  			}
   237  
   238  			b.ResetTimer()
   239  			for i := 0; i < b.N; i++ {
   240  				var ic idxconstraint.Instance
   241  				ic.Init(filters, nil /* optionalFilters */, indexCols, notNullCols, false /*isInverted */, &evalCtx, &f)
   242  				_ = ic.Constraint()
   243  				_ = ic.RemainingFilters()
   244  			}
   245  		})
   246  	}
   247  }
   248  
   249  // parseIndexColumns parses descriptions of index columns; each
   250  // string corresponds to an index column and is of the form:
   251  //   @id [ascending|asc|descending|desc] [not null]
   252  func parseIndexColumns(
   253  	tb testing.TB, md *opt.Metadata, colStrs []string,
   254  ) (columns []opt.OrderingColumn, notNullCols opt.ColSet) {
   255  	columns = make([]opt.OrderingColumn, len(colStrs))
   256  	for i := range colStrs {
   257  		fields := strings.Fields(colStrs[i])
   258  		if fields[0][0] != '@' {
   259  			tb.Fatal("index column must start with @<index>")
   260  		}
   261  		id, err := strconv.Atoi(fields[0][1:])
   262  		if err != nil {
   263  			tb.Fatal(err)
   264  		}
   265  		columns[i] = opt.MakeOrderingColumn(opt.ColumnID(id), false /* descending */)
   266  		fields = fields[1:]
   267  		for len(fields) > 0 {
   268  			switch strings.ToLower(fields[0]) {
   269  			case "ascending", "asc":
   270  				// ascending is the default.
   271  				fields = fields[1:]
   272  			case "descending", "desc":
   273  				columns[i] = -columns[i]
   274  				fields = fields[1:]
   275  
   276  			case "not":
   277  				if len(fields) < 2 || strings.ToLower(fields[1]) != "null" {
   278  					tb.Fatalf("unknown column attribute %s", fields)
   279  				}
   280  				notNullCols.Add(opt.ColumnID(id))
   281  				fields = fields[2:]
   282  			default:
   283  				tb.Fatalf("unknown column attribute %s", fields)
   284  			}
   285  		}
   286  	}
   287  	return columns, notNullCols
   288  }
   289  
   290  func buildFilters(
   291  	input string, semaCtx *tree.SemaContext, evalCtx *tree.EvalContext, f *norm.Factory,
   292  ) (memo.FiltersExpr, error) {
   293  	if input == "" {
   294  		return memo.TrueFilter, nil
   295  	}
   296  	expr, err := parser.ParseExpr(input)
   297  	if err != nil {
   298  		return memo.FiltersExpr{}, err
   299  	}
   300  	b := optbuilder.NewScalar(context.Background(), semaCtx, evalCtx, f)
   301  	if err := b.Build(expr); err != nil {
   302  		return memo.FiltersExpr{}, err
   303  	}
   304  	root := f.Memo().RootExpr().(opt.ScalarExpr)
   305  	if _, ok := root.(*memo.TrueExpr); ok {
   306  		return memo.TrueFilter, nil
   307  	}
   308  	return memo.FiltersExpr{f.ConstructFiltersItem(root)}, nil
   309  }