github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/index/index_lookup.go (about) 1 // Copyright 2020 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package index 16 17 import ( 18 "context" 19 "encoding/binary" 20 "fmt" 21 "io" 22 23 "github.com/dolthub/go-mysql-server/sql" 24 25 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 26 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable" 27 "github.com/dolthub/dolt/go/libraries/doltcore/row" 28 "github.com/dolthub/dolt/go/libraries/doltcore/table/typed/noms" 29 "github.com/dolthub/dolt/go/store/prolly" 30 "github.com/dolthub/dolt/go/store/prolly/tree" 31 "github.com/dolthub/dolt/go/store/types" 32 "github.com/dolthub/dolt/go/store/val" 33 ) 34 35 func RowIterForIndexLookup(ctx *sql.Context, t DoltTableable, lookup sql.IndexLookup, pkSch sql.PrimaryKeySchema, columns []uint64) (sql.RowIter, error) { 36 idx := lookup.Index.(*doltIndex) 37 durableState, err := idx.getDurableState(ctx, t) 38 if err != nil { 39 return nil, err 40 } 41 42 if types.IsFormat_DOLT(idx.Format()) { 43 prollyRanges, err := idx.prollyRanges(ctx, idx.ns, lookup.Ranges...) 44 if len(prollyRanges) > 1 { 45 return nil, fmt.Errorf("expected a single index range") 46 } 47 if err != nil { 48 return nil, err 49 } 50 return RowIterForProllyRange(ctx, idx, prollyRanges[0], pkSch, columns, durableState) 51 } else { 52 nomsRanges, err := idx.nomsRanges(ctx, lookup.Ranges...) 53 if err != nil { 54 return nil, err 55 } 56 return RowIterForNomsRanges(ctx, idx, nomsRanges, columns, durableState) 57 } 58 } 59 60 func RowIterForProllyRange(ctx *sql.Context, idx DoltIndex, r prolly.Range, pkSch sql.PrimaryKeySchema, projections []uint64, durableState *durableIndexState) (sql.RowIter, error) { 61 if projections == nil { 62 projections = idx.Schema().GetAllCols().Tags 63 } 64 65 if sql.IsKeyless(pkSch.Schema) { 66 // in order to resolve row cardinality, keyless indexes must always perform 67 // an indirect lookup through the clustered index. 68 return newProllyKeylessIndexIter(ctx, idx, r, pkSch, projections, durableState.Primary, durableState.Secondary) 69 } 70 71 covers := idx.coversColumns(durableState, projections) 72 if covers { 73 return newProllyCoveringIndexIter(ctx, idx, r, pkSch, projections, durableState.Secondary) 74 } 75 return newProllyIndexIter(ctx, idx, r, pkSch, projections, durableState.Primary, durableState.Secondary) 76 } 77 78 func RowIterForNomsRanges(ctx *sql.Context, idx DoltIndex, ranges []*noms.ReadRange, columns []uint64, durableState *durableIndexState) (sql.RowIter, error) { 79 if len(columns) == 0 { 80 columns = idx.Schema().GetAllCols().Tags 81 } 82 m := durable.NomsMapFromIndex(durableState.Secondary) 83 nrr := noms.NewNomsRangeReader(idx.valueReadWriter(), idx.IndexSchema(), m, ranges) 84 85 covers := idx.coversColumns(durableState, columns) 86 if covers || idx.ID() == "PRIMARY" { 87 return NewCoveringIndexRowIterAdapter(ctx, idx, nrr, columns), nil 88 } else { 89 return NewIndexLookupRowIterAdapter(ctx, idx, durableState, nrr, columns) 90 } 91 } 92 93 type IndexLookupKeyIterator interface { 94 // NextKey returns the next key if it exists, and io.EOF if it does not. 95 NextKey(ctx *sql.Context) (row.TaggedValues, error) 96 } 97 98 func NewRangePartitionIter(ctx *sql.Context, t DoltTableable, lookup sql.IndexLookup, isDoltFmt bool) (sql.PartitionIter, error) { 99 idx := lookup.Index.(*doltIndex) 100 if lookup.IsPointLookup && isDoltFmt { 101 return newPointPartitionIter(ctx, lookup, idx) 102 } 103 104 var prollyRanges []prolly.Range 105 var nomsRanges []*noms.ReadRange 106 var err error 107 if isDoltFmt { 108 prollyRanges, err = idx.prollyRanges(ctx, idx.ns, lookup.Ranges...) 109 } else { 110 nomsRanges, err = idx.nomsRanges(ctx, lookup.Ranges...) 111 } 112 if err != nil { 113 return nil, err 114 } 115 return &rangePartitionIter{ 116 nomsRanges: nomsRanges, 117 prollyRanges: prollyRanges, 118 curr: 0, 119 isDoltFmt: isDoltFmt, 120 isReverse: lookup.IsReverse, 121 }, nil 122 } 123 124 func newPointPartitionIter(ctx *sql.Context, lookup sql.IndexLookup, idx *doltIndex) (sql.PartitionIter, error) { 125 prollyRanges, err := idx.prollyRanges(ctx, idx.ns, lookup.Ranges[0]) 126 if err != nil { 127 return nil, err 128 } 129 return &pointPartition{ 130 r: prollyRanges[0], 131 }, nil 132 } 133 134 var _ sql.PartitionIter = (*pointPartition)(nil) 135 var _ sql.Partition = (*pointPartition)(nil) 136 137 type pointPartition struct { 138 r prolly.Range 139 used bool 140 } 141 142 func (p pointPartition) Key() []byte { 143 return []byte{0} 144 } 145 146 func (p *pointPartition) Close(c *sql.Context) error { 147 return nil 148 } 149 150 func (p *pointPartition) Next(c *sql.Context) (sql.Partition, error) { 151 if p.used { 152 return nil, io.EOF 153 } 154 p.used = true 155 return *p, nil 156 } 157 158 type rangePartitionIter struct { 159 nomsRanges []*noms.ReadRange 160 prollyRanges []prolly.Range 161 curr int 162 isDoltFmt bool 163 isReverse bool 164 } 165 166 // Close is required by the sql.PartitionIter interface. Does nothing. 167 func (itr *rangePartitionIter) Close(*sql.Context) error { 168 return nil 169 } 170 171 // Next returns the next partition if there is one, or io.EOF if there isn't. 172 func (itr *rangePartitionIter) Next(_ *sql.Context) (sql.Partition, error) { 173 if itr.isDoltFmt { 174 return itr.nextProllyPartition() 175 } 176 return itr.nextNomsPartition() 177 } 178 179 func (itr *rangePartitionIter) nextProllyPartition() (sql.Partition, error) { 180 if itr.curr >= len(itr.prollyRanges) { 181 return nil, io.EOF 182 } 183 184 var bytes [4]byte 185 binary.BigEndian.PutUint32(bytes[:], uint32(itr.curr)) 186 pr := itr.prollyRanges[itr.curr] 187 itr.curr += 1 188 189 return rangePartition{ 190 prollyRange: pr, 191 key: bytes[:], 192 isReverse: itr.isReverse, 193 }, nil 194 } 195 196 func (itr *rangePartitionIter) nextNomsPartition() (sql.Partition, error) { 197 if itr.curr >= len(itr.nomsRanges) { 198 return nil, io.EOF 199 } 200 201 var bytes [4]byte 202 binary.BigEndian.PutUint32(bytes[:], uint32(itr.curr)) 203 nr := itr.nomsRanges[itr.curr] 204 itr.curr += 1 205 206 return rangePartition{ 207 nomsRange: nr, 208 key: bytes[:], 209 isReverse: itr.isReverse, 210 }, nil 211 } 212 213 type rangePartition struct { 214 nomsRange *noms.ReadRange 215 prollyRange prolly.Range 216 key []byte 217 isReverse bool 218 } 219 220 func (rp rangePartition) Key() []byte { 221 return rp.key 222 } 223 224 // LookupBuilder generates secondary lookups for partitions and 225 // encapsulates fast path optimizations for certain point lookups. 226 type LookupBuilder interface { 227 // NewRowIter returns a new index iter for the given partition 228 NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) 229 Key() doltdb.DataCacheKey 230 } 231 232 func NewLookupBuilder( 233 ctx *sql.Context, 234 tab DoltTableable, 235 idx DoltIndex, 236 key doltdb.DataCacheKey, 237 projections []uint64, 238 pkSch sql.PrimaryKeySchema, 239 isDoltFormat bool, 240 ) (LookupBuilder, error) { 241 if projections == nil { 242 projections = idx.Schema().GetAllCols().Tags 243 } 244 245 di := idx.(*doltIndex) 246 s, err := di.getDurableState(ctx, tab) 247 if err != nil { 248 return nil, err 249 } 250 base := &baseLookupBuilder{ 251 idx: di, 252 key: key, 253 sch: pkSch, 254 projections: projections, 255 } 256 257 if isDoltFormat { 258 base.sec = durable.ProllyMapFromIndex(s.Secondary) 259 base.secKd, base.secVd = base.sec.Descriptors() 260 base.ns = base.sec.NodeStore() 261 base.prefDesc = base.secKd.PrefixDesc(len(di.columns)) 262 } 263 264 switch { 265 case !isDoltFormat: 266 return &nomsLookupBuilder{ 267 baseLookupBuilder: base, 268 s: s, 269 }, nil 270 case sql.IsKeyless(pkSch.Schema): 271 return &keylessLookupBuilder{ 272 baseLookupBuilder: base, 273 s: s, 274 }, nil 275 case idx.coversColumns(s, projections): 276 return newCoveringLookupBuilder(base), nil 277 case idx.ID() == "PRIMARY": 278 // If we are using the primary index, always use a covering lookup builder. In some cases, coversColumns 279 // can return false, for example if a column was modified in an older version and has a different tag than 280 // the current schema. In those cases, the primary index is still the best we have, so go ahead and use it. 281 return newCoveringLookupBuilder(base), nil 282 default: 283 return newNonCoveringLookupBuilder(s, base) 284 } 285 } 286 287 func newCoveringLookupBuilder(b *baseLookupBuilder) *coveringLookupBuilder { 288 var keyMap, valMap, ordMap val.OrdinalMapping 289 if b.idx.IsPrimaryKey() { 290 keyMap, valMap, ordMap = primaryIndexMapping(b.idx, b.sch, b.projections) 291 } else { 292 keyMap, ordMap = coveringIndexMapping(b.idx, b.projections) 293 } 294 return &coveringLookupBuilder{ 295 baseLookupBuilder: b, 296 keyMap: keyMap, 297 valMap: valMap, 298 ordMap: ordMap, 299 } 300 } 301 302 // newNonCoveringLookupBuilder returns a LookupBuilder that uses the specified index state and 303 // base lookup builder to create a nonCoveringLookupBuilder that uses the secondary index (from 304 // |b|) to find the PK row identifier, and then uses that PK to look up the complete row from 305 // the primary index (from |s|). If a baseLookupBuilder built on the primary index is passed in, 306 // this function returns an error. 307 func newNonCoveringLookupBuilder(s *durableIndexState, b *baseLookupBuilder) (*nonCoveringLookupBuilder, error) { 308 if b.idx.ID() == "PRIMARY" { 309 return nil, fmt.Errorf("incompatible index passed to newNonCoveringLookupBuilder: " + 310 "primary index passed, but only secondary indexes are supported") 311 } 312 313 primary := durable.ProllyMapFromIndex(s.Primary) 314 priKd, _ := primary.Descriptors() 315 tbBld := val.NewTupleBuilder(priKd) 316 pkMap := ordinalMappingFromIndex(b.idx) 317 keyProj, valProj, ordProj := projectionMappings(b.idx.Schema(), b.projections) 318 return &nonCoveringLookupBuilder{ 319 baseLookupBuilder: b, 320 pri: primary, 321 priKd: priKd, 322 pkBld: tbBld, 323 pkMap: pkMap, 324 keyMap: keyProj, 325 valMap: valProj, 326 ordMap: ordProj, 327 }, nil 328 } 329 330 var _ LookupBuilder = (*baseLookupBuilder)(nil) 331 var _ LookupBuilder = (*nomsLookupBuilder)(nil) 332 var _ LookupBuilder = (*coveringLookupBuilder)(nil) 333 var _ LookupBuilder = (*keylessLookupBuilder)(nil) 334 var _ LookupBuilder = (*nonCoveringLookupBuilder)(nil) 335 336 // baseLookupBuilder is a common lookup builder for prolly covering and 337 // non covering index lookups. 338 type baseLookupBuilder struct { 339 key doltdb.DataCacheKey 340 341 idx *doltIndex 342 sch sql.PrimaryKeySchema 343 projections []uint64 344 345 sec prolly.Map 346 secKd, secVd val.TupleDesc 347 prefDesc val.TupleDesc 348 ns tree.NodeStore 349 } 350 351 func (lb *baseLookupBuilder) Key() doltdb.DataCacheKey { 352 return lb.key 353 } 354 355 // NewRowIter implements IndexLookup 356 func (lb *baseLookupBuilder) NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) { 357 panic("cannot call NewRowIter on baseLookupBuilder") 358 } 359 360 // newPointLookup will create a cursor once, and then use the same cursor for 361 // every subsequent point lookup. Note that equality joins can have a mix of 362 // point lookups on concrete values, and range lookups for null matches. 363 func (lb *baseLookupBuilder) newPointLookup(ctx *sql.Context, rang prolly.Range) (iter prolly.MapIter, err error) { 364 err = lb.sec.GetPrefix(ctx, rang.Tup, lb.prefDesc, func(key val.Tuple, value val.Tuple) (err error) { 365 if key != nil && rang.Matches(key) { 366 iter = prolly.NewPointLookup(key, value) 367 } else { 368 iter = prolly.EmptyPointLookup 369 } 370 return 371 }) 372 return 373 } 374 375 func (lb *baseLookupBuilder) rangeIter(ctx *sql.Context, part sql.Partition) (prolly.MapIter, error) { 376 switch p := part.(type) { 377 case pointPartition: 378 return lb.newPointLookup(ctx, p.r) 379 case rangePartition: 380 if p.isReverse { 381 return lb.sec.IterRangeReverse(ctx, p.prollyRange) 382 } else { 383 return lb.sec.IterRange(ctx, p.prollyRange) 384 } 385 default: 386 panic(fmt.Sprintf("unexpected prolly partition type: %T", part)) 387 } 388 } 389 390 // coveringLookupBuilder constructs row iters for covering lookups, 391 // where we only need to cursor seek on a single index to both identify 392 // target keys and fill all requested projections 393 type coveringLookupBuilder struct { 394 *baseLookupBuilder 395 396 // keyMap transforms secondary index key tuples into SQL tuples. 397 // secondary index value tuples are assumed to be empty. 398 keyMap, valMap, ordMap val.OrdinalMapping 399 } 400 401 // NewRowIter implements IndexLookup 402 func (lb *coveringLookupBuilder) NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) { 403 rangeIter, err := lb.rangeIter(ctx, part) 404 if err != nil { 405 return nil, err 406 } 407 return prollyCoveringIndexIter{ 408 idx: lb.idx, 409 indexIter: rangeIter, 410 keyDesc: lb.secKd, 411 valDesc: lb.secVd, 412 keyMap: lb.keyMap, 413 valMap: lb.valMap, 414 ordMap: lb.ordMap, 415 sqlSch: lb.sch.Schema, 416 projections: lb.projections, 417 ns: lb.ns, 418 }, nil 419 } 420 421 // nonCoveringLookupBuilder constructs row iters for non-covering lookups, 422 // where we need to seek on the secondary table for key identity, and then 423 // the primary table to fill all requested projections. 424 type nonCoveringLookupBuilder struct { 425 *baseLookupBuilder 426 427 pri prolly.Map 428 priKd val.TupleDesc 429 pkBld *val.TupleBuilder 430 431 pkMap, keyMap, valMap, ordMap val.OrdinalMapping 432 } 433 434 // NewRowIter implements IndexLookup 435 func (lb *nonCoveringLookupBuilder) NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) { 436 rangeIter, err := lb.rangeIter(ctx, part) 437 if err != nil { 438 return nil, err 439 } 440 return prollyIndexIter{ 441 idx: lb.idx, 442 indexIter: rangeIter, 443 primary: lb.pri, 444 pkBld: lb.pkBld, 445 pkMap: lb.pkMap, 446 keyMap: lb.keyMap, 447 valMap: lb.valMap, 448 ordMap: lb.ordMap, 449 sqlSch: lb.sch.Schema, 450 projections: lb.projections, 451 }, nil 452 } 453 454 // TODO keylessLookupBuilder should be similar to the non-covering 455 // index case, where we will need to reference the primary index, 456 // but can take advantage of point lookup optimizations 457 type keylessLookupBuilder struct { 458 *baseLookupBuilder 459 s *durableIndexState 460 } 461 462 // NewRowIter implements IndexLookup 463 func (lb *keylessLookupBuilder) NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) { 464 var prollyRange prolly.Range 465 switch p := part.(type) { 466 case rangePartition: 467 prollyRange = p.prollyRange 468 case pointPartition: 469 prollyRange = p.r 470 } 471 return newProllyKeylessIndexIter(ctx, lb.idx, prollyRange, lb.sch, lb.projections, lb.s.Primary, lb.s.Secondary) 472 } 473 474 type nomsLookupBuilder struct { 475 *baseLookupBuilder 476 s *durableIndexState 477 } 478 479 // NewRowIter implements IndexLookup 480 func (lb *nomsLookupBuilder) NewRowIter(ctx *sql.Context, part sql.Partition) (sql.RowIter, error) { 481 p := part.(rangePartition) 482 ranges := []*noms.ReadRange{p.nomsRange} 483 return RowIterForNomsRanges(ctx, lb.idx, ranges, lb.projections, lb.s) 484 } 485 486 // boundsCase determines the case upon which the bounds are tested. 487 type boundsCase byte 488 489 // For each boundsCase, the first element is the lowerbound and the second element is the upperbound 490 const ( 491 boundsCase_infinity_infinity boundsCase = iota 492 boundsCase_infinity_lessEquals 493 boundsCase_infinity_less 494 boundsCase_greaterEquals_infinity 495 boundsCase_greaterEquals_lessEquals 496 boundsCase_greaterEquals_less 497 boundsCase_greater_infinity 498 boundsCase_greater_lessEquals 499 boundsCase_greater_less 500 boundsCase_isNull 501 ) 502 503 // columnBounds are used to compare a given value in the noms row iterator. 504 type columnBounds struct { 505 boundsCase 506 lowerbound types.Value 507 upperbound types.Value 508 } 509 510 // nomsRangeCheck is used to compare a tuple against a set of comparisons in the noms row iterator. 511 type nomsRangeCheck []columnBounds 512 513 var _ noms.InRangeCheck = nomsRangeCheck{} 514 515 // Between returns whether the given types.Value is between the bounds. In addition, this returns if the value is outside 516 // the bounds and above the upperbound. 517 func (cb columnBounds) Between(ctx context.Context, vr types.ValueReader, val types.Value) (ok bool, over bool, err error) { 518 // Only boundCase_isNull matches NULL values, 519 // otherwise we terminate the range scan. 520 // This is checked early to bypass unpredictable 521 // null type comparisons. 522 if val.Kind() == types.NullKind { 523 isNullCase := cb.boundsCase == boundsCase_isNull 524 return isNullCase, !isNullCase, nil 525 } 526 527 switch cb.boundsCase { 528 case boundsCase_infinity_infinity: 529 return true, false, nil 530 case boundsCase_infinity_lessEquals: 531 ok, err := cb.upperbound.Less(ctx, vr.Format(), val) 532 if err != nil || ok { 533 return false, true, err 534 } 535 case boundsCase_infinity_less: 536 ok, err := val.Less(ctx, vr.Format(), cb.upperbound) 537 if err != nil || !ok { 538 return false, true, err 539 } 540 case boundsCase_greaterEquals_infinity: 541 ok, err := val.Less(ctx, vr.Format(), cb.lowerbound) 542 if err != nil || ok { 543 return false, false, err 544 } 545 case boundsCase_greaterEquals_lessEquals: 546 ok, err := val.Less(ctx, vr.Format(), cb.lowerbound) 547 if err != nil || ok { 548 return false, false, err 549 } 550 ok, err = cb.upperbound.Less(ctx, vr.Format(), val) 551 if err != nil || ok { 552 return false, true, err 553 } 554 case boundsCase_greaterEquals_less: 555 ok, err := val.Less(ctx, vr.Format(), cb.lowerbound) 556 if err != nil || ok { 557 return false, false, err 558 } 559 ok, err = val.Less(ctx, vr.Format(), cb.upperbound) 560 if err != nil || !ok { 561 return false, true, err 562 } 563 case boundsCase_greater_infinity: 564 ok, err := cb.lowerbound.Less(ctx, vr.Format(), val) 565 if err != nil || !ok { 566 return false, false, err 567 } 568 case boundsCase_greater_lessEquals: 569 ok, err := cb.lowerbound.Less(ctx, vr.Format(), val) 570 if err != nil || !ok { 571 return false, false, err 572 } 573 ok, err = cb.upperbound.Less(ctx, vr.Format(), val) 574 if err != nil || ok { 575 return false, true, err 576 } 577 case boundsCase_greater_less: 578 ok, err := cb.lowerbound.Less(ctx, vr.Format(), val) 579 if err != nil || !ok { 580 return false, false, err 581 } 582 ok, err = val.Less(ctx, vr.Format(), cb.upperbound) 583 if err != nil || !ok { 584 return false, true, err 585 } 586 case boundsCase_isNull: 587 // an isNull scan skips non-nulls, but does not terminate 588 return false, false, nil 589 default: 590 return false, false, fmt.Errorf("unknown bounds") 591 } 592 return true, false, nil 593 } 594 595 // Equals returns whether the calling columnBounds is equivalent to the given columnBounds. 596 func (cb columnBounds) Equals(otherBounds columnBounds) bool { 597 if cb.boundsCase != otherBounds.boundsCase { 598 return false 599 } 600 if cb.lowerbound == nil || otherBounds.lowerbound == nil { 601 if cb.lowerbound != nil || otherBounds.lowerbound != nil { 602 return false 603 } 604 } else if !cb.lowerbound.Equals(otherBounds.lowerbound) { 605 return false 606 } 607 if cb.upperbound == nil || otherBounds.upperbound == nil { 608 if cb.upperbound != nil || otherBounds.upperbound != nil { 609 return false 610 } 611 } else if !cb.upperbound.Equals(otherBounds.upperbound) { 612 return false 613 } 614 return true 615 } 616 617 // Check implements the interface noms.InRangeCheck. 618 func (nrc nomsRangeCheck) Check(ctx context.Context, vr types.ValueReader, tuple types.Tuple) (valid bool, skip bool, err error) { 619 itr := types.TupleItrPool.Get().(*types.TupleIterator) 620 defer types.TupleItrPool.Put(itr) 621 err = itr.InitForTuple(tuple) 622 if err != nil { 623 return false, false, err 624 } 625 626 for i := 0; i < len(nrc) && itr.HasMore(); i++ { 627 if err := itr.Skip(); err != nil { 628 return false, false, err 629 } 630 _, val, err := itr.Next() 631 if err != nil { 632 return false, false, err 633 } 634 if val == nil { 635 break 636 } 637 638 ok, over, err := nrc[i].Between(ctx, vr, val) 639 if err != nil { 640 return false, false, err 641 } 642 if !ok { 643 return i != 0 || !over, true, nil 644 } 645 } 646 return true, false, nil 647 } 648 649 // Equals returns whether the calling nomsRangeCheck is equivalent to the given nomsRangeCheck. 650 func (nrc nomsRangeCheck) Equals(otherNrc nomsRangeCheck) bool { 651 if len(nrc) != len(otherNrc) { 652 return false 653 } 654 for i := range nrc { 655 if !nrc[i].Equals(otherNrc[i]) { 656 return false 657 } 658 } 659 return true 660 } 661 662 type nomsKeyIter interface { 663 ReadKey(ctx context.Context) (types.Tuple, error) 664 }