Skip to content

Enable virtual dataset cataloging#25

Merged
d33bs merged 24 commits into
WayScience:mainfrom
d33bs:virtual-data
Jul 15, 2026
Merged

Enable virtual dataset cataloging#25
d33bs merged 24 commits into
WayScience:mainfrom
d33bs:virtual-data

Conversation

@d33bs

@d33bs d33bs commented Jun 11, 2026

Copy link
Copy Markdown
Member

Description

This PR enables virtual dataset references. We also add OME-IRIS datasets for testing and upgrade the OME-Arrow requirements of the project.

What kind of change(s) are included?

  • Documentation (changes docs or other related content)
  • Bug fix (fixes an issue).
  • Enhancement (adds functionality).
  • Breaking change (these changes would cause existing functionality to not work as expected).

Checklist

Please ensure that all boxes are checked before indicating that this pull request is ready for review.

  • I have read and followed the CONTRIBUTING.md guidelines.
  • I have searched for existing content to ensure this is not a duplicate.
  • I have performed a self-review of these additions (including spelling, grammar, and related).
  • These changes pass all pre-commit checks.
  • I have added comments to my code to help provide understanding
  • I have added a test which covers the code changes found within this PR
  • I have deleted all non-relevant text in this pull request template.

Summary by CodeRabbit

  • New Features
    • Added catalog registration for directories and profile tables, including replacement and deregistration workflows.
    • Added catalog-free scanning directly to Arrow tables.
    • Added OME-Arrow dataset creation, opening, and writing helpers.
    • Added support for scanning remote OME-TIFF and Zarr locations.
  • Bug Fixes
    • Improved handling of dataset replacement and cleanup of related records.
  • Documentation
    • Expanded onboarding, CLI, remote storage, catalog, profile-table, and troubleshooting guidance.
  • Tests
    • Added coverage for virtual datasets, remote locations, profile publishing, and real-world image workflows.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR expands image registration and deregistration workflows, adds catalog-free Arrow-table scanning and profile-table publishing, supports remote TIFF URIs, broadens OME-Arrow integration, updates package exports, and adds documentation, configuration, unit tests, and network-gated integration tests.

Changes

Ingestion and publishing workflow expansion

