Question
Description
An audit of PyIceberg's production Avro definitions and schema-conversion code against the current Iceberg specification found several conformance and reader-compatibility gaps.
This is an umbrella issue. Individual findings can be addressed in separate PRs or issues.
Scope
The audit covered:
- Manifest entry schemas for format versions 1–3
data_file schemas for format versions 1–3
- Manifest-list (
manifest_file) schemas
- Partition field summaries
- Manifest Avro key-value metadata
- Position-delete helper schema
- Iceberg-to-Avro and Avro-to-Iceberg schema conversion
PyIceberg currently defines TableVersion = Literal[1, 2, 3]; format v4 was not treated as an implemented version.
Findings
1. equality_ids is encoded as list<long> instead of list<int>
Tracked separately by apache#3840.
Locations:
pyiceberg/manifest.py:298
pyiceberg/manifest.py:393
Current definition:
ListType(
element_id=136,
element_type=LongType(),
element_required=True,
)
The Iceberg specification requires:
135 equality_ids: list<136: int>
This affects the v2 and v3 schemas. V1 is not affected because it does not contain equality_ids.
The incorrect type is embedded in every v2 manifest written by PyIceberg, even when equality_ids is null. Avro permits int to long promotion but not long to int, so conforming readers may reject these manifests.
A fix must write int while retaining compatibility with historical PyIceberg manifests containing long. The read and write schemas may need to be separated.
2. V2 manifest metadata omits required schema-id
Location:
pyiceberg/manifest.py:1117
The V2 writer includes:
schema
partition-spec
partition-spec-id
format-version
content
It does not include the separately required schema-id Avro metadata property.
Expected metadata:
"schema-id": str(self._schema.schema_id)
The schema ID embedded inside the schema JSON does not replace this property.
3. Manifest readers discard conforming V2 and V3 fields
Locations:
pyiceberg/manifest.py:874
pyiceberg/manifest.py:997
Both readers always use DEFAULT_READ_VERSION, currently V2:
Consequences:
-
DATA_FILE_TYPE[2] omits optional V2 field:
143 referenced_data_file: string
Omitting it from a writer schema is allowed, but the read projection should retain it when another implementation writes it.
-
V3 manifest entries are projected through the V2 schema, discarding:
142 first_row_id
143 referenced_data_file
144 content_offset
145 content_size_in_bytes
-
V3 manifest lists are projected through the V2 schema, discarding:
These fields are needed for V3 row lineage and deletion-vector metadata.
4. Position-delete pos is defined as int instead of long
Also identified in apache#3618.
Location:
pyiceberg/manifest.py:778
Current definition:
POSITIONAL_DELETE_SCHEMA = Schema(
NestedField(2147483546, "file_path", StringType()),
NestedField(2147483545, "pos", IntegerType()),
)
The specification requires field 2147483545 pos to be long.
This schema is currently used only for metrics evaluation and is not written as an Avro file. It should nevertheless be corrected before position-delete writing is implemented.
5. Optional list elements are not represented as null unions
Location:
pyiceberg/utils/schema_conversion.py:553
The Iceberg Avro requirements state that optional array elements must be wrapped in an Avro union with null.
For:
ListType(
element_id=2,
element_type=IntegerType(),
element_required=False,
)
the converter currently emits:
{
"type": "array",
"element-id": 2,
"items": "int"
}
It should emit:
{
"type": "array",
"element-id": 2,
"items": ["null", "int"]
}
6. Optional map values are not represented as null unions
Location:
pyiceberg/utils/schema_conversion.py:559
For both native Avro maps and logical-map array representations, optional map values must use a union with null.
The logical-map path currently emits the value field directly:
{"name": "value", "type": value_result, "field-id": value_id}
It does not account for MapType.value_required=False.
The built-in manifest maps currently have required values, so findings 5 and 6 do not explain the existing manifest interoperability problem.
7. Avro-to-Iceberg conversion does not recognize timestamp-nanos
Location:
pyiceberg/utils/schema_conversion.py:373
The write converter emits the correct V3 annotations:
{
"type": "long",
"logicalType": "timestamp-nanos",
"adjust-to-utc": false
}
and:
{
"type": "long",
"logicalType": "timestamp-nanos",
"adjust-to-utc": true
}
However, _convert_logical_type only handles timestamp-micros. Parsing an Avro schema containing timestamp-nanos raises an unknown logical-type error.
8. Optional UnknownType produces an invalid duplicate union
Locations:
pyiceberg/utils/schema_conversion.py:526
pyiceberg/utils/schema_conversion.py:639
UnknownType converts to "null". An optional field then wraps it again:
Avro unions cannot contain duplicate branches. The Iceberg specification allows an unknown value to be represented as null or for the field to be omitted.
Definitions that conform
The following fixed definitions matched the Iceberg V1–V3 field IDs, types, and requiredness rules:
- V1
data_file
- V1–V3
manifest_entry, except for the nested equality_ids issue
- V1–V3
manifest_file
field_summary
- Partition field IDs copied into the manifest partition struct
- Avro primitive mappings for boolean, int, long, float, double, decimal, date, time, microsecond timestamps, string, UUID, fixed, binary, geometry, and geography
- Field-ID placement for struct fields, list elements, and logical-map key/value fields
Deprecated optional fields omitted by PyIceberg, such as file_ordinal, sort_columns, and distinct_counts, are allowed to be omitted and are not conformance failures.
Support gaps noted during the audit
These are broader feature gaps rather than incorrect existing definitions:
- Manifest and manifest-list writers reject format version 3 despite V3 schema constants being defined.
- Format version 4 is not represented by
TableVersion.
- V3 Variant Avro encoding is not implemented.
These may be better tracked separately from the concrete conformance defects above.
References
Question
Description
An audit of PyIceberg's production Avro definitions and schema-conversion code against the current Iceberg specification found several conformance and reader-compatibility gaps.
This is an umbrella issue. Individual findings can be addressed in separate PRs or issues.
Scope
The audit covered:
data_fileschemas for format versions 1–3manifest_file) schemasPyIceberg currently defines
TableVersion = Literal[1, 2, 3]; format v4 was not treated as an implemented version.Findings
1.
equality_idsis encoded aslist<long>instead oflist<int>Tracked separately by apache#3840.
Locations:
pyiceberg/manifest.py:298pyiceberg/manifest.py:393Current definition:
The Iceberg specification requires:
This affects the v2 and v3 schemas. V1 is not affected because it does not contain
equality_ids.The incorrect type is embedded in every v2 manifest written by PyIceberg, even when
equality_idsis null. Avro permitsinttolongpromotion but notlongtoint, so conforming readers may reject these manifests.A fix must write
intwhile retaining compatibility with historical PyIceberg manifests containinglong. The read and write schemas may need to be separated.list<int>list<long>manifests2. V2 manifest metadata omits required
schema-idLocation:
pyiceberg/manifest.py:1117The V2 writer includes:
schemapartition-specpartition-spec-idformat-versioncontentIt does not include the separately required
schema-idAvro metadata property.Expected metadata:
The schema ID embedded inside the
schemaJSON does not replace this property.schema-idfor V2 manifests3. Manifest readers discard conforming V2 and V3 fields
Locations:
pyiceberg/manifest.py:874pyiceberg/manifest.py:997Both readers always use
DEFAULT_READ_VERSION, currently V2:Consequences:
DATA_FILE_TYPE[2]omits optional V2 field:Omitting it from a writer schema is allowed, but the read projection should retain it when another implementation writes it.
V3 manifest entries are projected through the V2 schema, discarding:
142 first_row_id143 referenced_data_file144 content_offset145 content_size_in_bytesV3 manifest lists are projected through the V2 schema, discarding:
520 first_row_idThese fields are needed for V3 row lineage and deletion-vector metadata.
referenced_data_filewhen reading conforming V2 manifests4. Position-delete
posis defined asintinstead oflongAlso identified in apache#3618.
Location:
pyiceberg/manifest.py:778Current definition:
The specification requires field
2147483545 posto belong.This schema is currently used only for metrics evaluation and is not written as an Avro file. It should nevertheless be corrected before position-delete writing is implemented.
postoLongType2**31 - 15. Optional list elements are not represented as null unions
Location:
pyiceberg/utils/schema_conversion.py:553The Iceberg Avro requirements state that optional array elements must be wrapped in an Avro union with
null.For:
the converter currently emits:
{ "type": "array", "element-id": 2, "items": "int" }It should emit:
{ "type": "array", "element-id": 2, "items": ["null", "int"] }ListType.element_required6. Optional map values are not represented as null unions
Location:
pyiceberg/utils/schema_conversion.py:559For both native Avro maps and logical-map array representations, optional map values must use a union with
null.The logical-map path currently emits the value field directly:
{"name": "value", "type": value_result, "field-id": value_id}It does not account for
MapType.value_required=False.MapType.value_requiredThe built-in manifest maps currently have required values, so findings 5 and 6 do not explain the existing manifest interoperability problem.
7. Avro-to-Iceberg conversion does not recognize
timestamp-nanosLocation:
pyiceberg/utils/schema_conversion.py:373The write converter emits the correct V3 annotations:
{ "type": "long", "logicalType": "timestamp-nanos", "adjust-to-utc": false }and:
{ "type": "long", "logicalType": "timestamp-nanos", "adjust-to-utc": true }However,
_convert_logical_typeonly handlestimestamp-micros. Parsing an Avro schema containingtimestamp-nanosraises an unknown logical-type error.timestamp-nanoswithadjust-to-utc=falsetoTimestampNanoTypetimestamp-nanoswithadjust-to-utc=truetoTimestamptzNanoType8. Optional
UnknownTypeproduces an invalid duplicate unionLocations:
pyiceberg/utils/schema_conversion.py:526pyiceberg/utils/schema_conversion.py:639UnknownTypeconverts to"null". An optional field then wraps it again:Avro unions cannot contain duplicate branches. The Iceberg specification allows an unknown value to be represented as
nullor for the field to be omitted."null"schema forUnknownTypeDefinitions that conform
The following fixed definitions matched the Iceberg V1–V3 field IDs, types, and requiredness rules:
data_filemanifest_entry, except for the nestedequality_idsissuemanifest_filefield_summaryDeprecated optional fields omitted by PyIceberg, such as
file_ordinal,sort_columns, anddistinct_counts, are allowed to be omitted and are not conformance failures.Support gaps noted during the audit
These are broader feature gaps rather than incorrect existing definitions:
TableVersion.These may be better tracked separately from the concrete conformance defects above.
References
equality_idsissue: equality_ids written as list<long>, but spec and all other implementations use list<int> apache/iceberg-python#3840