vitess.io/vitess@v0.16.2/go/vt/vtgate/planbuilder/operators/queryprojection.go (about) 1 /* 2 Copyright 2021 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package operators 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "sort" 23 "strings" 24 25 "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" 26 "vitess.io/vitess/go/vt/vtgate/semantics" 27 28 "vitess.io/vitess/go/vt/vtgate/engine" 29 30 "vitess.io/vitess/go/vt/sqlparser" 31 "vitess.io/vitess/go/vt/vterrors" 32 ) 33 34 type ( 35 // SelectExpr provides whether the columns is aggregation expression or not. 36 SelectExpr struct { 37 Col sqlparser.SelectExpr 38 Aggr bool 39 } 40 41 // QueryProjection contains the information about the projections, group by and order by expressions used to do horizon planning. 42 QueryProjection struct { 43 // If you change the contents here, please update the toString() method 44 SelectExprs []SelectExpr 45 HasAggr bool 46 Distinct bool 47 groupByExprs []GroupBy 48 OrderExprs []OrderBy 49 CanPushDownSorting bool 50 HasStar bool 51 52 // AddedColumn keeps a counter for expressions added to solve HAVING expressions the user is not selecting 53 AddedColumn int 54 } 55 56 // OrderBy contains the expression to used in order by and also if ordering is needed at VTGate level then what the weight_string function expression to be sent down for evaluation. 57 OrderBy struct { 58 Inner *sqlparser.Order 59 WeightStrExpr sqlparser.Expr 60 } 61 62 // GroupBy contains the expression to used in group by and also if grouping is needed at VTGate level then what the weight_string function expression to be sent down for evaluation. 63 GroupBy struct { 64 Inner sqlparser.Expr 65 WeightStrExpr sqlparser.Expr 66 67 // The index at which the user expects to see this column. Set to nil, if the user does not ask for it 68 InnerIndex *int 69 70 // The original aliased expression that this group by is referring 71 aliasedExpr *sqlparser.AliasedExpr 72 } 73 74 // Aggr encodes all information needed for aggregation functions 75 Aggr struct { 76 Original *sqlparser.AliasedExpr 77 Func sqlparser.AggrFunc 78 OpCode engine.AggregateOpcode 79 Alias string 80 // The index at which the user expects to see this aggregated function. Set to nil, if the user does not ask for it 81 Index *int 82 Distinct bool 83 } 84 85 AggrRewriter struct { 86 qp *QueryProjection 87 st *semantics.SemTable 88 Err error 89 } 90 ) 91 92 func (b GroupBy) AsOrderBy() OrderBy { 93 return OrderBy{ 94 Inner: &sqlparser.Order{ 95 Expr: b.Inner, 96 Direction: sqlparser.AscOrder, 97 }, 98 WeightStrExpr: b.WeightStrExpr, 99 } 100 } 101 102 func (b GroupBy) AsAliasedExpr() *sqlparser.AliasedExpr { 103 if b.aliasedExpr != nil { 104 return b.aliasedExpr 105 } 106 col, isColName := b.Inner.(*sqlparser.ColName) 107 if isColName && b.WeightStrExpr != b.Inner { 108 return &sqlparser.AliasedExpr{ 109 Expr: b.WeightStrExpr, 110 As: col.Name, 111 } 112 } 113 if !isColName && b.WeightStrExpr != b.Inner { 114 panic("this should not happen - different inner and weighStringExpr and not a column alias") 115 } 116 117 return &sqlparser.AliasedExpr{ 118 Expr: b.WeightStrExpr, 119 } 120 } 121 122 // GetExpr returns the underlying sqlparser.Expr of our SelectExpr 123 func (s SelectExpr) GetExpr() (sqlparser.Expr, error) { 124 switch sel := s.Col.(type) { 125 case *sqlparser.AliasedExpr: 126 return sel.Expr, nil 127 default: 128 return nil, vterrors.VT13001(fmt.Sprintf("%T does not have an expression", s.Col)) 129 } 130 } 131 132 // GetAliasedExpr returns the SelectExpr as a *sqlparser.AliasedExpr if its type allows it, 133 // otherwise an error is returned. 134 func (s SelectExpr) GetAliasedExpr() (*sqlparser.AliasedExpr, error) { 135 switch expr := s.Col.(type) { 136 case *sqlparser.AliasedExpr: 137 return expr, nil 138 case *sqlparser.StarExpr: 139 return nil, vterrors.VT12001("'*' expression in cross-shard query") 140 default: 141 return nil, vterrors.VT12001(fmt.Sprintf("not an aliased expression: %T", expr)) 142 } 143 } 144 145 // CreateQPFromSelect creates the QueryProjection for the input *sqlparser.Select 146 func CreateQPFromSelect(ctx *plancontext.PlanningContext, sel *sqlparser.Select) (*QueryProjection, error) { 147 qp := &QueryProjection{ 148 Distinct: sel.Distinct, 149 } 150 151 err := qp.addSelectExpressions(sel) 152 if err != nil { 153 return nil, err 154 } 155 for _, group := range sel.GroupBy { 156 selectExprIdx, aliasExpr := qp.FindSelectExprIndexForExpr(ctx, group) 157 expr, weightStrExpr, err := qp.GetSimplifiedExpr(group) 158 if err != nil { 159 return nil, err 160 } 161 err = checkForInvalidGroupingExpressions(weightStrExpr) 162 if err != nil { 163 return nil, err 164 } 165 166 groupBy := GroupBy{ 167 Inner: expr, 168 WeightStrExpr: weightStrExpr, 169 InnerIndex: selectExprIdx, 170 aliasedExpr: aliasExpr, 171 } 172 173 qp.groupByExprs = append(qp.groupByExprs, groupBy) 174 } 175 176 err = qp.addOrderBy(sel.OrderBy) 177 if err != nil { 178 return nil, err 179 } 180 181 if qp.Distinct && !qp.HasAggr { 182 qp.groupByExprs = nil 183 } 184 185 return qp, nil 186 } 187 188 // RewriteDown stops the walker from entering inside aggregation functions 189 func (ar *AggrRewriter) RewriteDown() func(sqlparser.SQLNode, sqlparser.SQLNode) bool { 190 return func(node, _ sqlparser.SQLNode) bool { 191 if ar.Err != nil { 192 return true 193 } 194 _, ok := node.(sqlparser.AggrFunc) 195 return !ok 196 } 197 } 198 199 // RewriteUp will go through an expression, add aggregations to the QP, and rewrite them to use column offset 200 func (ar *AggrRewriter) RewriteUp() func(*sqlparser.Cursor) bool { 201 return func(cursor *sqlparser.Cursor) bool { 202 if ar.Err != nil { 203 return false 204 } 205 sqlNode := cursor.Node() 206 fExp, ok := sqlNode.(sqlparser.AggrFunc) 207 if !ok { 208 return true 209 } 210 for offset, expr := range ar.qp.SelectExprs { 211 ae, err := expr.GetAliasedExpr() 212 if err != nil { 213 ar.Err = err 214 return false 215 } 216 if ar.st.EqualsExpr(ae.Expr, fExp) { 217 cursor.Replace(sqlparser.NewOffset(offset, fExp)) 218 return true 219 } 220 } 221 222 col := SelectExpr{ 223 Aggr: true, 224 Col: &sqlparser.AliasedExpr{Expr: fExp}, 225 } 226 ar.qp.HasAggr = true 227 228 cursor.Replace(sqlparser.NewOffset(len(ar.qp.SelectExprs), fExp)) 229 ar.qp.SelectExprs = append(ar.qp.SelectExprs, col) 230 ar.qp.AddedColumn++ 231 232 return true 233 } 234 } 235 236 // AggrRewriter extracts 237 func (qp *QueryProjection) AggrRewriter(ctx *plancontext.PlanningContext) *AggrRewriter { 238 return &AggrRewriter{ 239 qp: qp, 240 st: ctx.SemTable, 241 } 242 } 243 244 func (qp *QueryProjection) addSelectExpressions(sel *sqlparser.Select) error { 245 for _, selExp := range sel.SelectExprs { 246 switch selExp := selExp.(type) { 247 case *sqlparser.AliasedExpr: 248 err := checkForInvalidAggregations(selExp) 249 if err != nil { 250 return err 251 } 252 col := SelectExpr{ 253 Col: selExp, 254 } 255 if sqlparser.ContainsAggregation(selExp.Expr) { 256 col.Aggr = true 257 qp.HasAggr = true 258 } 259 260 qp.SelectExprs = append(qp.SelectExprs, col) 261 case *sqlparser.StarExpr: 262 qp.HasStar = true 263 col := SelectExpr{ 264 Col: selExp, 265 } 266 qp.SelectExprs = append(qp.SelectExprs, col) 267 default: 268 return vterrors.VT13001(fmt.Sprintf("%T in select list", selExp)) 269 } 270 } 271 return nil 272 } 273 274 // CreateQPFromUnion creates the QueryProjection for the input *sqlparser.Union 275 func CreateQPFromUnion(union *sqlparser.Union) (*QueryProjection, error) { 276 qp := &QueryProjection{} 277 278 sel := sqlparser.GetFirstSelect(union) 279 err := qp.addSelectExpressions(sel) 280 if err != nil { 281 return nil, err 282 } 283 284 err = qp.addOrderBy(union.OrderBy) 285 if err != nil { 286 return nil, err 287 } 288 289 return qp, nil 290 } 291 292 func (qp *QueryProjection) addOrderBy(orderBy sqlparser.OrderBy) error { 293 canPushDownSorting := true 294 for _, order := range orderBy { 295 expr, weightStrExpr, err := qp.GetSimplifiedExpr(order.Expr) 296 if err != nil { 297 return err 298 } 299 if sqlparser.IsNull(weightStrExpr) { 300 // ORDER BY null can safely be ignored 301 continue 302 } 303 qp.OrderExprs = append(qp.OrderExprs, OrderBy{ 304 Inner: &sqlparser.Order{ 305 Expr: expr, 306 Direction: order.Direction, 307 }, 308 WeightStrExpr: weightStrExpr, 309 }) 310 canPushDownSorting = canPushDownSorting && !sqlparser.ContainsAggregation(weightStrExpr) 311 } 312 qp.CanPushDownSorting = canPushDownSorting 313 return nil 314 } 315 316 // GetGrouping returns a copy of the grouping parameters of the QP 317 func (qp *QueryProjection) GetGrouping() []GroupBy { 318 out := make([]GroupBy, len(qp.groupByExprs)) 319 copy(out, qp.groupByExprs) 320 return out 321 } 322 323 func checkForInvalidAggregations(exp *sqlparser.AliasedExpr) error { 324 return sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { 325 if aggrFunc, isAggregate := node.(sqlparser.AggrFunc); isAggregate { 326 if aggrFunc.GetArgs() != nil && 327 len(aggrFunc.GetArgs()) != 1 { 328 return false, vterrors.VT03001(sqlparser.String(node)) 329 } 330 return true, nil 331 } 332 333 return true, nil 334 }, exp.Expr) 335 } 336 337 func (qp *QueryProjection) isExprInGroupByExprs(ctx *plancontext.PlanningContext, expr SelectExpr) bool { 338 for _, groupByExpr := range qp.groupByExprs { 339 exp, err := expr.GetExpr() 340 if err != nil { 341 return false 342 } 343 if ctx.SemTable.EqualsExpr(groupByExpr.WeightStrExpr, exp) { 344 return true 345 } 346 } 347 return false 348 } 349 350 // GetSimplifiedExpr takes an expression used in ORDER BY or GROUP BY, and returns an expression that is simpler to evaluate 351 func (qp *QueryProjection) GetSimplifiedExpr(e sqlparser.Expr) (expr sqlparser.Expr, weightStrExpr sqlparser.Expr, err error) { 352 // If the ORDER BY is against a column alias, we need to remember the expression 353 // behind the alias. The weightstring(.) calls needs to be done against that expression and not the alias. 354 // Eg - select music.foo as bar, weightstring(music.foo) from music order by bar 355 356 colExpr, isColName := e.(*sqlparser.ColName) 357 if !isColName { 358 return e, e, nil 359 } 360 361 if sqlparser.IsNull(e) { 362 return e, nil, nil 363 } 364 365 if colExpr.Qualifier.IsEmpty() { 366 for _, selectExpr := range qp.SelectExprs { 367 aliasedExpr, isAliasedExpr := selectExpr.Col.(*sqlparser.AliasedExpr) 368 if !isAliasedExpr { 369 continue 370 } 371 isAliasExpr := !aliasedExpr.As.IsEmpty() 372 if isAliasExpr && colExpr.Name.Equal(aliasedExpr.As) { 373 return e, aliasedExpr.Expr, nil 374 } 375 } 376 } 377 378 return e, e, nil 379 } 380 381 // toString should only be used for tests 382 func (qp *QueryProjection) toString() string { 383 type output struct { 384 Select []string 385 Grouping []string 386 OrderBy []string 387 Distinct bool 388 } 389 out := output{ 390 Select: []string{}, 391 Grouping: []string{}, 392 OrderBy: []string{}, 393 Distinct: qp.NeedsDistinct(), 394 } 395 396 for _, expr := range qp.SelectExprs { 397 e := sqlparser.String(expr.Col) 398 399 if expr.Aggr { 400 e = "aggr: " + e 401 } 402 out.Select = append(out.Select, e) 403 } 404 405 for _, expr := range qp.groupByExprs { 406 out.Grouping = append(out.Grouping, sqlparser.String(expr.Inner)) 407 } 408 for _, expr := range qp.OrderExprs { 409 out.OrderBy = append(out.OrderBy, sqlparser.String(expr.Inner)) 410 } 411 412 bytes, _ := json.MarshalIndent(out, "", " ") 413 return string(bytes) 414 } 415 416 // NeedsAggregation returns true if we either have aggregate functions or grouping defined 417 func (qp *QueryProjection) NeedsAggregation() bool { 418 return qp.HasAggr || len(qp.groupByExprs) > 0 419 } 420 421 // NeedsProjecting returns true if we have projections that need to be evaluated at the vtgate level 422 // and can't be pushed down to MySQL 423 func (qp *QueryProjection) NeedsProjecting( 424 ctx *plancontext.PlanningContext, 425 pusher func(expr *sqlparser.AliasedExpr) (int, error), 426 ) (needsVtGateEval bool, expressions []sqlparser.Expr, colNames []string, err error) { 427 for _, se := range qp.SelectExprs { 428 var ae *sqlparser.AliasedExpr 429 ae, err = se.GetAliasedExpr() 430 if err != nil { 431 return false, nil, nil, err 432 } 433 434 expr := ae.Expr 435 colNames = append(colNames, ae.ColumnName()) 436 437 if _, isCol := expr.(*sqlparser.ColName); isCol { 438 offset, err := pusher(ae) 439 if err != nil { 440 return false, nil, nil, err 441 } 442 expressions = append(expressions, sqlparser.NewOffset(offset, expr)) 443 continue 444 } 445 446 stopOnError := func(sqlparser.SQLNode, sqlparser.SQLNode) bool { 447 return err == nil 448 } 449 rewriter := func(cursor *sqlparser.CopyOnWriteCursor) { 450 col, isCol := cursor.Node().(*sqlparser.ColName) 451 if !isCol { 452 return 453 } 454 var tableInfo semantics.TableInfo 455 tableInfo, err = ctx.SemTable.TableInfoForExpr(col) 456 if err != nil { 457 return 458 } 459 dt, isDT := tableInfo.(*semantics.DerivedTable) 460 if !isDT { 461 return 462 } 463 464 rewritten := semantics.RewriteDerivedTableExpression(col, dt) 465 if sqlparser.ContainsAggregation(rewritten) { 466 offset, tErr := pusher(&sqlparser.AliasedExpr{Expr: col}) 467 if tErr != nil { 468 err = tErr 469 return 470 } 471 472 cursor.Replace(sqlparser.NewOffset(offset, col)) 473 } 474 } 475 newExpr := sqlparser.CopyOnRewrite(expr, stopOnError, rewriter, nil) 476 477 if err != nil { 478 return 479 } 480 481 if newExpr != expr { 482 // if we changed the expression, it means that we have to evaluate the rest at the vtgate level 483 expressions = append(expressions, newExpr.(sqlparser.Expr)) 484 needsVtGateEval = true 485 continue 486 } 487 488 // we did not need to push any parts of this expression down. Let's check if we can push all of it 489 offset, err := pusher(ae) 490 if err != nil { 491 return false, nil, nil, err 492 } 493 expressions = append(expressions, sqlparser.NewOffset(offset, expr)) 494 } 495 496 return 497 } 498 499 func (qp *QueryProjection) onlyAggr() bool { 500 if !qp.HasAggr { 501 return false 502 } 503 for _, expr := range qp.SelectExprs { 504 if !expr.Aggr { 505 return false 506 } 507 } 508 return true 509 } 510 511 // NeedsDistinct returns true if the query needs explicit distinct 512 func (qp *QueryProjection) NeedsDistinct() bool { 513 if !qp.Distinct { 514 return false 515 } 516 if qp.onlyAggr() && len(qp.groupByExprs) == 0 { 517 return false 518 } 519 return true 520 } 521 522 func (qp *QueryProjection) AggregationExpressions(ctx *plancontext.PlanningContext) (out []Aggr, err error) { 523 orderBy: 524 for _, orderExpr := range qp.OrderExprs { 525 orderExpr := orderExpr.WeightStrExpr 526 for _, expr := range qp.SelectExprs { 527 col, ok := expr.Col.(*sqlparser.AliasedExpr) 528 if !ok { 529 continue 530 } 531 if ctx.SemTable.EqualsExpr(col.Expr, orderExpr) { 532 continue orderBy // we found the expression we were looking for! 533 } 534 } 535 qp.SelectExprs = append(qp.SelectExprs, SelectExpr{ 536 Col: &sqlparser.AliasedExpr{Expr: orderExpr}, 537 Aggr: sqlparser.ContainsAggregation(orderExpr), 538 }) 539 qp.AddedColumn++ 540 } 541 542 for idx, expr := range qp.SelectExprs { 543 aliasedExpr, err := expr.GetAliasedExpr() 544 if err != nil { 545 return nil, err 546 } 547 548 idxCopy := idx 549 550 if !sqlparser.ContainsAggregation(expr.Col) { 551 if !qp.isExprInGroupByExprs(ctx, expr) { 552 out = append(out, Aggr{ 553 Original: aliasedExpr, 554 OpCode: engine.AggregateRandom, 555 Alias: aliasedExpr.ColumnName(), 556 Index: &idxCopy, 557 }) 558 } 559 continue 560 } 561 fnc, isAggregate := aliasedExpr.Expr.(sqlparser.AggrFunc) 562 if !isAggregate { 563 return nil, vterrors.VT12001("in scatter query: complex aggregate expression") 564 } 565 566 opcode, found := engine.SupportedAggregates[strings.ToLower(fnc.AggrName())] 567 if !found { 568 return nil, vterrors.VT12001(fmt.Sprintf("in scatter query: aggregation function '%s'", fnc.AggrName())) 569 } 570 571 if opcode == engine.AggregateCount { 572 if _, isStar := fnc.(*sqlparser.CountStar); isStar { 573 opcode = engine.AggregateCountStar 574 } 575 } 576 577 aggr, _ := aliasedExpr.Expr.(sqlparser.AggrFunc) 578 579 if aggr.IsDistinct() { 580 switch opcode { 581 case engine.AggregateCount: 582 opcode = engine.AggregateCountDistinct 583 case engine.AggregateSum: 584 opcode = engine.AggregateSumDistinct 585 } 586 } 587 588 out = append(out, Aggr{ 589 Original: aliasedExpr, 590 Func: aggr, 591 OpCode: opcode, 592 Alias: aliasedExpr.ColumnName(), 593 Index: &idxCopy, 594 Distinct: aggr.IsDistinct(), 595 }) 596 } 597 return 598 } 599 600 // FindSelectExprIndexForExpr returns the index of the given expression in the select expressions, if it is part of it 601 // returns -1 otherwise. 602 func (qp *QueryProjection) FindSelectExprIndexForExpr(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (*int, *sqlparser.AliasedExpr) { 603 colExpr, isCol := expr.(*sqlparser.ColName) 604 605 for idx, selectExpr := range qp.SelectExprs { 606 aliasedExpr, isAliasedExpr := selectExpr.Col.(*sqlparser.AliasedExpr) 607 if !isAliasedExpr { 608 continue 609 } 610 if isCol { 611 isAliasExpr := !aliasedExpr.As.IsEmpty() 612 if isAliasExpr && colExpr.Name.Equal(aliasedExpr.As) { 613 return &idx, aliasedExpr 614 } 615 } 616 if ctx.SemTable.EqualsExpr(aliasedExpr.Expr, expr) { 617 return &idx, aliasedExpr 618 } 619 } 620 return nil, nil 621 } 622 623 // AlignGroupByAndOrderBy aligns the group by and order by columns, so they are in the same order 624 // The GROUP BY clause is a set - the order between the elements does not make any difference, 625 // so we can simply re-arrange the column order 626 // We are also free to add more ORDER BY columns than the user asked for which we leverage, 627 // so the input is already ordered according to the GROUP BY columns used 628 func (qp *QueryProjection) AlignGroupByAndOrderBy(ctx *plancontext.PlanningContext) { 629 // The ORDER BY can be performed before the OA 630 631 var newGrouping []GroupBy 632 if len(qp.OrderExprs) == 0 { 633 // The query didn't ask for any particular order, so we are free to add arbitrary ordering. 634 // We'll align the grouping and ordering by the output columns 635 newGrouping = qp.GetGrouping() 636 SortGrouping(newGrouping) 637 for _, groupBy := range newGrouping { 638 qp.OrderExprs = append(qp.OrderExprs, groupBy.AsOrderBy()) 639 } 640 } else { 641 // Here we align the GROUP BY and ORDER BY. 642 // First step is to make sure that the GROUP BY is in the same order as the ORDER BY 643 used := make([]bool, len(qp.groupByExprs)) 644 for _, orderExpr := range qp.OrderExprs { 645 for i, groupingExpr := range qp.groupByExprs { 646 if !used[i] && ctx.SemTable.EqualsExpr(groupingExpr.WeightStrExpr, orderExpr.WeightStrExpr) { 647 newGrouping = append(newGrouping, groupingExpr) 648 used[i] = true 649 } 650 } 651 } 652 if len(newGrouping) != len(qp.groupByExprs) { 653 // we are missing some groupings. We need to add them both to the new groupings list, but also to the ORDER BY 654 for i, added := range used { 655 if !added { 656 groupBy := qp.groupByExprs[i] 657 newGrouping = append(newGrouping, groupBy) 658 qp.OrderExprs = append(qp.OrderExprs, groupBy.AsOrderBy()) 659 } 660 } 661 } 662 } 663 664 qp.groupByExprs = newGrouping 665 } 666 667 // AddGroupBy does just that 668 func (qp *QueryProjection) AddGroupBy(by GroupBy) { 669 qp.groupByExprs = append(qp.groupByExprs, by) 670 } 671 672 func (qp *QueryProjection) GetColumnCount() int { 673 return len(qp.SelectExprs) - qp.AddedColumn 674 } 675 676 func checkForInvalidGroupingExpressions(expr sqlparser.Expr) error { 677 return sqlparser.Walk(func(node sqlparser.SQLNode) (bool, error) { 678 if _, isAggregate := node.(sqlparser.AggrFunc); isAggregate { 679 return false, vterrors.VT03005(sqlparser.String(expr)) 680 } 681 _, isSubQ := node.(*sqlparser.Subquery) 682 arg, isArg := node.(sqlparser.Argument) 683 if isSubQ || (isArg && strings.HasPrefix(string(arg), "__sq")) { 684 return false, vterrors.VT12001("subqueries in GROUP BY") 685 } 686 return true, nil 687 }, expr) 688 } 689 690 func SortAggregations(a []Aggr) { 691 sort.Slice(a, func(i, j int) bool { 692 return CompareRefInt(a[i].Index, a[j].Index) 693 }) 694 } 695 696 func SortGrouping(a []GroupBy) { 697 sort.Slice(a, func(i, j int) bool { 698 return CompareRefInt(a[i].InnerIndex, a[j].InnerIndex) 699 }) 700 } 701 702 // CompareRefInt compares two references of integers. 703 // In case either one is nil, it is considered to be smaller 704 func CompareRefInt(a *int, b *int) bool { 705 if a == nil { 706 return false 707 } 708 if b == nil { 709 return true 710 } 711 return *a < *b 712 }