leaves12138 commented on code in PR #9845: URL: https://github.com/apache/paimon/pull/9845#discussion_r4023765414
########## docs/docs/concepts/spec/manifest.md: ########## @@ -63,6 +63,242 @@ skip manifests before opening them. Each extra file belongs exclusively to one manifest. It is retained and cleaned up together with that manifest during snapshot, tag, or changelog deletion. +### Manifest Sidecar + +`ManifestSidecar` provides a binary sidecar for selecting complete Avro manifest blocks +using independent partition, row-ID and bucket coverage. A sidecar uses the +`<manifest-file-name>.avro.sidecar` naming convention. Readers find it through an explicit +`.avro.sidecar` reference in the manifest metadata's `_EXTRA_FILES`, without probing a +derived file name. The Avro schemas and `_VERSION` identifiers remain unchanged. + +The utility includes construction, validation, block selection and optional caching. Table +writers and scans do not yet invoke it automatically. Callers are responsible for publishing +sidecar references, managing file ownership, applying entry filters and reconciling ADD/DELETE +entries after block selection. `build` reads the completed physical manifest and returns +sidecar bytes; it does not write or publish another file. + +Callers decide whether to invoke `build` and `read`; these utilities have no read/write switches. +`build` and `Builder` accept `rowIdEnabled` and `bucketEnabled` arguments for independent +payload generation. Partition generation is always enabled, +including the empty partition tuple for unpartitioned tables. Missing or invalid +metadata makes only the affected block's dimension unavailable. There is no sidecar byte budget: +construction keeps complete coverage and `read` consumes the entire file once it is opened. + +`read` returns null for an absent sidecar reference or an `IOException`, allowing the caller +to fall back to the manifest. If the thread is interrupted, the I/O failure is propagated as +`UncheckedIOException`. Other exceptions and errors propagate unchanged. `select` validates +supplied bytes directly and reports invalid containers with `IOException`. + +Version 1 uses the following layout. Counts, lengths, offsets and the version use canonical +nonnegative unsigned LEB128 varints. Counts and payload lengths are bounded by `Integer.MAX_VALUE`; +block offsets, lengths and record counts are bounded by `Long.MAX_VALUE`. Row-ID envelope +endpoints remain fixed-width, eight-byte big-endian longs. Encoding IDs are unsigned bytes +with separate namespaces. The existing serialized partition tuple bytes are unchanged. + +```text +magic : 4 bytes // ASCII PMSC +formatVersion : varint // 1 +avroHeaderLength : varint +avroHeader : bytes // original schema, codec and sync marker +partitionCount : varint +partitionDictionary[] + partitionByteLength : varint + partitionBytes : bytes // existing manifest BinaryRow serialization +blockCount : varint +blocks[] // original physical order + offset : varint + length : varint // complete encoded block, including sync marker + recordCount : varint + partitionEncoding : byte + if partitionEncoding != 0: + partitionPayloadLength : varint + partitionPayload : bytes + rowIdEncoding : byte + if rowIdEncoding != 0: + rowIdPayloadLength : varint + rowIdPayload : bytes + bucketEncoding : byte + if bucketEncoding != 0: + bucketPayloadLength : varint + bucketPayload : bytes +checksum : 32 bytes // SHA-256 of all preceding bytes +``` + +The block ID is its position. Its first entry ordinal is the sum of preceding record counts +and is not stored. Each complete partition tuple appears once in the dictionary, including +all its fields and nulls. The scan's partition type interprets the existing serialized tuple. +Partition predicates are evaluated once per dictionary entry. + +| Dimension | Encoding | Payload | +| --- | --- | --- | +| Any | `0` | Unavailable; only the encoding byte is present. | +| Partition | `1` | `intsDeltaPayload` of sorted unique dictionary IDs. | +| Row ID | `1` | Minimum, maximum, and `intsDeltaPayload` of sorted interior interval endpoints. | +| Bucket | `1` | Two paired `intsDeltaPayload` sequences: sorted bucket IDs and their recorded total bucket counts. | +| Any | Other nonzero ID | Skip the declared payload length; treat only this dimension as unavailable. | + +Only nonzero encodings are followed by a length and payload. Payload lengths exclude the +encoding and length fields, but include the count and other fields within the payload. +Partition IDs and bucket pairs have positive counts no greater than the block's record count. +Row-ID coverage contains one or more intervals; its interior endpoint count can be zero for +a single interval. Encoding 0 represents unavailable coverage, not an empty known set. + +#### Integer Delta Payload + +Partition, row-ID and both bucket sequences share this structure: + +```text +intsDeltaPayload + count : varint + deltas[count] : varint +``` + +The count is in `[0, Integer.MAX_VALUE]`. Nonnegative deltas use unsigned LEB128 varints, +occupying one to nine bytes for values from 0 through +`Long.MAX_VALUE`. Seven value bits are stored per byte, least significant group first; the +high bit indicates another byte follows. +Encodings use the shortest representation without padding. + +A nondecreasing sequence is delta-encoded from a specified base. Each value contributes one +unsigned varint containing its difference from the preceding value. The first difference +is relative to the base: + +```text +deltas[] : varint +value[0] = base + deltas[0] +value[i] = value[i - 1] + deltas[i] +``` + +The shared `DeltaVarintCodec` utility writes the count and then each delta immediately, +and reads values on demand using `VarLengthIntUtils`. A reader consumes exactly the declared +number of values, leaving any following sequence available in the buffer. Callers check +their enclosing payload boundaries. Counts, overflow and value bounds are checked without +materializing arrays. Reads may stop early. + +Only the `totalBuckets` sequence uses signed differences, because totals need not increase +when pairs are sorted by bucket. Its differences use ZigZag before unsigned varint encoding: +`encoded = (delta << 1) ^ (delta >> 63)` and +`delta = (encoded >>> 1) ^ -(encoded & 1)`. Values are nonnegative ints, so encoded deltas +are at most `2 * Integer.MAX_VALUE` and require at most five bytes. The field defines this +signed mode; no additional mode byte is stored. Other sequences use nonnegative differences. + +#### Partition Payload + +When `partitionEncoding == 1`, the block stores IDs of all distinct partition tuples +represented by its entries: + +```text +partitionPayload + intsDeltaPayload // N > 0 dictionary IDs, base = 0 Review Comment: partitionPayload are ints, but row id payload are longs, I will comment it in manifest.md ########## paimon-core/src/main/java/org/apache/paimon/manifest/ManifestSidecar.java: ########## @@ -0,0 +1,960 @@ +/* + * 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.manifest; + +import org.apache.paimon.annotation.VisibleForTesting; +import org.apache.paimon.data.BinaryRow; +import org.apache.paimon.data.Segments; +import org.apache.paimon.fs.FileIO; +import org.apache.paimon.fs.Path; +import org.apache.paimon.fs.SeekableInputStream; +import org.apache.paimon.partition.PartitionPredicate; +import org.apache.paimon.types.DataField; +import org.apache.paimon.types.RowType; +import org.apache.paimon.utils.DeltaVarintCodec; +import org.apache.paimon.utils.RowRangeIndex; +import org.apache.paimon.utils.SegmentsCache; +import org.apache.paimon.utils.SerializationUtils; +import org.apache.paimon.utils.VarLengthIntUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiPredicate; + +import static org.apache.paimon.utils.VarLengthIntUtils.encodeLong; + +/** Independently usable partition, row-id and bucket coverage for each manifest block. */ +public final class ManifestSidecar { + + public static final String SUFFIX = ".avro.sidecar"; + private static final Logger LOG = LoggerFactory.getLogger(ManifestSidecar.class); + private static final int MAGIC = 0x504d5343; + private static final int FORMAT_VERSION = 1; + private static final int MIN_HEADER_BYTES = 29; + private static final int MIN_BLOCK_BYTES = 6; + private static final byte[] EMPTY = new byte[0]; + private static final int DIGEST_BYTES = 32; + private static final int SIDECAR_READ_BUFFER_BYTES = 1024 * 1024; + private static final int BLOCK_READ_BUFFER_BYTES = 4 * 1024 * 1024; + private static final ProjectedManifestEntry.Projection BLOCK_INDEX_PROJECTION = + createBlockIndexProjection(); + + private ManifestSidecar() {} + + public static Path path(Path manifest) { + return new Path(manifest.toString() + SUFFIX); + } + + @Nullable + public static String fileName(ManifestFileMeta manifest) { + if (manifest.extraFiles() != null) { + for (String extraFile : manifest.extraFiles()) { + if (extraFile.endsWith(SUFFIX)) { + return extraFile; + } + } + } + return null; + } + + /** Original file offset/length and zero-based manifest entry ordinal, not table row id. */ + public static final class Block { + public final long offset; + public final long length; + public final long firstRecord; + public final long recordCount; + + public Block(long offset, long length, long firstRecord, long recordCount) { + this.offset = offset; + this.length = length; + this.firstRecord = firstRecord; + this.recordCount = recordCount; + } + } + + /** Selected blocks in original file order. Empty means the manifest can be excluded. */ + public static final class Selection { + private final byte[] header; + private final List<Block> blocks; + + private Selection(byte[] header, List<Block> blocks) { + this.header = header; + this.blocks = Collections.unmodifiableList(blocks); + } + + public List<Block> blocks() { + return blocks; + } + } + + /** Builds a complete block directory with independently available coverage. */ + public static final class Builder { + private final boolean rowIdEnabled; + private final boolean bucketEnabled; + private final byte[] header; + private final TreeMap<Long, Long> ranges = new TreeMap<>(); + private final Map<ByteBuffer, Integer> dictionary = new LinkedHashMap<>(); + private final TreeSet<Integer> partitionIds = new TreeSet<>(); + private final TreeSet<Long> bucketPairs = new TreeSet<>(); + private final List<IndexedBlock> blocks = new ArrayList<>(); + private long nextOffset; + private long nextRecord; + private Block current; + private long entriesInBlock; + private boolean rowAvailable; + private boolean partitionAvailable; + private boolean bucketAvailable; + + public Builder(byte[] header, boolean rowIdEnabled, boolean bucketEnabled) { + this.rowIdEnabled = rowIdEnabled; + this.bucketEnabled = bucketEnabled; + this.header = Objects.requireNonNull(header); + nextOffset = header.length; + } + + public void beginBlock(long offset, long length, long records) throws IOException { + require(current == null && offset == nextOffset && length > 0 && records > 0); + current = new Block(offset, length, nextRecord, records); + entriesInBlock = 0; + rowAvailable = rowIdEnabled; + partitionAvailable = true; + bucketAvailable = bucketEnabled; + ranges.clear(); + partitionIds.clear(); + bucketPairs.clear(); + } + + @VisibleForTesting + public void add(@Nullable Long first, long count) { + add(first, count, null); + } + + @VisibleForTesting + public void add(@Nullable Long first, long count, @Nullable byte[] partition) { + add(first, count, partition, null, null); + } + + public void add( + @Nullable Long first, + long count, + @Nullable byte[] partition, + @Nullable Integer bucket, + @Nullable Integer totalBuckets) { + if (current == null) { + throw new IllegalStateException("No current Avro block"); + } + entriesInBlock++; + addPartition(partition); + addBucket(bucket, totalBuckets); + if (!rowAvailable) { + return; + } + if (first == null || first < 0 || count <= 0 || count - 1 > Long.MAX_VALUE - first) { + rowAvailable = false; + ranges.clear(); + return; + } + long start = first; + long end = first + (count - 1); + Map.Entry<Long, Long> before = ranges.floorEntry(start); + if (before != null && before.getValue() >= start - 1) { + start = before.getKey(); + end = Math.max(end, before.getValue()); + ranges.remove(before.getKey()); + } + Map.Entry<Long, Long> next; + while ((next = ranges.ceilingEntry(start)) != null + && (next.getKey() <= end || next.getKey() - end == 1)) { + end = Math.max(end, next.getValue()); + ranges.remove(next.getKey()); + } + ranges.put(start, end); + } + + private void addBucket(@Nullable Integer bucket, @Nullable Integer totalBuckets) { + if (!bucketAvailable) { + return; + } + if (bucket == null || totalBuckets == null || bucket < 0 || totalBuckets <= bucket) { + bucketAvailable = false; + bucketPairs.clear(); + return; + } + bucketPairs.add(((long) bucket << 32) | totalBuckets); + } + + private void addPartition(@Nullable byte[] bytes) { + if (!partitionAvailable) { + return; + } + if (bytes == null) { + partitionAvailable = false; + partitionIds.clear(); + return; + } + Integer id = dictionary.get(ByteBuffer.wrap(bytes)); + if (id == null) { + id = dictionary.size(); + dictionary.put(ByteBuffer.wrap(bytes.clone()), id); + } + partitionIds.add(id); + } + + public void endBlock() throws IOException { + require(current != null && entriesInBlock == current.recordCount); + blocks.add( + new IndexedBlock( + current, + partitionAvailable + ? encodeValues(partitionIds, partitionIds.size()) + : EMPTY, + rowAvailable ? encodeRanges() : EMPTY, + bucketAvailable ? encodeBuckets() : EMPTY)); + nextOffset = Math.addExact(current.offset, current.length); + nextRecord = Math.addExact(current.firstRecord, current.recordCount); + ranges.clear(); + partitionIds.clear(); + bucketPairs.clear(); + current = null; + } + + private byte[] encodeRanges() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(buffer); + long min = ranges.firstKey(); + long max = ranges.lastEntry().getValue(); + out.writeLong(min); + out.writeLong(max); + // The envelope supplies the first start and last end. Encode only interior endpoints. + DeltaVarintCodec.Writer encoder = + new DeltaVarintCodec.Writer(out, Math.multiplyExact(ranges.size() - 1, 2), min); + int index = 0; + for (Map.Entry<Long, Long> range : ranges.entrySet()) { + if (index > 0) { + encoder.write(range.getKey()); + } + if (++index < ranges.size()) { + encoder.write(range.getValue()); + } + } + return buffer.toByteArray(); + } + + private byte[] encodeBuckets() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(buffer); + DeltaVarintCodec.Writer buckets = + new DeltaVarintCodec.Writer(out, bucketPairs.size(), 0); + for (long pair : bucketPairs) { + buckets.write(pair >>> 32); + } + DeltaVarintCodec.Writer totals = + new DeltaVarintCodec.Writer(out, bucketPairs.size(), 0, true); + for (long pair : bucketPairs) { + totals.write((int) pair); + } + return buffer.toByteArray(); + } + + public byte[] serialize(long fileSize, long entryCount) throws IOException { + require(current == null && nextOffset == fileSize && nextRecord == entryCount); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(buffer); + out.writeInt(MAGIC); + encodeLong(out, FORMAT_VERSION); + encodeLong(out, header.length); + out.write(header); + encodeLong(out, dictionary.size()); + for (ByteBuffer bytes : dictionary.keySet()) { + encodeLong(out, bytes.remaining()); + out.write(bytes.array()); + } + encodeLong(out, blocks.size()); + for (IndexedBlock block : blocks) { + encodeLong(out, block.block.offset); + encodeLong(out, block.block.length); + encodeLong(out, block.block.recordCount); + writePayload(out, block.partitions); + writePayload(out, block.rowIds); + writePayload(out, block.buckets); + } + out.write(digest(buffer.toByteArray())); + return buffer.toByteArray(); + } + + private static byte[] encodeValues(Iterable<? extends Number> values, int count) + throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(buffer); + DeltaVarintCodec.Writer encoder = new DeltaVarintCodec.Writer(out, count, 0); + for (Number value : values) { + encoder.write(value.longValue()); + } + return buffer.toByteArray(); + } + + private static void writePayload(DataOutputStream out, byte[] payload) throws IOException { + out.writeByte(payload.length == 0 ? 0 : 1); + if (payload.length > 0) { + encodeLong(out, payload.length); + out.write(payload); + } + } + } + + private static final class IndexedBlock { + private final Block block; + private final byte[] partitions; + private final byte[] rowIds; + private final byte[] buckets; + + private IndexedBlock(Block block, byte[] partitions, byte[] rowIds, byte[] buckets) { + this.block = block; + this.partitions = partitions; + this.rowIds = rowIds; + this.buckets = buckets; + } + } + + private static ProjectedManifestEntry.Projection createBlockIndexProjection() { + List<DataField> fields = + new ArrayList<>( + ProjectedManifestEntry.ROW_RANGE_PROJECTION.projectedType().getFields()); + fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.BUCKET)); + fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.TOTAL_BUCKETS)); + return ProjectedManifestEntry.Projection.create(new RowType(false, fields)); + } + + /** + * Rebuild from the final physical blocks, including raw-copy and encoded rewrite paths. Callers + * decide whether to build a sidecar. Partition coverage is always generated. + */ + public static byte[] build( + FileIO io, + Path path, + long size, + long records, + boolean rowIdEnabled, + boolean bucketEnabled) + throws IOException { + try (ManifestAvroReader reader = new ManifestAvroReader(io.newInputStream(path))) { + Builder builder = new Builder(reader.headerBytes(), rowIdEnabled, bucketEnabled); + ProjectedManifestEntry.Projection projection = BLOCK_INDEX_PROJECTION; + ProjectedManifestEntry entry = projection.createEntry(); + while (reader.hasNext()) { + ManifestAvroReader.RawBlock block = reader.next(); + builder.beginBlock(reader.blockOffset(), reader.blockLength(), block.recordCount()); + ManifestAvroReader.RowIterator rows = block.toRows(projection.projectedType()); + while (rows.hasNext()) { + entry.replace(rows.next()); + builder.add( + rowIdEnabled ? entry.file().firstRowId() : null, + rowIdEnabled ? entry.file().rowCount() : 0, + entry.partitionBytes(), + bucketEnabled ? entry.bucket() : null, + bucketEnabled ? entry.totalBuckets() : null); + } + builder.endBlock(); + } + return builder.serialize(size, records); + } + } + + /** Selects blocks using row-ID coverage. A null query retains every block after validation. */ + public static Selection select( + byte[] data, ManifestFileMeta manifest, @Nullable RowRangeIndex query) + throws IOException { + return select(data, manifest, query, null, null); + } + + /** Validates framing and tests row ID, partition, then bucket coverage. */ + public static Selection select( + byte[] data, + ManifestFileMeta manifest, + @Nullable RowRangeIndex query, + @Nullable PartitionPredicate partitionFilter, + @Nullable RowType partitionType) + throws IOException { + return select(data, manifest, query, partitionFilter, partitionType, null); + } + + /** + * Selects blocks using independent filters. The bucket predicate must conservatively test only + * the bucket and recorded total bucket count; omit it if filtering requires an entry partition. + */ + public static Selection select( + byte[] data, + ManifestFileMeta manifest, + @Nullable RowRangeIndex query, + @Nullable PartitionPredicate partitionFilter, + @Nullable RowType partitionType, + @Nullable BiPredicate<Integer, Integer> bucketFilter) + throws IOException { + require(data.length >= MIN_HEADER_BYTES + DIGEST_BYTES); + int limit = data.length - DIGEST_BYTES; + require( + MessageDigest.isEqual( + digest(data, limit), Arrays.copyOfRange(data, limit, data.length))); + ByteBuffer in = ByteBuffer.wrap(data, 0, limit).slice(); + require(in.getInt() == MAGIC); + require(readInt(in) == FORMAT_VERSION); + long entries = Math.addExact(manifest.numAddedFiles(), manifest.numDeletedFiles()); + require(entries >= 0); + int headerLength = readInt(in); + require(headerLength >= 21 && headerLength <= in.remaining() - 2); + require(headerLength <= manifest.fileSize()); + byte[] header = new byte[headerLength]; + in.get(header); + require(header[0] == 'O' && header[1] == 'b' && header[2] == 'j' && header[3] == 1); + int partitions = readInt(in); + require(partitions <= in.remaining() / 13); + boolean[] matches = partitionFilter == null ? null : new boolean[partitions]; + Set<ByteBuffer> unique = new java.util.HashSet<>(); + for (int id = 0; id < partitions; id++) { + int length = readInt(in); + require(length >= 12 && length <= in.remaining()); + ByteBuffer encoded = in.slice(); + encoded.limit(length); + int arity = encoded.getInt(0); + require(arity >= 0 && 4L + ((arity + 71L) / 64) * 8 + arity * 8L <= length); + require(partitionType == null || arity == partitionType.getFieldCount()); + require(unique.add(encoded.asReadOnlyBuffer())); + if (partitionFilter != null) { + byte[] bytes = new byte[length]; + encoded.get(bytes); + BinaryRow partition = SerializationUtils.deserializeBinaryRow(bytes); + matches[id] = partitionFilter.test(partition); + } + in.position(in.position() + length); + } + int count = readInt(in); + require(count <= in.remaining() / MIN_BLOCK_BYTES); + long nextOffset = headerLength; + long firstRecord = 0; + List<Block> selected = new ArrayList<>(); + for (int i = 0; i < count; i++) { + require(in.remaining() >= MIN_BLOCK_BYTES); + long offset = VarLengthIntUtils.decodeLong(in); + long length = VarLengthIntUtils.decodeLong(in); + long records = VarLengthIntUtils.decodeLong(in); + require(offset == nextOffset && length > 0 && length <= manifest.fileSize() - offset); + require(records > 0 && records <= entries - firstRecord); + ByteBuffer partitionPayload = payload(in); + ByteBuffer rowPayload = payload(in); + ByteBuffer bucketPayload = payload(in); + DeltaVarintCodec.Reader ids = null; + if (partitionPayload != null) { + ids = new DeltaVarintCodec.Reader(partitionPayload, 0, partitions - 1L); + require(ids.count() > 0 && ids.count() <= records && ids.count() <= partitions); + } + long min = 0; + long max = 0; + DeltaVarintCodec.Reader endpoints = null; + if (rowPayload != null) { + require(rowPayload.remaining() >= 2 * Long.BYTES + 1); + min = rowPayload.getLong(); + max = rowPayload.getLong(); + endpoints = new DeltaVarintCodec.Reader(rowPayload, min, max); + require(endpoints.count() % 2 == 0 && endpoints.count() / 2L < records); + require(endpoints.count() != 0 || !rowPayload.hasRemaining()); + } + if (bucketPayload != null) { + ByteBuffer prefix = bucketPayload.duplicate(); + int pairs = readInt(prefix); + require(pairs > 0 && pairs <= records && 2L * pairs + 1 <= prefix.remaining()); + } + long blockFirstRecord = firstRecord; + nextOffset = offset + length; + firstRecord += records; + + if (query != null && rowPayload != null) { + if (!query.intersects(min, max)) { + continue; + } + int rangeCount = endpoints.count() / 2 + 1; + boolean rowHit = rangeCount == 1; + long start = min; + for (int range = 0; !rowHit && range < rangeCount; range++) { + long end = range + 1 == rangeCount ? max : endpoints.next(); + require(end >= start); + rowHit = query.intersects(start, end); + if (!rowHit && range + 1 < rangeCount) { + start = endpoints.next(); + require(start > end); + } + require(endpoints.hasNext() || !rowPayload.hasRemaining()); + } + if (!rowHit) { + continue; + } + } + + if (partitionFilter != null && partitionPayload != null) { + boolean partitionHit = false; + long previous = -1; + while (!partitionHit && ids.hasNext()) { + long id = ids.next(); + require(id > previous); + require(ids.hasNext() || !partitionPayload.hasRemaining()); + previous = id; + partitionHit = matches[(int) id]; + } + if (!partitionHit) { + continue; + } + } + + if (bucketFilter != null && bucketPayload != null) { + // Locate the second count-prefixed sequence without allocating value arrays. + ByteBuffer totalsData = bucketPayload.duplicate(); + DeltaVarintCodec.Reader directory = + new DeltaVarintCodec.Reader(totalsData, 0, Integer.MAX_VALUE); + while (directory.hasNext()) { + directory.next(); + } + bucketPayload.limit(totalsData.position()); + DeltaVarintCodec.Reader buckets = + new DeltaVarintCodec.Reader(bucketPayload, 0, Integer.MAX_VALUE); + DeltaVarintCodec.Reader totals = + new DeltaVarintCodec.Reader(totalsData, 0, Integer.MAX_VALUE, true); + require(totals.count() == buckets.count()); + boolean bucketHit = false; + int previousBucket = -1; + int previousTotal = -1; + while (!bucketHit && buckets.hasNext()) { + int bucket = (int) buckets.next(); + int totalBuckets = (int) totals.next(); + require(totalBuckets > bucket); + require( + bucket > previousBucket + || (bucket == previousBucket && totalBuckets > previousTotal)); + require( + buckets.hasNext() + || (!bucketPayload.hasRemaining() + && !totalsData.hasRemaining())); + previousBucket = bucket; + previousTotal = totalBuckets; + bucketHit = bucketFilter.test(bucket, totalBuckets); + } + if (!bucketHit) { + continue; + } + } + selected.add(new Block(offset, length, blockFirstRecord, records)); + } + require(!in.hasRemaining() && nextOffset == manifest.fileSize() && firstRecord == entries); + return new Selection(header, selected); + } + + /** Reads framing without expanding the compressed contents. */ + @Nullable + private static ByteBuffer payload(ByteBuffer in) throws IOException { + require(in.hasRemaining()); + int encoding = Byte.toUnsignedInt(in.get()); + if (encoding == 0) { + return null; + } + int length = readInt(in); + require(length <= in.remaining()); + ByteBuffer result = in.slice(); + result.limit(length); + in.position(in.position() + length); + if (encoding != 1) { + return null; + } + return result; + } + + private static int readInt(ByteBuffer in) throws IOException { + long value = VarLengthIntUtils.decodeLong(in); Review Comment: OK -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
