Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,15 +30,39 @@
import java.util.Objects;

/**
* Statistics of a partition, fields inside may be negative, indicating that some data has been
* removed.
* Statistics of a partition.
*
* <p>The numeric fields are read on two planes, and a negative value means a different thing on
* each. Which plane an instance belongs to follows from where it came from, never from the value:
*
* <ul>
* <li><b>Delta plane</b> — what a commit changed. A negative value is a decrement, and the server
* adds it to what it already holds. This is what a table snapshot commit reports.
* <li><b>Observation plane</b> — what a partition currently holds, as returned by {@code
* listPartitions}. A negative value ({@link #UNKNOWN}) means nobody ever reported that field,
* and {@code 0} means an exact zero. The two are not interchangeable: a consumer that treats
* unknown as zero plans against an empty partition that may hold a billion rows.
* </ul>
*
* <p>Unknown is per field, not per partition: a reporter that only knows the file count leaves the
* record count {@link #UNKNOWN} and fills the rest. Use {@link #isKnown(long)} rather than
* comparing against {@code -1}; any negative value on the observation plane is unknown.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@Public
public class PartitionStatistics implements Serializable {

private static final long serialVersionUID = 1L;

/**
* Canonical encoding of "this field was never reported" on the observation plane. Any negative
* value carries the same meaning; this is the one to write.
*/
public static final long UNKNOWN = -1L;

/** Format tables have no buckets, so their bucket count is always unknown. */
public static final int UNKNOWN_TOTAL_BUCKETS = -1;

public static final String FIELD_SPEC = "spec";
public static final String FIELD_RECORD_COUNT = "recordCount";
public static final String FIELD_FILE_SIZE_IN_BYTES = "fileSizeInBytes";
Expand DownExpand Up@@ -82,6 +106,20 @@ public PartitionStatistics(
this.totalBuckets = totalBuckets;
}

/** Statistics of a partition nobody ever reported on: every field {@link #UNKNOWN}. */
public static PartitionStatistics unknown(Map<String, String> spec) {
return new PartitionStatistics(
spec, UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN_TOTAL_BUCKETS);
}

/**
* Whether an observation-plane field carries a real measurement. Never apply this to a
* delta-plane value, where a negative number is a decrement rather than a missing measurement.
*/
public static boolean isKnown(long value) {
return value >= 0;
}

@JsonGetter(FIELD_SPEC)
public Map<String, String> spec() {
return spec;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,9 @@

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/** Test for {@link PartitionStatistics}. */
Expand All@@ -41,4 +44,33 @@ void testLegacyPartitionStatisticsDeserialization() {
assertThat(stats.lastFileCreationTime()).isEqualTo(123456789L);
assertThat(stats.totalBuckets()).isEqualTo(0);
}

@Test
void testZeroIsAKnownMeasurement() {
// The boundary the whole observation-plane contract rests on: an empty partition was
// measured, and a consumer that reads its zero as "nobody looked" plans against the wrong
// table.
assertThat(PartitionStatistics.isKnown(0L)).isTrue();
assertThat(PartitionStatistics.isKnown(1L)).isTrue();
assertThat(PartitionStatistics.isKnown(Long.MAX_VALUE)).isTrue();

assertThat(PartitionStatistics.isKnown(PartitionStatistics.UNKNOWN)).isFalse();
// Unknown is any negative value, not only the canonical -1.
assertThat(PartitionStatistics.isKnown(-2L)).isFalse();
assertThat(PartitionStatistics.isKnown(Long.MIN_VALUE)).isFalse();
}

@Test
void testUnknownLeavesEveryFieldUnknown() {
Map<String, String> spec = Collections.singletonMap("pt", "1");

PartitionStatistics stats = PartitionStatistics.unknown(spec);

assertThat(stats.spec()).isEqualTo(spec);
assertThat(PartitionStatistics.isKnown(stats.recordCount())).isFalse();
assertThat(PartitionStatistics.isKnown(stats.fileSizeInBytes())).isFalse();
assertThat(PartitionStatistics.isKnown(stats.fileCount())).isFalse();
assertThat(PartitionStatistics.isKnown(stats.lastFileCreationTime())).isFalse();
assertThat(PartitionStatistics.isKnown(stats.totalBuckets())).isFalse();
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
import org.apache.paimon.manifest.PartitionEntry;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.partition.PartitionPredicate.MultiplePartitionPredicate;
import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.table.FormatTable;
import org.apache.paimon.table.source.Split;
Expand DownExpand Up@@ -125,7 +126,16 @@ List<PartitionEntry> listPartitionEntries() {
List<PartitionEntry> partitionEntries = new ArrayList<>();
for (Pair<LinkedHashMap<String, String>, Path> partition2Path : partition2Paths) {
BinaryRow row = toPartitionRow(partition2Path.getKey());
partitionEntries.add(new PartitionEntry(row, -1L, -1L, -1L, -1L, -1));
// Discovering partitions from directories measures nothing about what is inside them,
// so every statistic is unknown rather than zero.
partitionEntries.add(
new PartitionEntry(
row,
PartitionStatistics.UNKNOWN,
PartitionStatistics.UNKNOWN,
PartitionStatistics.UNKNOWN,
PartitionStatistics.UNKNOWN,
PartitionStatistics.UNKNOWN_TOTAL_BUCKETS));
}
return partitionEntries;
}
Expand Down
Loading