Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
67 changes: 54 additions & 13 deletions core/src/main/java/org/apache/iceberg/ContentFileParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.types.Types;
import org.apache.iceberg.util.JsonUtil;

public class ContentFileParser {
Expand Down Expand Up @@ -89,7 +90,7 @@ public static void toJson(ContentFile<?> contentFile, PartitionSpec spec, JsonGe

if (contentFile.partition() != null) {
generator.writeFieldName(PARTITION);
SingleValueParser.toJson(spec.partitionType(), contentFile.partition(), generator);
partitionToJson(spec.partitionType(), contentFile.partition(), generator);
}

generator.writeNumberField(FILE_SIZE, contentFile.fileSizeInBytes());
Expand Down Expand Up @@ -152,18 +153,7 @@ public static ContentFile<?> fromJson(JsonNode jsonNode, Map<Integer, PartitionS

PartitionData partitionData = null;
if (jsonNode.has(PARTITION)) {
partitionData = new PartitionData(spec.partitionType());
StructLike structLike =
(StructLike) SingleValueParser.fromJson(spec.partitionType(), jsonNode.get(PARTITION));
Preconditions.checkState(
partitionData.size() == structLike.size(),
"Invalid partition data size: expected = %s, actual = %s",
partitionData.size(),
structLike.size());
for (int pos = 0; pos < partitionData.size(); ++pos) {
Class<?> javaClass = spec.partitionType().fields().get(pos).type().typeId().javaClass();
partitionData.set(pos, structLike.get(pos, javaClass));
}
partitionData = partitionFromJson(spec.partitionType(), jsonNode.get(PARTITION));
}

long fileSizeInBytes = JsonUtil.getLong(FILE_SIZE, jsonNode);
Expand Down Expand Up @@ -301,4 +291,55 @@ private static Metrics metricsFromJson(JsonNode jsonNode) {
lowerBounds,
upperBounds);
}

private static void partitionToJson(
Types.StructType partitionType, StructLike partitionData, JsonGenerator generator)
throws IOException {
generator.writeStartArray();
List<Types.NestedField> fields = partitionType.fields();
for (int pos = 0; pos < fields.size(); ++pos) {
Types.NestedField field = fields.get(pos);
Object partitionValue = partitionData.get(pos, Object.class);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We can use field.type().javaClass() here, instead of `Object.class).

SingleValueParser.toJson(field.type(), partitionValue, generator);
}
generator.writeEndArray();
}

private static PartitionData partitionFromJson(
Types.StructType partitionType, JsonNode partitionNode) {
List<Types.NestedField> fields = partitionType.fields();
PartitionData partitionData = new PartitionData(partitionType);

if (partitionNode.isArray()) {
Preconditions.checkArgument(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a comment like this

In 1.11 and after, partition data is serialized as an array with just partition field values.

partitionNode.size() == fields.size(),
"Invalid partition data size: expected = %s, actual = %s",
fields.size(),
partitionNode.size());

for (int pos = 0; pos < fields.size(); ++pos) {
Types.NestedField field = fields.get(pos);
Object partitionValue = SingleValueParser.fromJson(field.type(), partitionNode.get(pos));
partitionData.set(pos, partitionValue);
}
} else if (partitionNode.isObject()) {
// Handle legacy partition object serialization
Comment thread
geruh marked this conversation as resolved.
Outdated
for (int pos = 0; pos < fields.size(); ++pos) {
Types.NestedField field = fields.get(pos);
String fieldId = String.valueOf(field.fieldId());
if (partitionNode.has(fieldId)) {
Object partitionValue =
SingleValueParser.fromJson(field.type(), partitionNode.get(fieldId));
partitionData.set(pos, partitionValue);
}
}
Comment thread
geruh marked this conversation as resolved.
} else {
throw new IllegalArgumentException(
String.format(
"Invalid partition data for content file: expected array or object (%s)",
partitionNode));
}

return partitionData;
}
}
101 changes: 89 additions & 12 deletions core/src/test/java/org/apache/iceberg/TestContentFileParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public void testDataFile(PartitionSpec spec, DataFile dataFile, String expectedJ
assertThat(jsonStr).isEqualTo(expectedJson);
JsonNode jsonNode = JsonUtil.mapper().readTree(jsonStr);
ContentFile<?> deserializedContentFile =
ContentFileParser.fromJson(jsonNode, Map.of(TestBase.SPEC.specId(), spec));
ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec));
assertThat(deserializedContentFile).isInstanceOf(DataFile.class);
assertContentFileEquals(dataFile, deserializedContentFile, spec);
}
Expand All @@ -85,11 +85,88 @@ public void testDeleteFile(PartitionSpec spec, DeleteFile deleteFile, String exp
assertThat(jsonStr).isEqualTo(expectedJson);
JsonNode jsonNode = JsonUtil.mapper().readTree(jsonStr);
ContentFile<?> deserializedContentFile =
ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), TestBase.SPEC));
ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec));
assertThat(deserializedContentFile).isInstanceOf(DeleteFile.class);
assertContentFileEquals(deleteFile, deserializedContentFile, spec);
}

@Test
public void testPartitionJsonArrayWrongSize() throws Exception {
PartitionSpec spec = PartitionSpec.builderFor(TestBase.SCHEMA).identity("data").build();
String jsonStr =
"{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":[],\"file-size-in-bytes\":10,"
+ "\"record-count\":1}";

JsonNode jsonNode = JsonUtil.mapper().readTree(jsonStr);

assertThatThrownBy(() -> ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid partition data size");
}

@Test
public void testPartitionJsonInvalidType() throws Exception {
PartitionSpec spec = PartitionSpec.builderFor(TestBase.SCHEMA).identity("data").build();
String jsonStr =
"{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":\"invalid\",\"file-size-in-bytes\":10,"
+ "\"record-count\":1}";

JsonNode jsonNode = JsonUtil.mapper().readTree(jsonStr);

assertThatThrownBy(() -> ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("expected array or object");
}

@Test
public void testParsesFieldIdPartitionMap() throws Exception {
PartitionSpec spec = PartitionSpec.builderFor(TestBase.SCHEMA).identity("data").build();
String legacyJson =
"{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":\"foo\"},\"file-size-in-bytes\":10,"
+ "\"record-count\":1}";

JsonNode jsonNode = JsonUtil.mapper().readTree(legacyJson);
ContentFile<?> deserializedContentFile =
ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec));

assertThat(deserializedContentFile).isInstanceOf(DataFile.class);
assertThat(deserializedContentFile.partition().get(0, String.class)).isEqualTo("foo");
}

@Test
public void testPartitionArrayRespectsSpecOrder() throws Exception {
PartitionSpec spec =
PartitionSpec.builderFor(TestBase.SCHEMA).identity("id").identity("data").build();

PartitionData partitionData = new PartitionData(spec.partitionType());
partitionData.set(0, 4);
partitionData.set(1, "foo");

DataFile dataFile =
DataFiles.builder(spec)
.withPath("/path/to/data.parquet")
.withFileSizeInBytes(10)
.withRecordCount(1)
.withPartition(partitionData)
.build();

String jsonStr = ContentFileParser.toJson(dataFile, spec);

// Verify partition values are serialized as array in correct order
assertThat(jsonStr).contains("\"partition\":[4,\"foo\"]");

JsonNode jsonNode = JsonUtil.mapper().readTree(jsonStr);
ContentFile<?> deserializedContentFile =
ContentFileParser.fromJson(jsonNode, Map.of(spec.specId(), spec));

assertThat(deserializedContentFile).isInstanceOf(DataFile.class);
assertThat(deserializedContentFile.partition().get(0, Integer.class)).isEqualTo(4);
assertThat(deserializedContentFile.partition().get(1, String.class)).isEqualTo("foo");
}

private static Stream<Arguments> provideSpecAndDataFile() {
return Stream.of(
Arguments.of(
Expand Down Expand Up @@ -128,17 +205,17 @@ private static DataFile dataFileWithRequiredOnly(PartitionSpec spec) {
private static String dataFileJsonWithRequiredOnly(PartitionSpec spec) {
if (spec.isUnpartitioned()) {
return "{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-a.parquet\",\"file-format\":\"PARQUET\","
+ "\"partition\":{},\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0}";
+ "\"partition\":[],\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0}";
} else {
return "{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-a.parquet\",\"file-format\":\"PARQUET\","
+ "\"partition\":{\"1000\":1},\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0}";
+ "\"partition\":[1],\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0}";
}
}

private static String dataFileJsonWithAllOptional(PartitionSpec spec) {
if (spec.isUnpartitioned()) {
return "{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-with-stats.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{},\"file-size-in-bytes\":350,\"record-count\":10,"
+ "\"file-format\":\"PARQUET\",\"partition\":[],\"file-size-in-bytes\":350,\"record-count\":10,"
+ "\"column-sizes\":{\"keys\":[3,4],\"values\":[100,200]},"
+ "\"value-counts\":{\"keys\":[3,4],\"values\":[90,180]},"
+ "\"null-value-counts\":{\"keys\":[3,4],\"values\":[10,20]},"
Expand All @@ -149,7 +226,7 @@ private static String dataFileJsonWithAllOptional(PartitionSpec spec) {
+ "\"split-offsets\":[128,256],\"sort-order-id\":1}";
} else {
return "{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-with-stats.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":1},\"file-size-in-bytes\":350,\"record-count\":10,"
+ "\"file-format\":\"PARQUET\",\"partition\":[1],\"file-size-in-bytes\":350,\"record-count\":10,"
+ "\"column-sizes\":{\"keys\":[3,4],\"values\":[100,200]},"
+ "\"value-counts\":{\"keys\":[3,4],\"values\":[90,180]},"
+ "\"null-value-counts\":{\"keys\":[3,4],\"values\":[10,20]},"
Expand Down Expand Up @@ -245,7 +322,7 @@ private static DeleteFile deleteFileWithDataRef(PartitionSpec spec) {

private static String deleteFileWithDataRefJson() {
return "{\"spec-id\":0,\"content\":\"POSITION_DELETES\",\"file-path\":\"/path/to/delete.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":4},\"file-size-in-bytes\":1234,"
+ "\"file-format\":\"PARQUET\",\"partition\":[4],\"file-size-in-bytes\":1234,"
+ "\"record-count\":10,\"referenced-data-file\":\"/path/to/data/file.parquet\"}";
}

Expand All @@ -271,7 +348,7 @@ private static DeleteFile dv(PartitionSpec spec) {

private static String dvJson() {
return "{\"spec-id\":0,\"content\":\"POSITION_DELETES\",\"file-path\":\"/path/to/delete.puffin\","
+ "\"file-format\":\"PUFFIN\",\"partition\":{\"1000\":4},\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"file-format\":\"PUFFIN\",\"partition\":[4],\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"referenced-data-file\":\"/path/to/data/file.parquet\",\"content-offset\":4,\"content-size-in-bytes\":40}";
}

Expand Down Expand Up @@ -344,17 +421,17 @@ private static DeleteFile deleteFileWithAllOptional(PartitionSpec spec) {
private static String deleteFileJsonWithRequiredOnly(PartitionSpec spec) {
if (spec.isUnpartitioned()) {
return "{\"spec-id\":0,\"content\":\"POSITION_DELETES\",\"file-path\":\"/path/to/delete-a.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{},\"file-size-in-bytes\":1234,\"record-count\":9}";
+ "\"file-format\":\"PARQUET\",\"partition\":[],\"file-size-in-bytes\":1234,\"record-count\":9}";
} else {
return "{\"spec-id\":0,\"content\":\"POSITION_DELETES\",\"file-path\":\"/path/to/delete-a.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":9},\"file-size-in-bytes\":1234,\"record-count\":9}";
+ "\"file-format\":\"PARQUET\",\"partition\":[9],\"file-size-in-bytes\":1234,\"record-count\":9}";
}
}

private static String deleteFileJsonWithAllOptional(PartitionSpec spec) {
if (spec.isUnpartitioned()) {
return "{\"spec-id\":0,\"content\":\"EQUALITY_DELETES\",\"file-path\":\"/path/to/delete-with-stats.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{},\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"file-format\":\"PARQUET\",\"partition\":[],\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"column-sizes\":{\"keys\":[3,4],\"values\":[100,200]},"
+ "\"value-counts\":{\"keys\":[3,4],\"values\":[90,180]},"
+ "\"null-value-counts\":{\"keys\":[3,4],\"values\":[10,20]},"
Expand All @@ -365,7 +442,7 @@ private static String deleteFileJsonWithAllOptional(PartitionSpec spec) {
+ "\"split-offsets\":[128],\"equality-ids\":[3],\"sort-order-id\":1}";
} else {
return "{\"spec-id\":0,\"content\":\"EQUALITY_DELETES\",\"file-path\":\"/path/to/delete-with-stats.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":9},\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"file-format\":\"PARQUET\",\"partition\":[9],\"file-size-in-bytes\":1234,\"record-count\":10,"
+ "\"column-sizes\":{\"keys\":[3,4],\"values\":[100,200]},"
+ "\"value-counts\":{\"keys\":[3,4],\"values\":[90,180]},"
+ "\"null-value-counts\":{\"keys\":[3,4],\"values\":[10,20]},"
Expand Down
21 changes: 19 additions & 2 deletions core/src/test/java/org/apache/iceberg/TestDataTaskParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public void missingFields() throws Exception {
+ "\"value\":\"string\",\"value-required\":true}}]},"
+ "\"metadata-file\":{\"spec-id\":0,\"content\":\"DATA\","
+ "\"file-path\":\"/tmp/metadata2.json\","
+ "\"file-format\":\"METADATA\",\"partition\":{},"
+ "\"file-format\":\"METADATA\",\"partition\":[],"
+ "\"file-size-in-bytes\":0,\"record-count\":2,\"sort-order-id\":0}"
+ "}";
JsonNode missingTableRowsNode = mapper.reader().readTree(missingTableRowsStr);
Expand All @@ -164,6 +164,23 @@ public void roundTripSerde() {
assertDataTaskEquals(dataTask, deserializedTask);
}

@Test
public void testDataTaskParsesFieldIdPartitionMap() {
String jsonStr =
"{\"task-type\":\"data-task\","
+ "\"schema\":{\"type\":\"struct\",\"schema-id\":0,"
+ "\"fields\":[{\"id\":1,\"name\":\"committed_at\",\"required\":true,\"type\":\"timestamptz\"}]},"
+ "\"projection\":{\"type\":\"struct\",\"schema-id\":0,"
+ "\"fields\":[{\"id\":1,\"name\":\"committed_at\",\"required\":true,\"type\":\"timestamptz\"}]},"
+ "\"metadata-file\":{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/tmp/metadata.json\","
+ "\"file-format\":\"METADATA\",\"partition\":{},\"file-size-in-bytes\":0,\"record-count\":1,\"sort-order-id\":0},"
+ "\"rows\":[{\"1\":\"2009-02-13T23:31:30+00:00\"}]}";

StaticDataTask deserializedTask = (StaticDataTask) ScanTaskParser.fromJson(jsonStr, true);

assertThat(deserializedTask.metadataFile().partition().size()).isEqualTo(0);
}

private DataTask createDataTask() {
Map<String, String> summary1 =
ImmutableMap.of(
Expand Down Expand Up @@ -248,7 +265,7 @@ private String snapshotsDataTaskJson() {
+ "\"value\":\"string\",\"value-required\":true}}]},"
+ "\"metadata-file\":{\"spec-id\":0,\"content\":\"DATA\","
+ "\"file-path\":\"/tmp/metadata2.json\","
+ "\"file-format\":\"METADATA\",\"partition\":{},"
+ "\"file-format\":\"METADATA\",\"partition\":[],"
+ "\"file-size-in-bytes\":0,\"record-count\":2,\"sort-order-id\":0},"
+ "\"rows\":[{\"1\":\"2009-02-13T23:31:30+00:00\",\"2\":1,\"4\":\"append\","
+ "\"5\":\"file:/tmp/manifest1.avro\","
Expand Down
38 changes: 35 additions & 3 deletions core/src/test/java/org/apache/iceberg/TestFileScanTaskParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ public void testScanTaskParserWithoutTaskTypeField(boolean caseSensitive) {
assertFileScanTaskEquals(fileScanTask, deserializedTask, spec, caseSensitive);
}

@Test
public void testFileScanTaskParsesFieldIdPartitionMap() {
boolean caseSensitive = true;
PartitionSpec spec = TestBase.SPEC;
FileScanTask expected = createFileScanTask(spec, caseSensitive);

FileScanTask deserializedTask =
ScanTaskParser.fromJson(fileScanTaskFieldIdPartitionMapJson(), caseSensitive);

assertFileScanTaskEquals(expected, deserializedTask, spec, caseSensitive);
}

private FileScanTask createFileScanTask(PartitionSpec spec, boolean caseSensitive) {
ResidualEvaluator residualEvaluator;
if (spec.isUnpartitioned()) {
Expand All @@ -85,19 +97,39 @@ private String fileScanTaskJsonWithoutTaskType() {
+ "\"spec\":{\"spec-id\":0,\"fields\":[{\"name\":\"data_bucket\","
+ "\"transform\":\"bucket[16]\",\"source-id\":4,\"field-id\":1000}]},"
+ "\"data-file\":{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-a.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":0},"
+ "\"file-format\":\"PARQUET\",\"partition\":[0],"
+ "\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0},"
+ "\"start\":0,\"length\":10,"
+ "\"delete-files\":[{\"spec-id\":0,\"content\":\"POSITION_DELETES\","
+ "\"file-path\":\"/path/to/data-a-deletes.parquet\",\"file-format\":\"PARQUET\","
+ "\"partition\":{\"1000\":0},\"file-size-in-bytes\":10,\"record-count\":1},"
+ "\"partition\":[0],\"file-size-in-bytes\":10,\"record-count\":1},"
+ "{\"spec-id\":0,\"content\":\"EQUALITY_DELETES\",\"file-path\":\"/path/to/data-a2-deletes.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":{\"1000\":0},\"file-size-in-bytes\":10,"
+ "\"file-format\":\"PARQUET\",\"partition\":[0],\"file-size-in-bytes\":10,"
+ "\"record-count\":1,\"equality-ids\":[1],\"sort-order-id\":0}],"
+ "\"residual-filter\":{\"type\":\"eq\",\"term\":\"id\",\"value\":1}}";
}

private String fileScanTaskJson() {
return "{\"task-type\":\"file-scan-task\","
+ "\"schema\":{\"type\":\"struct\",\"schema-id\":0,\"fields\":["
+ "{\"id\":3,\"name\":\"id\",\"required\":true,\"type\":\"int\"},"
+ "{\"id\":4,\"name\":\"data\",\"required\":true,\"type\":\"string\"}]},"
+ "\"spec\":{\"spec-id\":0,\"fields\":[{\"name\":\"data_bucket\","
+ "\"transform\":\"bucket[16]\",\"source-id\":4,\"field-id\":1000}]},"
+ "\"data-file\":{\"spec-id\":0,\"content\":\"DATA\",\"file-path\":\"/path/to/data-a.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":[0],"
+ "\"file-size-in-bytes\":10,\"record-count\":1,\"sort-order-id\":0},"
+ "\"start\":0,\"length\":10,"
+ "\"delete-files\":[{\"spec-id\":0,\"content\":\"POSITION_DELETES\","
+ "\"file-path\":\"/path/to/data-a-deletes.parquet\",\"file-format\":\"PARQUET\","
+ "\"partition\":[0],\"file-size-in-bytes\":10,\"record-count\":1},"
+ "{\"spec-id\":0,\"content\":\"EQUALITY_DELETES\",\"file-path\":\"/path/to/data-a2-deletes.parquet\","
+ "\"file-format\":\"PARQUET\",\"partition\":[0],\"file-size-in-bytes\":10,"
+ "\"record-count\":1,\"equality-ids\":[1],\"sort-order-id\":0}],"
+ "\"residual-filter\":{\"type\":\"eq\",\"term\":\"id\",\"value\":1}}";
}

private String fileScanTaskFieldIdPartitionMapJson() {
return "{\"task-type\":\"file-scan-task\","
+ "\"schema\":{\"type\":\"struct\",\"schema-id\":0,\"fields\":["
+ "{\"id\":3,\"name\":\"id\",\"required\":true,\"type\":\"int\"},"
Expand Down
Loading