Skip to content

feat: first attempt at item table logic - #1

Merged
hrodmn merged 23 commits into
mainfrom
feat/item-table
Sep 11, 2026
Merged

feat: first attempt at item table logic#1
hrodmn merged 23 commits into
mainfrom
feat/item-table

Conversation

@hrodmn

@hrodmn hrodmn commented Jan 29, 2026

Copy link
Copy Markdown
Collaborator

This is the initial attempt at icestac: the STAC extension for Apache Iceberg.

It defers almost all Iceberg operations to pyiceberg but includes some helper functions for generating Iceberg table schemas from STAC items and a function for loading items into an existing table.

At first I had a special IcebergCatalog class that handled some partitioning defaults and other table mechanics, but after some deliberation I decided that it would be better to keep the icestac layer as thin as possible for now and provide some working examples on how to stuff STAC items into Iceberg tables.

The docker-compose.yml file will spin up a local S3 storage backend and an Iceberg REST service that you can interact with via main.py which loads some HLS STAC items into a table in the local Iceberg catalog.

@hrodmn
hrodmn requested review from bitner and gadomski February 26, 2026 21:15
@hrodmn hrodmn self-assigned this Feb 26, 2026
@hrodmn hrodmn linked an issue Feb 26, 2026 that may be closed by this pull request
Comment thread .github/workflows/ci.yml Outdated
Comment thread src/icestac/config.py Outdated
Comment thread src/icestac/config.py Outdated
Comment thread src/icestac/item_table.py Outdated
Comment thread src/icestac/lambda_handler.py Outdated
@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@hrodmn
hrodmn requested a review from gadomski February 28, 2026 16:46
Comment thread src/icestac/catalog.py Outdated
Comment thread src/icestac/catalog.py Outdated

@gadomski gadomski 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.

Sorry about getting nitpicky on the API design, but I think this is a critical PR ... if we get it right now, it'll be smooth later.

Comment thread src/icestac/catalog.py Outdated
Comment thread src/icestac/catalog.py Outdated

