From fa09809769525047000e7fd8841a18ac363d2c11 Mon Sep 17 00:00:00 2001 From: Bilal Akhtar Date: Mon, 14 Nov 2022 15:47:30 -0500 Subject: [PATCH] *: remove mergingIter.elideRangeTombstone Simplify the mergingIter range tombstone elision logic to not need to know whether it's part of an Iterator or a compaction iterator. Previously if elideRangeTombstone was true, the mergingIter was part of an `Iterator` and we'd skip any RANGEDEL keys coming from child iterators, which really only happened to be synthetic bound keys or ignorable bound keys (set as such in i.boundaryContext). For compaction iterators, we'd continue to surface all RANGEDELs observed (and elideRangeTombstone was false). Now, rangedels from child iterators that are `!isIgnorableBoundaryKey` are surfaced by the merging iter, and the last remaining case where we wanted to surface a key but `isIgnorableBoundaryKey=false` was in skipEmptyFileBackward and that has also been changed to isIgnorableBoundaryKey=true. This removes the need for elideRangeTombstone so that has been cleared up, and any tests that relied on elideRangeTombstone have been updated. --- db.go | 1 - external_iterator.go | 1 - iterator_test.go | 1 - level_iter.go | 3 ++ merging_iter.go | 25 +++++++-------- testdata/iterator | 73 ++++++++++++++++--------------------------- testdata/merging_iter | 4 +-- 7 files changed, 44 insertions(+), 64 deletions(-) diff --git a/db.go b/db.go index 2e2e019123..1d9d2caa36 100644 --- a/db.go +++ b/db.go @@ -1175,7 +1175,6 @@ func (i *Iterator) constructPointIter(memtables flushableList, buf *iterAlloc) { buf.merging.init(&i.opts, &i.stats.InternalStats, i.comparer.Compare, i.comparer.Split, mlevels...) buf.merging.snapshot = i.seqNum buf.merging.batchSnapshot = i.batchSeqNum - buf.merging.elideRangeTombstones = true buf.merging.combinedIterState = &i.lazyCombinedIter.combinedIterState i.pointIter = &buf.merging i.merging = &buf.merging diff --git a/external_iterator.go b/external_iterator.go index b5f6fae776..b73c034032 100644 --- a/external_iterator.go +++ b/external_iterator.go @@ -248,7 +248,6 @@ func createExternalPointIter(it *Iterator) (internalIterator, error) { it.alloc.merging.init(&it.opts, &it.stats.InternalStats, it.comparer.Compare, it.comparer.Split, mlevels...) it.alloc.merging.snapshot = base.InternalKeySeqNumMax - it.alloc.merging.elideRangeTombstones = true return &it.alloc.merging, nil } diff --git a/iterator_test.go b/iterator_test.go index 7976fb652e..ee97c2b516 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -513,7 +513,6 @@ func TestIterator(t *testing.T) { vals: vals, }) iter.snapshot = seqNum - iter.elideRangeTombstones = true // NB: This Iterator cannot be cloned since it is not constructed // with a readState. It suffices for this test. it.iter = newInvalidatingIter(iter) diff --git a/level_iter.go b/level_iter.go index 58567c8dd6..eadad960b5 100644 --- a/level_iter.go +++ b/level_iter.go @@ -1045,6 +1045,9 @@ func (l *levelIter) skipEmptyFileBackward() (*InternalKey, base.LazyValue) { // If the boundary is a range deletion tombstone, return that key. if l.iterFile.SmallestPointKey.Kind() == InternalKeyKindRangeDelete { l.smallestBoundary = &l.iterFile.SmallestPointKey + if l.boundaryContext != nil { + l.boundaryContext.isIgnorableBoundaryKey = true + } return l.smallestBoundary, base.LazyValue{} } // If the last point iterator positioning op skipped keys, it's diff --git a/merging_iter.go b/merging_iter.go index c47712387d..60bd2ea664 100644 --- a/merging_iter.go +++ b/merging_iter.go @@ -51,14 +51,19 @@ type levelIterBoundaryContext struct { // largestUserKey bound is exclusive. isLargestUserKeyExclusive bool // isSyntheticIterBoundsKey is set to true iff the key returned by the level - // iterator is a synthetic key derived from the iterator bounds. This is - // used to prevent the mergingIter from being stuck at such a synthetic key - // if it becomes the top element of the heap. + // iterator is a synthetic key derived from the iterator bounds. This is used + // to prevent the mergingIter from being stuck at such a synthetic key if it + // becomes the top element of the heap. When used with a user-facing Iterator, + // the only range deletions exposed by this mergingIter should be those with + // `isSyntheticIterBoundsKey || isIgnorableBoundaryKey`. isSyntheticIterBoundsKey bool // isIgnorableBoundaryKey is set to true iff the key returned by the level - // iterator is a file boundary key that should be ignored. This is used to + // iterator is a file boundary key that should be ignored when returning to + // the parent iterator. File boundary keys are used by the level iter to // keep a levelIter file's range deletion iterator open as long as other - // levels within the merging iterator require it. + // levels within the merging iterator require it. When used with a user-facing + // Iterator, the only range deletions exposed by this mergingIter should be + // those with `isSyntheticIterBoundsKey || isIgnorableBoundaryKey`. isIgnorableBoundaryKey bool } @@ -246,10 +251,6 @@ type mergingIter struct { combinedIterState *combinedIterState - // Elide range tombstones from being returned during iteration. Set to true - // when mergingIter is a child of Iterator and the mergingIter is processing - // range tombstones. - elideRangeTombstones bool // Used in some tests to disable the random disabling of seek optimizations. forceEnableSeekOpt bool } @@ -732,8 +733,7 @@ func (m *mergingIter) findNextEntry() (*InternalKey, base.LazyValue) { continue } if item.key.Visible(m.snapshot, m.batchSnapshot) && - (!m.levels[item.index].isIgnorableBoundaryKey) && - (item.key.Kind() != InternalKeyKindRangeDelete || !m.elideRangeTombstones) { + (!m.levels[item.index].isIgnorableBoundaryKey) { return &item.key, item.value } m.nextEntry(item) @@ -886,8 +886,7 @@ func (m *mergingIter) findPrevEntry() (*InternalKey, base.LazyValue) { continue } if item.key.Visible(m.snapshot, m.batchSnapshot) && - (!m.levels[item.index].isIgnorableBoundaryKey) && - (item.key.Kind() != InternalKeyKindRangeDelete || !m.elideRangeTombstones) { + (!m.levels[item.index].isIgnorableBoundaryKey) { return &item.key, item.value } m.prevEntry(item) diff --git a/testdata/iterator b/testdata/iterator index 0876535fce..6c9223f30b 100644 --- a/testdata/iterator +++ b/testdata/iterator @@ -634,17 +634,11 @@ stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, (internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 3, key-bytes 7, value-bytes 2, tombstoned: 0)) -# NB: RANGEDEL entries are ignored. define -a.RANGEDEL.4:c a.MERGE.3:d -a.RANGEDEL.2:c a.MERGE.2:c -a.RANGEDEL.1:c a.SET.1:b -b.RANGEDEL.3:c b.MERGE.2:b -b.RANGEDEL.1:c b.MERGE.1:a ---- @@ -659,7 +653,7 @@ b: (ab, .) . b: (ab, .) stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 5), (rev, 1, 2)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 8, value-bytes 8, tombstoned: 0)) iter seq=3 seek-ge a @@ -668,7 +662,7 @@ next a: (bc, .) b: (ab, .) stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=2 seek-ge a @@ -677,7 +671,7 @@ next a: (b, .) b: (a, .) stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=4 seek-lt c @@ -690,7 +684,7 @@ a: (bcd, .) . a: (bcd, .) stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 1, 5)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 16, key-bytes 16, value-bytes 16, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 8, value-bytes 8, tombstoned: 0)) iter seq=3 seek-lt c @@ -699,7 +693,7 @@ prev b: (ab, .) a: (bc, .) stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 4)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=2 seek-lt c @@ -708,7 +702,7 @@ prev b: (a, .) a: (b, .) stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 2)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=4 seek-ge a @@ -721,7 +715,7 @@ b: (ab, .) a: (bcd, .) b: (ab, .) stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 10), (rev, 1, 5)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=3 seek-ge a @@ -734,7 +728,7 @@ b: (ab, .) a: (bc, .) b: (ab, .) stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 8), (rev, 1, 4)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=2 seek-ge a @@ -747,7 +741,7 @@ b: (a, .) a: (b, .) b: (a, .) stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 4), (rev, 1, 2)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=4 seek-lt c @@ -760,7 +754,7 @@ a: (bcd, .) b: (ab, .) a: (bcd, .) stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 5), (rev, 2, 10)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=3 seek-lt c @@ -773,7 +767,7 @@ a: (bc, .) b: (ab, .) a: (bc, .) stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 2, 8)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=2 seek-lt c @@ -786,7 +780,7 @@ a: (b, .) b: (a, .) a: (b, .) stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 2, 4)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 30, key-bytes 30, value-bytes 30, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 15, key-bytes 15, value-bytes 15, tombstoned: 0)) iter seq=3 seek-prefix-ge a @@ -795,7 +789,7 @@ next a: (bc, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 8, value-bytes 8, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 4, value-bytes 4, tombstoned: 0)) iter seq=2 seek-prefix-ge a @@ -804,7 +798,7 @@ next a: (b, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=4 seek-prefix-ge a @@ -813,7 +807,7 @@ next a: (bcd, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 8, value-bytes 8, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 4, value-bytes 4, tombstoned: 0)) iter seq=2 seek-prefix-ge a @@ -822,7 +816,7 @@ next a: (b, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 10, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) iter seq=3 seek-prefix-ge a @@ -831,7 +825,7 @@ next a: (bc, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 8, value-bytes 8, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 4, value-bytes 4, tombstoned: 0)) iter seq=3 seek-prefix-ge c @@ -844,31 +838,23 @@ seek-prefix-ge 1 ---- . stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 4, value-bytes 4, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 2, key-bytes 2, value-bytes 2, tombstoned: 0)) iter seq=3 seek-prefix-ge a ---- a: (bc, .) stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 6, key-bytes 6, value-bytes 6, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 3, key-bytes 3, value-bytes 3, tombstoned: 0)) -# NB: RANGEDEL entries are ignored. define -a.RANGEDEL.4:c a.MERGE.3:d -a.RANGEDEL.2:c a.MERGE.2:c -a.RANGEDEL.1:c a.MERGE.1:b -aa.RANGEDEL.3:c aa.MERGE.2:b -aa.RANGEDEL.1:c aa.MERGE.1:a -b.RANGEDEL.3:c b.MERGE.2:b -b.RANGEDEL.1:c b.MERGE.1:a ---- @@ -881,7 +867,7 @@ a: (bc, .) . . stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 10, value-bytes 8, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 5, value-bytes 4, tombstoned: 0)) iter seq=2 seek-prefix-ge a @@ -892,7 +878,7 @@ a: (b, .) . . stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 14, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 7, value-bytes 5, tombstoned: 0)) iter seq=4 seek-prefix-ge a @@ -901,7 +887,7 @@ next a: (bcd, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 8, key-bytes 10, value-bytes 8, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 4, key-bytes 5, value-bytes 4, tombstoned: 0)) iter seq=2 seek-prefix-ge a @@ -910,7 +896,7 @@ next a: (b, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 10, key-bytes 14, value-bytes 10, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 7, value-bytes 5, tombstoned: 0)) iter seq=3 seek-prefix-ge aa @@ -919,14 +905,14 @@ next aa: (ab, .) . stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 6, key-bytes 10, value-bytes 6, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 3, key-bytes 5, value-bytes 3, tombstoned: 0)) iter seq=4 seek-prefix-ge aa ---- aa: (ab, .) stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 6, key-bytes 10, value-bytes 6, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 3, key-bytes 5, value-bytes 3, tombstoned: 0)) define a.SET.1:a @@ -1372,13 +1358,9 @@ aa: (aa, .) stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 1, key-bytes 2, value-bytes 2, tombstoned: 0)) -# NB: RANGEDEL entries are ignored. define -a.RANGEDEL.2:c a.SET.1:a -b.RANGEDEL.3:d b.SET.2:b -b.RANGEDEL.1:d ---- iter seq=4 @@ -1390,7 +1372,7 @@ a: (a, .) b: (b, .) . stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 5, key-bytes 5, value-bytes 5, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 2, key-bytes 2, value-bytes 2, tombstoned: 0)) define a.SINGLEDEL.1: @@ -1550,7 +1532,6 @@ stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, define a.SINGLEDEL.3: -a.RANGEDEL.2:c a.SET.1:val ---- @@ -1559,7 +1540,7 @@ first ---- . stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), -(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 3, key-bytes 3, value-bytes 4, tombstoned: 0)) +(internal-stats: (block-bytes: (total 0 B, cached 0 B)), (points: (count 2, key-bytes 2, value-bytes 3, tombstoned: 0)) # Exercise iteration with limits, when there are no deletes. define diff --git a/testdata/merging_iter b/testdata/merging_iter index 0124b5bd97..a7cddba4a1 100644 --- a/testdata/merging_iter +++ b/testdata/merging_iter @@ -444,11 +444,11 @@ prev f#0,1:1 f#3,1:3 e#0,1:1 -c#4,15: b#0,1:1 a#0,1:1 a#2,1:2 . +. # Verify the upper bound is respected when switching directions at a RANGEDEL # boundary. @@ -477,7 +477,7 @@ seek-ge krgywquurww prev ---- . -kq#100,15: +koujdlp#37,2:37 # Verify the lower bound is respected when switching directions at a RANGEDEL # boundary.