Skip to content

Commit 6894d10

Browse files
committed
refactor(upsert): split validate_join_cols by table and input side
1 parent ca97964 commit 6894d10

1 file changed

Lines changed: 58 additions & 50 deletions

File tree

‎pyiceberg/table/upsert_util.py‎

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,61 +35,69 @@ def validate_join_cols(df: pyarrow_table, join_cols: list[str], table_schema: pa
3535
"""Validate join-key presence and types before Arrow comparison or hashing."""
3636
if not isinstance(join_cols, (list, tuple)):
3737
raise ValueError(f"join_cols must be a list of column names, got {type(join_cols).__name__}.")
38+
3839
duplicates = sorted(col for col, count in Counter(join_cols).items() if count > 1)
3940
if duplicates:
4041
raise ValueError(f"join_cols contains duplicates: {', '.join(duplicates)}.")
4142

42-
df_column_names = set(df.schema.names)
43-
4443
for col in join_cols:
45-
if col not in table_schema.names:
46-
parent = col.split(".", 1)[0]
47-
if "." in col and parent in table_schema.names and pa.types.is_nested(table_schema.field(parent).type):
48-
raise ValueError(f"Join column '{col}' is a field inside struct '{parent}' and cannot be used as a join key.")
49-
raise ValueError(
50-
f"Join column '{col}' does not exist in the table. Available columns: {', '.join(table_schema.names)}."
51-
)
52-
table_field = table_schema.field(col)
53-
# Table-level rejections: These types are fundamentally unreliable or
54-
# unsupported as join keys regardless of the input data format.
55-
if pa.types.is_floating(table_field.type):
56-
raise ValueError(
57-
f"Join column '{col}' is floating point and cannot be used as a join key "
58-
"because floating point equality is unreliable."
59-
)
60-
if pa.types.is_nested(table_field.type):
61-
raise ValueError(
62-
f"Join column '{col}' has nested type '{table_field.type}'; only primitive columns can be join keys."
63-
)
64-
if isinstance(table_field.type, pa.BaseExtensionType):
65-
raise NotImplementedError(
66-
f"Join column '{col}' has type '{table_field.type}', which is not yet supported as a join key."
67-
)
68-
69-
# Schema compatibility permits missing optional fields, but upsert needs every join key.
70-
if col not in df_column_names:
71-
raise ValueError(f"Join column '{col}' is missing from the input.")
72-
# Some input representations are unsupported even when the table type is valid.
73-
arr = df.column(col)
74-
if pa.types.is_dictionary(arr.type):
75-
raise NotImplementedError(
76-
f"Input column '{col}' is dictionary-encoded, which is not yet supported for join keys. Decode it first."
77-
)
78-
if pa.types.is_null(arr.type):
79-
raise ValueError(f"Input column '{col}' has the null type and cannot be used as a join key.")
80-
if pa.types.is_string_view(arr.type) or pa.types.is_binary_view(arr.type):
81-
plain = "string" if pa.types.is_string_view(arr.type) else "binary"
82-
raise NotImplementedError(
83-
f"Input column '{col}' has type '{arr.type}', which is not yet supported for join keys. "
84-
f"Cast it to '{plain}' first."
85-
)
86-
if isinstance(arr.type, pa.BaseExtensionType):
87-
raise NotImplementedError(
88-
f"Input column '{col}' has extension type '{arr.type}', which is not yet supported for join keys."
89-
)
90-
# Null keys cannot be expressed as Iceberg literals in the match filter.
91-
if arr.null_count > 0:
92-
raise ValueError(f"Input column '{col}' contains null values, which cannot be join keys.")
44+
_validate_table_join_col(col, table_schema)
45+
_validate_input_join_col(col, df)
46+
47+
48+
def _validate_table_join_col(col: str, table_schema: pa.Schema) -> None:
49+
"""Reject types that are unreliable or unsupported as join keys regardless of the input."""
50+
if col not in table_schema.names:
51+
parent = col.split(".", 1)[0]
52+
if "." in col and parent in table_schema.names and pa.types.is_nested(table_schema.field(parent).type):
53+
raise ValueError(f"Join column '{col}' is a field inside struct '{parent}' and cannot be used as a join key.")
54+
raise ValueError(f"Join column '{col}' does not exist in the table. Available columns: {', '.join(table_schema.names)}.")
55+
56+
field_type = table_schema.field(col).type
57+
58+
if pa.types.is_floating(field_type):
59+
raise ValueError(
60+
f"Join column '{col}' is floating point and cannot be used as a join key "
61+
"because floating point equality is unreliable."
62+
)
63+
64+
if pa.types.is_nested(field_type):
65+
raise ValueError(f"Join column '{col}' has nested type '{field_type}'; only primitive columns can be join keys.")
66+
67+
if isinstance(field_type, pa.BaseExtensionType):
68+
raise NotImplementedError(f"Join column '{col}' has type '{field_type}', which is not yet supported as a join key.")
69+
70+
71+
def _validate_input_join_col(col: str, df: pyarrow_table) -> None:
72+
"""Reject input representations that are unsupported even when the table type is valid."""
73+
# Schema compatibility permits missing optional fields, but upsert needs every join key.
74+
if col not in df.schema.names:
75+
raise ValueError(f"Join column '{col}' is missing from the input.")
76+
77+
arr = df.column(col)
78+
79+
if pa.types.is_dictionary(arr.type):
80+
raise NotImplementedError(
81+
f"Input column '{col}' is dictionary-encoded, which is not yet supported for join keys. Decode it first."
82+
)
83+
84+
if pa.types.is_null(arr.type):
85+
raise ValueError(f"Input column '{col}' has the null type and cannot be used as a join key.")
86+
87+
if pa.types.is_string_view(arr.type) or pa.types.is_binary_view(arr.type):
88+
plain = "string" if pa.types.is_string_view(arr.type) else "binary"
89+
raise NotImplementedError(
90+
f"Input column '{col}' has type '{arr.type}', which is not yet supported for join keys. Cast it to '{plain}' first."
91+
)
92+
93+
if isinstance(arr.type, pa.BaseExtensionType):
94+
raise NotImplementedError(
95+
f"Input column '{col}' has extension type '{arr.type}', which is not yet supported for join keys."
96+
)
97+
98+
# Null keys cannot be expressed as Iceberg literals in the match filter.
99+
if arr.null_count > 0:
100+
raise ValueError(f"Input column '{col}' contains null values, which cannot be join keys.")
93101

94102

95103
def create_match_filter(df: pyarrow_table, join_cols: list[str]) -> BooleanExpression:

0 commit comments

Comments
 (0)