Layer / File(s) Summary
Publishing and dataset deletion
src/iceberg_bioimage/publishing/*
Adds profile-table publishing and dataset-scoped deletion for image-assets and chunk-index tables.
Registration and Arrow-table APIs
src/iceberg_bioimage/api.py, src/iceberg_bioimage/__init__.py
Adds replacement registration, directory registration, deregistration, profile registration, Arrow-table scan helpers, and top-level exports.
Remote scanning adapters
src/iceberg_bioimage/adapters/*, tests/test_s3_support.py
Adds remote OME-TIFF opening through fsspec and tests S3, GS, HTTP, and memory-backed URI handling.
OME-Arrow helpers
src/iceberg_bioimage/integrations/ome_arrow.py, tests/test_ome_arrow_integration.py
Adds TIFF/Zarr constructors and dataset open/write wrappers with optional-dependency and delegation coverage.
Registration, profile, and virtual-dataset validation
tests/test_registration_update.py, tests/test_profile_namespace.py, tests/test_virtual_datasets.py, tests/test_iris_datasets.py
Covers replacement ordering, directory discovery, profile schemas and aliases, Arrow conversion, and Iris dataset workflows.
Documentation and project configuration
README.md, pyproject.toml, .pre-commit-config.yaml
Updates workflow documentation, optional dependency versions, lint settings, network-test defaults, and hook revisions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant API
  participant Adapter
  participant Catalog
  Caller->>API: register_directory(path, catalog, namespace)
  API->>Adapter: scan discovered image URI
  Adapter-->>API: ScanResult
  API->>Catalog: delete existing dataset rows when replace=True
  API->>Catalog: append image assets and optional chunk index
  API-->>Caller: WarehouseIngestResult
Loading

Possibly related PRs

Suggested reviewers: dependabot[bot]

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling virtual dataset cataloging support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@d33bs

d33bs commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/iceberg_bioimage/api.py (1)

32-33: ⚡ Quick win

Avoid relying on private _dataset_id across modules.

At Line 32, importing _dataset_id from another module couples this API to a private implementation detail. Please promote a public helper (or move it to a shared utility module) to avoid brittle cross-file contracts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/iceberg_bioimage/api.py` around lines 32 - 33, The import of the private
symbol `_dataset_id` into this module creates a fragile cross-module dependency;
refactor by exposing a public helper (e.g., rename `_dataset_id` to
`get_dataset_id` or `dataset_id_for` and place it in a shared utility module)
and update callers (including `delete_dataset_image_assets`) to import and call
the new public helper instead of referencing the private name; ensure any tests
and all references to `_dataset_id` are updated to use the new public function
and adjust docstrings/comments accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/iceberg_bioimage/api.py`:
- Around line 81-88: Currently the code calls
_deregister_by_dataset_id(_dataset_id(scan_result.source_uri), ...) when
replace=True before publishing, which can permanently drop dataset rows if
publish_image_assets(...) or publish_chunk_index(...) fails; change the flow so
deletions occur only after both publish_image_assets and publish_chunk_index
complete successfully (or implement an atomic swap/transactional approach),
i.e., defer the call to _deregister_by_dataset_id until after successful returns
from publish_image_assets and publish_chunk_index (or add rollback logic in the
exception path) and ensure _dataset_id(scan_result.source_uri) is the same id
used for the deferred deletion.

In `@src/iceberg_bioimage/publishing/image_assets.py`:
- Around line 76-83: In _build_profile_schema, replace the private import
pyiceberg.io.pyarrow._pyarrow_to_schema_without_ids with the public
pyiceberg.io.pyarrow.pyarrow_to_schema and call
pyarrow_to_schema(arrow_table.schema); if that raises or indicates missing
Iceberg field IDs, pass a generated minimal name_mapping (e.g., map each field
name to a unique id) to pyarrow_to_schema so it can assign IDs—wrap the import
and call in the same try/except and raise the same RuntimeError on ImportError,
ensuring the function returns the result of pyarrow_to_schema instead of the
private helper.

In `@tests/test_iris_datasets.py`:
- Around line 208-209: The test currently asserts that any .parquet exists in
the iris_nf1 directory (parquet_files = list(iris_nf1.glob("*.parquet")) ;
assert parquet_files...), which is too loose; update the assertion to
specifically check for the presence of "profiles.parquet" in the iris_nf1
directory (e.g., verify (iris_nf1 / "profiles.parquet").exists() or assert
"profiles.parquet" is in the list of file names from
iris_nf1.glob("*.parquet")), so the test explicitly ensures profiles.parquet is
present.
- Around line 166-171: The test currently allows vacuous success when there are
no NF1 images; before calling scan_as_arrow_table and before computing dataset
IDs ensure the image list is non-empty by asserting the tiff_files variable is
truthy. Specifically, add an assertion like "assert tiff_files" before the list
comprehension that builds tables (used with scan_as_arrow_table) and likewise
before the block that computes dataset IDs/uniqueness so the tests fail if no
.tif files are found.

In `@tests/test_profile_namespace.py`:
- Around line 212-220: The test
test_iris_nf1_profiles_alias_resolves_well_and_site currently asserts
resolved["plate_id"] but should verify site resolution; update the assertion for
the resolved mapping returned by resolve_microscopy_profile_columns to assert
resolved["site_id"] is not None (or add that assertion alongside the existing
plate_id check) so the test actually validates the "site" alias (refer to
variables well_id, plate_id, site_id and the function
resolve_microscopy_profile_columns).

---

Nitpick comments:
In `@src/iceberg_bioimage/api.py`:
- Around line 32-33: The import of the private symbol `_dataset_id` into this
module creates a fragile cross-module dependency; refactor by exposing a public
helper (e.g., rename `_dataset_id` to `get_dataset_id` or `dataset_id_for` and
place it in a shared utility module) and update callers (including
`delete_dataset_image_assets`) to import and call the new public helper instead
of referencing the private name; ensure any tests and all references to
`_dataset_id` are updated to use the new public function and adjust
docstrings/comments accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a9f2ba9-4a8c-4b0b-979b-b807e41c827b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc8f97 and 286c8f4.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • .pre-commit-config.yaml
  • README.md
  • docs/src/examples/basic-workflow.py
  • docs/src/examples/metadata-workflow.py
  • pyproject.toml
  • src/iceberg_bioimage/__init__.py
  • src/iceberg_bioimage/adapters/ome_tiff.py
  • src/iceberg_bioimage/adapters/zarr_v2.py
  • src/iceberg_bioimage/api.py
  • src/iceberg_bioimage/integrations/ome_arrow.py
  • src/iceberg_bioimage/publishing/chunk_index.py
  • src/iceberg_bioimage/publishing/image_assets.py
  • src/iceberg_bioimage/validation/contracts.py
  • tests/__init__.py
  • tests/conftest.py
  • tests/fakes.py
  • tests/test_iris_datasets.py
  • tests/test_ome_arrow_integration.py
  • tests/test_profile_namespace.py
  • tests/test_registration_update.py
  • tests/test_s3_support.py
  • tests/test_virtual_datasets.py

Comment thread src/iceberg_bioimage/api.py
Comment thread src/iceberg_bioimage/publishing/image_assets.py Outdated
Comment thread tests/test_iris_datasets.py
Comment thread tests/test_iris_datasets.py Outdated
Comment thread tests/test_profile_namespace.py Outdated
@d33bs
d33bs marked this pull request as ready for review June 11, 2026 21:22
@d33bs
d33bs requested a review from gwaybio June 17, 2026 16:36

@gwaybio gwaybio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Exciting to see this! I made several comments which should be addressed prior to merging. You'll notice the comments are concentrated in the readme, which is currently quite terse. I would recommend paying very careful attention to language and clarity, especially at this stage in the project. Paying off this debt early will be a big win long-term!

Comment thread src/iceberg_bioimage/integrations/ome_arrow.py
Comment thread src/iceberg_bioimage/publishing/chunk_index.py Outdated
Comment thread src/iceberg_bioimage/publishing/image_assets.py Outdated
Comment thread src/iceberg_bioimage/publishing/image_assets.py
Comment thread src/iceberg_bioimage/publishing/image_assets.py Outdated
Comment thread README.md
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md
Comment thread README.md
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
@d33bs

d33bs commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Thanks @gwaybio ! After working through your comments and making changes I think this is ready for another review when there's a chance.

@d33bs
d33bs requested a review from gwaybio July 2, 2026 22:14

@gwaybio gwaybio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few more comments - looking good.

The README still needs a bit of work. It's still quite terse and I'm often wondering why would I want to do this or cool, but why should I care. The README is a very important document and we need to make sure it's accurately and understandably describing the motivation for the work. It needs to self-justify, at this moment.

More README improvements, might be beyond scope of this PR, so feel free to punt for now

Comment thread src/iceberg_bioimage/integrations/ome_arrow.py Outdated
Comment thread src/iceberg_bioimage/validation/contracts.py Outdated
Comment thread src/iceberg_bioimage/api.py Outdated
Comment thread tests/test_profile_namespace.py
Comment thread tests/test_profile_namespace.py
Comment thread README.md
Comment thread README.md
Comment thread README.md Outdated
Comment thread README.md
Comment thread README.md
d33bs and others added 2 commits July 15, 2026 09:20
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
d33bs and others added 5 commits July 15, 2026 10:31
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
Co-Authored-By: Gregory Way <gregory.way@gmail.com>
@d33bs

d33bs commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Thank you @gwaybio for this review! Merging this in after making changes here.

@d33bs
d33bs merged commit 6ba5841 into WayScience:main Jul 15, 2026
8 of 9 checks passed
@d33bs
d33bs deleted the virtual-data branch July 15, 2026 17:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyproject.toml (1)

145-150: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a CI job for pytest -m network

test_matrix runs plain pytest, so ini_options.addopts = "-m 'not network'" excludes every @pytest.mark.network test from CI. Add a separate job/step that runs the network-marked tests explicitly to keep those integration checks covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 145 - 150, Add a separate CI job or step that
explicitly runs pytest with the network marker, using the project’s existing
test configuration and environment setup. Ensure it invokes pytest with “-m
network” so tests excluded by the default addopts in the pytest configuration
are executed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 39-40: Update the scan description in README.md to remove the
unconditional “constant-time regardless of image size” claim, and describe
instead that metadata-only scanning typically avoids reads proportional to pixel
data while acknowledging runtime may vary.
- Around line 58-59: Update the OME-Arrow version shown in the README install
commands for the `ome-arrow` extra from >=0.0.10 to >=0.0.11, and ensure the
corresponding OME-Arrow references in the integration section use the same
minimum version.
- Around line 281-291: Update the README S3 documentation to state that Zarr
access via S3 requires the `s3` extra, or an equivalent S3 backend, because
`s3fs` is provided only by `project.optional-dependencies.s3`. Remove the claim
that Zarr S3 works without an extra install and revise the surrounding
dependency explanation accordingly.

In `@src/iceberg_bioimage/publishing/image_assets.py`:
- Around line 92-127: Update image asset publishing around
_concretize_null_columns and the table.append flow to use the loaded destination
table schema when coercing all-null columns, preserving each existing field’s
type; retain the explicit string fallback only when creating a new table. In
tests/test_profile_namespace.py lines 168-188, replace the unconditional
string-type assertion with coverage that appends an all-null column to an
existing non-string field and verifies the append succeeds with the destination
type.

---

Outside diff comments:
In `@pyproject.toml`:
- Around line 145-150: Add a separate CI job or step that explicitly runs pytest
with the network marker, using the project’s existing test configuration and
environment setup. Ensure it invokes pytest with “-m network” so tests excluded
by the default addopts in the pytest configuration are executed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 518ae4b2-8762-479b-8c50-ea35c7ee900d

📥 Commits

Reviewing files that changed from the base of the PR and between b643fb9 and 046a144.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • .pre-commit-config.yaml
  • README.md
  • pyproject.toml
  • src/iceberg_bioimage/api.py
  • src/iceberg_bioimage/integrations/ome_arrow.py
  • src/iceberg_bioimage/publishing/chunk_index.py
  • src/iceberg_bioimage/publishing/image_assets.py
  • tests/test_profile_namespace.py
  • tests/test_virtual_datasets.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_virtual_datasets.py
  • src/iceberg_bioimage/publishing/chunk_index.py
  • src/iceberg_bioimage/integrations/ome_arrow.py
  • src/iceberg_bioimage/api.py

Comment thread README.md
Comment on lines +39 to +40
- **scan** — read an image's metadata (shape, dtype, axes, chunk layout) without
loading pixel data. Fast and constant-time regardless of image size.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid promising constant-time scans.

Metadata-only scanning avoids pixel-sized reads, but runtime can still depend on metadata/page count, directory enumeration, and remote-storage latency. Replace this with wording such as “typically avoids reads proportional to pixel data.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 39 - 40, Update the scan description in README.md to
remove the unconditional “constant-time regardless of image size” claim, and
describe instead that metadata-only scanning typically avoids reads proportional
to pixel data while acknowledging runtime may vary.

Comment thread README.md
Comment on lines +58 to +59
pip install 'iceberg-bioimage[ome-arrow]' # OME-Arrow image pixel-data access (≥0.0.10)
pip install 'iceberg-bioimage[s3]' # S3 / remote URI support for OME-TIFF

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use one OME-Arrow minimum version everywhere.

The install section says >=0.0.10, while the integration section says >=0.0.11 and the PR objective identifies 0.0.11 as the upgraded requirement. Update Lines 58-59 to >=0.0.11.

Also applies to: 314-317

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 58 - 59, Update the OME-Arrow version shown in the
README install commands for the `ome-arrow` extra from >=0.0.10 to >=0.0.11, and
ensure the corresponding OME-Arrow references in the integration section use the
same minimum version.

Comment thread README.md
Comment on lines +281 to +291
Zarr stores on S3 work out of the box — no extra install needed — because `zarr` already pulls in `fsspec`.
OME-TIFF on S3 requires an extra install because `tifffile` does not:

```bash
pip install 'iceberg-bioimage[s3]'
```

Install it with `uv sync --group ome-arrow` or
`pip install 'iceberg-bioimage[ome-arrow]'`.
The reason for the difference: `zarr` already depends on `fsspec`, so `s3://`
paths work immediately. `tifffile` (used for OME-TIFF) does not understand
`fsspec` URLs on its own, so this package adds an `fsspec`-backed file opener
behind the `s3` extra to bridge the gap.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'fsspec|s3fs|zarr|\[project\.optional-dependencies\]' pyproject.toml

Repository: WayScience/iceberg-bioimage

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- pyproject.toml (dependency sections) ---'
nl -ba pyproject.toml | sed -n '1,120p'

echo
echo '--- README.md (S3 wording area) ---'
nl -ba README.md | sed -n '270,300p'

Repository: WayScience/iceberg-bioimage

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import tomllib

data = tomllib.loads(Path('pyproject.toml').read_text())

print('project.dependencies:')
for dep in data.get('project', {}).get('dependencies', []):
    print('  ', dep)

print('\nproject.optional-dependencies:')
for extra, deps in data.get('project', {}).get('optional-dependencies', {}).items():
    print(f'  [{extra}]')
    for dep in deps:
        print('    ', dep)
PY

Repository: WayScience/iceberg-bioimage

Length of output: 388


Document the s3 extra for Zarr S3 access
zarr is a base dependency, but s3fs only comes from project.optional-dependencies.s3, so “no extra install needed” is misleading. Call out that Zarr on S3 also needs the s3 extra (or another S3 backend).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 281 - 291, Update the README S3 documentation to
state that Zarr access via S3 requires the `s3` extra, or an equivalent S3
backend, because `s3fs` is provided only by `project.optional-dependencies.s3`.
Remove the claim that Zarr S3 works without an extra install and revise the
surrounding dependency explanation accordingly.

Comment on lines +92 to +127
publish_table = _concretize_null_columns(arrow_table)

def _build_profile_schema() -> object:
from pyiceberg.io.pyarrow import pyarrow_to_schema
from pyiceberg.table.name_mapping import MappedField, NameMapping

name_mapping = NameMapping(
[
MappedField(field_id=i + 1, names=[field.name])
for i, field in enumerate(publish_table.schema)
]
)
return pyarrow_to_schema(publish_table.schema, name_mapping=name_mapping)

table = _load_or_create_table(
catalog,
namespace,
table_name,
schema_builder=_build_profile_schema,
)
table.append(publish_table)
return publish_table.num_rows


def _concretize_null_columns(arrow_table: pa.Table) -> pa.Table:
"""Cast all-null columns to strings for Iceberg schema compatibility."""

normalized = arrow_table
for index, field in enumerate(arrow_table.schema):
if not pa.types.is_null(field.type):
continue
normalized = normalized.set_column(
index,
pa.field(field.name, pa.string(), nullable=True, metadata=field.metadata),
arrow_table.column(index).cast(pa.string()),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the surrounding implementation.
ast-grep outline src/iceberg_bioimage/publishing/image_assets.py --view expanded || true
echo "---- image_assets.py excerpt ----"
sed -n '1,220p' src/iceberg_bioimage/publishing/image_assets.py | cat -n

echo "---- tests/test_profile_namespace.py excerpt ----"
sed -n '1,260p' tests/test_profile_namespace.py | cat -n

echo "---- search for all-null / concretize helpers ----"
rg -n "_concretize_null_columns|all-null|null columns|pyarrow_to_schema|MappedField|NameMapping|register_profile_table|append\(" src tests

Repository: WayScience/iceberg-bioimage

Length of output: 29431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- image_assets.py _load_or_create_table section ----"
sed -n '221,320p' src/iceberg_bioimage/publishing/image_assets.py | cat -n

echo "---- api.py register_profile_table wrapper ----"
sed -n '420,475p' src/iceberg_bioimage/api.py | cat -n

echo "---- tests/fakes.py ----"
sed -n '1,120p' tests/fakes.py | cat -n

Repository: WayScience/iceberg-bioimage

Length of output: 9495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- dependency versions ----"
sed -n '1,220p' pyproject.toml | cat -n

echo "---- any tests around appending to existing tables or schema mismatch ----"
rg -n "append.*schema|schema mismatch|existing table|load_table|create_table|publish_profile_table|register_profile_table" tests src

echo "---- pyarrow null-to-string / schema inference probe ----"
python3 - <<'PY'
import pyarrow as pa

t = pa.table({"a": pa.nulls(2), "b": [1, 2]})
print("source schema:", t.schema)
print("casted schema:", t.set_column(0, pa.field("a", pa.string()), t.column(0).cast(pa.string())).schema)
print("source column types:", [f.type for f in t.schema])
PY

Repository: WayScience/iceberg-bioimage

Length of output: 11750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- tests/test_profile_namespace.py around the remaining profile tests ----"
sed -n '236,285p' tests/test_profile_namespace.py | cat -n

echo "---- source around schema derivation helper in chunk_index for comparison ----"
sed -n '1,110p' src/iceberg_bioimage/publishing/chunk_index.py | cat -n

Repository: WayScience/iceberg-bioimage

Length of output: 6239


🌐 Web query:

PyIceberg append pyarrow schema type compatibility null column cast string existing table schema

💡 Result:

In PyIceberg, schema compatibility during an append operation is validated by comparing the PyArrow schema of the input DataFrame against the Iceberg table's schema [1][2]. When attempting to append a column containing only nulls (PyArrow pa.null() type) to an existing Iceberg table, several factors influence compatibility and successful execution [3][4]. Schema Compatibility and Null Columns: 1. PyArrow Null Type: For V1 and V2 Iceberg tables, pa.null() is often not automatically promoted to the target Iceberg column type because there is no direct Iceberg type mapping for a pure null type [3]. This can lead to validation errors during append [3][5]. 2. V3 Tables: In newer Iceberg specifications (V3+), pa.null() may be mapped to UnknownType, which is intended to be promotable to any type [3]. 3. Recommended Workaround: If you encounter compatibility errors when appending a column that consists entirely of nulls, explicitly define the schema of your PyArrow DataFrame before the append operation [3]. By creating the PyArrow table with the correct target type (e.g., pa.string()) even when all values are null, you ensure that the PyArrow schema matches the Iceberg table schema expectations [3]. Example of explicit schema definition: import pyarrow as pa # Define the schema explicitly to match the Iceberg table's expected column type schema = pa.schema([ pa.field("existing_column_name", pa.string, nullable=True), ]) # Create the table with this schema, even if the data is all None df = pa.Table.from_pylist([{"existing_column_name": None}], schema=schema) # Perform the append table.append(df) Additional Considerations: - Type Promotion: PyIceberg supports type promotion on write (e.g., pa.int32() to LongType), but this applies to compatible primitive types, not necessarily from a raw null type [2][6]. - Schema Projection/Ordering: Modern PyIceberg versions (0.7.x+) handle arbitrary column ordering and subsets of the table schema automatically, meaning you do not need to order your DataFrame columns to match the Iceberg table exactly [1][2]. - Metadata Errors: Historically, issues were reported where writing columns containing only nulls caused errors in metadata collection [4]. Using recent versions of PyIceberg is recommended to ensure these edge cases are handled [1][2].

Citations:


Preserve existing field types for all-null profile columns. Casting null-typed columns to string is only safe for table creation; on an existing table it can make a valid append incompatible with the table’s actual field type. Coerce all-null columns from the loaded table schema when appending, and keep the creation-time fallback explicit.

  • src/iceberg_bioimage/publishing/image_assets.py#L92-L127: use the destination schema for null columns before append.
  • tests/test_profile_namespace.py#L168-L188: replace the unconditional string assertion with coverage for appending an all-null column to an existing non-string field.
📍 Affects 2 files
  • src/iceberg_bioimage/publishing/image_assets.py#L92-L127 (this comment)
  • tests/test_profile_namespace.py#L168-L188
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/iceberg_bioimage/publishing/image_assets.py` around lines 92 - 127,
Update image asset publishing around _concretize_null_columns and the
table.append flow to use the loaded destination table schema when coercing
all-null columns, preserving each existing field’s type; retain the explicit
string fallback only when creating a new table. In
tests/test_profile_namespace.py lines 168-188, replace the unconditional
string-type assertion with coverage that appends an all-null column to an
existing non-string field and verifies the append succeeds with the destination
type.

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.

2 participants