Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flink: Add null check to writers to prevent resurrecting null values #12049

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

mxm
Copy link
Contributor

@mxm mxm commented Jan 22, 2025

Flink's BinaryRowData uses a magic byte to indicate null values in the backing byte arrays. Flink's internal RowData#createFieldGetter method which Iceberg uses, only adds a null check whenever a type is nullable. We map Iceberg's optional attribute to nullable, but Iceberg's required attribute to non-nullable. The latter creates an issue when the user, by mistake, nulls a field. The resulting RowData field will then be interpreted as actual data because the null field is not checked. This yields random values which should have been null and produced an error in the writer.

The solution is to always check if a field is nullable before attempting to read data from it.

// This will produce incorrect writes instead of failing with a NullPointerException.
if (struct.isNullAt(index)) {
return null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actual fix is here.

Copy link
Contributor

Choose a reason for hiding this comment

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

do we need to fix the FlinkOrcWriters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. I revised the approach because it was too easy to miss instances. I'm instead wrapping RowData#createFieldGetter to make sure to null-check also for required / NonNull types. I'll raise an issue on the Flink side as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also see #12049 (comment).

@mxm mxm force-pushed the null-value-check branch from 5ecff96 to f763c54 Compare January 22, 2025 18:01
@pvary
Copy link
Contributor

pvary commented Jan 23, 2025

@mxm: Please remove the 1.18, 1.19 changes from the PR. It is much easier to review this way, and apply changes required by the reviewer. When the PR has been merged, we can backport the changes to the other Flink versions.

QQ: What happens when we have a type discrepancy between the Iceberg type and the RawData type? Could we have issues with other conversions? Do we have a way to prevent those?

@mxm mxm force-pushed the null-value-check branch 3 times, most recently from b8700c9 to 734b7bb Compare January 23, 2025 11:18
@mxm
Copy link
Contributor Author

mxm commented Jan 23, 2025

@mxm: Please remove the 1.18, 1.19 changes from the PR. It is much easier to review this way, and apply changes required by the reviewer. When the PR has been merged, we can backport the changes to the other Flink versions.

Makes sense! Done.

QQ: What happens when we have a type discrepancy between the Iceberg type and the RawData type? Could we have issues with other conversions? Do we have a way to prevent those?

Type discrepancies between Iceberg and Flink types will error in Flink's TypeSerializer for a given field. For example, an int field will use IntSerializer which only accepts Integer. This will raise an NoSuchMethodError during serialization. As long as we use the same serializer also for deserialization, we should be fine. That is the case.

@mxm
Copy link
Contributor Author

mxm commented Jan 23, 2025

@pvary I had to re-add the 1.18 and 1.19 changes, but they are in a separate commit. The reason is that I modified a test base class which affects also 1.18 and 1.19. We can't build otherwise.

@mxm mxm force-pushed the null-value-check branch 4 times, most recently from f4893cc to 59dacfb Compare January 23, 2025 13:58
@mxm
Copy link
Contributor Author

mxm commented Jan 23, 2025

Tests are green.

@mxm
Copy link
Contributor Author

mxm commented Jan 23, 2025

CC @stevenzwu

@mxm mxm force-pushed the null-value-check branch from e743c35 to 133b047 Compare January 27, 2025 12:57
@mxm
Copy link
Contributor Author

mxm commented Jan 27, 2025

Rebased.

@mxm mxm force-pushed the null-value-check branch 3 times, most recently from def98b6 to 46f8c23 Compare January 27, 2025 13:47
@mxm
Copy link
Contributor Author

mxm commented Jan 27, 2025

Flaky Spark test, otherwise passing.

@mxm mxm force-pushed the null-value-check branch from 46f8c23 to d3d4e46 Compare January 28, 2025 09:52
@mxm mxm force-pushed the null-value-check branch 2 times, most recently from bbc627a to 8f90712 Compare January 28, 2025 17:38
@mxm mxm force-pushed the null-value-check branch from 8f90712 to 0e96bea Compare January 31, 2025 19:48
@mxm mxm force-pushed the null-value-check branch 6 times, most recently from 6b69d52 to ead98ae Compare January 31, 2025 20:42
Comment on lines +28 to +41
public static RowData.FieldGetter createFieldGetter(LogicalType fieldType, int fieldPos) {
RowData.FieldGetter flinkFieldGetter = RowData.createFieldGetter(fieldType, fieldPos);
return rowData -> {
// Be sure to check for null values, even if the field is required. Flink
// RowData.createFieldGetter(..) does not null-check optional / nullable types. Without this
// explicit null check, the null flag of BinaryRowData will be ignored and random bytes will
// be parsed as actual values. This will produce incorrect writes instead of failing with a
// NullPointerException.
if (!fieldType.isNullable() && rowData.isNullAt(fieldPos)) {
return null;
}
return flinkFieldGetter.getFieldOrNull(rowData);
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the core change here to replace all the RowData#createFieldGetter calls. The idea is to also perform a null check for non-null types to prevent interpreting nulled fields in BinaryRowData as actual values. Unfortunately, Flink itself only adds the null check for nullable types and defers additional null checks to the caller. I'll report this in upstream Flink as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this new approach.

I am also wondering if this is also a bug in Flink where RowData.createFieldGetter couldn't handle the BinaryRowData properly regarding null value.

mxm added 2 commits January 31, 2025 22:20
Flink's BinaryRowData uses a magic byte to indicate null values in the backing
byte arrays. Flink's internal RowData#createFieldGetter method which Iceberg
uses, only adds a null check whenever a type is nullable. We map Iceberg's
optional attribute to nullable, but Iceberg's required attribute to
non-nullable. The latter creates an issue when the user, by mistake, nulls a
field. The resulting RowData field will then be interpreted as actual data
because the null field is not checked. This yields random values which should
have been null and produced an error in the writer.

The solution is to always check if a field is nullable before attempting to read
data from it.
@mxm mxm force-pushed the null-value-check branch from ead98ae to c4aba0b Compare January 31, 2025 21:20

@Test
public void testWriteNullValueForRequiredType() {
Assumptions.assumeThat(supportsDefaultValues()).isTrue();
Copy link
Contributor

Choose a reason for hiding this comment

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

where is supportsDefaultValues() defined?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants