Skip to content

Commit

Permalink
*: remove mergingIter.elideRangeTombstone
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
itsbilal committed Nov 16, 2022
1 parent d8c2514 commit fa09809
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 64 deletions.
1 change: 0 additions & 1 deletion db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion external_iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
1 change: 0 additions & 1 deletion iterator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions level_iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 12 additions & 13 deletions merging_iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
73 changes: 27 additions & 46 deletions testdata/iterator
Original file line number Diff line number Diff line change
Expand Up @@ -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
----

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
----

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
----

Expand All @@ -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
Expand Down
Loading

0 comments on commit fa09809

Please sign in to comment.