Line data Source code
1 : // Copyright 2022 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2 : // of this source code is governed by a BSD-style license that can be found in
3 : // the LICENSE file.
4 :
5 : package pebble
6 :
7 : import (
8 : "context"
9 : "fmt"
10 : "sort"
11 :
12 : "github.com/cockroachdb/errors"
13 : "github.com/cockroachdb/pebble/internal/base"
14 : "github.com/cockroachdb/pebble/internal/keyspan"
15 : "github.com/cockroachdb/pebble/internal/manifest"
16 : "github.com/cockroachdb/pebble/sstable"
17 : )
18 :
19 : // ExternalIterOption provide an interface to specify open-time options to
20 : // NewExternalIter.
21 : type ExternalIterOption interface {
22 : // iterApply is called on the iterator during opening in order to set internal
23 : // parameters.
24 : iterApply(*Iterator)
25 : // readerOptions returns any reader options added by this iter option.
26 : readerOptions() []sstable.ReaderOption
27 : }
28 :
29 : type externalIterReaderOptions struct {
30 : opts []sstable.ReaderOption
31 : }
32 :
33 0 : func (e *externalIterReaderOptions) iterApply(iterator *Iterator) {
34 0 : // Do nothing.
35 0 : }
36 :
37 0 : func (e *externalIterReaderOptions) readerOptions() []sstable.ReaderOption {
38 0 : return e.opts
39 0 : }
40 :
41 : // ExternalIterReaderOptions returns an ExternalIterOption that specifies
42 : // sstable.ReaderOptions to be applied on sstable readers in NewExternalIter.
43 0 : func ExternalIterReaderOptions(opts ...sstable.ReaderOption) ExternalIterOption {
44 0 : return &externalIterReaderOptions{opts: opts}
45 0 : }
46 :
47 : // ExternalIterForwardOnly is an ExternalIterOption that specifies this iterator
48 : // will only be used for forward positioning operations (First, SeekGE, Next).
49 : // This could enable optimizations that take advantage of this invariant.
50 : // Behaviour when a reverse positioning operation is done on an iterator
51 : // opened with this option is unpredictable, though in most cases it should.
52 : type ExternalIterForwardOnly struct{}
53 :
54 0 : func (e ExternalIterForwardOnly) iterApply(iter *Iterator) {
55 0 : iter.forwardOnly = true
56 0 : }
57 :
58 0 : func (e ExternalIterForwardOnly) readerOptions() []sstable.ReaderOption {
59 0 : return nil
60 0 : }
61 :
62 : // NewExternalIter takes an input 2d array of sstable files which may overlap
63 : // across subarrays but not within a subarray (at least as far as points are
64 : // concerned; range keys are allowed to overlap arbitrarily even within a
65 : // subarray), and returns an Iterator over the merged contents of the sstables.
66 : // Input sstables may contain point keys, range keys, range deletions, etc. The
67 : // input files slice must be sorted in reverse chronological ordering. A key in a
68 : // file at a lower index subarray will shadow a key with an identical user key
69 : // contained within a file at a higher index subarray. Each subarray must be
70 : // sorted in internal key order, where lower index files contain keys that sort
71 : // left of files with higher indexes.
72 : //
73 : // Input sstables must only contain keys with the zero sequence number.
74 : //
75 : // Iterators constructed through NewExternalIter do not support all iterator
76 : // options, including block-property and table filters. NewExternalIter errors
77 : // if an incompatible option is set.
78 : func NewExternalIter(
79 : o *Options,
80 : iterOpts *IterOptions,
81 : files [][]sstable.ReadableFile,
82 : extraOpts ...ExternalIterOption,
83 0 : ) (it *Iterator, err error) {
84 0 : return NewExternalIterWithContext(context.Background(), o, iterOpts, files, extraOpts...)
85 0 : }
86 :
87 : // NewExternalIterWithContext is like NewExternalIter, and additionally
88 : // accepts a context for tracing.
89 : func NewExternalIterWithContext(
90 : ctx context.Context,
91 : o *Options,
92 : iterOpts *IterOptions,
93 : files [][]sstable.ReadableFile,
94 : extraOpts ...ExternalIterOption,
95 0 : ) (it *Iterator, err error) {
96 0 : if iterOpts != nil {
97 0 : if err := validateExternalIterOpts(iterOpts); err != nil {
98 0 : return nil, err
99 0 : }
100 : }
101 :
102 0 : var readers [][]*sstable.Reader
103 0 :
104 0 : // Ensure we close all the opened readers if we error out.
105 0 : defer func() {
106 0 : if err != nil {
107 0 : for i := range readers {
108 0 : for j := range readers[i] {
109 0 : _ = readers[i][j].Close()
110 0 : }
111 : }
112 : }
113 : }()
114 0 : seqNumOffset := 0
115 0 : var extraReaderOpts []sstable.ReaderOption
116 0 : for i := range extraOpts {
117 0 : extraReaderOpts = append(extraReaderOpts, extraOpts[i].readerOptions()...)
118 0 : }
119 0 : for _, levelFiles := range files {
120 0 : seqNumOffset += len(levelFiles)
121 0 : }
122 0 : for _, levelFiles := range files {
123 0 : var subReaders []*sstable.Reader
124 0 : seqNumOffset -= len(levelFiles)
125 0 : subReaders, err = openExternalTables(o, levelFiles, seqNumOffset, o.MakeReaderOptions(), extraReaderOpts...)
126 0 : readers = append(readers, subReaders)
127 0 : }
128 0 : if err != nil {
129 0 : return nil, err
130 0 : }
131 :
132 0 : buf := iterAllocPool.Get().(*iterAlloc)
133 0 : dbi := &buf.dbi
134 0 : *dbi = Iterator{
135 0 : ctx: ctx,
136 0 : alloc: buf,
137 0 : merge: o.Merger.Merge,
138 0 : comparer: *o.Comparer,
139 0 : readState: nil,
140 0 : keyBuf: buf.keyBuf,
141 0 : prefixOrFullSeekKey: buf.prefixOrFullSeekKey,
142 0 : boundsBuf: buf.boundsBuf,
143 0 : batch: nil,
144 0 : // Add the readers to the Iterator so that Close closes them, and
145 0 : // SetOptions can re-construct iterators from them.
146 0 : externalReaders: readers,
147 0 : newIters: func(
148 0 : ctx context.Context, f *manifest.FileMetadata, opts *IterOptions,
149 0 : internalOpts internalIterOpts) (internalIterator, keyspan.FragmentIterator, error) {
150 0 : // NB: External iterators are currently constructed without any
151 0 : // `levelIters`. newIters should never be called. When we support
152 0 : // organizing multiple non-overlapping files into a single level
153 0 : // (see TODO below), we'll need to adjust this tableNewIters
154 0 : // implementation to open iterators by looking up f in a map
155 0 : // of readers indexed by *fileMetadata.
156 0 : panic("unreachable")
157 : },
158 : seqNum: base.InternalKeySeqNumMax,
159 : }
160 0 : if iterOpts != nil {
161 0 : dbi.opts = *iterOpts
162 0 : dbi.processBounds(iterOpts.LowerBound, iterOpts.UpperBound)
163 0 : }
164 0 : for i := range extraOpts {
165 0 : extraOpts[i].iterApply(dbi)
166 0 : }
167 0 : finishInitializingExternal(ctx, dbi)
168 0 : return dbi, nil
169 : }
170 :
171 0 : func validateExternalIterOpts(iterOpts *IterOptions) error {
172 0 : switch {
173 0 : case iterOpts.TableFilter != nil:
174 0 : return errors.Errorf("pebble: external iterator: TableFilter unsupported")
175 0 : case iterOpts.PointKeyFilters != nil:
176 0 : return errors.Errorf("pebble: external iterator: PointKeyFilters unsupported")
177 0 : case iterOpts.RangeKeyFilters != nil:
178 0 : return errors.Errorf("pebble: external iterator: RangeKeyFilters unsupported")
179 0 : case iterOpts.OnlyReadGuaranteedDurable:
180 0 : return errors.Errorf("pebble: external iterator: OnlyReadGuaranteedDurable unsupported")
181 0 : case iterOpts.UseL6Filters:
182 0 : return errors.Errorf("pebble: external iterator: UseL6Filters unsupported")
183 : }
184 0 : return nil
185 : }
186 :
187 0 : func createExternalPointIter(ctx context.Context, it *Iterator) (internalIterator, error) {
188 0 : // TODO(jackson): In some instances we could generate fewer levels by using
189 0 : // L0Sublevels code to organize nonoverlapping files into the same level.
190 0 : // This would allow us to use levelIters and keep a smaller set of data and
191 0 : // files in-memory. However, it would also require us to identify the bounds
192 0 : // of all the files upfront.
193 0 :
194 0 : if !it.opts.pointKeys() {
195 0 : return emptyIter, nil
196 0 : } else if it.pointIter != nil {
197 0 : return it.pointIter, nil
198 0 : }
199 0 : mlevels := it.alloc.mlevels[:0]
200 0 :
201 0 : if len(it.externalReaders) > cap(mlevels) {
202 0 : mlevels = make([]mergingIterLevel, 0, len(it.externalReaders))
203 0 : }
204 0 : for _, readers := range it.externalReaders {
205 0 : var combinedIters []internalIterator
206 0 : for _, r := range readers {
207 0 : var (
208 0 : rangeDelIter keyspan.FragmentIterator
209 0 : pointIter internalIterator
210 0 : err error
211 0 : )
212 0 : // We could set hideObsoletePoints=true, since we are reading at
213 0 : // InternalKeySeqNumMax, but we don't bother since these sstables should
214 0 : // not have obsolete points (so the performance optimization is
215 0 : // unnecessary), and we don't want to bother constructing a
216 0 : // BlockPropertiesFilterer that includes obsoleteKeyBlockPropertyFilter.
217 0 : pointIter, err = r.NewIterWithBlockPropertyFiltersAndContextEtc(
218 0 : ctx, it.opts.LowerBound, it.opts.UpperBound, nil, /* BlockPropertiesFilterer */
219 0 : false /* hideObsoletePoints */, false, /* useFilterBlock */
220 0 : &it.stats.InternalStats, sstable.TrivialReaderProvider{Reader: r})
221 0 : if err != nil {
222 0 : return nil, err
223 0 : }
224 0 : rangeDelIter, err = r.NewRawRangeDelIter()
225 0 : if err != nil {
226 0 : return nil, err
227 0 : }
228 0 : if rangeDelIter == nil && pointIter != nil && it.forwardOnly {
229 0 : // TODO(bilal): Consider implementing range key pausing in
230 0 : // simpleLevelIter so we can reduce mergingIterLevels even more by
231 0 : // sending all sstable iterators to combinedIters, not just those
232 0 : // corresponding to sstables without range deletes.
233 0 : combinedIters = append(combinedIters, pointIter)
234 0 : continue
235 : }
236 0 : mlevels = append(mlevels, mergingIterLevel{
237 0 : iter: pointIter,
238 0 : rangeDelIter: rangeDelIter,
239 0 : })
240 : }
241 0 : if len(combinedIters) == 1 {
242 0 : mlevels = append(mlevels, mergingIterLevel{
243 0 : iter: combinedIters[0],
244 0 : })
245 0 : } else if len(combinedIters) > 1 {
246 0 : sli := &simpleLevelIter{
247 0 : cmp: it.cmp,
248 0 : iters: combinedIters,
249 0 : }
250 0 : sli.init(it.opts)
251 0 : mlevels = append(mlevels, mergingIterLevel{
252 0 : iter: sli,
253 0 : rangeDelIter: nil,
254 0 : })
255 0 : }
256 : }
257 0 : if len(mlevels) == 1 && mlevels[0].rangeDelIter == nil {
258 0 : // Set closePointIterOnce to true. This is because we're bypassing the
259 0 : // merging iter, which turns Close()s on it idempotent for any child
260 0 : // iterators. The outer Iterator could call Close() on a point iter twice,
261 0 : // which sstable iterators do not support (as they release themselves to
262 0 : // a pool).
263 0 : it.closePointIterOnce = true
264 0 : return mlevels[0].iter, nil
265 0 : }
266 :
267 0 : it.alloc.merging.init(&it.opts, &it.stats.InternalStats, it.comparer.Compare, it.comparer.Split, mlevels...)
268 0 : it.alloc.merging.snapshot = base.InternalKeySeqNumMax
269 0 : if len(mlevels) <= cap(it.alloc.levelsPositioned) {
270 0 : it.alloc.merging.levelsPositioned = it.alloc.levelsPositioned[:len(mlevels)]
271 0 : }
272 0 : return &it.alloc.merging, nil
273 : }
274 :
275 0 : func finishInitializingExternal(ctx context.Context, it *Iterator) {
276 0 : pointIter, err := createExternalPointIter(ctx, it)
277 0 : if err != nil {
278 0 : it.pointIter = &errorIter{err: err}
279 0 : } else {
280 0 : it.pointIter = pointIter
281 0 : }
282 0 : it.iter = it.pointIter
283 0 :
284 0 : if it.opts.rangeKeys() {
285 0 : it.rangeKeyMasking.init(it, it.comparer.Compare, it.comparer.Split)
286 0 : var rangeKeyIters []keyspan.FragmentIterator
287 0 : if it.rangeKey == nil {
288 0 : // We could take advantage of the lack of overlaps in range keys within
289 0 : // each slice in it.externalReaders, and generate keyspan.LevelIters
290 0 : // out of those. However, since range keys are expected to be sparse to
291 0 : // begin with, the performance gain might not be significant enough to
292 0 : // warrant it.
293 0 : //
294 0 : // TODO(bilal): Explore adding a simpleRangeKeyLevelIter that does not
295 0 : // operate on FileMetadatas (similar to simpleLevelIter), and implements
296 0 : // this optimization.
297 0 : for _, readers := range it.externalReaders {
298 0 : for _, r := range readers {
299 0 : if rki, err := r.NewRawRangeKeyIter(); err != nil {
300 0 : rangeKeyIters = append(rangeKeyIters, &errorKeyspanIter{err: err})
301 0 : } else if rki != nil {
302 0 : rangeKeyIters = append(rangeKeyIters, rki)
303 0 : }
304 : }
305 : }
306 0 : if len(rangeKeyIters) > 0 {
307 0 : it.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState)
308 0 : it.rangeKey.init(it.comparer.Compare, it.comparer.Split, &it.opts)
309 0 : it.rangeKey.rangeKeyIter = it.rangeKey.iterConfig.Init(
310 0 : &it.comparer,
311 0 : base.InternalKeySeqNumMax,
312 0 : it.opts.LowerBound, it.opts.UpperBound,
313 0 : &it.hasPrefix, &it.prefixOrFullSeekKey,
314 0 : true /* onlySets */, &it.rangeKey.internal,
315 0 : )
316 0 : for i := range rangeKeyIters {
317 0 : it.rangeKey.iterConfig.AddLevel(rangeKeyIters[i])
318 0 : }
319 : }
320 : }
321 0 : if it.rangeKey != nil {
322 0 : it.rangeKey.iiter.Init(&it.comparer, it.iter, it.rangeKey.rangeKeyIter,
323 0 : keyspan.InterleavingIterOpts{
324 0 : Mask: &it.rangeKeyMasking,
325 0 : LowerBound: it.opts.LowerBound,
326 0 : UpperBound: it.opts.UpperBound,
327 0 : })
328 0 : it.iter = &it.rangeKey.iiter
329 0 : }
330 : }
331 : }
332 :
333 : func openExternalTables(
334 : o *Options,
335 : files []sstable.ReadableFile,
336 : seqNumOffset int,
337 : readerOpts sstable.ReaderOptions,
338 : extraReaderOpts ...sstable.ReaderOption,
339 0 : ) (readers []*sstable.Reader, err error) {
340 0 : readers = make([]*sstable.Reader, 0, len(files))
341 0 : for i := range files {
342 0 : readable, err := sstable.NewSimpleReadable(files[i])
343 0 : if err != nil {
344 0 : return readers, err
345 0 : }
346 0 : r, err := sstable.NewReader(readable, readerOpts, extraReaderOpts...)
347 0 : if err != nil {
348 0 : return readers, err
349 0 : }
350 : // Use the index of the file in files as the sequence number for all of
351 : // its keys.
352 0 : r.Properties.GlobalSeqNum = uint64(len(files) - i + seqNumOffset)
353 0 : readers = append(readers, r)
354 : }
355 0 : return readers, err
356 : }
357 :
358 : // simpleLevelIter is similar to a levelIter in that it merges the points
359 : // from multiple point iterators that are non-overlapping in the key ranges
360 : // they return. It is only expected to support forward iteration and forward
361 : // regular seeking; reverse iteration and prefix seeking is not supported.
362 : // Intended to be a low-overhead, non-FileMetadata dependent option for
363 : // NewExternalIter. To optimize seeking and forward iteration, it maintains
364 : // two slices of child iterators; one of all iterators, and a subset of it that
365 : // contains just the iterators that contain point keys within the current
366 : // bounds.
367 : //
368 : // Note that this levelIter does not support pausing at file boundaries
369 : // in case of range tombstones in this file that could apply to points outside
370 : // of this file (and outside of this level). This is sufficient for optimizing
371 : // the main use cases of NewExternalIter, however for completeness it would make
372 : // sense to build this pausing functionality in.
373 : type simpleLevelIter struct {
374 : cmp Compare
375 : err error
376 : lowerBound []byte
377 : iters []internalIterator
378 : filtered []internalIterator
379 : firstKeys [][]byte
380 : firstKeysBuf []byte
381 : currentIdx int
382 : }
383 :
384 : var _ internalIterator = &simpleLevelIter{}
385 :
386 : // init initializes this simpleLevelIter.
387 0 : func (s *simpleLevelIter) init(opts IterOptions) {
388 0 : s.currentIdx = 0
389 0 : s.lowerBound = opts.LowerBound
390 0 : s.resetFilteredIters()
391 0 : }
392 :
393 0 : func (s *simpleLevelIter) resetFilteredIters() {
394 0 : s.filtered = s.filtered[:0]
395 0 : s.firstKeys = s.firstKeys[:0]
396 0 : s.firstKeysBuf = s.firstKeysBuf[:0]
397 0 : s.err = nil
398 0 : for i := range s.iters {
399 0 : var iterKey *base.InternalKey
400 0 : if s.lowerBound != nil {
401 0 : iterKey, _ = s.iters[i].SeekGE(s.lowerBound, base.SeekGEFlagsNone)
402 0 : } else {
403 0 : iterKey, _ = s.iters[i].First()
404 0 : }
405 0 : if iterKey != nil {
406 0 : s.filtered = append(s.filtered, s.iters[i])
407 0 : bufStart := len(s.firstKeysBuf)
408 0 : s.firstKeysBuf = append(s.firstKeysBuf, iterKey.UserKey...)
409 0 : s.firstKeys = append(s.firstKeys, s.firstKeysBuf[bufStart:bufStart+len(iterKey.UserKey)])
410 0 : } else if err := s.iters[i].Error(); err != nil {
411 0 : s.err = err
412 0 : }
413 : }
414 : }
415 :
416 : func (s *simpleLevelIter) SeekGE(
417 : key []byte, flags base.SeekGEFlags,
418 0 : ) (*base.InternalKey, base.LazyValue) {
419 0 : if s.err != nil {
420 0 : return nil, base.LazyValue{}
421 0 : }
422 : // Find the first file that is entirely >= key. The file before that could
423 : // contain the key we're looking for.
424 0 : n := sort.Search(len(s.firstKeys), func(i int) bool {
425 0 : return s.cmp(key, s.firstKeys[i]) <= 0
426 0 : })
427 0 : if n > 0 {
428 0 : s.currentIdx = n - 1
429 0 : } else {
430 0 : s.currentIdx = n
431 0 : }
432 0 : if s.currentIdx < len(s.filtered) {
433 0 : if iterKey, val := s.filtered[s.currentIdx].SeekGE(key, flags); iterKey != nil {
434 0 : return iterKey, val
435 0 : }
436 0 : if err := s.filtered[s.currentIdx].Error(); err != nil {
437 0 : s.err = err
438 0 : }
439 0 : s.currentIdx++
440 : }
441 0 : return s.skipEmptyFileForward(key, flags)
442 : }
443 :
444 : func (s *simpleLevelIter) skipEmptyFileForward(
445 : seekKey []byte, flags base.SeekGEFlags,
446 0 : ) (*base.InternalKey, base.LazyValue) {
447 0 : var iterKey *base.InternalKey
448 0 : var val base.LazyValue
449 0 : for s.currentIdx >= 0 && s.currentIdx < len(s.filtered) && s.err == nil {
450 0 : if seekKey != nil {
451 0 : iterKey, val = s.filtered[s.currentIdx].SeekGE(seekKey, flags)
452 0 : } else if s.lowerBound != nil {
453 0 : iterKey, val = s.filtered[s.currentIdx].SeekGE(s.lowerBound, flags)
454 0 : } else {
455 0 : iterKey, val = s.filtered[s.currentIdx].First()
456 0 : }
457 0 : if iterKey != nil {
458 0 : return iterKey, val
459 0 : }
460 0 : if err := s.filtered[s.currentIdx].Error(); err != nil {
461 0 : s.err = err
462 0 : }
463 0 : s.currentIdx++
464 : }
465 0 : return nil, base.LazyValue{}
466 : }
467 :
468 : func (s *simpleLevelIter) SeekPrefixGE(
469 : prefix, key []byte, flags base.SeekGEFlags,
470 0 : ) (*base.InternalKey, base.LazyValue) {
471 0 : panic("unimplemented")
472 : }
473 :
474 : func (s *simpleLevelIter) SeekLT(
475 : key []byte, flags base.SeekLTFlags,
476 0 : ) (*base.InternalKey, base.LazyValue) {
477 0 : panic("unimplemented")
478 : }
479 :
480 0 : func (s *simpleLevelIter) First() (*base.InternalKey, base.LazyValue) {
481 0 : if s.err != nil {
482 0 : return nil, base.LazyValue{}
483 0 : }
484 0 : s.currentIdx = 0
485 0 : return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone)
486 : }
487 :
488 0 : func (s *simpleLevelIter) Last() (*base.InternalKey, base.LazyValue) {
489 0 : panic("unimplemented")
490 : }
491 :
492 0 : func (s *simpleLevelIter) Next() (*base.InternalKey, base.LazyValue) {
493 0 : if s.err != nil {
494 0 : return nil, base.LazyValue{}
495 0 : }
496 0 : if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
497 0 : return nil, base.LazyValue{}
498 0 : }
499 0 : if iterKey, val := s.filtered[s.currentIdx].Next(); iterKey != nil {
500 0 : return iterKey, val
501 0 : }
502 0 : s.currentIdx++
503 0 : return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone)
504 : }
505 :
506 0 : func (s *simpleLevelIter) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) {
507 0 : if s.err != nil {
508 0 : return nil, base.LazyValue{}
509 0 : }
510 0 : if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
511 0 : return nil, base.LazyValue{}
512 0 : }
513 0 : if iterKey, val := s.filtered[s.currentIdx].NextPrefix(succKey); iterKey != nil {
514 0 : return iterKey, val
515 0 : }
516 0 : s.currentIdx++
517 0 : return s.skipEmptyFileForward(succKey /* seekKey */, base.SeekGEFlagsNone)
518 : }
519 :
520 0 : func (s *simpleLevelIter) Prev() (*base.InternalKey, base.LazyValue) {
521 0 : panic("unimplemented")
522 : }
523 :
524 0 : func (s *simpleLevelIter) Error() error {
525 0 : if s.currentIdx >= 0 && s.currentIdx < len(s.filtered) {
526 0 : s.err = firstError(s.err, s.filtered[s.currentIdx].Error())
527 0 : }
528 0 : return s.err
529 : }
530 :
531 0 : func (s *simpleLevelIter) Close() error {
532 0 : var err error
533 0 : for i := range s.iters {
534 0 : err = firstError(err, s.iters[i].Close())
535 0 : }
536 0 : return err
537 : }
538 :
539 0 : func (s *simpleLevelIter) SetBounds(lower, upper []byte) {
540 0 : s.currentIdx = -1
541 0 : s.lowerBound = lower
542 0 : for i := range s.iters {
543 0 : s.iters[i].SetBounds(lower, upper)
544 0 : }
545 0 : s.resetFilteredIters()
546 : }
547 :
548 0 : func (s *simpleLevelIter) String() string {
549 0 : if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
550 0 : return "simpleLevelIter: current=<nil>"
551 0 : }
552 0 : return fmt.Sprintf("simpleLevelIter: current=%s", s.filtered[s.currentIdx])
553 : }
554 :
555 : var _ internalIterator = &simpleLevelIter{}
|