Skip to content

[BSE-4458] Refactor Iceberg IO to Use PyIceberg Under the Hood - #235

Merged
srilman merged 12 commits into
mainfrom
slade/pyiceberg-refactor
Feb 19, 2025
Merged

srilman merged 12 commits into
mainfrom
slade/pyiceberg-refactor

Conversation

@srilman

@srilman srilman commented Feb 12, 2025 •

Copy link
Copy Markdown
Contributor

Changes included in this PR

Refactor our Iceberg IO to support PyIceberg as a backend. Replaces a lot of metadata-related operations to use PyIceberg instead, delete any helper code that PyIceberg already does, and limit the use of the Iceberg connector.

Testing strategy

PR Ci and Nightly both

User facing changes

Users no longer need to install the Iceberg connector for Iceberg support. Instead, they need to either install bodo[iceberg] or bodo and pyiceberg.

Checklist

  • Pipelines passed before requesting review. To run CI you must include [run CI] in your commit message.
  • I am familiar with the Contributing Guide
  • I have installed + ran pre-commit hooks.

@srilman
srilman force-pushed the slade/pyiceberg-refactor branch 2 times, most recently from 7362849 to ef8e93f Compare February 13, 2025 16:42
@srilman

srilman commented Feb 13, 2025

Copy link
Copy Markdown
Contributor Author

