Skip to content

🤖 Name your tables on every engine: table_names across the multi-engine backends - #252

Open
alxmrs wants to merge 3 commits into
mainfrom
engines-with-table-names
Open

🤖 Name your tables on every engine: table_names across the multi-engine backends#252
alxmrs wants to merge 3 commits into
mainfrom
engines-with-table-names

Conversation

@alxmrs

@alxmrs alxmrs commented Sep 5, 2026

Copy link
Copy Markdown
Member

This feature adds the table_names feature to all engine backends so each SQL engine behaves similar to the native datafusion experience.

🤖 below (with human edits).

🤖 This PR was drafted by Claude Code (Opus 5) and is up for human review.

ctx.from_dataset('era5', ds, chunks=dict(time=6), table_names={
    ('time', 'latitude', 'longitude'): 'surface',
    ('time', 'level', 'latitude', 'longitude'): 'atmosphere',
})

ctx.sql('SELECT AVG("2m_temperature") FROM era5.surface WHERE ...')

The table_names keyword only ever reached DataFusion. Since #227 a Dataset can be
registered on DuckDB or scanned by Polars, and on those paths the tables went
back to being named after their dimensions. This routes table_names through
the register seam, so it means the same thing wherever a Dataset is
registered:

con = duckdb.connect()
xql.register(con, 'era5', ds, table_names={
    ('time', 'latitude', 'longitude'): 'surface',
    ('time', 'level', 'latitude', 'longitude'): 'atmosphere',
})

con.sql('SELECT AVG("2m_temperature") FROM era5.surface WHERE ...')

What each engine does

DuckDB. con.register can only place an object in DuckDB's temporary
namespace, so each dimension group is still registered flat as era5_surface,
and the groups are additionally mirrored as views in a schema of their own so
era5.surface resolves. Both spellings hit the same scan — chunk pruning and
column projection travel through the view (pinned by a test: a WHERE time = ... query reads 1 of 6 chunks and projects two columns). A read-only
connection cannot create a schema; that path warns and leaves the flat tables
rather than losing the registration.

A plain datafusion.SessionContext. It now splits mixed-dimension
Datasets into a schema the way XarrayContext does. Previously it registered
the whole Dataset as a single table, which a mixed-dimension Dataset cannot
be.

Polars, and anything else consuming the pyarrow dataset protocol, has no
connection object to dispatch on, so it takes the tables directly:

tables = xql.arrow_datasets(ds, 'era5', table_names={...})

ctx = pl.SQLContext()
for table, dataset in tables.items():   # 'era5_surface', 'era5_atmosphere'
    ctx.register(table, pl.scan_pyarrow_dataset(dataset))

arrow_datasets is the new public function here — arrow_dataset wants a
Dataset whose variables share one set of dimensions, and this is the
mixed-dimension form of it. It reads the shared dimension coordinates once for
all the tables it returns, which is a network round-trip saved per dimension
per group on Zarr-backed stores.

Naming rules

One rule, df.resolve_table_names, serves every adapter:

  • Groups you do not name keep their dimensions joined by underscores; the
    group holding scalar variables, if any, is scalar.
  • Keys naming a dimension group the Dataset does not have are ignored, so one
    canonical naming map can be reused across Datasets holding different subsets
    of the same variables.
  • Two groups claiming the same name raises, instead of silently registering
    one table over the other.

Also here

The geospatial benchmark suite carried its own copy of the split
(_group_tables), written when the library had no cross-engine version. It
now calls arrow_datasets, so benchmark naming cannot drift from the
library's.

docs/limitations.md listed "mixed-dimension datasets split into one DuckDB
table per dim group" as a sharp edge; it is now the narrower read-only-
connection caveat.

Testing

tests/test_table_names.py holds the contract in one place: the same naming
map on XarrayContext, a plain SessionContext, DuckDB, and Polars,
including a parity test that runs one SQL string against era5.surface on
three engines and asserts one number. Plus the spellings (dotted and flat),
pushdown through the view, the round-trip back to a Dataset, the read-only
warning, and the two naming rules above.

