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 }