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

fix: load_table_from_dataframe method for issue 1692 #1698

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions google/cloud/bigquery/_pandas_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,20 @@ def bq_to_arrow_array(series, bq_field):
return pyarrow.Array.from_pandas(series, type=arrow_type)


def _check_nullability(arrow_fields, dataframe):
"""Throws error if dataframe has null values and column doesn't allow nullable"""
if dataframe.index.name:
Copy link
Contributor

@Linchin Linchin Nov 6, 2023

Choose a reason for hiding this comment

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

Could you please help me understand what lines 307-308 are for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's hard to let you know which exact code as I am on vacation. But when dataframe with index is used, index name is transformed as airow column name. There were two way to fix it. One was to put exception for this case or the create another column with index name. I choose the second option as it's easier. Without this the dataframe unit test case where they use index names will fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, hope you are having a good time in vacation! I played with dataframe's index a little bit, and I think there are several corner cases (which are likely non-comprehensive) that we need to cover:

  • Index doesn't have a name, does it still get converted into arrow?
  • Multiple index
  • Index with the same name as columns, which is possible with dataframes
  • Index columns have the same names (possible too)
  • multiindex

Copy link
Contributor

Choose a reason for hiding this comment

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

@Gaurang033 Thanks for offering up this PR.
@Linchin I appreciate this summary of additional edge cases that may not be covered by this solution.

I too worry about the edge cases, but more importantly, I worry about spending too much time and energy trying to create a work around for what we all agree is a problem in pyarrow. This feels like it creates greater complexity in our code, increased fragility, and a higher maintenance burden in the long run. Am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am also uncertain if we should add logic in our repo to correct an issue with pyarrow. I have been thinking about this PR as more of a temporary patchwork that maybe reverted later, but for now does help our customers. However if the logic covering the corner cases get too convoluted with the behaviors of pyarrow, I agree that perhaps it's a better idea to open an issue with pyarrow instead.

dataframe[dataframe.index.name] = dataframe.index
for arrow_field in arrow_fields:
if arrow_field:
col_name = arrow_field.name
if (
not arrow_field.nullable
and dataframe[arrow_field.name].isnull().values.any()
):
raise ValueError(f"required field {col_name} can not be nulls")


def get_column_or_index(dataframe, name):
"""Return a column or index as a pandas series."""
if name in dataframe.columns:
Expand Down Expand Up @@ -587,6 +601,7 @@ def dataframe_to_arrow(dataframe, bq_schema):
)
arrow_fields.append(bq_to_arrow_field(bq_field, arrow_arrays[-1].type))

_check_nullability(arrow_fields, dataframe)
if all((field is not None for field in arrow_fields)):
return pyarrow.Table.from_arrays(
arrow_arrays, schema=pyarrow.schema(arrow_fields)
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8677,6 +8677,32 @@ def test_load_table_from_dataframe_w_nulls(self):
assert sent_config.schema == schema
assert sent_config.source_format == job.SourceFormat.PARQUET

@unittest.skipIf(pandas is None, "Requires `pandas`")
@unittest.skipIf(pyarrow is None, "Requires `pyarrow`")
def test_load_table_from_dataframe_w_nulls_for_required_cols(self):
"""Test that a DataFrame with null columns should throw error if
corresponding field in bigquery schema is required.

See: https://github.com/googleapis/python-bigquery/issues/1692
"""
from google.cloud.bigquery.schema import SchemaField
from google.cloud.bigquery import job

client = self._make_client()
records = [{"name": None, "age": None}, {"name": None, "age": None}]
dataframe = pandas.DataFrame(records, columns=["name", "age"])
schema = [
SchemaField("name", "STRING"),
SchemaField("age", "INTEGER", mode="REQUIRED"),
]
job_config = job.LoadJobConfig(schema=schema)
with pytest.raises(ValueError) as e:
client.load_table_from_dataframe(
dataframe, self.TABLE_REF, job_config=job_config, location=self.LOCATION
)

assert str(e.value) == "required field age can not be nulls"

@unittest.skipIf(pandas is None, "Requires `pandas`")
def test_load_table_from_dataframe_w_invaild_job_config(self):
from google.cloud.bigquery import job
Expand Down