Full suite: 332 passed, 2 skipped. ruff and mypy clean.

The benchmark cases themselves need ARCO-ERA5 from GCS, so the harness change
was exercised against a synthetic ERA5-shaped Dataset on all three engines
rather than through a suite run.

Not done

CHANGELOG.md is untouched — its Unreleased section predates both 0.4.0-rc.1
and #227, so it looked retired. Happy to add an entry if it is still live.

🤖 Generated with Claude Code

https://claude.ai/code/session_013LfwhbX9VLmvoN6ka7py5w

alxmrs and others added 3 commits September 5, 2026 12:41
`table_names` gave ARCO-ERA5's two dimension groups the names `surface`
and `atmosphere` instead of `time_latitude_longitude` and
`time_level_latitude_longitude`. It only ever reached DataFusion. Route
it through the register seam so it means the same thing wherever a
Dataset is registered.

One naming rule, `df.resolve_table_names`, now serves every adapter:

* DuckDB takes `table_names`, keeps registering the flat `era5_surface`
  tables, and mirrors the groups as views in a schema so `era5.surface`
  resolves too — the DataFusion spelling, so query text moves between
  engines unchanged. Pushdown and projection travel through the view.
  A read-only connection cannot create a schema; that path warns and
  leaves the flat tables rather than losing the registration.
* A plain `SessionContext` splits mixed-dimension Datasets into a
  schema the way `XarrayContext` does, instead of registering them as
  one table they cannot be.
* `arrow_datasets(ds, name, table_names=...)` returns the split tables
  named, for Polars and anything else scanning the pyarrow dataset
  protocol, reading the shared dimension coordinates once.

Groups left unnamed keep their joined dimension names. Keys naming a
group the Dataset does not have are ignored, so one map can be reused
across variable subsets; two groups claiming one name is an error
rather than a silent clobber.

The geospatial suite's private copy of the split is gone — the harness
calls `arrow_datasets`, so benchmark naming cannot drift from the
library's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LfwhbX9VLmvoN6ka7py5w
@alxmrs
alxmrs marked this pull request as ready for review September 5, 2026 20:11
registered under.
"""
try:
con.execute(f"CREATE SCHEMA IF NOT EXISTS {_quote(name)}")

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.

con.register only puts things in the temp namespace but this schema is durable. so on a file backed db the views stick around after the session and point at nothing. reopened one and got Table with name era5_surface does not exist!

tried CREATE SCHEMA temp.era5 and duckdb won't allow it, so maybe only mirror on in-memory connections? or at least mention it in limitations.md

con.register_table(name, read_xarray_table(ds, chunks, **kwargs))
return con

# A mixed-dimension Dataset becomes one table per dimension group

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.

the old path did register a mixed dim dataset as one table. on a 4x3 / 4x2x3 fixture i get COUNT(*) = 24 with t2m broadcast across level, so it worked, just weirdly.

no objection to splitting, but it's a break and nothing records it. no test on the old shape and changelog is untouched

# One group is one table, named `name` — there is no group to
# tell apart. Without a `name`, it takes the group's own.
only = name if name else next(iter(names.values()), "scalar")
return {only: arrow_dataset(ds, chunks, **kwargs)}

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.

this branch calls arrow_dataset and the other builds XarrayPushdownDataset, so which kwargs work depends on the data. arrow_datasets(uniform_ds, "x", _iteration_callback=f) is a TypeError, the mixed one is fine.

using XarrayPushdownDataset here too would line them up

variables, if any, takes ``scalar``.
**kwargs: Forwarded to
[arrow_dataset][xarray_sql.backends.pyarrow.arrow_dataset]
(``batch_size``, ``prefetch``, ``geometry``, ...), applied to

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.

since geometry goes to every group it can never work on a mixed dim dataset. geometry dims ['lat', 'lon'] are not columns of the table; available: ['proj']

skip the groups that don't have those dims, or say here that geometry needs a uniform dataset

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