xarray_sql 0.3.2__tar.gz → 0.4.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/Cargo.lock +1 -1
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/Cargo.toml +1 -1
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/PKG-INFO +65 -29
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/README.md +56 -27
- xarray_sql-0.4.0rc1/benchmarks/duckdb_pushdown.py +103 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/01_ndvi.py +4 -4
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/02_climatology.py +7 -5
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/03_zonal_mean.py +8 -6
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/04_anomaly.py +7 -5
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/05_forecast_skill.py +4 -4
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/06_zonal_vector.py +8 -6
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/07_reproject_udf.py +20 -59
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/09_warp.py +6 -37
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/README.md +1 -1
- xarray_sql-0.4.0rc1/benchmarks/geospatial/_engines.py +291 -0
- xarray_sql-0.4.0rc1/benchmarks/geospatial/engine_suite.py +699 -0
- xarray_sql-0.4.0rc1/docs/engines.md +259 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/examples.md +12 -1
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/geospatial.md +153 -32
- xarray_sql-0.4.0rc1/docs/limitations.md +157 -0
- xarray_sql-0.4.0rc1/docs/performance.md +245 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/pyproject.toml +17 -1
- xarray_sql-0.4.0rc1/tests/test_arrow_dataset.py +542 -0
- xarray_sql-0.4.0rc1/tests/test_arrow_dataset_integration.py +405 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_cft.py +8 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_df.py +245 -10
- xarray_sql-0.4.0rc1/tests/test_duckdb_backend.py +387 -0
- xarray_sql-0.4.0rc1/tests/test_geometry.py +170 -0
- xarray_sql-0.4.0rc1/tests/test_lazy_roundtrip.py +377 -0
- xarray_sql-0.4.0rc1/tests/test_proj.py +158 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_sql.py +0 -53
- xarray_sql-0.4.0rc1/tests/test_sql_recipes.py +61 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_to_dataset_perf.py +18 -9
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/__init__.py +7 -0
- xarray_sql-0.4.0rc1/xarray_sql/backends/__init__.py +34 -0
- xarray_sql-0.4.0rc1/xarray_sql/backends/base.py +118 -0
- xarray_sql-0.4.0rc1/xarray_sql/backends/datafusion.py +46 -0
- xarray_sql-0.4.0rc1/xarray_sql/backends/duckdb.py +89 -0
- xarray_sql-0.4.0rc1/xarray_sql/backends/pyarrow.py +1143 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/cftime.py +17 -6
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/df.py +146 -21
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/ds.py +280 -127
- xarray_sql-0.4.0rc1/xarray_sql/geometry.py +131 -0
- xarray_sql-0.4.0rc1/xarray_sql/lazyscan.py +369 -0
- xarray_sql-0.4.0rc1/xarray_sql/proj.py +235 -0
- xarray_sql-0.4.0rc1/xarray_sql/roundtrip.py +495 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/sql.py +17 -18
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/zensical.toml +23 -8
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/.gitignore +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/AGENTS.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/LICENSE +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/08_regrid_weights.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/_harness.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/perf_summary.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/run_all.sh +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/run_perf.sh +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/assets/logo.svg +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/contributing.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/index.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/docs/reference/xarray_sql.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/src/lib.rs +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/__init__.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/conftest.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_ds.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_reader.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/tests/test_stats.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/core.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0rc1}/xarray_sql/reader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xarray_sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0rc1
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -25,15 +25,22 @@ Requires-Dist: pytest ; extra == 'dev'
|
|
|
25
25
|
Requires-Dist: watchfiles ; extra == 'dev'
|
|
26
26
|
Requires-Dist: zensical ; extra == 'docs'
|
|
27
27
|
Requires-Dist: mkdocstrings[python] ; extra == 'docs'
|
|
28
|
+
Requires-Dist: duckdb>=1.4.0 ; extra == 'duckdb'
|
|
29
|
+
Requires-Dist: pyproj ; extra == 'geo'
|
|
30
|
+
Requires-Dist: polars>=1.33 ; extra == 'polars'
|
|
28
31
|
Requires-Dist: cftime ; extra == 'test'
|
|
32
|
+
Requires-Dist: xarray-sql[duckdb,polars,geo] ; extra == 'test'
|
|
29
33
|
Requires-Dist: pytest ; extra == 'test'
|
|
30
34
|
Requires-Dist: xarray[io] ; extra == 'test'
|
|
31
35
|
Requires-Dist: gcsfs ; extra == 'test'
|
|
32
36
|
Provides-Extra: dev
|
|
33
37
|
Provides-Extra: docs
|
|
38
|
+
Provides-Extra: duckdb
|
|
39
|
+
Provides-Extra: geo
|
|
40
|
+
Provides-Extra: polars
|
|
34
41
|
Provides-Extra: test
|
|
35
42
|
License-File: LICENSE
|
|
36
|
-
Summary:
|
|
43
|
+
Summary: Query Xarray with SQL.
|
|
37
44
|
Author-email: Alexander Merose <al@merose.com>
|
|
38
45
|
License: Apache-2.0
|
|
39
46
|
Requires-Python: >=3.10
|
|
@@ -45,10 +52,13 @@ Project-URL: Issues, https://github.com/alxmrs/xarray-sql/issues
|
|
|
45
52
|
|
|
46
53
|
_Query [Xarray](https://xarray.dev/) with SQL_
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
[
|
|
56
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci.yml)
|
|
57
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/lint.yml)
|
|
58
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci-build.yml)
|
|
59
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci-rust.yml)
|
|
60
|
+
[](https://pepy.tech/projects/xarray-sql)
|
|
61
|
+
[](https://pepy.tech/projects/xarray-sql)
|
|
52
62
|
|
|
53
63
|
```shell
|
|
54
64
|
pip install xarray-sql
|
|
@@ -58,7 +68,11 @@ pip install xarray-sql
|
|
|
58
68
|
|
|
59
69
|
This is an experiment to provide a SQL interface for array datasets.
|
|
60
70
|
Succinctly, we "pivot" Xarray Datasets to treat them like tables so we can run
|
|
61
|
-
SQL queries against them.
|
|
71
|
+
SQL queries against them — on the query engine of your choice. xarray-sql
|
|
72
|
+
translates data, not queries: it registers a lazy Dataset as a table on
|
|
73
|
+
DataFusion (built in), DuckDB, or Polars, and turns any engine's Arrow result
|
|
74
|
+
back into a labeled Dataset. Dialects, geometry functions, and optimizers stay
|
|
75
|
+
with the engine.
|
|
62
76
|
|
|
63
77
|
## Quickstart
|
|
64
78
|
|
|
@@ -101,6 +115,21 @@ clim_ds["air"].plot() # in a script, call matplotlib.pyplot.show() to display
|
|
|
101
115
|
That's the round trip — Xarray in, SQL in the middle, Xarray (and a plot) back
|
|
102
116
|
out.
|
|
103
117
|
|
|
118
|
+
The same Dataset registers on other engines with one call — DuckDB gets a
|
|
119
|
+
native lazy table with predicate pushdown, Polars scans the same object:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import duckdb
|
|
123
|
+
|
|
124
|
+
con = duckdb.connect()
|
|
125
|
+
xql.register(con, 'air', ds, chunks=dict(time=100))
|
|
126
|
+
rel = con.sql('SELECT time, AVG("air") AS air FROM air GROUP BY time ORDER BY time')
|
|
127
|
+
xql.to_dataset(rel, template=ds) # any engine's Arrow result round-trips
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/engines/) for the support matrix, DuckDB/Polars details,
|
|
131
|
+
and the lazy chunked round-trip.
|
|
132
|
+
|
|
104
133
|
## A bigger example: ARCO-ERA5
|
|
105
134
|
|
|
106
135
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -173,6 +202,8 @@ result = ctx.sql('''
|
|
|
173
202
|
# | 775 | -2.3064649711534457 |
|
|
174
203
|
# +-------+----------------------+
|
|
175
204
|
|
|
205
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
206
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
176
207
|
ctx.sql('''
|
|
177
208
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
178
209
|
FROM era5.surface
|
|
@@ -180,8 +211,6 @@ ctx.sql('''
|
|
|
180
211
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
181
212
|
GROUP BY latitude, longitude
|
|
182
213
|
ORDER BY latitude DESC, longitude
|
|
183
|
-
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
184
|
-
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
185
214
|
''').to_dataset(template=ds)
|
|
186
215
|
# <xarray.Dataset> Size: 8MB
|
|
187
216
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -198,7 +227,7 @@ ctx.sql('''
|
|
|
198
227
|
```
|
|
199
228
|
|
|
200
229
|
_(A runnable version of this example lives at
|
|
201
|
-
[`perf_tests/era5_temp_profile.py`](perf_tests/era5_temp_profile.py).)_
|
|
230
|
+
[`perf_tests/era5_temp_profile.py`](https://github.com/xqlsystems/xarray-sql/blob/main/perf_tests/era5_temp_profile.py).)_
|
|
202
231
|
|
|
203
232
|
## Why build this?
|
|
204
233
|
|
|
@@ -231,6 +260,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
231
260
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
232
261
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
233
262
|
that lets the DB engine translate the underlying Dataset arrays into DataFusion partitions.
|
|
263
|
+
The same chunks-to-batches translation is also exposed as a
|
|
264
|
+
`pyarrow.dataset.Dataset` with predicate and projection pushdown, which is how
|
|
265
|
+
DuckDB and Polars consume registered Datasets with no engine-specific code.
|
|
234
266
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
235
267
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
236
268
|
|
|
@@ -253,15 +285,17 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
253
285
|
reproduces the published result that GraphCast beats Pangu at every lead.
|
|
254
286
|
* **Raster × vector zonal stats** — a range `JOIN` of the ERA5 grid against a
|
|
255
287
|
table of regions.
|
|
256
|
-
* **Reprojection and regridding** — a
|
|
257
|
-
|
|
288
|
+
* **Reprojection and regridding** — a `reproject(x, y, src_crs, dst_crs)`
|
|
289
|
+
scalar PROJ UDF, shipped as the optional geo extension
|
|
290
|
+
(`pip install xarray-sql[geo]`, validated against Earth Engine's own
|
|
291
|
+
geodesy via [Xee](https://github.com/google/Xee)) and a
|
|
258
292
|
sparse-weight-table `JOIN` (regridding real SRTM terrain).
|
|
259
293
|
|
|
260
294
|
Every case matches its array reference. The headline finding: these operations
|
|
261
295
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
262
296
|
functions, and `CASE` in disguise, and a query engine runs them at scale. See
|
|
263
|
-
[`benchmarks/geospatial/`](benchmarks/geospatial/) and the write-up,
|
|
264
|
-
[Geospatial operations are relational operations](
|
|
297
|
+
[`benchmarks/geospatial/`](https://github.com/xqlsystems/xarray-sql/tree/main/benchmarks/geospatial/) and the write-up,
|
|
298
|
+
[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/geospatial/).
|
|
265
299
|
|
|
266
300
|
## Why does this work?
|
|
267
301
|
|
|
@@ -269,15 +303,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
269
303
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
270
304
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
271
305
|
just changes this metadata (via a `ravel()`/`reshape()`), back into a column
|
|
272
|
-
amenable to a DataFrame. We take advantage of this
|
|
273
|
-
make chunked information scannable by a DB engine (DataFusion
|
|
306
|
+
amenable to a DataFrame. We take advantage of this lightweight metadata change to
|
|
307
|
+
make chunked information scannable by a DB engine (DataFusion, DuckDB, Polars —
|
|
308
|
+
anything that speaks Arrow).
|
|
274
309
|
|
|
275
310
|
## What are the current limitations?
|
|
276
311
|
|
|
277
|
-
|
|
312
|
+
The sharp edges we know about — per engine and fundamental — are cataloged in
|
|
313
|
+
[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/limitations/). Currently, we're looking for
|
|
278
314
|
early users – "tire kickers", if you will. We'd love your input to shape the direction of this
|
|
279
|
-
project! Please, give this a try and [file issues](https://github.com/
|
|
280
|
-
you see fit. Check out our [contributing guide](
|
|
315
|
+
project! Please, give this a try and [file issues](https://github.com/xqlsystems/xarray-sql/issues) as
|
|
316
|
+
you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/contributing/), too 😉.
|
|
281
317
|
|
|
282
318
|
## What would a deeper integration look like?
|
|
283
319
|
|
|
@@ -290,7 +326,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
290
326
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
291
327
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
292
328
|
and BigQuery. More thoughts on this
|
|
293
|
-
in [#4](https://github.com/
|
|
329
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
294
330
|
|
|
295
331
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
296
332
|
|
|
@@ -300,18 +336,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
300
336
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
301
337
|
|
|
302
338
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
303
|
-
- [DuckDB-Zarr](https://github.com/
|
|
339
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
304
340
|
|
|
305
341
|
## Roadmap
|
|
306
342
|
|
|
307
|
-
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/
|
|
308
|
-
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/
|
|
309
|
-
- [x] Support core datafusion optimizations to scan less data, like [104](https://github.com/
|
|
310
|
-
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/
|
|
311
|
-
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/
|
|
312
|
-
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/
|
|
313
|
-
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/
|
|
314
|
-
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/
|
|
343
|
+
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/xqlsystems/xarray-sql/issues/93).~ _Implemented in [#100](https://github.com/xqlsystems/xarray-sql/pull/100)_
|
|
344
|
+
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/xqlsystems/xarray-sql/issues/106)
|
|
345
|
+
- [x] Support core datafusion optimizations to scan less data, like [#104](https://github.com/xqlsystems/xarray-sql/issues/104), ...
|
|
346
|
+
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/xqlsystems/xarray-sql/issues/85).
|
|
347
|
+
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/xqlsystems/xarray-sql/issues/68) or Apache Ballista [#98](https://github.com/xqlsystems/xarray-sql/issues/98).
|
|
348
|
+
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/xqlsystems/xarray-sql/issues/36).
|
|
349
|
+
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
350
|
+
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/xqlsystems/xarray-sql/issues/34).
|
|
315
351
|
|
|
316
352
|
## Sponsors & Contributors
|
|
317
353
|
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
_Query [Xarray](https://xarray.dev/) with SQL_
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[
|
|
6
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/lint.yml)
|
|
8
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci-build.yml)
|
|
9
|
+
[](https://github.com/xqlsystems/xarray-sql/actions/workflows/ci-rust.yml)
|
|
10
|
+
[](https://pepy.tech/projects/xarray-sql)
|
|
11
|
+
[](https://pepy.tech/projects/xarray-sql)
|
|
9
12
|
|
|
10
13
|
```shell
|
|
11
14
|
pip install xarray-sql
|
|
@@ -15,7 +18,11 @@ pip install xarray-sql
|
|
|
15
18
|
|
|
16
19
|
This is an experiment to provide a SQL interface for array datasets.
|
|
17
20
|
Succinctly, we "pivot" Xarray Datasets to treat them like tables so we can run
|
|
18
|
-
SQL queries against them.
|
|
21
|
+
SQL queries against them — on the query engine of your choice. xarray-sql
|
|
22
|
+
translates data, not queries: it registers a lazy Dataset as a table on
|
|
23
|
+
DataFusion (built in), DuckDB, or Polars, and turns any engine's Arrow result
|
|
24
|
+
back into a labeled Dataset. Dialects, geometry functions, and optimizers stay
|
|
25
|
+
with the engine.
|
|
19
26
|
|
|
20
27
|
## Quickstart
|
|
21
28
|
|
|
@@ -58,6 +65,21 @@ clim_ds["air"].plot() # in a script, call matplotlib.pyplot.show() to display
|
|
|
58
65
|
That's the round trip — Xarray in, SQL in the middle, Xarray (and a plot) back
|
|
59
66
|
out.
|
|
60
67
|
|
|
68
|
+
The same Dataset registers on other engines with one call — DuckDB gets a
|
|
69
|
+
native lazy table with predicate pushdown, Polars scans the same object:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import duckdb
|
|
73
|
+
|
|
74
|
+
con = duckdb.connect()
|
|
75
|
+
xql.register(con, 'air', ds, chunks=dict(time=100))
|
|
76
|
+
rel = con.sql('SELECT time, AVG("air") AS air FROM air GROUP BY time ORDER BY time')
|
|
77
|
+
xql.to_dataset(rel, template=ds) # any engine's Arrow result round-trips
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/engines/) for the support matrix, DuckDB/Polars details,
|
|
81
|
+
and the lazy chunked round-trip.
|
|
82
|
+
|
|
61
83
|
## A bigger example: ARCO-ERA5
|
|
62
84
|
|
|
63
85
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -130,6 +152,8 @@ result = ctx.sql('''
|
|
|
130
152
|
# | 775 | -2.3064649711534457 |
|
|
131
153
|
# +-------+----------------------+
|
|
132
154
|
|
|
155
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
156
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
133
157
|
ctx.sql('''
|
|
134
158
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
135
159
|
FROM era5.surface
|
|
@@ -137,8 +161,6 @@ ctx.sql('''
|
|
|
137
161
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
138
162
|
GROUP BY latitude, longitude
|
|
139
163
|
ORDER BY latitude DESC, longitude
|
|
140
|
-
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
141
|
-
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
142
164
|
''').to_dataset(template=ds)
|
|
143
165
|
# <xarray.Dataset> Size: 8MB
|
|
144
166
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -155,7 +177,7 @@ ctx.sql('''
|
|
|
155
177
|
```
|
|
156
178
|
|
|
157
179
|
_(A runnable version of this example lives at
|
|
158
|
-
[`perf_tests/era5_temp_profile.py`](perf_tests/era5_temp_profile.py).)_
|
|
180
|
+
[`perf_tests/era5_temp_profile.py`](https://github.com/xqlsystems/xarray-sql/blob/main/perf_tests/era5_temp_profile.py).)_
|
|
159
181
|
|
|
160
182
|
## Why build this?
|
|
161
183
|
|
|
@@ -188,6 +210,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
188
210
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
189
211
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
190
212
|
that lets the DB engine translate the underlying Dataset arrays into DataFusion partitions.
|
|
213
|
+
The same chunks-to-batches translation is also exposed as a
|
|
214
|
+
`pyarrow.dataset.Dataset` with predicate and projection pushdown, which is how
|
|
215
|
+
DuckDB and Polars consume registered Datasets with no engine-specific code.
|
|
191
216
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
192
217
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
193
218
|
|
|
@@ -210,15 +235,17 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
210
235
|
reproduces the published result that GraphCast beats Pangu at every lead.
|
|
211
236
|
* **Raster × vector zonal stats** — a range `JOIN` of the ERA5 grid against a
|
|
212
237
|
table of regions.
|
|
213
|
-
* **Reprojection and regridding** — a
|
|
214
|
-
|
|
238
|
+
* **Reprojection and regridding** — a `reproject(x, y, src_crs, dst_crs)`
|
|
239
|
+
scalar PROJ UDF, shipped as the optional geo extension
|
|
240
|
+
(`pip install xarray-sql[geo]`, validated against Earth Engine's own
|
|
241
|
+
geodesy via [Xee](https://github.com/google/Xee)) and a
|
|
215
242
|
sparse-weight-table `JOIN` (regridding real SRTM terrain).
|
|
216
243
|
|
|
217
244
|
Every case matches its array reference. The headline finding: these operations
|
|
218
245
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
219
246
|
functions, and `CASE` in disguise, and a query engine runs them at scale. See
|
|
220
|
-
[`benchmarks/geospatial/`](benchmarks/geospatial/) and the write-up,
|
|
221
|
-
[Geospatial operations are relational operations](
|
|
247
|
+
[`benchmarks/geospatial/`](https://github.com/xqlsystems/xarray-sql/tree/main/benchmarks/geospatial/) and the write-up,
|
|
248
|
+
[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/geospatial/).
|
|
222
249
|
|
|
223
250
|
## Why does this work?
|
|
224
251
|
|
|
@@ -226,15 +253,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
226
253
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
227
254
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
228
255
|
just changes this metadata (via a `ravel()`/`reshape()`), back into a column
|
|
229
|
-
amenable to a DataFrame. We take advantage of this
|
|
230
|
-
make chunked information scannable by a DB engine (DataFusion
|
|
256
|
+
amenable to a DataFrame. We take advantage of this lightweight metadata change to
|
|
257
|
+
make chunked information scannable by a DB engine (DataFusion, DuckDB, Polars —
|
|
258
|
+
anything that speaks Arrow).
|
|
231
259
|
|
|
232
260
|
## What are the current limitations?
|
|
233
261
|
|
|
234
|
-
|
|
262
|
+
The sharp edges we know about — per engine and fundamental — are cataloged in
|
|
263
|
+
[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/limitations/). Currently, we're looking for
|
|
235
264
|
early users – "tire kickers", if you will. We'd love your input to shape the direction of this
|
|
236
|
-
project! Please, give this a try and [file issues](https://github.com/
|
|
237
|
-
you see fit. Check out our [contributing guide](
|
|
265
|
+
project! Please, give this a try and [file issues](https://github.com/xqlsystems/xarray-sql/issues) as
|
|
266
|
+
you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/contributing/), too 😉.
|
|
238
267
|
|
|
239
268
|
## What would a deeper integration look like?
|
|
240
269
|
|
|
@@ -247,7 +276,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
247
276
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
248
277
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
249
278
|
and BigQuery. More thoughts on this
|
|
250
|
-
in [#4](https://github.com/
|
|
279
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
251
280
|
|
|
252
281
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
253
282
|
|
|
@@ -257,18 +286,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
257
286
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
258
287
|
|
|
259
288
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
260
|
-
- [DuckDB-Zarr](https://github.com/
|
|
289
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
261
290
|
|
|
262
291
|
## Roadmap
|
|
263
292
|
|
|
264
|
-
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/
|
|
265
|
-
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/
|
|
266
|
-
- [x] Support core datafusion optimizations to scan less data, like [104](https://github.com/
|
|
267
|
-
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/
|
|
268
|
-
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/
|
|
269
|
-
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/
|
|
270
|
-
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/
|
|
271
|
-
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/
|
|
293
|
+
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/xqlsystems/xarray-sql/issues/93).~ _Implemented in [#100](https://github.com/xqlsystems/xarray-sql/pull/100)_
|
|
294
|
+
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/xqlsystems/xarray-sql/issues/106)
|
|
295
|
+
- [x] Support core datafusion optimizations to scan less data, like [#104](https://github.com/xqlsystems/xarray-sql/issues/104), ...
|
|
296
|
+
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/xqlsystems/xarray-sql/issues/85).
|
|
297
|
+
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/xqlsystems/xarray-sql/issues/68) or Apache Ballista [#98](https://github.com/xqlsystems/xarray-sql/issues/98).
|
|
298
|
+
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/xqlsystems/xarray-sql/issues/36).
|
|
299
|
+
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
300
|
+
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/xqlsystems/xarray-sql/issues/34).
|
|
272
301
|
|
|
273
302
|
## Sponsors & Contributors
|
|
274
303
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Benchmark: DuckDB re-scannable stream vs pushdown dataset vs ceiling.
|
|
2
|
+
|
|
3
|
+
Times the three ways DuckDB can consume the same 10M-row synthetic
|
|
4
|
+
dataset — the re-scannable stream (no pushdown), the default
|
|
5
|
+
``register()`` pushdown dataset, and an in-memory ``pyarrow.dataset``
|
|
6
|
+
as the ceiling — and asserts at the end that all three returned the
|
|
7
|
+
same answers. Cross-engine comparisons live in
|
|
8
|
+
``benchmarks/geospatial/``; this measures the adapter paths within one
|
|
9
|
+
engine.
|
|
10
|
+
|
|
11
|
+
Usage: python benchmarks/duckdb_pushdown.py (needs duckdb installed)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import statistics
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import duckdb
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import pyarrow.dataset as pads
|
|
22
|
+
import xarray as xr
|
|
23
|
+
|
|
24
|
+
import xarray_sql as xql
|
|
25
|
+
from xarray_sql.backends.duckdb import XarrayArrowStream
|
|
26
|
+
|
|
27
|
+
np.random.seed(0)
|
|
28
|
+
N_TIME, N_LAT, N_LON = 1000, 100, 100 # 10M rows
|
|
29
|
+
ds = xr.Dataset(
|
|
30
|
+
{
|
|
31
|
+
"temperature": (
|
|
32
|
+
["time", "lat", "lon"],
|
|
33
|
+
np.random.rand(N_TIME, N_LAT, N_LON),
|
|
34
|
+
),
|
|
35
|
+
"humidity": (
|
|
36
|
+
["time", "lat", "lon"],
|
|
37
|
+
np.random.rand(N_TIME, N_LAT, N_LON),
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
coords={
|
|
41
|
+
"time": pd.date_range("2020-01-01", periods=N_TIME, freq="h"),
|
|
42
|
+
"lat": np.linspace(-90, 90, N_LAT),
|
|
43
|
+
"lon": np.linspace(-180, 180, N_LON),
|
|
44
|
+
},
|
|
45
|
+
).chunk({"time": 50}) # 20 partitions
|
|
46
|
+
|
|
47
|
+
con = duckdb.connect()
|
|
48
|
+
|
|
49
|
+
QUERIES = {
|
|
50
|
+
"full AVG scan": "SELECT AVG(temperature) FROM {t}",
|
|
51
|
+
"1pct time filter": (
|
|
52
|
+
"SELECT AVG(temperature) FROM {t} WHERE time < '2020-01-01 10:00:00'"
|
|
53
|
+
),
|
|
54
|
+
"bbox filter": (
|
|
55
|
+
"SELECT AVG(temperature) FROM {t} "
|
|
56
|
+
"WHERE lat BETWEEN 0 AND 10 AND lon BETWEEN 0 AND 20"
|
|
57
|
+
),
|
|
58
|
+
"projection (1 of 2 vars)": "SELECT AVG(humidity) FROM {t}",
|
|
59
|
+
"count only": "SELECT COUNT(*) FROM {t}",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def bench(table, label, n=5):
|
|
64
|
+
"""Times each query; returns {query: answer} for equivalence checks."""
|
|
65
|
+
print(f"\n== {label} ==")
|
|
66
|
+
answers = {}
|
|
67
|
+
for qname, q in QUERIES.items():
|
|
68
|
+
sql = q.format(t=table)
|
|
69
|
+
times = []
|
|
70
|
+
for _ in range(n):
|
|
71
|
+
t0 = time.perf_counter()
|
|
72
|
+
r = con.sql(sql).fetchall()
|
|
73
|
+
times.append(time.perf_counter() - t0)
|
|
74
|
+
answers[qname] = r[0][0]
|
|
75
|
+
med = statistics.median(times)
|
|
76
|
+
print(
|
|
77
|
+
f" {qname:28s} {med:8.3f}s "
|
|
78
|
+
f"(min {min(times):.3f} / max {max(times):.3f}) -> {r[0][0]:.6g}"
|
|
79
|
+
)
|
|
80
|
+
return answers
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# re-scannable stream, registered via the stream wrapper explicitly:
|
|
84
|
+
# DuckDB scans every row, no filter/projection pushdown
|
|
85
|
+
con.register("t_stream", XarrayArrowStream(ds))
|
|
86
|
+
stream = bench("t_stream", "stream (no pushdown)")
|
|
87
|
+
|
|
88
|
+
# default register(): the pushdown pyarrow-dataset path
|
|
89
|
+
xql.register(con, "t_pushdown", ds)
|
|
90
|
+
pushdown = bench("t_pushdown", "register() [pushdown]")
|
|
91
|
+
|
|
92
|
+
# ceiling: materialized pa.Table via pyarrow.dataset
|
|
93
|
+
table = xql.read_xarray(ds).read_all()
|
|
94
|
+
con.register("t_ceiling", pads.dataset(table))
|
|
95
|
+
ceiling = bench("t_ceiling", "ceiling: in-memory pyarrow.dataset")
|
|
96
|
+
|
|
97
|
+
# The timings are only meaningful if every path computed the same thing.
|
|
98
|
+
for qname in QUERIES:
|
|
99
|
+
a, b, c = stream[qname], pushdown[qname], ceiling[qname]
|
|
100
|
+
assert math.isclose(a, b, rel_tol=1e-9) and math.isclose(
|
|
101
|
+
a, c, rel_tol=1e-9
|
|
102
|
+
), f"{qname}: paths disagree — stream={a} pushdown={b} ceiling={c}"
|
|
103
|
+
print("\nall paths agree")
|
|
@@ -45,8 +45,7 @@ from __future__ import annotations
|
|
|
45
45
|
|
|
46
46
|
import xarray as xr
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
from _engines import EngineContext
|
|
50
49
|
from _harness import (
|
|
51
50
|
CaseSkipped,
|
|
52
51
|
assert_grid_close,
|
|
@@ -111,7 +110,8 @@ def main() -> None:
|
|
|
111
110
|
f" scene window: {dict(scene.sizes)} ({n:,} pixels, B04=red/B08=NIR)"
|
|
112
111
|
)
|
|
113
112
|
|
|
114
|
-
ctx =
|
|
113
|
+
ctx = EngineContext()
|
|
114
|
+
print(f" engine: {ctx.flavor}")
|
|
115
115
|
ctx.from_dataset("scene", scene, chunks={"y": 256, "x": 256})
|
|
116
116
|
|
|
117
117
|
sql = """
|
|
@@ -122,7 +122,7 @@ def main() -> None:
|
|
|
122
122
|
show_sql(sql)
|
|
123
123
|
|
|
124
124
|
for _ in measured("SQL NDVI"):
|
|
125
|
-
got = ctx.
|
|
125
|
+
got = ctx.sql_to_dataset(sql, dims=["y", "x"]).ndvi
|
|
126
126
|
|
|
127
127
|
# Array reference: the same formula in pure xarray. ``.compute()`` reads the
|
|
128
128
|
# window and evaluates it here (the scene is lazy), so this measures the same
|
|
@@ -42,8 +42,7 @@ import datetime
|
|
|
42
42
|
|
|
43
43
|
import xarray as xr
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
from _engines import EngineContext
|
|
47
46
|
from _harness import (
|
|
48
47
|
CaseSkipped,
|
|
49
48
|
assert_grid_close,
|
|
@@ -81,7 +80,8 @@ def main() -> None:
|
|
|
81
80
|
except Exception as exc: # noqa: BLE001 — any failure → skip, not crash
|
|
82
81
|
raise CaseSkipped(f"ARCO-ERA5 unavailable ({exc})") from exc
|
|
83
82
|
|
|
84
|
-
ctx =
|
|
83
|
+
ctx = EngineContext()
|
|
84
|
+
print(f" engine: {ctx.flavor}")
|
|
85
85
|
with timed("register full ERA5 (lazy)"):
|
|
86
86
|
ctx.from_dataset(
|
|
87
87
|
"era5",
|
|
@@ -110,8 +110,10 @@ def main() -> None:
|
|
|
110
110
|
# A climatology is a gridded product: round-trip the result back to an
|
|
111
111
|
# xarray Dataset keyed by (latitude, longitude, hour) — how it is used.
|
|
112
112
|
for _ in measured("SQL diurnal climatology (lazy read)"):
|
|
113
|
-
got = ctx.
|
|
114
|
-
|
|
113
|
+
got = ctx.sql_to_dataset(
|
|
114
|
+
sql,
|
|
115
|
+
dims=["latitude", "longitude", "hour"],
|
|
116
|
+
param_values=_PARAMS,
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
# Array reference: the textbook groupby-over-the-cycle reduction, in °C —
|
|
@@ -36,8 +36,7 @@ import datetime
|
|
|
36
36
|
|
|
37
37
|
import xarray as xr
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
from _engines import EngineContext
|
|
41
40
|
from _harness import (
|
|
42
41
|
CaseSkipped,
|
|
43
42
|
assert_grid_close,
|
|
@@ -75,7 +74,8 @@ def main() -> None:
|
|
|
75
74
|
|
|
76
75
|
# ERA5 mixes surface (time, lat, lon) and atmospheric (… level …) variables,
|
|
77
76
|
# so register it as two tables under an ``era5`` schema.
|
|
78
|
-
ctx =
|
|
77
|
+
ctx = EngineContext()
|
|
78
|
+
print(f" engine: {ctx.flavor}")
|
|
79
79
|
with timed("register full ERA5"):
|
|
80
80
|
ctx.from_dataset(
|
|
81
81
|
"era5",
|
|
@@ -101,9 +101,11 @@ def main() -> None:
|
|
|
101
101
|
|
|
102
102
|
# Round-trip the profile back to an xarray Dataset keyed by latitude.
|
|
103
103
|
for _ in measured("SQL zonal mean (reads one day)"):
|
|
104
|
-
got = ctx.
|
|
105
|
-
sql,
|
|
106
|
-
|
|
104
|
+
got = ctx.sql_to_dataset(
|
|
105
|
+
sql,
|
|
106
|
+
dims=["latitude"],
|
|
107
|
+
param_values={"start": _START, "end": _END},
|
|
108
|
+
)
|
|
107
109
|
|
|
108
110
|
# Array reference: reduce the same day over the two un-grouped axes.
|
|
109
111
|
for _ in measured("xarray reference"):
|
|
@@ -40,8 +40,7 @@ import datetime
|
|
|
40
40
|
|
|
41
41
|
import xarray as xr
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
from _engines import EngineContext
|
|
45
44
|
from _harness import (
|
|
46
45
|
CaseSkipped,
|
|
47
46
|
assert_grid_close,
|
|
@@ -74,7 +73,8 @@ def main() -> None:
|
|
|
74
73
|
except Exception as exc: # noqa: BLE001 — any failure → skip, not crash
|
|
75
74
|
raise CaseSkipped(f"ARCO-ERA5 unavailable ({exc})") from exc
|
|
76
75
|
|
|
77
|
-
ctx =
|
|
76
|
+
ctx = EngineContext()
|
|
77
|
+
print(f" engine: {ctx.flavor}")
|
|
78
78
|
with timed("register full ERA5 (lazy)"):
|
|
79
79
|
ctx.from_dataset(
|
|
80
80
|
"era5",
|
|
@@ -113,8 +113,10 @@ def main() -> None:
|
|
|
113
113
|
|
|
114
114
|
# The anomaly is a gridded field; round-trip it to (time, lat, lon).
|
|
115
115
|
for _ in measured("SQL anomaly (climatology CTE self-join, lazy read)"):
|
|
116
|
-
got = ctx.
|
|
117
|
-
|
|
116
|
+
got = ctx.sql_to_dataset(
|
|
117
|
+
sql,
|
|
118
|
+
dims=["time", "latitude", "longitude"],
|
|
119
|
+
param_values=_PARAMS,
|
|
118
120
|
)
|
|
119
121
|
|
|
120
122
|
# Array reference: grouped broadcast-subtract, in pure xarray (lazy window).
|