From bbd2eeac3bce98b2347d624b5d0d6cc01b0405d3 Mon Sep 17 00:00:00 2001 From: Radu Berinde Date: Mon, 10 Sep 2018 17:03:49 -0400 Subject: [PATCH] opt: fix join cardinality calculation The calculation was incorrect because it accounted for the "outerness" using `AtLeast` but only with the `Min` value; the `Max` value needs to be taken into account as well. This change fixes this and makes the following improvements: - `AtLeast` now takes a cardinality instead of a value; - `AtMost` is renamed to `Limit` because it's inconsistent with the new `AtLeast`; - the cardinality for full outer join with a false filter is improved; - extensive unit tests for makeJoinCardinality. Release note: None --- pkg/sql/opt/memo/logical_props_builder.go | 58 +++-- .../opt/memo/logical_props_builder_test.go | 209 ++++++++++++++++++ pkg/sql/opt/memo/testdata/logprops/join | 4 +- pkg/sql/opt/props/cardinality.go | 61 +++-- pkg/sql/opt/props/cardinality_test.go | 80 +++---- 5 files changed, 318 insertions(+), 94 deletions(-) create mode 100644 pkg/sql/opt/memo/logical_props_builder_test.go diff --git a/pkg/sql/opt/memo/logical_props_builder.go b/pkg/sql/opt/memo/logical_props_builder.go index 0a3b4c91bc83..cc1f997d162e 100644 --- a/pkg/sql/opt/memo/logical_props_builder.go +++ b/pkg/sql/opt/memo/logical_props_builder.go @@ -174,9 +174,9 @@ func (b *logicalPropsBuilder) buildScanProps(ev ExprView) props.Logical { if def.Constraint != nil && def.Constraint.IsContradiction() { relational.Cardinality = props.ZeroCardinality } else if relational.FuncDeps.HasMax1Row() { - relational.Cardinality = relational.Cardinality.AtMost(1) + relational.Cardinality = relational.Cardinality.Limit(1) } else if hardLimit > 0 && hardLimit < math.MaxUint32 { - relational.Cardinality = relational.Cardinality.AtMost(uint32(hardLimit)) + relational.Cardinality = relational.Cardinality.Limit(uint32(hardLimit)) } // Statistics @@ -276,7 +276,7 @@ func (b *logicalPropsBuilder) buildSelectProps(ev ExprView) props.Logical { if filter.Operator() == opt.FalseOp || filterProps.Constraints == constraint.Contradiction { relational.Cardinality = props.ZeroCardinality } else if relational.FuncDeps.HasMax1Row() { - relational.Cardinality = relational.Cardinality.AtMost(1) + relational.Cardinality = relational.Cardinality.Limit(1) } // Statistics @@ -526,7 +526,7 @@ func (b *logicalPropsBuilder) buildJoinProps(ev ExprView) props.Logical { // Calculate cardinality, depending on join type. relational.Cardinality = b.makeJoinCardinality(leftProps.Cardinality, &h) if relational.FuncDeps.HasMax1Row() { - relational.Cardinality = relational.Cardinality.AtMost(1) + relational.Cardinality = relational.Cardinality.Limit(1) } // Statistics @@ -633,7 +633,7 @@ func (b *logicalPropsBuilder) buildGroupByProps(ev ExprView) props.Logical { // will also be returned by GroupBy and DistinctOn. relational.Cardinality = inputProps.Cardinality.AsLowAs(1) if relational.FuncDeps.HasMax1Row() { - relational.Cardinality = relational.Cardinality.AtMost(1) + relational.Cardinality = relational.Cardinality.Limit(1) } } @@ -864,7 +864,7 @@ func (b *logicalPropsBuilder) buildLimitProps(ev ExprView) props.Logical { if constLimit <= 0 { relational.Cardinality = props.ZeroCardinality } else if constLimit < math.MaxUint32 { - relational.Cardinality = relational.Cardinality.AtMost(uint32(constLimit)) + relational.Cardinality = relational.Cardinality.Limit(uint32(constLimit)) } // Statistics @@ -959,7 +959,7 @@ func (b *logicalPropsBuilder) buildMax1RowProps(ev ExprView) props.Logical { // Cardinality // ----------- // Max1Row ensures that zero or one row is returned from input. - relational.Cardinality = inputProps.Cardinality.AtMost(1) + relational.Cardinality = inputProps.Cardinality.Limit(1) // Statistics // ---------- @@ -1190,7 +1190,6 @@ func makeTableFuncDep(md *opt.Metadata, tabID opt.TableID) *props.FuncDepSet { func (b *logicalPropsBuilder) makeJoinCardinality( left props.Cardinality, h *joinPropsHelper, ) props.Cardinality { - var card props.Cardinality switch h.joinType { case opt.SemiJoinOp, opt.SemiJoinApplyOp, opt.AntiJoinOp, opt.AntiJoinApplyOp: // Semi/Anti join cardinality never exceeds left input cardinality, and @@ -1200,32 +1199,45 @@ func (b *logicalPropsBuilder) makeJoinCardinality( // Other join types can return up to cross product of rows. right := h.rightCardinality - card = left.Product(right) + innerJoinCard := left.Product(right) // Apply filter to cardinality. if !h.filterIsTrue { if h.filterIsFalse { - card = props.ZeroCardinality + innerJoinCard = props.ZeroCardinality } else { - card = card.AsLowAs(0) + innerJoinCard = innerJoinCard.AsLowAs(0) } } // Outer joins return minimum number of rows, depending on type. switch h.joinType { - case opt.LeftJoinOp, opt.LeftJoinApplyOp, opt.FullJoinOp, opt.FullJoinApplyOp: - card = card.AtLeast(left.Min) - } - switch h.joinType { - case opt.RightJoinOp, opt.RightJoinApplyOp, opt.FullJoinOp, opt.FullJoinApplyOp: - card = card.AtLeast(right.Min) - } - switch h.joinType { + case opt.LeftJoinOp, opt.LeftJoinApplyOp: + return innerJoinCard.AtLeast(left) + + case opt.RightJoinOp, opt.RightJoinApplyOp: + return innerJoinCard.AtLeast(right) + case opt.FullJoinOp, opt.FullJoinApplyOp: - card = card.AsHighAs(left.Min + right.Min) - } + if innerJoinCard.IsZero() { + // In this case, we know that each left or right row will generate an + // output row. + return left.Add(right) + } + var c props.Cardinality + // We get at least MAX(left.Min, right.Min) rows. + c.Min = left.Min + if c.Min < right.Min { + c.Min = right.Min + } + // We could get left.Max + right.Max rows (if the filter doesn't match + // anything). We use Add here because it handles overflow. + c.Max = left.Add(right).Max + return innerJoinCard.AtLeast(c) - return card + default: + return innerJoinCard + } } func (b *logicalPropsBuilder) makeSetCardinality( @@ -1240,7 +1252,7 @@ func (b *logicalPropsBuilder) makeSetCardinality( case opt.IntersectOp, opt.IntersectAllOp: // Use minimum of left and right Max cardinality. card = props.Cardinality{Min: 0, Max: left.Max} - card = card.AtMost(right.Max) + card = card.Limit(right.Max) case opt.ExceptOp, opt.ExceptAllOp: // Use left Max cardinality. diff --git a/pkg/sql/opt/memo/logical_props_builder_test.go b/pkg/sql/opt/memo/logical_props_builder_test.go new file mode 100644 index 000000000000..9b90db52b26d --- /dev/null +++ b/pkg/sql/opt/memo/logical_props_builder_test.go @@ -0,0 +1,209 @@ +// Copyright 2018 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package memo + +import ( + "fmt" + "testing" + + "github.com/cockroachdb/cockroach/pkg/sql/opt" + "github.com/cockroachdb/cockroach/pkg/sql/opt/props" +) + +func TestJoinCardinality(t *testing.T) { + c := func(min, max uint32) props.Cardinality { + return props.Cardinality{Min: min, Max: max} + } + + type testCase struct { + left props.Cardinality + right props.Cardinality + expected props.Cardinality + } + + testCaseGroups := []struct { + joinType opt.Operator + filter string // "true", "false", or "other" + testCases []testCase + }{ + { // Inner join, true filter. + joinType: opt.InnerJoinOp, + filter: "true", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(25, 100)}, + }, + }, + + { // Inner join, false filter. + joinType: opt.InnerJoinOp, + filter: "false", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 0)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 0)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 0)}, + {left: c(5, 10), right: c(5, 10), expected: c(0, 0)}, + }, + }, + + { // Inner join, other filter. + joinType: opt.InnerJoinOp, + filter: "other", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(0, 100)}, + }, + }, + + { // Left join, true filter. + joinType: opt.LeftJoinOp, + filter: "true", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(25, 100)}, + }, + }, + + { // Left join, false filter. + joinType: opt.LeftJoinOp, + filter: "false", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 10)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 10)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 10)}, + {left: c(5, 10), right: c(5, 10), expected: c(5, 10)}, + }, + }, + + { // Left join, other filter. + joinType: opt.LeftJoinOp, + filter: "other", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(5, 100)}, + }, + }, + + { // Right join, true filter. + joinType: opt.RightJoinOp, + filter: "true", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(25, 100)}, + }, + }, + + { // Right join, false filter. + joinType: opt.RightJoinOp, + filter: "false", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 10)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 10)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 10)}, + {left: c(5, 10), right: c(5, 10), expected: c(5, 10)}, + }, + }, + + { // Right join, other filter. + joinType: opt.RightJoinOp, + filter: "other", + testCases: []testCase{ + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(5, 100)}, + }, + }, + + { // Full join, true filter. + joinType: opt.FullJoinOp, + filter: "true", + testCases: []testCase{ + {left: c(0, 1), right: c(0, 1), expected: c(0, 2)}, + {left: c(1, 1), right: c(1, 1), expected: c(1, 2)}, + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(25, 100)}, + {left: c(7, 10), right: c(8, 10), expected: c(56, 100)}, + {left: c(8, 10), right: c(7, 10), expected: c(56, 100)}, + }, + }, + + { // Full join, false filter. + joinType: opt.FullJoinOp, + filter: "false", + testCases: []testCase{ + {left: c(0, 1), right: c(0, 1), expected: c(0, 2)}, + {left: c(1, 1), right: c(1, 1), expected: c(2, 2)}, + {left: c(2, 5), right: c(3, 8), expected: c(5, 13)}, + {left: c(0, 10), right: c(0, 10), expected: c(0, 20)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 20)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 20)}, + {left: c(5, 10), right: c(5, 10), expected: c(10, 20)}, + {left: c(7, 10), right: c(8, 10), expected: c(15, 20)}, + {left: c(8, 10), right: c(7, 10), expected: c(15, 20)}, + }, + }, + + { // Full join, other filter. + joinType: opt.FullJoinOp, + filter: "other", + testCases: []testCase{ + {left: c(0, 1), right: c(0, 1), expected: c(0, 2)}, + {left: c(1, 1), right: c(1, 1), expected: c(1, 2)}, + {left: c(2, 5), right: c(3, 8), expected: c(3, 40)}, + {left: c(0, 10), right: c(0, 10), expected: c(0, 100)}, + {left: c(5, 10), right: c(0, 10), expected: c(5, 100)}, + {left: c(0, 10), right: c(5, 10), expected: c(5, 100)}, + {left: c(5, 10), right: c(5, 10), expected: c(5, 100)}, + {left: c(7, 10), right: c(8, 10), expected: c(8, 100)}, + {left: c(8, 10), right: c(7, 10), expected: c(8, 100)}, + }, + }, + } + + for _, group := range testCaseGroups { + t.Run(fmt.Sprintf("%s/%s", group.joinType, group.filter), func(t *testing.T) { + for i, tc := range group.testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + b := &logicalPropsBuilder{} + h := &joinPropsHelper{} + h.rightCardinality = tc.right + h.joinType = group.joinType + h.filterIsTrue = (group.filter == "true") + h.filterIsFalse = (group.filter == "false") + + res := b.makeJoinCardinality(tc.left, h) + if res != tc.expected { + t.Errorf( + "left=%s right=%s: expected %s, got %s\n", tc.left, tc.right, tc.expected, res, + ) + } + }) + } + }) + } +} diff --git a/pkg/sql/opt/memo/testdata/logprops/join b/pkg/sql/opt/memo/testdata/logprops/join index b59e630f8950..5b5d03aadc91 100644 --- a/pkg/sql/opt/memo/testdata/logprops/join +++ b/pkg/sql/opt/memo/testdata/logprops/join @@ -1026,13 +1026,13 @@ full-join └── filters [type=bool] └── true [type=bool] -# Calculate full-join cardinality with filter. +# Calculate full-join cardinality with false filter. build SELECT * FROM (VALUES (NULL), (NULL)) a FULL JOIN (VALUES (NULL), (NULL)) b ON a.column1=b.column1 ---- full-join ├── columns: column1:1(unknown) column1:2(unknown) - ├── cardinality: [2 - 4] + ├── cardinality: [4 - 4] ├── prune: (1,2) ├── reject-nulls: (1,2) ├── values diff --git a/pkg/sql/opt/props/cardinality.go b/pkg/sql/opt/props/cardinality.go index 5a9106d5a252..3fd22993318c 100644 --- a/pkg/sql/opt/props/cardinality.go +++ b/pkg/sql/opt/props/cardinality.go @@ -64,45 +64,28 @@ func (c Cardinality) CanBeZero() bool { // AsLowAs ratchets the min bound downwards in order to ensure that it allows // values that are >= the min value. func (c Cardinality) AsLowAs(min uint32) Cardinality { - if min < c.Min { - return Cardinality{Min: min, Max: c.Max} + return Cardinality{ + Min: minVal(c.Min, min), + Max: c.Max, } - return c } -// AsHighAs ratchets the max bound upwards in order to ensure that it allows -// values that are <= the max value. -func (c Cardinality) AsHighAs(max uint32) Cardinality { - if max > c.Max { - return Cardinality{Min: c.Min, Max: max} - } - return c -} - -// AtLeast ratchets the bounds upwards so that they're at least as big as the -// given min value. -func (c Cardinality) AtLeast(min uint32) Cardinality { - if c.Min > min { - min = c.Min - } - max := min - if c.Max > max { - max = c.Max +// Limit ratchets the bounds downwards so that they're no bigger than the given +// max value. +func (c Cardinality) Limit(max uint32) Cardinality { + return Cardinality{ + Min: minVal(c.Min, max), + Max: minVal(c.Max, max), } - return Cardinality{Min: min, Max: max} } -// AtMost ratchets the bounds downwards so that they're no bigger than the given -// max value. -func (c Cardinality) AtMost(max uint32) Cardinality { - min := max - if c.Min < min { - min = c.Min - } - if c.Max < max { - max = c.Max +// AtLeast ratchets the bounds upwards so that they're at least as large as the +// bounds in the given cardinality. +func (c Cardinality) AtLeast(other Cardinality) Cardinality { + return Cardinality{ + Min: maxVal(c.Min, other.Min), + Max: maxVal(c.Max, other.Max), } - return Cardinality{Min: min, Max: max} } // Add sums the min and max bounds to get a combined count of rows. @@ -157,3 +140,17 @@ func (c Cardinality) String() string { } return fmt.Sprintf("[%d - %d]", c.Min, c.Max) } + +func minVal(a, b uint32) uint32 { + if a <= b { + return a + } + return b +} + +func maxVal(a, b uint32) uint32 { + if a >= b { + return a + } + return b +} diff --git a/pkg/sql/opt/props/cardinality_test.go b/pkg/sql/opt/props/cardinality_test.go index f20d54a8e82a..876f8d20b8e6 100644 --- a/pkg/sql/opt/props/cardinality_test.go +++ b/pkg/sql/opt/props/cardinality_test.go @@ -22,54 +22,60 @@ import ( ) func TestCardinality(t *testing.T) { - test := func(card props.Cardinality, expected string) { + test := func(card, expected props.Cardinality) { t.Helper() - if card.String() != expected { - t.Errorf("expected: %s, actual: %s", expected, card.String()) + if card != expected { + t.Errorf("expected: %s, actual: %s", expected, card) } } - maxCard := props.Cardinality{Min: math.MaxUint32, Max: math.MaxUint32} + c := func(min, max uint32) props.Cardinality { + return props.Cardinality{Min: min, Max: max} + } + inf := uint32(math.MaxUint32) - // Filter variations. - test(props.Cardinality{Min: 0, Max: 10}.AsLowAs(0), "[0 - 10]") - test(props.Cardinality{Min: 1, Max: 10}.AsLowAs(0), "[0 - 10]") - test(props.Cardinality{Min: 5, Max: 10}.AsLowAs(1), "[1 - 10]") - test(props.Cardinality{Min: 1, Max: 10}.AsLowAs(5), "[1 - 10]") - test(props.Cardinality{Min: 1, Max: 10}.AsLowAs(20), "[1 - 10]") - test(props.AnyCardinality.AsLowAs(1), "[0 - ]") + // AsLowAs variations. + test(c(0, 10).AsLowAs(0), c(0, 10)) + test(c(1, 10).AsLowAs(0), c(0, 10)) + test(c(5, 10).AsLowAs(1), c(1, 10)) + test(c(1, 10).AsLowAs(5), c(1, 10)) + test(c(1, 10).AsLowAs(20), c(1, 10)) + test(props.AnyCardinality.AsLowAs(1), c(0, inf)) - // AtLeast variations. - test(props.Cardinality{Min: 0, Max: 10}.AtLeast(1), "[1 - 10]") - test(props.Cardinality{Min: 1, Max: 10}.AtLeast(5), "[5 - 10]") - test(props.Cardinality{Min: 5, Max: 10}.AtLeast(15), "[15 - 15]") - test(props.Cardinality{Min: 5, Max: 10}.AtLeast(1), "[5 - 10]") - test(props.AnyCardinality.AtLeast(1), "[1 - ]") - test(props.AnyCardinality.AtLeast(math.MaxUint32), "[4294967295 - ]") + // Limit variations. + test(c(0, 10).Limit(5), c(0, 5)) + test(c(1, 10).Limit(10), c(1, 10)) + test(c(5, 10).Limit(1), c(1, 1)) + test(props.AnyCardinality.Limit(1), c(0, 1)) - // AtMost variations. - test(props.Cardinality{Min: 0, Max: 10}.AtMost(5), "[0 - 5]") - test(props.Cardinality{Min: 1, Max: 10}.AtMost(10), "[1 - 10]") - test(props.Cardinality{Min: 5, Max: 10}.AtMost(1), "[1 - 1]") - test(props.AnyCardinality.AtMost(1), "[0 - 1]") + // AtLeast variations. + test(c(0, 10).AtLeast(c(1, 1)), c(1, 10)) + test(c(1, 10).AtLeast(c(5, 15)), c(5, 15)) + test(c(5, 10).AtLeast(c(1, 2)), c(5, 10)) + test(c(5, 10).AtLeast(c(1, 8)), c(5, 10)) + test(c(5, 10).AtLeast(c(7, 8)), c(7, 10)) + test(c(5, 10).AtLeast(c(1, 15)), c(5, 15)) + test(c(5, 10).AtLeast(c(7, 15)), c(7, 15)) + test(props.AnyCardinality.AtLeast(c(1, 10)), c(1, inf)) + test(props.AnyCardinality.AtLeast(c(inf, inf)), c(inf, inf)) // Add variations. - test(props.Cardinality{Min: 0, Max: 10}.Add(props.Cardinality{Min: 5, Max: 5}), "[5 - 15]") - test(props.Cardinality{Min: 0, Max: 10}.Add(props.Cardinality{Min: 20, Max: 30}), "[20 - 40]") - test(props.Cardinality{Min: 1, Max: 10}.Add(props.AnyCardinality), "[1 - ]") - test(maxCard.Add(props.AnyCardinality), "[4294967295 - ]") + test(c(0, 10).Add(c(5, 5)), c(5, 15)) + test(c(0, 10).Add(c(20, 30)), c(20, 40)) + test(c(1, 10).Add(props.AnyCardinality), c(1, inf)) + test(c(inf, inf).Add(props.AnyCardinality), c(inf, inf)) // Product variations. - test(props.Cardinality{Min: 0, Max: 10}.Product(props.Cardinality{Min: 5, Max: 5}), "[0 - 50]") - test(props.Cardinality{Min: 1, Max: 10}.Product(props.Cardinality{Min: 2, Max: 2}), "[2 - 20]") - test(props.Cardinality{Min: 1, Max: 10}.Product(props.AnyCardinality), "[0 - ]") - test(maxCard.Product(props.OneCardinality), "[4294967295 - ]") - test(maxCard.Product(maxCard), "[4294967295 - ]") + test(c(0, 10).Product(c(5, 5)), c(0, 50)) + test(c(1, 10).Product(c(2, 2)), c(2, 20)) + test(c(1, 10).Product(props.AnyCardinality), c(0, inf)) + test(c(inf, inf).Product(props.OneCardinality), c(inf, inf)) + test(c(inf, inf).Product(c(inf, inf)), c(inf, inf)) // Skip variations. - test(props.Cardinality{Min: 0, Max: 0}.Skip(1), "[0 - 0]") - test(props.Cardinality{Min: 0, Max: 10}.Skip(5), "[0 - 5]") - test(props.Cardinality{Min: 5, Max: 10}.Skip(5), "[0 - 5]") - test(props.AnyCardinality.Skip(5), "[0 - ]") - test(maxCard.Skip(5), "[4294967290 - ]") + test(c(0, 0).Skip(1), c(0, 0)) + test(c(0, 10).Skip(5), c(0, 5)) + test(c(5, 10).Skip(5), c(0, 5)) + test(props.AnyCardinality.Skip(5), c(0, inf)) + test(c(inf, inf).Skip(5), c(inf-5, inf)) }