@dataclass
class IcestacCatalog:
"""Icestac client class for pyiceberg Catalog"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I find it quite interesting that we get to share the Catalog name between STAC and iceberg. This can be convenient, and it has the potential to introduce "shadowing" to our namespace, such that we get confused about which "catalog" we're referring to.

No action required, just an observation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have not thought about it this way. In Iceberg the Catalog is like a database in PostgreSQL.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Which makes sense, if we think of a single pgstac instance as being a Catalog?

Comment thread src/icestac/catalog.py Outdated
Comment thread src/icestac/catalog.py Outdated
Comment thread src/icestac/catalog.py Outdated
Comment on lines +73 to +81
partition_spec=PartitionSpec(
# TODO: make temporal partitioning configurable
PartitionField(
source_id=iceberg_schema.find_field("datetime").field_id,
field_id=1000,
transform=MonthTransform(),
name="datetime_month",
)
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fact that there's a partition_spec in Iceberg is encouraging to me. Are there other partition specs out there that we could coalesce into a generalized "STAC partition spec"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think a STAC Iceberg spec should include some kind of spatial structure but I'm not sure if we would partition on a spatial key necessarily.

The pyiceberg docs show this example on how to apply a sort_key which could be used for sorting records spatially (with a Hilbert curve or something):

# Sort on the symbol
sort_order = SortOrder(SortField(source_id=2, transform='identity'))

catalog.create_table(
    identifier="docs_example.bids",
    schema=schema,
    partition_spec=partition_spec,
    sort_order=sort_order,
)

Comment thread src/icestac/schema.py Outdated
Comment thread src/icestac/schema.py Outdated
Comment thread src/icestac/schema.py Outdated
Comment thread src/icestac/schema.py Outdated
Comment thread src/icestac/schema.py Outdated
Comment thread src/icestac/schema.py Outdated
@hrodmn

hrodmn commented Mar 3, 2026

Copy link
Copy Markdown
Collaborator Author

I still want to explore ideas around the STAC PartitionSpec, but the rest of the library is looking pretty good to me. While working on the config changes I realized that most of the settings were just being passed onto pyiceberg and should therefore be delegated to pyiceberg's config system, so I ripped it out. @gadomski does that make sense to you?

@gadomski

gadomski commented Mar 3, 2026

Copy link
Copy Markdown

@gadomski does that make sense to you?

Yup!

@hrodmn

hrodmn commented May 19, 2026

Copy link
Copy Markdown
Collaborator Author

After putting this on ice for a few months I came back to finally address @gadomski's comments. The package is set up to create a catalog structure like this:

  • create one table per collection
    • collection id must match the table id which places some limits on the characters in a table id (can't have - 😢)
  • items can be loaded into a collection table directly from arrow tables or STAC json dicts
  • items are partitioned temporally by month for now
  • items are not partitioned or ordered spatially (but we should look into that next)

I haven't thought much about schema migration yet but that is something that icestac is theoretically good for since Iceberg has methods for this.

I need to revisit how we are representing the spatial columns and/or spatial metadata to ensure the tables are making best use of the geoparquet format and downstream capabilities. I am a bit lost with the state of geometry in parquet/geoparquet/arrow so if anyone has any ideas on the right way to set that up in 2026 let me know! Maybe that's not necessary to sort out in this PR but definitely before we do any kind of release.

@gadomski

Copy link
Copy Markdown

collection id must match the table id which places some limits on the characters in a table id (can't have - 😢)

This is a bummer, - in collection IDs is quite common. Are there any workarounds we could explore?

Other than that, 👍🏼 to picking reasonable defaults for now, leaving the customization knobs for later.

I am a bit lost with the state of geometry in parquet/geoparquet/arrow so if anyone has any ideas on the right way to set that up in 2026 let me know! Maybe that's not necessary to sort out in this PR but definitely before we do any kind of release.

Agreed, it's messy, and (in my experience) often abstracted away into upstream tooling (but not always).

@hrodmn

hrodmn commented May 20, 2026

Copy link
Copy Markdown
Collaborator Author

This is a bummer, - in collection IDs is quite common. Are there any workarounds we could explore?

Actually, I am mistaken. Hyphens are allowed in Iceberg table names but some catalog backends don't allow them (i.e. AWS Glue). The only character is that is universally not allowed is . which is a delimiter between the namespace and the table. FWIW duckdb is not crazy about - in table names - you have to quote the table name when you query it.

SELECT id, datetime, collection, geometry
FROM catalog.icestac."icesat2-boreal-v3-1-agb"
LIMIT 10;

The table name should match collection ids exactly so clients can confidently do queries by collection without any guesswork. I suppose we could specify a slugification system for translating any collection id into a valid table name, but then client applications would need to be aware of the translation function. e.g. collection id icesat2-boreal-v3.1-agb -> table icesat2_boreal_v3_1_agb. There are tradeoffs either way (limiting collection ids to valid table names vs allowing table names to drift from the actual collection ids) and I'm not sure what is best.

@gadomski

Copy link
Copy Markdown

My instinct is that it's better to keep the table name === collection id constraint, with some documentation guidance on how to "fix" datasets that don't work on (e.g.) AWS Glue.

hrodmn added 4 commits August 20, 2026 20:59
Preserve nullable STAC fields and Arrow metadata, and reject invalid or
cross-collection loads. Align the HLS demo and documentation with the
one-table-per-collection contract.
@hrodmn

hrodmn commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

I am back again with a few updates, this time I think the item handling API might be ready to ship:

  1. allow users to pass PartitionSpec and SortOrder objects directly in IcestacCatalog.create_item_table. Rather than creating some abstraction or custom API around the pyiceberg API, I refactored the API so that the user can explicitly define the partitioning and sorting scheme. The default is still a monthly partitioning scheme on the datetime property, but now that is fully configurable.

    I like the idea of defining a more well-defined StacPartitionSpec, but with spatial support in Iceberg being so limited today, it would probably be better to just demonstrate strategies for non-native spatial partitioning with derived fields for now.

    from pyiceberg.catalog import load_catalog
    from pyiceberg.partitioning import PartitionField, PartitionSpec
    from pyiceberg.table.sorting import SortField, SortOrder
    from pyiceberg.transforms import YearTransform
    
    from icestac.catalog import IcestacCatalog
    from icestac.schema import get_schema_from_items
    
    catalog = IcestacCatalog(catalog=load_catalog())
    
    items = ...
    
    iceberg_schema = get_schema_from_items(items)
    
    datetime_id = iceberg_schema.find_field("datetime").field_id
    sort_id = iceberg_schema.find_field("sortme").field_id
    catalog.create_item_table(
        collection_id=collection_id,
        iceberg_schema=iceberg_schema,
        partition_spec=PartitionSpec(
            PartitionField(
                source_id=datetime_id,
                field_id=1000,
                transform=YearTransform(),
                name="datetime_year",
            )
        ),
        sort_order=SortOrder(SortField(source_id=sort_id)),
    )
  2. add the evolve_schema boolean arg to IcestacCatalog.load_items (default False) - this makes it possible to load items where the schema may have drifted from the existing table

    I actually hit this in my demo script (main.py) because the schema in the HLS items changed in June 2026 😅

  3. since users will need the actual Iceberg schema to get field IDs for custom partitioning/sorting schemes the get_schema_from_items returns an Iceberg schema (instead of an arrow schema).

@gadomski
gadomski self-requested a review August 21, 2026 11:35
@hrodmn
hrodmn merged commit c85da06 into main Sep 11, 2026
4 checks passed
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.

Add methods for loading items

5 participants