commit 8805b95
Author: Srinivas Lade <[email protected]>
Date:   Fri Jan 31 11:43:13 2025 -0500

    [BSE-4471] Move SF Prefetch Support to Python (#189)

commit 9821639
Author: Srinivas Lade <[email protected]>
Date:   Wed Jan 29 12:33:52 2025 -0500

    [BSE-4470][BSE-4474] Add PyIceberg Read Support (#160)
@srilman
srilman force-pushed the slade/pyiceberg-refactor branch from 2c4a674 to 85fb58b Compare February 18, 2025 16:33
@srilman

srilman commented Feb 18, 2025

Copy link
Copy Markdown
Contributor Author

// Benefits: Potentially some speed up in collecting repeated metadata like
// schema
// Downsides: Does not refresh if another program modifies the Catalog
return CachingCatalog.wrap(catalog);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the CachingCatalog change because it was causing issues when jumping between PyIceberg and Iceberg-Java. This shouldn't be as much as a problem anymore because we use some caching on the PyIceberg side, which is still the predominant caller for getting metadata. Iceberg Java is only used for more heavy handed stuff like commits.

catalog = S3TablesBuilder.create(connStr);
return CachingCatalog.wrap(catalog);
if (connStr.startsWith("iceberg+arn:aws:s3tables") && catalogType.equals("s3tables")) {
catalog = S3TablesBuilder.create(connStr.replaceFirst("^iceberg\\+", ""));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Was a bug, I removed the part where we strip the prefix during compilation.


def _table_path(self, identifier: Identifier) -> str:
wh_path = self.warehouse_path.removesuffix("/")
return f"{wh_path}/{'/'.join(identifier)}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Bug here to remove cases where {warehouse_path}//...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You mean this fixes a bug? Could you add comment in the code instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To clarify it was a bug in my own code in the base read PR that was discovered when running nightly.

@@ -0,0 +1,83 @@
import os

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a FileIO wrapper to call into PyArrow's AzureFileSystem

Comment thread bodo/io/iceberg/common.py
conn_str = (
f"snowflake://{account}.snowflakecomputing.com/?{urlencode(conn_contents)}"
)
def _fs_from_file_path(file_path: str, io: FileIO) -> FileSystem:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mostly a duplicate of a internal function from PyIceberg, but slightly modified to take use of our monkey-patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you add a link to the original function that was copied?

@@ -0,0 +1,79 @@
import pyiceberg.io

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sadly I needed to monkey patch a few things from PyIceberg to fix various issues. See below

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can make the monkey patch more robust by following the numba_compat.py format (check hash for code changes and add comments with "BODO change" notes).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I'm going to leave it like this for the time being because I will have to make some large changes when PyIceberg 0.9 comes out anyways

Monkey-patched version of pyiceberg.schema.build_position_accessors to
use the new _BuildPositionAccessors class.
"""
return visit(schema_or_type, _BuildPositionAccessors())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fix to apply filters to top-level struct fields. Right now, PyIceberg only works on inner struct fields

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's add as comment in the code.

pyiceberg.schema.build_position_accessors = build_position_accessors

# Moneky-patch inferred IO to support Bodo's
pyiceberg.io.pyarrow.PyArrowFileIO = BodoPyArrowFileIO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hook to apply our AzureFileSystem changes to PyIceberg. I need to do this here because there are places where PyIceberg just calls PyArrowFileIO directly instead of using their own hooks for custom FileIO.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's add the comment in the code.

pa.Schema,
# Runtime Schema
list[str],
list[types.ArrayCompatible],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I messed up this part initially which caused some schema evolution tests to fail. Basically, this function should return the original schema info (names, types, PyArrow schema) and schema after apply BodoSQL column pruning.

Comment thread bodo/ir/iceberg_ext.py
return DateLiteral((val - datetime.date(1970, 1, 1)).days)
if isinstance(val, datetime.datetime):
return TimestampLiteral((val - datetime.datetime(1970, 1, 1)).microseconds)
if isinstance(val, (list, pd.core.arrays.ExtensionArray)):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Had to add pd.core.arrays.ExtensionArray

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why? This is for nested types?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A comment in the code would be good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure exactly why, just that sometimes Pandas arrays are used instead of lists especially when coming from BodoSQL

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The list/array difference is fine, but my point is that we may need other nested types (struct/map) here too. A TODO may be enough for now.

@srilman
srilman marked this pull request as ready for review February 18, 2025 16:58

@IsaacWarren IsaacWarren left a comment

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.

Just reviewed the places commented on, assuming others are unchanged. Thanks Srinivas!

"AZURE_STORAGE_ACCOUNT_NAME"
)

account_key = self.properties.get(ADLS_ACCOUNT_KEY) or os.environ.get(

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.

Add a todo to pass sas token for arrow 19? Also we should support ADLS_CLIENT_ID, ADLS_TENANT_ID, and ADLS_CLIENT_SECRET

@codecov

codecov Bot commented Feb 18, 2025 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 64.54616% with 457 lines in your changes missing coverage. Please review.

Project coverage is 66.98%. Comparing base (6c7b124) to head (80e211e).
Report is 36 commits behind head on main.

❌ Your project status has failed because the head coverage (66.98%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #235      +/-   ##
==========================================
+ Coverage   61.48%   66.98%   +5.50%     
==========================================
  Files         169      176       +7     
  Lines       61920    62627     +707     
  Branches     8663     8729      +66     
==========================================
+ Hits        38070    41950    +3880     
+ Misses      21430    18077    -3353     
- Partials     2420     2600     +180     

@ehsantn ehsantn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the huge effort @srilman. I just skimmed through to see if anything stands out since most of the code is already reviewed.

Comment on lines +18 to +20
pytest.mark.skip(
reason="[BSE-4569] MERGE INTO with PyIceberg is not supported yet"
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we disabling our current MERGE INTO support?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes for the time being

Comment thread bodo/io/fs_io.py

return impl
this_module = sys.modules[__name__]
PyArrowFSType, pyarrow_fs_type = install_opaque_class(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did we decide that this doesn't need memory management?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes I believe so, because it is something that we don't explicitly create a reference to, and the transaction that we use later on in the function hold a reference to it


def _table_path(self, identifier: Identifier) -> str:
wh_path = self.warehouse_path.removesuffix("/")
return f"{wh_path}/{'/'.join(identifier)}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You mean this fixes a bug? Could you add comment in the code instead.

Comment thread bodo/io/iceberg/catalog/dir.py Outdated
Comment on lines +103 to +104
# TODO
pass

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is supposed to be TODO? Raise an error instead?

Comment thread bodo/io/iceberg/catalog/dir.py Outdated
Comment on lines +145 to +151
def drop_table(self, identifier: str | Identifier) -> None:
# TODO
pass

def purge_table(self, identifier: str | Identifier) -> None:
# TODO
pass

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same.

Comment thread bodo/ir/iceberg_ext.py Outdated
# that have been pushed down to I/O.
rtjf_terms: list[tuple[ir.Var, tuple[int], tuple[int, int, str]]] | None = None,
):
# Info requireds to connect to the catalog and table

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Info requireds to connect to the catalog and table
# Info required to connect to the catalog and table

Comment thread bodo/ir/iceberg_ext.py
return DateLiteral((val - datetime.date(1970, 1, 1)).days)
if isinstance(val, datetime.datetime):
return TimestampLiteral((val - datetime.datetime(1970, 1, 1)).microseconds)
if isinstance(val, (list, pd.core.arrays.ExtensionArray)):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why? This is for nested types?

Comment thread bodo/ir/iceberg_ext.py
return DateLiteral((val - datetime.date(1970, 1, 1)).days)
if isinstance(val, datetime.datetime):
return TimestampLiteral((val - datetime.datetime(1970, 1, 1)).microseconds)
if isinstance(val, (list, pd.core.arrays.ExtensionArray)):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A comment in the code would be good.

Comment thread bodo/ir/iceberg_ext.py Outdated
rtjf_filters = pie.And(
rtjf_filters, pie.In(col, literal(unique_vals))
)
print(rtjf_filters, unique_vals)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove print?

Suggested change
print(rtjf_filters, unique_vals)

from bodo.tests.utils import pytest_mark_one_rank

pytestmark = pytest.mark.iceberg
pytestmark = [pytest.mark.iceberg, pytest.mark.skip]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are we skipping this file. Add a comment in the code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh just missed that, undid it

@srilman
srilman merged commit 151eb1e into main Feb 19, 2025
@srilman
srilman deleted the slade/pyiceberg-refactor branch February 19, 2025 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants