From f47f45c4f454b388310a3a5901aa524ae13c4941 Mon Sep 17 00:00:00 2001 From: Arnav Balyan Date: Sat, 27 Jun 2026 17:38:57 -0700 Subject: [PATCH] update --- .../globalindex/ReversedKeySerializer.java | 67 ++++++ .../globalindex/btree/BTreeGlobalIndexer.java | 19 +- .../btree/ReverseBTreeGlobalIndexer.java | 50 +++++ .../ReverseBTreeGlobalIndexerFactory.java | 40 ++++ .../btree/ReverseLazyFilteredBTreeReader.java | 97 +++++++++ ...he.paimon.globalindex.GlobalIndexerFactory | 1 + .../ReversedKeySerializerTest.java | 63 ++++++ .../btree/ReverseBTreeEndsWithTest.java | 190 ++++++++++++++++++ 8 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 paimon-common/src/main/java/org/apache/paimon/globalindex/ReversedKeySerializer.java create mode 100644 paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexer.java create mode 100644 paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexerFactory.java create mode 100644 paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseLazyFilteredBTreeReader.java create mode 100644 paimon-common/src/test/java/org/apache/paimon/globalindex/ReversedKeySerializerTest.java create mode 100644 paimon-common/src/test/java/org/apache/paimon/globalindex/btree/ReverseBTreeEndsWithTest.java diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/ReversedKeySerializer.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/ReversedKeySerializer.java new file mode 100644 index 000000000000..840af81c22bc --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/ReversedKeySerializer.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex; + +import org.apache.paimon.memory.MemorySlice; + +import java.util.Comparator; + +/** A {@link KeySerializer} that reverses serialized key bytes. */ +public class ReversedKeySerializer implements KeySerializer { + + private final KeySerializer delegate; + + public ReversedKeySerializer(KeySerializer delegate) { + this.delegate = delegate; + } + + @Override + public byte[] serialize(Object key) { + return reverse(delegate.serialize(key)); + } + + @Override + public Object deserialize(MemorySlice data) { + return delegate.deserialize(MemorySlice.wrap(reverse(data.copyBytes()))); + } + + @Override + public Comparator createComparator() { + return (left, right) -> compareUnsigned(serialize(left), serialize(right)); + } + + private static byte[] reverse(byte[] bytes) { + byte[] out = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + out[i] = bytes[bytes.length - 1 - i]; + } + return out; + } + + private static int compareUnsigned(byte[] left, byte[] right) { + int len = Math.min(left.length, right.length); + for (int i = 0; i < len; i++) { + int cmp = (left[i] & 0xFF) - (right[i] & 0xFF); + if (cmp != 0) { + return cmp; + } + } + return left.length - right.length; + } +} diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeGlobalIndexer.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeGlobalIndexer.java index 24be4096058a..d44586b4c833 100644 --- a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeGlobalIndexer.java +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeGlobalIndexer.java @@ -66,7 +66,11 @@ public class BTreeGlobalIndexer implements GlobalIndexer { private final LazyField cacheManager; public BTreeGlobalIndexer(DataField dataField, Options options) { - this.keySerializer = KeySerializer.create(dataField.type()); + this(KeySerializer.create(dataField.type()), options); + } + + public BTreeGlobalIndexer(KeySerializer keySerializer, Options options) { + this.keySerializer = keySerializer; this.options = options; this.fallbackScanMaxSize = options.get(BTreeIndexOptions.BTREE_INDEX_FALLBACK_SCAN_MAX_SIZE).getBytes(); @@ -99,7 +103,7 @@ public GlobalIndexReader createReader( GlobalIndexFileReader fileReader, List files, ExecutorService executor) { - return new LazyFilteredBTreeReader( + return newReader( files, keySerializer, fileReader, @@ -107,4 +111,15 @@ public GlobalIndexReader createReader( fallbackScanMaxSize, executor); } + + protected LazyFilteredBTreeReader newReader( + List files, + KeySerializer keySerializer, + GlobalIndexFileReader fileReader, + CacheManager cacheManager, + long fallbackScanMaxSize, + ExecutorService executor) { + return new LazyFilteredBTreeReader( + files, keySerializer, fileReader, cacheManager, fallbackScanMaxSize, executor); + } } diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexer.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexer.java new file mode 100644 index 000000000000..cd259d33392e --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexer.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex.btree; + +import org.apache.paimon.globalindex.GlobalIndexIOMeta; +import org.apache.paimon.globalindex.KeySerializer; +import org.apache.paimon.globalindex.ReversedKeySerializer; +import org.apache.paimon.globalindex.io.GlobalIndexFileReader; +import org.apache.paimon.io.cache.CacheManager; +import org.apache.paimon.options.Options; +import org.apache.paimon.types.DataField; + +import java.util.List; +import java.util.concurrent.ExecutorService; + +/** A btree global index over reversed keys. */ +public class ReverseBTreeGlobalIndexer extends BTreeGlobalIndexer { + + public ReverseBTreeGlobalIndexer(DataField dataField, Options options) { + super(new ReversedKeySerializer(KeySerializer.create(dataField.type())), options); + } + + @Override + protected LazyFilteredBTreeReader newReader( + List files, + KeySerializer keySerializer, + GlobalIndexFileReader fileReader, + CacheManager cacheManager, + long fallbackScanMaxSize, + ExecutorService executor) { + return new ReverseLazyFilteredBTreeReader( + files, keySerializer, fileReader, cacheManager, fallbackScanMaxSize, executor); + } +} diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexerFactory.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexerFactory.java new file mode 100644 index 000000000000..39197a100059 --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseBTreeGlobalIndexerFactory.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex.btree; + +import org.apache.paimon.globalindex.GlobalIndexer; +import org.apache.paimon.globalindex.GlobalIndexerFactory; +import org.apache.paimon.options.Options; +import org.apache.paimon.types.DataField; + +/** Factory for {@link ReverseBTreeGlobalIndexer}. */ +public class ReverseBTreeGlobalIndexerFactory implements GlobalIndexerFactory { + + public static final String IDENTIFIER = "reverse-btree"; + + @Override + public String identifier() { + return IDENTIFIER; + } + + @Override + public GlobalIndexer create(DataField dataField, Options options) { + return new ReverseBTreeGlobalIndexer(dataField, options); + } +} diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseLazyFilteredBTreeReader.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseLazyFilteredBTreeReader.java new file mode 100644 index 000000000000..279b35cddf51 --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/ReverseLazyFilteredBTreeReader.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex.btree; + +import org.apache.paimon.globalindex.GlobalIndexIOMeta; +import org.apache.paimon.globalindex.GlobalIndexResult; +import org.apache.paimon.globalindex.KeySerializer; +import org.apache.paimon.globalindex.io.GlobalIndexFileReader; +import org.apache.paimon.io.cache.CacheManager; +import org.apache.paimon.predicate.FieldRef; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** A {@link LazyFilteredBTreeReader} over reversed keys. */ +public class ReverseLazyFilteredBTreeReader extends LazyFilteredBTreeReader { + + public ReverseLazyFilteredBTreeReader( + List files, + KeySerializer keySerializer, + GlobalIndexFileReader fileReader, + CacheManager cacheManager, + long fallbackScanMaxSize, + ExecutorService executor) { + super(files, keySerializer, fileReader, cacheManager, fallbackScanMaxSize, executor); + } + + @Override + public CompletableFuture> visitEndsWith( + FieldRef fieldRef, Object literal) { + return super.visitStartsWith(fieldRef, literal); + } + + @Override + public CompletableFuture> visitStartsWith( + FieldRef fieldRef, Object literal) { + return unsupported(); + } + + @Override + public CompletableFuture> visitLessThan( + FieldRef fieldRef, Object literal) { + return unsupported(); + } + + @Override + public CompletableFuture> visitLessOrEqual( + FieldRef fieldRef, Object literal) { + return unsupported(); + } + + @Override + public CompletableFuture> visitGreaterThan( + FieldRef fieldRef, Object literal) { + return unsupported(); + } + + @Override + public CompletableFuture> visitGreaterOrEqual( + FieldRef fieldRef, Object literal) { + return unsupported(); + } + + @Override + public CompletableFuture> visitBetween( + FieldRef fieldRef, Object from, Object to) { + return unsupported(); + } + + @Override + public CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return unsupported(); + } + + private static CompletableFuture> unsupported() { + return CompletableFuture.completedFuture(Optional.empty()); + } +} diff --git a/paimon-common/src/main/resources/META-INF/services/org.apache.paimon.globalindex.GlobalIndexerFactory b/paimon-common/src/main/resources/META-INF/services/org.apache.paimon.globalindex.GlobalIndexerFactory index 06722ccd2d78..25eb22dc7c7a 100644 --- a/paimon-common/src/main/resources/META-INF/services/org.apache.paimon.globalindex.GlobalIndexerFactory +++ b/paimon-common/src/main/resources/META-INF/services/org.apache.paimon.globalindex.GlobalIndexerFactory @@ -14,4 +14,5 @@ # limitations under the License. org.apache.paimon.globalindex.btree.BTreeGlobalIndexerFactory +org.apache.paimon.globalindex.btree.ReverseBTreeGlobalIndexerFactory org.apache.paimon.globalindex.bitmap.BitmapGlobalIndexerFactory diff --git a/paimon-common/src/test/java/org/apache/paimon/globalindex/ReversedKeySerializerTest.java b/paimon-common/src/test/java/org/apache/paimon/globalindex/ReversedKeySerializerTest.java new file mode 100644 index 000000000000..45b44618ed27 --- /dev/null +++ b/paimon-common/src/test/java/org/apache/paimon/globalindex/ReversedKeySerializerTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.memory.MemorySlice; +import org.apache.paimon.types.VarCharType; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link ReversedKeySerializer}. */ +public class ReversedKeySerializerTest { + + private final KeySerializer serializer = + new ReversedKeySerializer( + KeySerializer.create(new VarCharType(VarCharType.MAX_LENGTH))); + + @Test + public void testRoundTrip() { + for (String s : new String[] {"", "a", "hello", "ends_with_suffix"}) { + BinaryString key = BinaryString.fromString(s); + assertThat(serializer.deserialize(MemorySlice.wrap(serializer.serialize(key)))) + .isEqualTo(key); + } + } + + @Test + public void testComparatorOrdersBySuffix() { + Comparator cmp = serializer.createComparator(); + List input = + Arrays.asList( + BinaryString.fromString("2b"), + BinaryString.fromString("1a"), + BinaryString.fromString("2a"), + BinaryString.fromString("1b")); + List sorted = + input.stream().sorted(cmp).map(BinaryString::toString).collect(Collectors.toList()); + assertThat(sorted).containsExactly("1a", "2a", "1b", "2b"); + } +} diff --git a/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/ReverseBTreeEndsWithTest.java b/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/ReverseBTreeEndsWithTest.java new file mode 100644 index 000000000000..cbeea65e3a8e --- /dev/null +++ b/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/ReverseBTreeEndsWithTest.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.paimon.globalindex.btree; + +import org.apache.paimon.data.BinaryString; +import org.apache.paimon.fs.FileIO; +import org.apache.paimon.fs.Path; +import org.apache.paimon.fs.PositionOutputStream; +import org.apache.paimon.fs.local.LocalFileIO; +import org.apache.paimon.globalindex.GlobalIndexIOMeta; +import org.apache.paimon.globalindex.GlobalIndexReader; +import org.apache.paimon.globalindex.GlobalIndexResult; +import org.apache.paimon.globalindex.GlobalIndexSingleColumnWriter; +import org.apache.paimon.globalindex.KeySerializer; +import org.apache.paimon.globalindex.ResultEntry; +import org.apache.paimon.globalindex.ReversedKeySerializer; +import org.apache.paimon.globalindex.io.GlobalIndexFileReader; +import org.apache.paimon.globalindex.io.GlobalIndexFileWriter; +import org.apache.paimon.options.MemorySize; +import org.apache.paimon.options.Options; +import org.apache.paimon.predicate.FieldRef; +import org.apache.paimon.types.DataField; +import org.apache.paimon.types.DataType; +import org.apache.paimon.types.VarCharType; +import org.apache.paimon.utils.Pair; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +import static org.apache.paimon.shade.guava30.com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link ReverseBTreeGlobalIndexer}. */ +public class ReverseBTreeEndsWithTest { + + private static final DataType TYPE = new VarCharType(VarCharType.MAX_LENGTH); + private static final FieldRef REF = new FieldRef(1, "f", TYPE); + + @TempDir java.nio.file.Path tempPath; + + private List> data; + + @Test + public void testEndsWithAndLikeAreAccelerated() throws Exception { + String[] suffixes = {"red", "green", "blue", "gold"}; + ReversedKeySerializer reversed = new ReversedKeySerializer(KeySerializer.create(TYPE)); + + try (GlobalIndexReader reader = buildIndex(reversed, suffixes)) { + for (String suffix : suffixes) { + List expected = idsEndingWith(suffix); + + GlobalIndexResult endsWith = + reader.visitEndsWith(REF, BinaryString.fromString(suffix)).join().get(); + assertThat(rowIds(endsWith)).containsExactlyInAnyOrderElementsOf(expected); + + GlobalIndexResult like = + reader.visitLike(REF, BinaryString.fromString("%" + suffix)).join().get(); + assertThat(rowIds(like)).containsExactlyInAnyOrderElementsOf(expected); + } + + Optional none = + reader.visitEndsWith(REF, BinaryString.fromString("nosuchsuffix")).join(); + assertThat(none).isPresent(); + assertThat(none.get().results().isEmpty()).isTrue(); + } + } + + @Test + public void testOrderDependentPredicatesDecline() throws Exception { + String[] suffixes = {"red", "green", "blue", "gold"}; + ReversedKeySerializer reversed = new ReversedKeySerializer(KeySerializer.create(TYPE)); + + try (GlobalIndexReader reader = buildIndex(reversed, suffixes)) { + BinaryString probe = BinaryString.fromString("row5"); + assertThat(reader.visitStartsWith(REF, probe).join()).isEmpty(); + assertThat(reader.visitLessThan(REF, probe).join()).isEmpty(); + assertThat(reader.visitGreaterThan(REF, probe).join()).isEmpty(); + assertThat(reader.visitLessOrEqual(REF, probe).join()).isEmpty(); + assertThat(reader.visitGreaterOrEqual(REF, probe).join()).isEmpty(); + assertThat(reader.visitBetween(REF, probe, BinaryString.fromString("row9")).join()) + .isEmpty(); + } + } + + @Test + public void testEqualityStillWorksOnReversedIndex() throws Exception { + String[] suffixes = {"red", "green", "blue", "gold"}; + ReversedKeySerializer reversed = new ReversedKeySerializer(KeySerializer.create(TYPE)); + + try (GlobalIndexReader reader = buildIndex(reversed, suffixes)) { + BinaryString exact = (BinaryString) data.get(0).getKey(); + List expected = + data.stream() + .filter(p -> p.getKey().equals(exact)) + .map(Pair::getValue) + .collect(Collectors.toList()); + GlobalIndexResult equal = reader.visitEqual(REF, exact).join().get(); + assertThat(rowIds(equal)).containsExactlyInAnyOrderElementsOf(expected); + } + } + + private GlobalIndexReader buildIndex(ReversedKeySerializer reversed, String[] suffixes) + throws IOException { + FileIO fileIO = LocalFileIO.create(); + GlobalIndexFileWriter fileWriter = + new GlobalIndexFileWriter() { + @Override + public String newFileName(String prefix) { + return "reverse-btree-" + prefix; + } + + @Override + public PositionOutputStream newOutputStream(String fileName) + throws IOException { + return fileIO.newOutputStream( + new Path(new Path(tempPath.toUri()), fileName), true); + } + }; + GlobalIndexFileReader fileReader = + meta -> + fileIO.newInputStream( + new Path(new Path(tempPath.toUri()), meta.filePath())); + + Options options = new Options(); + options.set(BTreeIndexOptions.BTREE_INDEX_CACHE_SIZE, MemorySize.ofMebiBytes(8)); + ReverseBTreeGlobalIndexer indexer = + new ReverseBTreeGlobalIndexer(new DataField(1, "f", TYPE), options); + Comparator cmp = reversed.createComparator(); + + Random rnd = new Random(7); + data = new ArrayList<>(); + for (int i = 0; i < 20000; i++) { + String v = "row" + rnd.nextInt(1_000_000) + suffixes[rnd.nextInt(suffixes.length)]; + data.add(Pair.of(BinaryString.fromString(v), (long) i)); + } + data.sort((a, b) -> cmp.compare(a.getKey(), b.getKey())); + + GlobalIndexSingleColumnWriter writer = indexer.createWriter(fileWriter); + for (Pair p : data) { + writer.write(p.getKey(), p.getValue()); + } + List metas = new ArrayList<>(); + for (ResultEntry e : writer.finish()) { + Path path = new Path(new Path(tempPath.toUri()), e.fileName()); + metas.add(new GlobalIndexIOMeta(path, fileIO.getFileSize(path), e.meta())); + } + return indexer.createReader(fileReader, metas, newDirectExecutorService()); + } + + private List idsEndingWith(String suffix) { + return data.stream() + .filter(p -> ((BinaryString) p.getKey()).toString().endsWith(suffix)) + .map(Pair::getValue) + .collect(Collectors.toList()); + } + + private static List rowIds(GlobalIndexResult result) { + List out = new ArrayList<>(); + Iterator it = result.results().iterator(); + while (it.hasNext()) { + out.add(it.next()); + } + return out; + } +}