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

     1  // Copyright 2018 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 constraint
    12  
    13  import (
    14  	"fmt"
    15  	"testing"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/opt"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    20  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    21  )
    22  
    23  func TestConstraintUnion(t *testing.T) {
    24  	test := func(t *testing.T, evalCtx *tree.EvalContext, left, right *Constraint, expected string) {
    25  		t.Helper()
    26  		clone := *left
    27  		clone.UnionWith(evalCtx, right)
    28  
    29  		if actual := clone.String(); actual != expected {
    30  			format := "left: %s, right: %s, expected: %v, actual: %v"
    31  			t.Errorf(format, left.String(), right.String(), expected, actual)
    32  		}
    33  	}
    34  
    35  	st := cluster.MakeTestingClusterSettings()
    36  	evalCtx := tree.MakeTestingEvalContext(st)
    37  	data := newConstraintTestData(&evalCtx)
    38  
    39  	// Union constraint with itself.
    40  	test(t, &evalCtx, &data.c1to10, &data.c1to10, "/1: [/1 - /10]")
    41  
    42  	// Merge first spans in each constraint.
    43  	test(t, &evalCtx, &data.c1to10, &data.c5to25, "/1: [/1 - /25)")
    44  	test(t, &evalCtx, &data.c5to25, &data.c1to10, "/1: [/1 - /25)")
    45  
    46  	// Disjoint spans in each constraint.
    47  	test(t, &evalCtx, &data.c1to10, &data.c40to50, "/1: [/1 - /10] [/40 - /50]")
    48  	test(t, &evalCtx, &data.c40to50, &data.c1to10, "/1: [/1 - /10] [/40 - /50]")
    49  
    50  	// Adjacent disjoint spans in each constraint.
    51  	test(t, &evalCtx, &data.c20to30, &data.c30to40, "/1: [/20 - /40]")
    52  	test(t, &evalCtx, &data.c30to40, &data.c20to30, "/1: [/20 - /40]")
    53  
    54  	// Merge multiple spans down to single span.
    55  	var left, right Constraint
    56  	left = data.c1to10
    57  	left.UnionWith(&evalCtx, &data.c20to30)
    58  	left.UnionWith(&evalCtx, &data.c40to50)
    59  
    60  	right = data.c5to25
    61  	right.UnionWith(&evalCtx, &data.c30to40)
    62  
    63  	test(t, &evalCtx, &left, &right, "/1: [/1 - /50]")
    64  	test(t, &evalCtx, &right, &left, "/1: [/1 - /50]")
    65  
    66  	// Multiple disjoint spans on each side.
    67  	left = data.c1to10
    68  	left.UnionWith(&evalCtx, &data.c20to30)
    69  
    70  	right = data.c40to50
    71  	right.UnionWith(&evalCtx, &data.c60to70)
    72  
    73  	test(t, &evalCtx, &left, &right, "/1: [/1 - /10] [/20 - /30) [/40 - /50] (/60 - /70)")
    74  	test(t, &evalCtx, &right, &left, "/1: [/1 - /10] [/20 - /30) [/40 - /50] (/60 - /70)")
    75  
    76  	// Multiple spans that yield the unconstrained span.
    77  	left = data.cLt10
    78  	right = data.c5to25
    79  	right.UnionWith(&evalCtx, &data.cGt20)
    80  
    81  	test(t, &evalCtx, &left, &right, "/1: unconstrained")
    82  	test(t, &evalCtx, &right, &left, "/1: unconstrained")
    83  
    84  	if left.String() != "/1: [ - /10)" {
    85  		t.Errorf("tryUnionWith failed, but still modified one of the spans: %v", left.String())
    86  	}
    87  	if right.String() != "/1: (/5 - ]" {
    88  		t.Errorf("tryUnionWith failed, but still modified one of the spans: %v", right.String())
    89  	}
    90  
    91  	// Multiple columns.
    92  	expected := "/1/2: [/'cherry'/true - /'strawberry']"
    93  	test(t, &evalCtx, &data.cherryRaspberry, &data.mangoStrawberry, expected)
    94  	test(t, &evalCtx, &data.mangoStrawberry, &data.cherryRaspberry, expected)
    95  }
    96  
    97  func TestConstraintIntersect(t *testing.T) {
    98  	test := func(t *testing.T, evalCtx *tree.EvalContext, left, right *Constraint, expected string) {
    99  		t.Helper()
   100  		clone := *left
   101  		clone.IntersectWith(evalCtx, right)
   102  		if actual := clone.String(); actual != expected {
   103  			format := "left: %s, right: %s, expected: %v, actual: %v"
   104  			t.Errorf(format, left.String(), right.String(), expected, actual)
   105  		}
   106  	}
   107  
   108  	st := cluster.MakeTestingClusterSettings()
   109  	evalCtx := tree.MakeTestingEvalContext(st)
   110  	data := newConstraintTestData(&evalCtx)
   111  
   112  	// Intersect constraint with itself.
   113  	test(t, &evalCtx, &data.c1to10, &data.c1to10, "/1: [/1 - /10]")
   114  
   115  	// Intersect first spans in each constraint.
   116  	test(t, &evalCtx, &data.c1to10, &data.c5to25, "/1: (/5 - /10]")
   117  	test(t, &evalCtx, &data.c5to25, &data.c1to10, "/1: (/5 - /10]")
   118  
   119  	// Disjoint spans in each constraint.
   120  	test(t, &evalCtx, &data.c1to10, &data.c40to50, "/1: contradiction")
   121  	test(t, &evalCtx, &data.c40to50, &data.c1to10, "/1: contradiction")
   122  
   123  	// Intersect multiple spans.
   124  	var left, right Constraint
   125  	left = data.c1to10
   126  	left.UnionWith(&evalCtx, &data.c20to30)
   127  	left.UnionWith(&evalCtx, &data.c40to50)
   128  
   129  	right = data.c5to25
   130  	right.UnionWith(&evalCtx, &data.c30to40)
   131  
   132  	test(t, &evalCtx, &right, &left, "/1: (/5 - /10] [/20 - /25) [/40 - /40]")
   133  	test(t, &evalCtx, &left, &right, "/1: (/5 - /10] [/20 - /25) [/40 - /40]")
   134  
   135  	// Intersect multiple disjoint spans.
   136  	left = data.c1to10
   137  	left.UnionWith(&evalCtx, &data.c20to30)
   138  
   139  	right = data.c40to50
   140  	right.UnionWith(&evalCtx, &data.c60to70)
   141  
   142  	test(t, &evalCtx, &left, &right, "/1: contradiction")
   143  	test(t, &evalCtx, &right, &left, "/1: contradiction")
   144  
   145  	if left.String() != "/1: [/1 - /10] [/20 - /30)" {
   146  		t.Errorf("tryIntersectWith failed, but still modified one of the spans: %v", left.String())
   147  	}
   148  	if right.String() != "/1: [/40 - /50] (/60 - /70)" {
   149  		t.Errorf("tryIntersectWith failed, but still modified one of the spans: %v", right.String())
   150  	}
   151  
   152  	// Multiple columns.
   153  	expected := "/1/2: [/'mango'/false - /'raspberry'/false)"
   154  	test(t, &evalCtx, &data.cherryRaspberry, &data.mangoStrawberry, expected)
   155  	test(t, &evalCtx, &data.mangoStrawberry, &data.cherryRaspberry, expected)
   156  }
   157  
   158  func TestConstraintContainsSpan(t *testing.T) {
   159  	st := cluster.MakeTestingClusterSettings()
   160  	evalCtx := tree.MakeTestingEvalContext(st)
   161  
   162  	// Each test case has a bunch of spans that are expected to be contained, and
   163  	// a bunch of spans that are expected not to be contained.
   164  	testData := []struct {
   165  		constraint        string
   166  		containedSpans    string
   167  		notContainedSpans string
   168  	}{
   169  		{
   170  			constraint:        "/1: [/1 - /3]",
   171  			containedSpans:    "[/1 - /1] (/1 - /2) (/1 - /3) [/2 - /3] [/1 - /3]",
   172  			notContainedSpans: "[/0 - /1] (/0 - /1] (/0 - /2] (/0 - /3) [/1 - /4) [/2 - /5]",
   173  		},
   174  		{
   175  			constraint: "/1/2: [ - /2] [/4 - /4] [/5/3 - /7) [/9 - /9/20]",
   176  			containedSpans: "[ - /1] [ - /2) [ - /2] [/1 - /2] [/2 - /2] [/4 - /4] " +
   177  				"[/5/3 - /5/3/1] [/6 - /6] [/5/5 - /7) [/9/10 - /9/15] [/9/19 - /9/20]",
   178  			notContainedSpans: "[ - /3] [/1 - /3] [/3 - /4] [/3 - /6] [/5/3 - /7] [/6 - /8] " +
   179  				"[/9/20 - /9/21] [/8 - /9]",
   180  		},
   181  		{
   182  			constraint:        "/1/-2: [/1/5 - /1/2] [/3/5 - /5/2] [/7 - ]",
   183  			containedSpans:    "[/1/5 - /1/2] [/1/4 - /1/3] [/1/4 - /1/2] [/4 - /5) [/4/6 - /5/3] [/7/1 - ]",
   184  			notContainedSpans: "[/1/5 - /1/1] [/1/3 - /1/1] [/3/6 - /3/5] [/4 - /5] [/4 - /5/1] [/6/10 - ]",
   185  		},
   186  	}
   187  
   188  	for i, tc := range testData {
   189  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   190  			c := ParseConstraint(&evalCtx, tc.constraint)
   191  
   192  			spans := parseSpans(&evalCtx, tc.containedSpans)
   193  			for i := 0; i < spans.Count(); i++ {
   194  				if sp := spans.Get(i); !c.ContainsSpan(&evalCtx, sp) {
   195  					t.Errorf("%s should contain span %s", c, sp)
   196  				}
   197  			}
   198  			spans = parseSpans(&evalCtx, tc.notContainedSpans)
   199  			for i := 0; i < spans.Count(); i++ {
   200  				if sp := spans.Get(i); c.ContainsSpan(&evalCtx, sp) {
   201  					t.Errorf("%s should not contain span %s", c, sp)
   202  				}
   203  			}
   204  		})
   205  	}
   206  }
   207  
   208  func TestConstraintCombine(t *testing.T) {
   209  	st := cluster.MakeTestingClusterSettings()
   210  	evalCtx := tree.MakeTestingEvalContext(st)
   211  
   212  	testData := []struct {
   213  		a, b, e string
   214  	}{
   215  		{
   216  			a: "/1/2: [ - /2] [/4 - /4] [/5/30 - /7] [/9 - /9/20]",
   217  			b: "/2: [/10 - /10] [/20 - /20] [/30 - /30] [/40 - /40]",
   218  			e: "/1/2: [ - /2/40] [/4/10 - /4/10] [/4/20 - /4/20] [/4/30 - /4/30] [/4/40 - /4/40] " +
   219  				"[/5/30 - /7/40] [/9/10 - /9/20]",
   220  		},
   221  		{
   222  			a: "/1/2/3: [ - /1/10] [/2 - /3/20] [/4/30 - /5] [/6/10 - /6/10]",
   223  			b: "/3: [/50 - /50] [/60 - /70]",
   224  			e: "/1/2/3: [ - /1/10/70] [/2 - /3/20/70] [/4/30/50 - /5] [/6/10/50 - /6/10/50] " +
   225  				"[/6/10/60 - /6/10/70]",
   226  		},
   227  		{
   228  			a: "/1/2/3/4: [ - /10] [/15 - /15] [/20 - /20/10] [/30 - /40) [/80 - ]",
   229  			b: "/2/3/4: [/20 - /20/10] [/30 - /30] [/40 - /40]",
   230  			e: "/1/2/3/4: [ - /10/40] [/15/20 - /15/20/10] [/15/30 - /15/30] [/15/40 - /15/40] " +
   231  				"[/30/20 - /40) [/80/20 - ]",
   232  		},
   233  		{
   234  			a: "/1/2/3/4: [ - /10/40] [/15/20 - /15/20/10] [/15/30 - /15/30] [/15/40 - /15/40] " +
   235  				"[/30/20 - /40) [/80/20 - ]",
   236  			b: "/4: [/20/10 - /30] [/40 - /40]",
   237  			e: "/1/2/3/4: [ - /10/40] [/15/20 - /15/20/10/40] [/15/30 - /15/30] [/15/40 - /15/40] " +
   238  				"[/30/20 - /40) [/80/20 - ]",
   239  		},
   240  		{
   241  			a: "/1/2: [/1 - /1/6]",
   242  			b: "/2: [/8 - /8]",
   243  			e: "/1/2: contradiction",
   244  		},
   245  	}
   246  
   247  	for i, tc := range testData {
   248  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   249  			a := ParseConstraint(&evalCtx, tc.a)
   250  			b := ParseConstraint(&evalCtx, tc.b)
   251  			a.Combine(&evalCtx, &b)
   252  			if res := a.String(); res != tc.e {
   253  				t.Errorf("expected\n  %s; got\n  %s", tc.e, res)
   254  			}
   255  		})
   256  	}
   257  }
   258  
   259  func TestConsolidateSpans(t *testing.T) {
   260  	defer leaktest.AfterTest(t)()
   261  	st := cluster.MakeTestingClusterSettings()
   262  	evalCtx := tree.MakeTestingEvalContext(st)
   263  
   264  	testData := []struct {
   265  		s string
   266  		// expected value
   267  		e string
   268  	}{
   269  		{
   270  			s: "[/1 - /2] [/3 - /5] [/7 - /9]",
   271  			e: "[/1 - /5] [/7 - /9]",
   272  		},
   273  		{
   274  			s: "[/1 - /2] (/3 - /5] [/7 - /9]",
   275  			e: "[/1 - /2] (/3 - /5] [/7 - /9]",
   276  		},
   277  		{
   278  			s: "[/1 - /2) [/3 - /5] [/7 - /9]",
   279  			e: "[/1 - /2) [/3 - /5] [/7 - /9]",
   280  		},
   281  		{
   282  			s: "[/1 - /2) (/3 - /5] [/7 - /9]",
   283  			e: "[/1 - /2) (/3 - /5] [/7 - /9]",
   284  		},
   285  		{
   286  			s: "[/1/1 - /1/3] [/1/4 - /2]",
   287  			e: "[/1/1 - /2]",
   288  		},
   289  		{
   290  			s: "[/1/1/5 - /1/1/3] [/1/1/2 - /1/1/1]",
   291  			e: "[/1/1/5 - /1/1/1]",
   292  		},
   293  		{
   294  			s: "[/1/1/5 - /1/1/3] [/1/2/2 - /1/2/1]",
   295  			e: "[/1/1/5 - /1/1/3] [/1/2/2 - /1/2/1]",
   296  		},
   297  		{
   298  			s: "[/1 - /2] [/3 - /4] [/5 - /6] [/8 - /9] [/10 - /11] [/12 - /13] [/15 - /16]",
   299  			e: "[/1 - /6] [/8 - /13] [/15 - /16]",
   300  		},
   301  		{
   302  			// Test that consolidating two spans preserves the correct type of ending
   303  			// boundary (#38878).
   304  			s: "[/1 - /2] [/3 - /5)",
   305  			e: "[/1 - /5)",
   306  		},
   307  	}
   308  
   309  	kc := testKeyContext(1, 2, -3)
   310  	for i, tc := range testData {
   311  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   312  			spans := parseSpans(&evalCtx, tc.s)
   313  			var c Constraint
   314  			c.Init(kc, &spans)
   315  			c.ConsolidateSpans(kc.EvalCtx)
   316  			if res := c.Spans.String(); res != tc.e {
   317  				t.Errorf("expected  %s  got  %s", tc.e, res)
   318  			}
   319  		})
   320  	}
   321  }
   322  
   323  func TestExactPrefix(t *testing.T) {
   324  	defer leaktest.AfterTest(t)()
   325  	st := cluster.MakeTestingClusterSettings()
   326  	evalCtx := tree.MakeTestingEvalContext(st)
   327  
   328  	testData := []struct {
   329  		s string
   330  		// expected value
   331  		e int
   332  	}{
   333  		{
   334  			s: "",
   335  			e: 0,
   336  		},
   337  		{
   338  			s: "[/1 - /1]",
   339  			e: 1,
   340  		},
   341  		{
   342  			s: "[/1 - /2]",
   343  			e: 0,
   344  		},
   345  		{
   346  			s: "[/1/2/3 - /1/2/3]",
   347  			e: 3,
   348  		},
   349  		{
   350  			s: "[/1/2/3 - /1/2/3] [/1/2/5 - /1/2/8]",
   351  			e: 2,
   352  		},
   353  		{
   354  			s: "[/1/2/3 - /1/2/3] [/1/2/5 - /1/3/8]",
   355  			e: 1,
   356  		},
   357  		{
   358  			s: "[/1/2/3 - /1/2/3] [/1/3/3 - /1/3/3]",
   359  			e: 1,
   360  		},
   361  		{
   362  			s: "[/1/2/3 - /1/2/3] [/3 - /4]",
   363  			e: 0,
   364  		},
   365  		{
   366  			s: "[/1/2/1 - /1/2/1] [/1/3/1 - /1/4/1]",
   367  			e: 1,
   368  		},
   369  	}
   370  
   371  	kc := testKeyContext(1, 2, 3)
   372  	for i, tc := range testData {
   373  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   374  			spans := parseSpans(&evalCtx, tc.s)
   375  			var c Constraint
   376  			c.Init(kc, &spans)
   377  			if res := c.ExactPrefix(kc.EvalCtx); res != tc.e {
   378  				t.Errorf("expected  %d  got  %d", tc.e, res)
   379  			}
   380  		})
   381  	}
   382  }
   383  
   384  type constraintTestData struct {
   385  	cLt10           Constraint // [ - /10)
   386  	cGt20           Constraint // (/20 - ]
   387  	c1to10          Constraint // [/1 - /10]
   388  	c5to25          Constraint // (/5 - /25)
   389  	c20to30         Constraint // [/20 - /30)
   390  	c30to40         Constraint // [/30 - /40]
   391  	c40to50         Constraint // [/40 - /50]
   392  	c60to70         Constraint // (/60 - /70)
   393  	cherryRaspberry Constraint // [/'cherry'/true - /'raspberry'/false)
   394  	mangoStrawberry Constraint // [/'mango'/false - /'strawberry']
   395  }
   396  
   397  func newConstraintTestData(evalCtx *tree.EvalContext) *constraintTestData {
   398  	data := &constraintTestData{}
   399  
   400  	key1 := MakeKey(tree.NewDInt(1))
   401  	key5 := MakeKey(tree.NewDInt(5))
   402  	key10 := MakeKey(tree.NewDInt(10))
   403  	key20 := MakeKey(tree.NewDInt(20))
   404  	key25 := MakeKey(tree.NewDInt(25))
   405  	key30 := MakeKey(tree.NewDInt(30))
   406  	key40 := MakeKey(tree.NewDInt(40))
   407  	key50 := MakeKey(tree.NewDInt(50))
   408  	key60 := MakeKey(tree.NewDInt(60))
   409  	key70 := MakeKey(tree.NewDInt(70))
   410  
   411  	kc12 := testKeyContext(1, 2)
   412  	kc1 := testKeyContext(1)
   413  
   414  	cherry := MakeCompositeKey(tree.NewDString("cherry"), tree.DBoolTrue)
   415  	mango := MakeCompositeKey(tree.NewDString("mango"), tree.DBoolFalse)
   416  	raspberry := MakeCompositeKey(tree.NewDString("raspberry"), tree.DBoolFalse)
   417  	strawberry := MakeKey(tree.NewDString("strawberry"))
   418  
   419  	var span Span
   420  
   421  	// [ - /10)
   422  	span.Init(EmptyKey, IncludeBoundary, key10, ExcludeBoundary)
   423  	data.cLt10.InitSingleSpan(kc1, &span)
   424  
   425  	// (/20 - ]
   426  	span.Init(key20, ExcludeBoundary, EmptyKey, IncludeBoundary)
   427  	data.cGt20.InitSingleSpan(kc1, &span)
   428  
   429  	// [/1 - /10]
   430  	span.Init(key1, IncludeBoundary, key10, IncludeBoundary)
   431  	data.c1to10.InitSingleSpan(kc1, &span)
   432  
   433  	// (/5 - /25)
   434  	span.Init(key5, ExcludeBoundary, key25, ExcludeBoundary)
   435  	data.c5to25.InitSingleSpan(kc1, &span)
   436  
   437  	// [/20 - /30)
   438  	span.Init(key20, IncludeBoundary, key30, ExcludeBoundary)
   439  	data.c20to30.InitSingleSpan(kc1, &span)
   440  
   441  	// [/30 - /40]
   442  	span.Init(key30, IncludeBoundary, key40, IncludeBoundary)
   443  	data.c30to40.InitSingleSpan(kc1, &span)
   444  
   445  	// [/40 - /50]
   446  	span.Init(key40, IncludeBoundary, key50, IncludeBoundary)
   447  	data.c40to50.InitSingleSpan(kc1, &span)
   448  
   449  	// (/60 - /70)
   450  	span.Init(key60, ExcludeBoundary, key70, ExcludeBoundary)
   451  	data.c60to70.InitSingleSpan(kc1, &span)
   452  
   453  	// [/'cherry'/true - /'raspberry'/false)
   454  	span.Init(cherry, IncludeBoundary, raspberry, ExcludeBoundary)
   455  	data.cherryRaspberry.InitSingleSpan(kc12, &span)
   456  
   457  	// [/'mango'/false - /'strawberry']
   458  	span.Init(mango, IncludeBoundary, strawberry, IncludeBoundary)
   459  	data.mangoStrawberry.InitSingleSpan(kc12, &span)
   460  
   461  	return data
   462  }
   463  
   464  func TestExtractNotNullCols(t *testing.T) {
   465  	st := cluster.MakeTestingClusterSettings()
   466  	evalCtx := tree.MakeTestingEvalContext(st)
   467  
   468  	testData := []struct {
   469  		c string
   470  		e []opt.ColumnID
   471  	}{
   472  		{ // 0
   473  			c: "/1: [/2 - ]",
   474  			e: []opt.ColumnID{1},
   475  		},
   476  		{ // 1
   477  			c: "/1: [ - /2]",
   478  			e: []opt.ColumnID{},
   479  		},
   480  		{ // 2
   481  			c: "/1: [/NULL - /4]",
   482  			e: []opt.ColumnID{},
   483  		},
   484  		{ // 3
   485  			c: "/1: (/NULL - /4]",
   486  			e: []opt.ColumnID{1},
   487  		},
   488  		{ // 4
   489  			c: "/-1: [ - /2]",
   490  			e: []opt.ColumnID{1},
   491  		},
   492  		{ // 5
   493  			c: "/-1: [/2 - ]",
   494  			e: []opt.ColumnID{},
   495  		},
   496  		{ // 6
   497  			c: "/-1: [/4 - /NULL]",
   498  			e: []opt.ColumnID{},
   499  		},
   500  		{ // 7
   501  			c: "/-1: [/4 - /NULL)",
   502  			e: []opt.ColumnID{1},
   503  		},
   504  		{ // 8
   505  			c: "/1/2/3: [/1/1/1 - /1/1/2] [/3/3/3 - /3/3/4]",
   506  			e: []opt.ColumnID{1, 2, 3},
   507  		},
   508  		{ // 9
   509  			c: "/1/2/3/4: [/1/1/1/1 - /1/1/2/1] [/3/3/3/1 - /3/3/4/1]",
   510  			e: []opt.ColumnID{1, 2, 3},
   511  		},
   512  		{ // 10
   513  			c: "/1/2/3: [/1/1 - /1/1/2] [/3/3/3 - /3/3/4]",
   514  			e: []opt.ColumnID{1, 2},
   515  		},
   516  		{ // 11
   517  			c: "/1/-2/-3: [/1/1/2 - /1/1] [/3/3/4 - /3/3/3]",
   518  			e: []opt.ColumnID{1, 2},
   519  		},
   520  		{ // 12
   521  			c: "/1/2/3: [/1/1/1 - /1/1/2] [/3/3/3 - /3/3/4] [/4/4/1 - /5]",
   522  			e: []opt.ColumnID{1},
   523  		},
   524  		{ // 13
   525  			c: "/1/2/3: [/1/1/NULL - /1/1/2] [/3/3/3 - /3/3/4]",
   526  			e: []opt.ColumnID{1, 2},
   527  		},
   528  		{ // 13
   529  			c: "/1/2/3: [/1/1/1 - /1/1/1] [/2/NULL/2 - /2/NULL/3]",
   530  			e: []opt.ColumnID{1, 3},
   531  		},
   532  	}
   533  
   534  	for i, tc := range testData {
   535  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   536  			c := ParseConstraint(&evalCtx, tc.c)
   537  			cols := c.ExtractNotNullCols(&evalCtx)
   538  			if exp := opt.MakeColSet(tc.e...); !cols.Equals(exp) {
   539  				t.Errorf("expected %s; got %s", exp, cols)
   540  			}
   541  		})
   542  	}
   543  }