xarray_sql 0.3.2__tar.gz → 0.4.0__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.4.0/.envrc +1 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/.gitignore +1 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/Cargo.lock +2 -1
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/Cargo.toml +6 -5
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/PKG-INFO +71 -29
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/README.md +62 -27
- xarray_sql-0.4.0/benchmarks/duckdb_pushdown.py +103 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/01_ndvi.py +4 -4
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/02_climatology.py +7 -5
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/03_zonal_mean.py +8 -6
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/04_anomaly.py +7 -5
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/05_forecast_skill.py +4 -4
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/06_zonal_vector.py +8 -6
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/07_reproject_udf.py +20 -59
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/09_warp.py +6 -37
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/README.md +1 -1
- xarray_sql-0.4.0/benchmarks/geospatial/_engines.py +276 -0
- xarray_sql-0.4.0/benchmarks/geospatial/engine_suite.py +699 -0
- xarray_sql-0.4.0/docs/engines.md +297 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/examples.md +56 -2
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/geospatial.md +153 -32
- xarray_sql-0.4.0/docs/limitations.md +180 -0
- xarray_sql-0.4.0/docs/performance.md +245 -0
- xarray_sql-0.4.0/flake.lock +61 -0
- xarray_sql-0.4.0/flake.nix +44 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/pyproject.toml +19 -1
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/src/lib.rs +466 -94
- xarray_sql-0.4.0/tests/test_arrow_dataset.py +542 -0
- xarray_sql-0.4.0/tests/test_arrow_dataset_integration.py +405 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_cft.py +8 -0
- xarray_sql-0.4.0/tests/test_coord_lookup.py +167 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_df.py +328 -10
- xarray_sql-0.4.0/tests/test_duckdb_backend.py +387 -0
- xarray_sql-0.4.0/tests/test_geometry.py +170 -0
- xarray_sql-0.4.0/tests/test_lazy_roundtrip.py +377 -0
- xarray_sql-0.4.0/tests/test_proj.py +158 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_reader.py +268 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_sql.py +31 -53
- xarray_sql-0.4.0/tests/test_sql_recipes.py +61 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_stats.py +48 -0
- xarray_sql-0.4.0/tests/test_table_names.py +368 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_to_dataset_perf.py +18 -9
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/__init__.py +8 -0
- xarray_sql-0.4.0/xarray_sql/backends/__init__.py +36 -0
- xarray_sql-0.4.0/xarray_sql/backends/base.py +142 -0
- xarray_sql-0.4.0/xarray_sql/backends/datafusion.py +78 -0
- xarray_sql-0.4.0/xarray_sql/backends/duckdb.py +193 -0
- xarray_sql-0.4.0/xarray_sql/backends/pyarrow.py +1239 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/cftime.py +21 -8
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/df.py +306 -44
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/ds.py +350 -140
- xarray_sql-0.4.0/xarray_sql/geometry.py +131 -0
- xarray_sql-0.4.0/xarray_sql/lazyscan.py +375 -0
- xarray_sql-0.4.0/xarray_sql/proj.py +235 -0
- xarray_sql-0.4.0/xarray_sql/roundtrip.py +495 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/sql.py +27 -26
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/zensical.toml +27 -8
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/AGENTS.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/LICENSE +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/08_regrid_weights.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/_harness.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/perf_summary.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/run_all.sh +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/benchmarks/geospatial/run_perf.sh +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/assets/logo.svg +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/contributing.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/index.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/docs/reference/xarray_sql.md +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/__init__.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/conftest.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/tests/test_ds.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/core.py +0 -0
- {xarray_sql-0.3.2 → xarray_sql-0.4.0}/xarray_sql/reader.py +0 -0
xarray_sql-0.4.0/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake
|
|
@@ -3367,7 +3367,7 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
|
|
3367
3367
|
|
|
3368
3368
|
[[package]]
|
|
3369
3369
|
name = "xarray_sql"
|
|
3370
|
-
version = "0.
|
|
3370
|
+
version = "0.4.0"
|
|
3371
3371
|
dependencies = [
|
|
3372
3372
|
"arrow",
|
|
3373
3373
|
"async-stream",
|
|
@@ -3375,6 +3375,7 @@ dependencies = [
|
|
|
3375
3375
|
"datafusion",
|
|
3376
3376
|
"datafusion-ffi",
|
|
3377
3377
|
"futures",
|
|
3378
|
+
"half",
|
|
3378
3379
|
"pyo3",
|
|
3379
3380
|
"pyo3-build-config",
|
|
3380
3381
|
"tokio",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "xarray_sql"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
authors = ["Alex Merose"]
|
|
5
5
|
edition = "2021"
|
|
6
6
|
exclude = [
|
|
@@ -25,11 +25,12 @@ async-trait = "0.1"
|
|
|
25
25
|
datafusion = { version = "54.0.0" }
|
|
26
26
|
datafusion-ffi = { version = "54.0.0" }
|
|
27
27
|
futures = { version = "0.3" }
|
|
28
|
+
half = "2.7"
|
|
28
29
|
# `abi3-py310` builds against CPython's stable ABI, so a single wheel per
|
|
29
|
-
# platform works on all CPython >= 3.10 (matching `requires-python`).
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
pyo3 = { version = "0.28.0", features = ["
|
|
30
|
+
# platform works on all CPython >= 3.10 (matching `requires-python`). Maturin
|
|
31
|
+
# enables `pyo3/extension-module` through pyproject.toml for wheel builds; it
|
|
32
|
+
# must stay disabled for ordinary Cargo test binaries so they link libpython.
|
|
33
|
+
pyo3 = { version = "0.28.0", features = ["abi3-py310"] }
|
|
33
34
|
tokio = { version = "1.46.1", features = ["rt"] }
|
|
34
35
|
|
|
35
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xarray_sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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,24 @@ 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
|
+
`table_names` (below) works the same way on every engine, so a query written
|
|
131
|
+
against `era5.surface` is not tied to the engine it was written for.
|
|
132
|
+
|
|
133
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/latest/engines/) for the support matrix, DuckDB/Polars details,
|
|
134
|
+
and the lazy chunked round-trip.
|
|
135
|
+
|
|
104
136
|
## A bigger example: ARCO-ERA5
|
|
105
137
|
|
|
106
138
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -173,6 +205,8 @@ result = ctx.sql('''
|
|
|
173
205
|
# | 775 | -2.3064649711534457 |
|
|
174
206
|
# +-------+----------------------+
|
|
175
207
|
|
|
208
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
209
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
176
210
|
ctx.sql('''
|
|
177
211
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
178
212
|
FROM era5.surface
|
|
@@ -180,8 +214,6 @@ ctx.sql('''
|
|
|
180
214
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
181
215
|
GROUP BY latitude, longitude
|
|
182
216
|
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
217
|
''').to_dataset(template=ds)
|
|
186
218
|
# <xarray.Dataset> Size: 8MB
|
|
187
219
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -198,7 +230,7 @@ ctx.sql('''
|
|
|
198
230
|
```
|
|
199
231
|
|
|
200
232
|
_(A runnable version of this example lives at
|
|
201
|
-
[`perf_tests/era5_temp_profile.py`](perf_tests/era5_temp_profile.py).)_
|
|
233
|
+
[`perf_tests/era5_temp_profile.py`](https://github.com/xqlsystems/xarray-sql/blob/main/perf_tests/era5_temp_profile.py).)_
|
|
202
234
|
|
|
203
235
|
## Why build this?
|
|
204
236
|
|
|
@@ -231,6 +263,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
231
263
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
232
264
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
233
265
|
that lets the DB engine translate the underlying Dataset arrays into DataFusion partitions.
|
|
266
|
+
The same chunks-to-batches translation is also exposed as a
|
|
267
|
+
`pyarrow.dataset.Dataset` with predicate and projection pushdown, which is how
|
|
268
|
+
DuckDB and Polars consume registered Datasets with no engine-specific code.
|
|
234
269
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
235
270
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
236
271
|
|
|
@@ -253,15 +288,17 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
253
288
|
reproduces the published result that GraphCast beats Pangu at every lead.
|
|
254
289
|
* **Raster × vector zonal stats** — a range `JOIN` of the ERA5 grid against a
|
|
255
290
|
table of regions.
|
|
256
|
-
* **Reprojection and regridding** — a
|
|
257
|
-
|
|
291
|
+
* **Reprojection and regridding** — a `reproject(x, y, src_crs, dst_crs)`
|
|
292
|
+
scalar PROJ UDF, shipped as the optional geo extension
|
|
293
|
+
(`pip install xarray-sql[geo]`, validated against Earth Engine's own
|
|
294
|
+
geodesy via [Xee](https://github.com/google/Xee)) and a
|
|
258
295
|
sparse-weight-table `JOIN` (regridding real SRTM terrain).
|
|
259
296
|
|
|
260
297
|
Every case matches its array reference. The headline finding: these operations
|
|
261
298
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
262
299
|
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](
|
|
300
|
+
[`benchmarks/geospatial/`](https://github.com/xqlsystems/xarray-sql/tree/main/benchmarks/geospatial/) and the write-up,
|
|
301
|
+
[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/latest/geospatial/).
|
|
265
302
|
|
|
266
303
|
## Why does this work?
|
|
267
304
|
|
|
@@ -269,15 +306,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
269
306
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
270
307
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
271
308
|
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
|
|
309
|
+
amenable to a DataFrame. We take advantage of this lightweight metadata change to
|
|
310
|
+
make chunked information scannable by a DB engine (DataFusion, DuckDB, Polars —
|
|
311
|
+
anything that speaks Arrow).
|
|
274
312
|
|
|
275
313
|
## What are the current limitations?
|
|
276
314
|
|
|
277
|
-
|
|
315
|
+
The sharp edges we know about — per engine and fundamental — are cataloged in
|
|
316
|
+
[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/latest/limitations/). Currently, we're looking for
|
|
278
317
|
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](
|
|
318
|
+
project! Please, give this a try and [file issues](https://github.com/xqlsystems/xarray-sql/issues) as
|
|
319
|
+
you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/latest/contributing/), too 😉.
|
|
281
320
|
|
|
282
321
|
## What would a deeper integration look like?
|
|
283
322
|
|
|
@@ -290,7 +329,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
290
329
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
291
330
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
292
331
|
and BigQuery. More thoughts on this
|
|
293
|
-
in [#4](https://github.com/
|
|
332
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
294
333
|
|
|
295
334
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
296
335
|
|
|
@@ -300,18 +339,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
300
339
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
301
340
|
|
|
302
341
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
303
|
-
- [DuckDB-Zarr](https://github.com/
|
|
342
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
304
343
|
|
|
305
344
|
## Roadmap
|
|
306
345
|
|
|
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/
|
|
346
|
+
- [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)_
|
|
347
|
+
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/xqlsystems/xarray-sql/issues/106)
|
|
348
|
+
- [x] Support core datafusion optimizations to scan less data, like [#104](https://github.com/xqlsystems/xarray-sql/issues/104), ...
|
|
349
|
+
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/xqlsystems/xarray-sql/issues/85).
|
|
350
|
+
- [ ] 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).
|
|
351
|
+
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/xqlsystems/xarray-sql/issues/36).
|
|
352
|
+
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
353
|
+
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/xqlsystems/xarray-sql/issues/34).
|
|
315
354
|
|
|
316
355
|
## Sponsors & Contributors
|
|
317
356
|
|
|
@@ -331,6 +370,9 @@ I want to give a special thanks to the following folks and institutions:
|
|
|
331
370
|
changes.
|
|
332
371
|
- Aman Kumar for spending a considerable amount of his GSoC internship
|
|
333
372
|
contributing to this project.
|
|
373
|
+
- Miguel Moncada Isla for reimagining this project to be a cross SQL engine
|
|
374
|
+
interface to and from Xarray. He brought new life to this experiment, in my
|
|
375
|
+
opinion.
|
|
334
376
|
|
|
335
377
|
|
|
336
378
|
## License
|
|
@@ -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,24 @@ 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
|
+
`table_names` (below) works the same way on every engine, so a query written
|
|
81
|
+
against `era5.surface` is not tied to the engine it was written for.
|
|
82
|
+
|
|
83
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/latest/engines/) for the support matrix, DuckDB/Polars details,
|
|
84
|
+
and the lazy chunked round-trip.
|
|
85
|
+
|
|
61
86
|
## A bigger example: ARCO-ERA5
|
|
62
87
|
|
|
63
88
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -130,6 +155,8 @@ result = ctx.sql('''
|
|
|
130
155
|
# | 775 | -2.3064649711534457 |
|
|
131
156
|
# +-------+----------------------+
|
|
132
157
|
|
|
158
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
159
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
133
160
|
ctx.sql('''
|
|
134
161
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
135
162
|
FROM era5.surface
|
|
@@ -137,8 +164,6 @@ ctx.sql('''
|
|
|
137
164
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
138
165
|
GROUP BY latitude, longitude
|
|
139
166
|
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
167
|
''').to_dataset(template=ds)
|
|
143
168
|
# <xarray.Dataset> Size: 8MB
|
|
144
169
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -155,7 +180,7 @@ ctx.sql('''
|
|
|
155
180
|
```
|
|
156
181
|
|
|
157
182
|
_(A runnable version of this example lives at
|
|
158
|
-
[`perf_tests/era5_temp_profile.py`](perf_tests/era5_temp_profile.py).)_
|
|
183
|
+
[`perf_tests/era5_temp_profile.py`](https://github.com/xqlsystems/xarray-sql/blob/main/perf_tests/era5_temp_profile.py).)_
|
|
159
184
|
|
|
160
185
|
## Why build this?
|
|
161
186
|
|
|
@@ -188,6 +213,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
188
213
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
189
214
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
190
215
|
that lets the DB engine translate the underlying Dataset arrays into DataFusion partitions.
|
|
216
|
+
The same chunks-to-batches translation is also exposed as a
|
|
217
|
+
`pyarrow.dataset.Dataset` with predicate and projection pushdown, which is how
|
|
218
|
+
DuckDB and Polars consume registered Datasets with no engine-specific code.
|
|
191
219
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
192
220
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
193
221
|
|
|
@@ -210,15 +238,17 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
210
238
|
reproduces the published result that GraphCast beats Pangu at every lead.
|
|
211
239
|
* **Raster × vector zonal stats** — a range `JOIN` of the ERA5 grid against a
|
|
212
240
|
table of regions.
|
|
213
|
-
* **Reprojection and regridding** — a
|
|
214
|
-
|
|
241
|
+
* **Reprojection and regridding** — a `reproject(x, y, src_crs, dst_crs)`
|
|
242
|
+
scalar PROJ UDF, shipped as the optional geo extension
|
|
243
|
+
(`pip install xarray-sql[geo]`, validated against Earth Engine's own
|
|
244
|
+
geodesy via [Xee](https://github.com/google/Xee)) and a
|
|
215
245
|
sparse-weight-table `JOIN` (regridding real SRTM terrain).
|
|
216
246
|
|
|
217
247
|
Every case matches its array reference. The headline finding: these operations
|
|
218
248
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
219
249
|
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](
|
|
250
|
+
[`benchmarks/geospatial/`](https://github.com/xqlsystems/xarray-sql/tree/main/benchmarks/geospatial/) and the write-up,
|
|
251
|
+
[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/latest/geospatial/).
|
|
222
252
|
|
|
223
253
|
## Why does this work?
|
|
224
254
|
|
|
@@ -226,15 +256,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
226
256
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
227
257
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
228
258
|
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
|
|
259
|
+
amenable to a DataFrame. We take advantage of this lightweight metadata change to
|
|
260
|
+
make chunked information scannable by a DB engine (DataFusion, DuckDB, Polars —
|
|
261
|
+
anything that speaks Arrow).
|
|
231
262
|
|
|
232
263
|
## What are the current limitations?
|
|
233
264
|
|
|
234
|
-
|
|
265
|
+
The sharp edges we know about — per engine and fundamental — are cataloged in
|
|
266
|
+
[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/latest/limitations/). Currently, we're looking for
|
|
235
267
|
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](
|
|
268
|
+
project! Please, give this a try and [file issues](https://github.com/xqlsystems/xarray-sql/issues) as
|
|
269
|
+
you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/latest/contributing/), too 😉.
|
|
238
270
|
|
|
239
271
|
## What would a deeper integration look like?
|
|
240
272
|
|
|
@@ -247,7 +279,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
247
279
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
248
280
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
249
281
|
and BigQuery. More thoughts on this
|
|
250
|
-
in [#4](https://github.com/
|
|
282
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
251
283
|
|
|
252
284
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
253
285
|
|
|
@@ -257,18 +289,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
257
289
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
258
290
|
|
|
259
291
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
260
|
-
- [DuckDB-Zarr](https://github.com/
|
|
292
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
261
293
|
|
|
262
294
|
## Roadmap
|
|
263
295
|
|
|
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/
|
|
296
|
+
- [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)_
|
|
297
|
+
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/xqlsystems/xarray-sql/issues/106)
|
|
298
|
+
- [x] Support core datafusion optimizations to scan less data, like [#104](https://github.com/xqlsystems/xarray-sql/issues/104), ...
|
|
299
|
+
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/xqlsystems/xarray-sql/issues/85).
|
|
300
|
+
- [ ] 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).
|
|
301
|
+
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/xqlsystems/xarray-sql/issues/36).
|
|
302
|
+
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
303
|
+
- [ ] (To be formally announced eventually): The 100 Trillion Row Challenge [#34](https://github.com/xqlsystems/xarray-sql/issues/34).
|
|
272
304
|
|
|
273
305
|
## Sponsors & Contributors
|
|
274
306
|
|
|
@@ -288,6 +320,9 @@ I want to give a special thanks to the following folks and institutions:
|
|
|
288
320
|
changes.
|
|
289
321
|
- Aman Kumar for spending a considerable amount of his GSoC internship
|
|
290
322
|
contributing to this project.
|
|
323
|
+
- Miguel Moncada Isla for reimagining this project to be a cross SQL engine
|
|
324
|
+
interface to and from Xarray. He brought new life to this experiment, in my
|
|
325
|
+
opinion.
|
|
291
326
|
|
|
292
327
|
|
|
293
328
|
## License
|
|
@@ -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 —
|