From 4481006e1c37313fae71d11dd6f3b0c4e52eeb9f Mon Sep 17 00:00:00 2001 From: tiancaiamao Date: Wed, 18 Mar 2020 13:35:23 +0800 Subject: [PATCH] planner/core: get back range columns pruning after last refactor (#15169) --- planner/core/partition_pruning_test.go | 114 ++++++++++- planner/core/rule_partition_processor.go | 237 +++++++++++++++++++---- 2 files changed, 304 insertions(+), 47 deletions(-) diff --git a/planner/core/partition_pruning_test.go b/planner/core/partition_pruning_test.go index c37fbd70201b8..0e4568ffea43c 100644 --- a/planner/core/partition_pruning_test.go +++ b/planner/core/partition_pruning_test.go @@ -44,13 +44,15 @@ func (s *testPartitionPruningSuite) TestCanBePrune(c *C) { "create table t (d datetime not null)", "to_days(d)", ) - lessThan := lessThanData{data: []int64{733108, 733132}, maxvalue: false} + lessThan := lessThanDataInt{data: []int64{733108, 733132}, maxvalue: false} + prunner := &rangePruner{lessThan, tc.col, tc.fn} + queryExpr := tc.expr("d < '2000-03-08 00:00:00'") - result := partitionRangeForCNFExpr(tc.sctx, queryExpr, lessThan, tc.col, tc.fn, fullRange(len(lessThan.data))) + result := partitionRangeForCNFExpr(tc.sctx, queryExpr, prunner, fullRange(len(lessThan.data))) c.Assert(equalPartitionRangeOR(result, partitionRangeOR{{0, 1}}), IsTrue) queryExpr = tc.expr("d > '2018-03-08 00:00:00'") - result = partitionRangeForCNFExpr(tc.sctx, queryExpr, lessThan, tc.col, tc.fn, fullRange(len(lessThan.data))) + result = partitionRangeForCNFExpr(tc.sctx, queryExpr, prunner, fullRange(len(lessThan.data))) c.Assert(equalPartitionRangeOR(result, partitionRangeOR{}), IsTrue) // For the following case: @@ -68,13 +70,15 @@ func (s *testPartitionPruningSuite) TestCanBePrune(c *C) { "create table t (report_updated timestamp)", "unix_timestamp(report_updated)", ) - lessThan = lessThanData{data: []int64{1199145600, 1207008000, 1262304000, 0}, maxvalue: true} + lessThan = lessThanDataInt{data: []int64{1199145600, 1207008000, 1262304000, 0}, maxvalue: true} + prunner = &rangePruner{lessThan, tc.col, tc.fn} + queryExpr = tc.expr("report_updated > '2008-05-01 00:00:00'") - result = partitionRangeForCNFExpr(tc.sctx, queryExpr, lessThan, tc.col, tc.fn, fullRange(len(lessThan.data))) + result = partitionRangeForCNFExpr(tc.sctx, queryExpr, prunner, fullRange(len(lessThan.data))) c.Assert(equalPartitionRangeOR(result, partitionRangeOR{{2, 4}}), IsTrue) queryExpr = tc.expr("report_updated > unix_timestamp('2008-05-01 00:00:00')") - partitionRangeForCNFExpr(tc.sctx, queryExpr, lessThan, tc.col, tc.fn, fullRange(len(lessThan.data))) + partitionRangeForCNFExpr(tc.sctx, queryExpr, prunner, fullRange(len(lessThan.data))) // TODO: Uncomment the check after fixing issue https://github.com/pingcap/tidb/issues/12028 // c.Assert(equalPartitionRangeOR(result, partitionRangeOR{{2, 4}}), IsTrue) // report_updated > unix_timestamp('2008-05-01 00:00:00') is converted to gt(t.t.report_updated, ) @@ -83,7 +87,7 @@ func (s *testPartitionPruningSuite) TestCanBePrune(c *C) { } func (s *testPartitionPruningSuite) TestPruneUseBinarySearch(c *C) { - lessThan := lessThanData{data: []int64{4, 7, 11, 14, 17, 0}, maxvalue: true} + lessThan := lessThanDataInt{data: []int64{4, 7, 11, 14, 17, 0}, maxvalue: true} cases := []struct { input dataForPrune result partitionRange @@ -126,7 +130,7 @@ type testCtx struct { schema *expression.Schema columns []*expression.Column names types.NameSlice - lessThan lessThanData + lessThan lessThanDataInt col *expression.Column fn *expression.ScalarFunction } @@ -165,7 +169,8 @@ func (s *testPartitionPruningSuite) TestPartitionRangeForExpr(c *C) { "create table t (a int)", "a", ) - lessThan := lessThanData{data: []int64{4, 7, 11, 14, 17, 0}, maxvalue: true} + lessThan := lessThanDataInt{data: []int64{4, 7, 11, 14, 17, 0}, maxvalue: true} + prunner := &rangePruner{lessThan, tc.columns[0], nil} cases := []struct { input string result partitionRangeOR @@ -188,7 +193,7 @@ func (s *testPartitionPruningSuite) TestPartitionRangeForExpr(c *C) { expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) c.Assert(err, IsNil) result := fullRange(lessThan.length()) - result = partitionRangeForExpr(tc.sctx, expr[0], lessThan, tc.columns[0], nil, result) + result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) c.Assert(equalPartitionRangeOR(ca.result, result), IsTrue, Commentf("unexpected:", ca.input)) } } @@ -266,3 +271,92 @@ func (s *testPartitionPruningSuite) TestPartitionRangeOperation(c *C) { c.Assert(equalPartitionRangeOR(ca.result, result), IsTrue, Commentf("failed %d", i)) } } + +func (s *testPartitionPruningSuite) TestPartitionRangePrunner2VarChar(c *C) { + tc := prepareTestCtx(c, + "create table t (a varchar(32))", + "a", + ) + lessThanDataInt := []string{"'c'", "'f'", "'h'", "'l'", "'t'"} + lessThan := make([]expression.Expression, len(lessThanDataInt)+1) // +1 for maxvalue + for i, str := range lessThanDataInt { + tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) + c.Assert(err, IsNil) + lessThan[i] = tmp[0] + } + + prunner := &rangeColumnPruner{lessThan, tc.columns[0], true} + cases := []struct { + input string + result partitionRangeOR + }{ + {"a > 'g'", partitionRangeOR{{2, 6}}}, + {"a < 'h'", partitionRangeOR{{0, 3}}}, + {"a >= 'm'", partitionRangeOR{{4, 6}}}, + {"a > 'm'", partitionRangeOR{{4, 6}}}, + {"a < 'f'", partitionRangeOR{{0, 2}}}, + {"a = 'c'", partitionRangeOR{{1, 2}}}, + {"a > 't'", partitionRangeOR{{5, 6}}}, + {"a > 'c' and a < 'q'", partitionRangeOR{{1, 5}}}, + {"a < 'l' or a >= 'w'", partitionRangeOR{{0, 4}, {5, 6}}}, + {"a is null", partitionRangeOR{{0, 1}}}, + {"'mm' > a", partitionRangeOR{{0, 5}}}, + {"'f' <= a", partitionRangeOR{{2, 6}}}, + {"'f' >= a", partitionRangeOR{{0, 3}}}, + } + + for _, ca := range cases { + expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) + c.Assert(err, IsNil) + result := fullRange(len(lessThan)) + result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) + c.Assert(equalPartitionRangeOR(ca.result, result), IsTrue, Commentf("unexpected:", ca.input)) + } +} + +func (s *testPartitionPruningSuite) TestPartitionRangePrunner2Date(c *C) { + tc := prepareTestCtx(c, + "create table t (a date)", + "a", + ) + lessThanDataInt := []string{ + "'1999-06-01'", + "'2000-05-01'", + "'2008-04-01'", + "'2010-03-01'", + "'2016-02-01'", + "'2020-01-01'"} + lessThan := make([]expression.Expression, len(lessThanDataInt)) + for i, str := range lessThanDataInt { + tmp, err := expression.ParseSimpleExprsWithNames(tc.sctx, str, tc.schema, tc.names) + c.Assert(err, IsNil) + lessThan[i] = tmp[0] + } + + prunner := &rangeColumnPruner{lessThan, tc.columns[0], false} + cases := []struct { + input string + result partitionRangeOR + }{ + {"a < '1943-02-12'", partitionRangeOR{{0, 1}}}, + {"a >= '1969-02-13'", partitionRangeOR{{0, 6}}}, + {"a > '2003-03-13'", partitionRangeOR{{2, 6}}}, + {"a < '2006-02-03'", partitionRangeOR{{0, 3}}}, + {"a = '2007-07-07'", partitionRangeOR{{2, 3}}}, + {"a > '1949-10-10'", partitionRangeOR{{0, 6}}}, + {"a > '2016-02-01' and a < '2000-01-03'", partitionRangeOR{}}, + {"a < '1969-11-12' or a >= '2019-09-18'", partitionRangeOR{{0, 1}, {5, 6}}}, + {"a is null", partitionRangeOR{{0, 1}}}, + {"'2003-02-27' >= a", partitionRangeOR{{0, 3}}}, + {"'2014-10-24' < a", partitionRangeOR{{4, 6}}}, + {"'2003-03-30' > a", partitionRangeOR{{0, 3}}}, + } + + for _, ca := range cases { + expr, err := expression.ParseSimpleExprsWithNames(tc.sctx, ca.input, tc.schema, tc.names) + c.Assert(err, IsNil) + result := fullRange(len(lessThan)) + result = partitionRangeForExpr(tc.sctx, expr[0], prunner, result) + c.Assert(equalPartitionRangeOR(ca.result, result), IsTrue, Commentf("unexpected:", ca.input)) + } +} diff --git a/planner/core/rule_partition_processor.go b/planner/core/rule_partition_processor.go index d14cd0c7f70e3..5d42630c6f326 100644 --- a/planner/core/rule_partition_processor.go +++ b/planner/core/rule_partition_processor.go @@ -21,6 +21,7 @@ import ( "github.com/pingcap/errors" "github.com/pingcap/parser/ast" "github.com/pingcap/parser/model" + "github.com/pingcap/parser/mysql" "github.com/pingcap/tidb/expression" "github.com/pingcap/tidb/planner/util" "github.com/pingcap/tidb/sessionctx" @@ -175,16 +176,16 @@ func (*partitionProcessor) name() string { return "partition_processor" } -type lessThanData struct { +type lessThanDataInt struct { data []int64 maxvalue bool } -func (lt *lessThanData) length() int { +func (lt *lessThanDataInt) length() int { return len(lt.data) } -func (lt *lessThanData) compare(ith int, v int64) int { +func (lt *lessThanDataInt) compare(ith int, v int64) int { if ith == len(lt.data)-1 { if lt.maxvalue { return 1 @@ -313,34 +314,34 @@ func intersectionRange(start, end, newStart, newEnd int) (int, int) { } func (s *partitionProcessor) pruneRangePartition(ds *DataSource, pi *model.PartitionInfo) (LogicalPlan, error) { - var col *expression.Column - var fn *expression.ScalarFunction - var err error - // Partition by range. - if len(pi.Columns) == 0 { - col, fn, err = makePartitionByFnCol(ds.ctx, ds.TblCols, ds.names, pi.Expr) - } else if len(pi.Columns) == 1 { - // Convert partition by range columns to range. - col, fn, err = makePartitionByFnCol(ds.ctx, ds.TblCols, ds.names, pi.Columns[0].L) + // Partition by range columns. + if len(pi.Columns) > 0 { + return s.pruneRangeColumnsPartition(ds, pi) } + + // Partition by range. + col, fn, err := makePartitionByFnCol(ds.ctx, ds.TblCols, ds.names, pi.Expr) if err != nil { return nil, err } result := fullRange(len(pi.Definitions)) + // Extract the partition column, if the column is not null, it's possible to prune. if col != nil { + // TODO: Store LessThanData in the partitionExpr, avoid allocating here. lessThan, err := makeLessThanData(pi) if err != nil { return nil, err } - result = partitionRangeForCNFExpr(ds.ctx, ds.allConds, lessThan, col, fn, result) + pruner := rangePruner{lessThan, col, fn} + result = partitionRangeForCNFExpr(ds.ctx, ds.allConds, &pruner, result) } return s.makeUnionAllChildren(ds, pi, result) } // makeLessThanData extracts the less than parts from 'partition p0 less than xx ... partitoin p1 less than ...' -func makeLessThanData(pi *model.PartitionInfo) (lessThanData, error) { +func makeLessThanData(pi *model.PartitionInfo) (lessThanDataInt, error) { var maxValue bool lessThan := make([]int64, len(pi.Definitions)) for i := 0; i < len(pi.Definitions); i++ { @@ -351,11 +352,11 @@ func makeLessThanData(pi *model.PartitionInfo) (lessThanData, error) { var err error lessThan[i], err = strconv.ParseInt(pi.Definitions[i].LessThan[0], 10, 64) if err != nil { - return lessThanData{}, errors.WithStack(err) + return lessThanDataInt{}, errors.WithStack(err) } } } - return lessThanData{lessThan, maxValue}, nil + return lessThanDataInt{lessThan, maxValue}, nil } // makePartitionByFnCol extracts the column and function information in 'partition by ... fn(col)'. @@ -380,43 +381,69 @@ func makePartitionByFnCol(sctx sessionctx.Context, columns []*expression.Column, return col, fn, nil } -func partitionRangeForCNFExpr(sctx sessionctx.Context, exprs []expression.Expression, lessThan lessThanData, - col *expression.Column, partFn *expression.ScalarFunction, result partitionRangeOR) partitionRangeOR { +func partitionRangeForCNFExpr(sctx sessionctx.Context, exprs []expression.Expression, + pruner partitionRangePruner, result partitionRangeOR) partitionRangeOR { for i := 0; i < len(exprs); i++ { - result = partitionRangeForExpr(sctx, exprs[i], lessThan, col, partFn, result) + result = partitionRangeForExpr(sctx, exprs[i], pruner, result) } return result } // partitionRangeForExpr calculate the partitions for the expression. -func partitionRangeForExpr(sctx sessionctx.Context, expr expression.Expression, lessThan lessThanData, - col *expression.Column, partFn *expression.ScalarFunction, result partitionRangeOR) partitionRangeOR { +func partitionRangeForExpr(sctx sessionctx.Context, expr expression.Expression, + pruner partitionRangePruner, result partitionRangeOR) partitionRangeOR { // Handle AND, OR respectively. if op, ok := expr.(*expression.ScalarFunction); ok { if op.FuncName.L == ast.LogicAnd { - return partitionRangeForCNFExpr(sctx, op.GetArgs(), lessThan, col, partFn, result) + return partitionRangeForCNFExpr(sctx, op.GetArgs(), pruner, result) } else if op.FuncName.L == ast.LogicOr { args := op.GetArgs() - newRange := partitionRangeForOrExpr(sctx, args[0], args[1], lessThan, col, partFn) + newRange := partitionRangeForOrExpr(sctx, args[0], args[1], pruner) return result.intersection(newRange) } } // Handle a single expression. - dataForPrune, ok := extractDataForPrune(sctx, expr, col, partFn) + start, end, ok := pruner.partitionRangeForExpr(sctx, expr) if !ok { // Can't prune, return the whole range. return result } - start, end := pruneUseBinarySearch(lessThan, dataForPrune) return result.intersectionRange(start, end) } +type partitionRangePruner interface { + partitionRangeForExpr(sessionctx.Context, expression.Expression) (start, end int, succ bool) + fullRange() partitionRangeOR +} + +var _ partitionRangePruner = &rangePruner{} + +// rangePruner is used by 'partition by range'. +type rangePruner struct { + lessThan lessThanDataInt + col *expression.Column + partFn *expression.ScalarFunction +} + +func (p *rangePruner) partitionRangeForExpr(sctx sessionctx.Context, expr expression.Expression) (int, int, bool) { + dataForPrune, ok := p.extractDataForPrune(sctx, expr) + if !ok { + return 0, 0, false + } + start, end := pruneUseBinarySearch(p.lessThan, dataForPrune) + return start, end, true +} + +func (p *rangePruner) fullRange() partitionRangeOR { + return fullRange(p.lessThan.length()) +} + // partitionRangeForOrExpr calculate the partitions for or(expr1, expr2) -func partitionRangeForOrExpr(sctx sessionctx.Context, expr1, expr2 expression.Expression, lessThan lessThanData, - col *expression.Column, partFn *expression.ScalarFunction) partitionRangeOR { - tmp1 := partitionRangeForExpr(sctx, expr1, lessThan, col, partFn, fullRange(lessThan.length())) - tmp2 := partitionRangeForExpr(sctx, expr2, lessThan, col, partFn, fullRange(lessThan.length())) +func partitionRangeForOrExpr(sctx sessionctx.Context, expr1, expr2 expression.Expression, + pruner partitionRangePruner) partitionRangeOR { + tmp1 := partitionRangeForExpr(sctx, expr1, pruner, pruner.fullRange()) + tmp2 := partitionRangeForExpr(sctx, expr2, pruner, pruner.fullRange()) return tmp1.union(tmp2) } @@ -434,7 +461,7 @@ type dataForPrune struct { // extractDataForPrune extracts data from the expression for pruning. // The expression should have this form: 'f(x) op const', otherwise it can't be pruned. -func extractDataForPrune(sctx sessionctx.Context, expr expression.Expression, partCol *expression.Column, partFn *expression.ScalarFunction) (dataForPrune, bool) { +func (p *rangePruner) extractDataForPrune(sctx sessionctx.Context, expr expression.Expression) (dataForPrune, bool) { var ret dataForPrune op, ok := expr.(*expression.ScalarFunction) if !ok { @@ -445,7 +472,7 @@ func extractDataForPrune(sctx sessionctx.Context, expr expression.Expression, pa ret.op = op.FuncName.L case ast.IsNull: // isnull(col) - if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == partCol.ID { + if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.col.ID { ret.op = ast.IsNull return ret, true } @@ -456,11 +483,11 @@ func extractDataForPrune(sctx sessionctx.Context, expr expression.Expression, pa var col *expression.Column var con *expression.Constant - if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == partCol.ID { + if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.col.ID { if arg1, ok := op.GetArgs()[1].(*expression.Constant); ok { col, con = arg0, arg1 } - } else if arg0, ok := op.GetArgs()[1].(*expression.Column); ok && arg0.ID == partCol.ID { + } else if arg0, ok := op.GetArgs()[1].(*expression.Column); ok && arg0.ID == p.col.ID { if arg1, ok := op.GetArgs()[0].(*expression.Constant); ok { ret.op = opposite(ret.op) col, con = arg0, arg1 @@ -472,12 +499,12 @@ func extractDataForPrune(sctx sessionctx.Context, expr expression.Expression, pa // Current expression is 'col op const' var constExpr expression.Expression - if partFn != nil { + if p.partFn != nil { // If the partition expression is fn(col), change constExpr to fn(constExpr). // No 'copy on write' for the expression here, this is a dangerous operation. - args := partFn.GetArgs() + args := p.partFn.GetArgs() args[0] = con - constExpr = partFn + constExpr = p.partFn // Sometimes we need to relax the condition, < to <=, > to >=. // For example, the following case doesn't hold: // col < '2020-02-11 17:34:11' => to_days(col) < to_days(2020-02-11 17:34:11) @@ -529,7 +556,7 @@ func relaxOP(op string) string { return op } -func pruneUseBinarySearch(lessThan lessThanData, data dataForPrune) (start int, end int) { +func pruneUseBinarySearch(lessThan lessThanDataInt, data dataForPrune) (start int, end int) { length := lessThan.length() switch data.op { case ast.EQ: @@ -624,3 +651,139 @@ func (s *partitionProcessor) makeUnionAllChildren(ds *DataSource, pi *model.Part unionAll.SetSchema(ds.schema.Clone()) return unionAll, nil } + +func (s *partitionProcessor) pruneRangeColumnsPartition(ds *DataSource, pi *model.PartitionInfo) (LogicalPlan, error) { + result := fullRange(len(pi.Definitions)) + + if len(pi.Columns) != 1 { + // We only support single column. + return s.makeUnionAllChildren(ds, pi, result) + } + + pruner, err := makeRangeColumnPruner(ds, pi) + if err == nil { + result = partitionRangeForCNFExpr(ds.ctx, ds.allConds, pruner, result) + } + return s.makeUnionAllChildren(ds, pi, result) +} + +var _ partitionRangePruner = &rangeColumnPruner{} + +// rangeColumnPruner is used by 'partition by range columns'. +type rangeColumnPruner struct { + data []expression.Expression + partCol *expression.Column + maxvalue bool +} + +func makeRangeColumnPruner(ds *DataSource, pi *model.PartitionInfo) (*rangeColumnPruner, error) { + var maxValue bool + data := make([]expression.Expression, len(pi.Definitions)) + schema := expression.NewSchema(ds.TblCols...) + exprs, err := expression.ParseSimpleExprsWithNames(ds.ctx, pi.Columns[0].L, schema, ds.names) + if err != nil { + return nil, err + } + partCol := exprs[0].(*expression.Column) + for i := 0; i < len(pi.Definitions); i++ { + if strings.EqualFold(pi.Definitions[i].LessThan[0], "MAXVALUE") { + // Use a bool flag instead of math.MaxInt64 to avoid the corner cases. + maxValue = true + } else { + tmp, err := expression.ParseSimpleExprsWithNames(ds.ctx, pi.Definitions[i].LessThan[0], schema, ds.names) + if err != nil { + return nil, err + } + data[i] = tmp[0] + } + } + return &rangeColumnPruner{data, partCol, maxValue}, nil +} + +func (p *rangeColumnPruner) fullRange() partitionRangeOR { + return fullRange(len(p.data)) +} + +func (p *rangeColumnPruner) partitionRangeForExpr(sctx sessionctx.Context, expr expression.Expression) (int, int, bool) { + op, ok := expr.(*expression.ScalarFunction) + if !ok { + return 0, len(p.data), false + } + + switch op.FuncName.L { + case ast.EQ, ast.LT, ast.GT, ast.LE, ast.GE: + case ast.IsNull: + // isnull(col) + if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.partCol.ID { + return 0, 1, true + } + return 0, len(p.data), false + default: + return 0, len(p.data), false + } + opName := op.FuncName.L + + var col *expression.Column + var con *expression.Constant + if arg0, ok := op.GetArgs()[0].(*expression.Column); ok && arg0.ID == p.partCol.ID { + if arg1, ok := op.GetArgs()[1].(*expression.Constant); ok { + col, con = arg0, arg1 + } + } else if arg0, ok := op.GetArgs()[1].(*expression.Column); ok && arg0.ID == p.partCol.ID { + if arg1, ok := op.GetArgs()[0].(*expression.Constant); ok { + opName = opposite(opName) + col, con = arg0, arg1 + } + } + if col == nil || con == nil { + return 0, len(p.data), false + } + + start, end := p.pruneUseBinarySearch(sctx, opName, con) + return start, end, true +} + +func (p *rangeColumnPruner) pruneUseBinarySearch(sctx sessionctx.Context, op string, data *expression.Constant) (start int, end int) { + var err error + var isNull bool + compare := func(ith int, op string, v *expression.Constant) bool { + if ith == len(p.data)-1 { + if p.maxvalue { + return true + } + } + var expr expression.Expression + expr, err = expression.NewFunction(sctx, op, types.NewFieldType(mysql.TypeLonglong), p.data[ith], v) + var val int64 + val, isNull, err = expr.EvalInt(sctx, chunk.Row{}) + return val > 0 + } + + length := len(p.data) + switch op { + case ast.EQ: + pos := sort.Search(length, func(i int) bool { return compare(i, ast.GT, data) }) + start, end = pos, pos+1 + case ast.LT: + pos := sort.Search(length, func(i int) bool { return compare(i, ast.GE, data) }) + start, end = 0, pos+1 + case ast.GE, ast.GT: + pos := sort.Search(length, func(i int) bool { return compare(i, ast.GT, data) }) + start, end = pos, length + case ast.LE: + pos := sort.Search(length, func(i int) bool { return compare(i, ast.GT, data) }) + start, end = 0, pos+1 + default: + start, end = 0, length + } + + // Something goes wrong, abort this prunning. + if err != nil || isNull { + return 0, len(p.data) + } + + if end > length { + end = length + } + return start, end +}