xarray_sql 0.3.3__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.3 → xarray_sql-0.4.0}/.gitignore +1 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/Cargo.lock +2 -1
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/Cargo.toml +6 -5
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/PKG-INFO +62 -28
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/README.md +55 -25
- xarray_sql-0.4.0/benchmarks/duckdb_pushdown.py +103 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/01_ndvi.py +4 -4
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/02_climatology.py +7 -5
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/03_zonal_mean.py +8 -6
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/04_anomaly.py +7 -5
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/05_forecast_skill.py +4 -4
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/06_zonal_vector.py +8 -6
- 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.3 → xarray_sql-0.4.0}/docs/examples.md +56 -2
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/docs/geospatial.md +135 -24
- 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.3 → xarray_sql-0.4.0}/pyproject.toml +16 -2
- {xarray_sql-0.3.3 → 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.4.0/tests/test_coord_lookup.py +167 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/test_df.py +180 -23
- 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.3.3 → xarray_sql-0.4.0}/tests/test_reader.py +268 -0
- {xarray_sql-0.3.3 → 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.3 → 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.3 → 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.3 → xarray_sql-0.4.0}/xarray_sql/cftime.py +10 -6
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/xarray_sql/df.py +260 -43
- {xarray_sql-0.3.3 → 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.3.3 → xarray_sql-0.4.0}/xarray_sql/proj.py +5 -5
- xarray_sql-0.4.0/xarray_sql/roundtrip.py +495 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/xarray_sql/sql.py +14 -26
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/zensical.toml +27 -8
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/AGENTS.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/LICENSE +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/07_reproject_udf.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/08_regrid_weights.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/09_warp.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/README.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/_harness.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/perf_summary.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/run_all.sh +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/benchmarks/geospatial/run_perf.sh +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/docs/assets/logo.svg +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/docs/contributing.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/docs/index.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/docs/reference/xarray_sql.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/__init__.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/conftest.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/test_cft.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/test_ds.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/test_proj.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/tests/test_to_dataset_perf.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0}/xarray_sql/core.py +0 -0
- {xarray_sql-0.3.3 → 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,18 +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'
|
|
28
29
|
Requires-Dist: pyproj ; extra == 'geo'
|
|
30
|
+
Requires-Dist: polars>=1.33 ; extra == 'polars'
|
|
29
31
|
Requires-Dist: cftime ; extra == 'test'
|
|
30
|
-
Requires-Dist: xarray-sql[geo] ; extra == 'test'
|
|
32
|
+
Requires-Dist: xarray-sql[duckdb,polars,geo] ; extra == 'test'
|
|
31
33
|
Requires-Dist: pytest ; extra == 'test'
|
|
32
34
|
Requires-Dist: xarray[io] ; extra == 'test'
|
|
33
35
|
Requires-Dist: gcsfs ; extra == 'test'
|
|
34
36
|
Provides-Extra: dev
|
|
35
37
|
Provides-Extra: docs
|
|
38
|
+
Provides-Extra: duckdb
|
|
36
39
|
Provides-Extra: geo
|
|
40
|
+
Provides-Extra: polars
|
|
37
41
|
Provides-Extra: test
|
|
38
42
|
License-File: LICENSE
|
|
39
|
-
Summary:
|
|
43
|
+
Summary: Query Xarray with SQL.
|
|
40
44
|
Author-email: Alexander Merose <al@merose.com>
|
|
41
45
|
License: Apache-2.0
|
|
42
46
|
Requires-Python: >=3.10
|
|
@@ -49,10 +53,10 @@ Project-URL: Issues, https://github.com/alxmrs/xarray-sql/issues
|
|
|
49
53
|
_Query [Xarray](https://xarray.dev/) with SQL_
|
|
50
54
|
|
|
51
55
|

|
|
52
|
-
[](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)
|
|
56
60
|
[](https://pepy.tech/projects/xarray-sql)
|
|
57
61
|
[](https://pepy.tech/projects/xarray-sql)
|
|
58
62
|
|
|
@@ -64,7 +68,11 @@ pip install xarray-sql
|
|
|
64
68
|
|
|
65
69
|
This is an experiment to provide a SQL interface for array datasets.
|
|
66
70
|
Succinctly, we "pivot" Xarray Datasets to treat them like tables so we can run
|
|
67
|
-
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.
|
|
68
76
|
|
|
69
77
|
## Quickstart
|
|
70
78
|
|
|
@@ -107,6 +115,24 @@ clim_ds["air"].plot() # in a script, call matplotlib.pyplot.show() to display
|
|
|
107
115
|
That's the round trip — Xarray in, SQL in the middle, Xarray (and a plot) back
|
|
108
116
|
out.
|
|
109
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
|
+
|
|
110
136
|
## A bigger example: ARCO-ERA5
|
|
111
137
|
|
|
112
138
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -179,6 +205,8 @@ result = ctx.sql('''
|
|
|
179
205
|
# | 775 | -2.3064649711534457 |
|
|
180
206
|
# +-------+----------------------+
|
|
181
207
|
|
|
208
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
209
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
182
210
|
ctx.sql('''
|
|
183
211
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
184
212
|
FROM era5.surface
|
|
@@ -186,8 +214,6 @@ ctx.sql('''
|
|
|
186
214
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
187
215
|
GROUP BY latitude, longitude
|
|
188
216
|
ORDER BY latitude DESC, longitude
|
|
189
|
-
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
190
|
-
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
191
217
|
''').to_dataset(template=ds)
|
|
192
218
|
# <xarray.Dataset> Size: 8MB
|
|
193
219
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -204,7 +230,7 @@ ctx.sql('''
|
|
|
204
230
|
```
|
|
205
231
|
|
|
206
232
|
_(A runnable version of this example lives at
|
|
207
|
-
[`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).)_
|
|
208
234
|
|
|
209
235
|
## Why build this?
|
|
210
236
|
|
|
@@ -237,6 +263,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
237
263
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
238
264
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
239
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.
|
|
240
269
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
241
270
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
242
271
|
|
|
@@ -268,8 +297,8 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
268
297
|
Every case matches its array reference. The headline finding: these operations
|
|
269
298
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
270
299
|
functions, and `CASE` in disguise, and a query engine runs them at scale. See
|
|
271
|
-
[`benchmarks/geospatial/`](benchmarks/geospatial/) and the write-up,
|
|
272
|
-
[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/).
|
|
273
302
|
|
|
274
303
|
## Why does this work?
|
|
275
304
|
|
|
@@ -277,15 +306,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
277
306
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
278
307
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
279
308
|
just changes this metadata (via a `ravel()`/`reshape()`), back into a column
|
|
280
|
-
amenable to a DataFrame. We take advantage of this
|
|
281
|
-
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).
|
|
282
312
|
|
|
283
313
|
## What are the current limitations?
|
|
284
314
|
|
|
285
|
-
|
|
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
|
|
286
317
|
early users – "tire kickers", if you will. We'd love your input to shape the direction of this
|
|
287
|
-
project! Please, give this a try and [file issues](https://github.com/
|
|
288
|
-
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 😉.
|
|
289
320
|
|
|
290
321
|
## What would a deeper integration look like?
|
|
291
322
|
|
|
@@ -298,7 +329,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
298
329
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
299
330
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
300
331
|
and BigQuery. More thoughts on this
|
|
301
|
-
in [#4](https://github.com/
|
|
332
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
302
333
|
|
|
303
334
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
304
335
|
|
|
@@ -308,18 +339,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
308
339
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
309
340
|
|
|
310
341
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
311
|
-
- [DuckDB-Zarr](https://github.com/
|
|
342
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
312
343
|
|
|
313
344
|
## Roadmap
|
|
314
345
|
|
|
315
|
-
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/
|
|
316
|
-
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/
|
|
317
|
-
- [x] Support core datafusion optimizations to scan less data, like [104](https://github.com/
|
|
318
|
-
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/
|
|
319
|
-
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/
|
|
320
|
-
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/
|
|
321
|
-
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/
|
|
322
|
-
- [ ] (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).
|
|
323
354
|
|
|
324
355
|
## Sponsors & Contributors
|
|
325
356
|
|
|
@@ -339,6 +370,9 @@ I want to give a special thanks to the following folks and institutions:
|
|
|
339
370
|
changes.
|
|
340
371
|
- Aman Kumar for spending a considerable amount of his GSoC internship
|
|
341
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.
|
|
342
376
|
|
|
343
377
|
|
|
344
378
|
## License
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
_Query [Xarray](https://xarray.dev/) with SQL_
|
|
4
4
|
|
|
5
5
|

|
|
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
10
|
[](https://pepy.tech/projects/xarray-sql)
|
|
11
11
|
[](https://pepy.tech/projects/xarray-sql)
|
|
12
12
|
|
|
@@ -18,7 +18,11 @@ pip install xarray-sql
|
|
|
18
18
|
|
|
19
19
|
This is an experiment to provide a SQL interface for array datasets.
|
|
20
20
|
Succinctly, we "pivot" Xarray Datasets to treat them like tables so we can run
|
|
21
|
-
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.
|
|
22
26
|
|
|
23
27
|
## Quickstart
|
|
24
28
|
|
|
@@ -61,6 +65,24 @@ clim_ds["air"].plot() # in a script, call matplotlib.pyplot.show() to display
|
|
|
61
65
|
That's the round trip — Xarray in, SQL in the middle, Xarray (and a plot) back
|
|
62
66
|
out.
|
|
63
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
|
+
|
|
64
86
|
## A bigger example: ARCO-ERA5
|
|
65
87
|
|
|
66
88
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -133,6 +155,8 @@ result = ctx.sql('''
|
|
|
133
155
|
# | 775 | -2.3064649711534457 |
|
|
134
156
|
# +-------+----------------------+
|
|
135
157
|
|
|
158
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
159
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
136
160
|
ctx.sql('''
|
|
137
161
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
138
162
|
FROM era5.surface
|
|
@@ -140,8 +164,6 @@ ctx.sql('''
|
|
|
140
164
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
141
165
|
GROUP BY latitude, longitude
|
|
142
166
|
ORDER BY latitude DESC, longitude
|
|
143
|
-
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
144
|
-
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
145
167
|
''').to_dataset(template=ds)
|
|
146
168
|
# <xarray.Dataset> Size: 8MB
|
|
147
169
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -158,7 +180,7 @@ ctx.sql('''
|
|
|
158
180
|
```
|
|
159
181
|
|
|
160
182
|
_(A runnable version of this example lives at
|
|
161
|
-
[`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).)_
|
|
162
184
|
|
|
163
185
|
## Why build this?
|
|
164
186
|
|
|
@@ -191,6 +213,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
191
213
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
192
214
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
193
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.
|
|
194
219
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
195
220
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
196
221
|
|
|
@@ -222,8 +247,8 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
222
247
|
Every case matches its array reference. The headline finding: these operations
|
|
223
248
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
224
249
|
functions, and `CASE` in disguise, and a query engine runs them at scale. See
|
|
225
|
-
[`benchmarks/geospatial/`](benchmarks/geospatial/) and the write-up,
|
|
226
|
-
[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/).
|
|
227
252
|
|
|
228
253
|
## Why does this work?
|
|
229
254
|
|
|
@@ -231,15 +256,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
231
256
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
232
257
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
233
258
|
just changes this metadata (via a `ravel()`/`reshape()`), back into a column
|
|
234
|
-
amenable to a DataFrame. We take advantage of this
|
|
235
|
-
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).
|
|
236
262
|
|
|
237
263
|
## What are the current limitations?
|
|
238
264
|
|
|
239
|
-
|
|
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
|
|
240
267
|
early users – "tire kickers", if you will. We'd love your input to shape the direction of this
|
|
241
|
-
project! Please, give this a try and [file issues](https://github.com/
|
|
242
|
-
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 😉.
|
|
243
270
|
|
|
244
271
|
## What would a deeper integration look like?
|
|
245
272
|
|
|
@@ -252,7 +279,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
252
279
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
253
280
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
254
281
|
and BigQuery. More thoughts on this
|
|
255
|
-
in [#4](https://github.com/
|
|
282
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
256
283
|
|
|
257
284
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
258
285
|
|
|
@@ -262,18 +289,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
262
289
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
263
290
|
|
|
264
291
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
265
|
-
- [DuckDB-Zarr](https://github.com/
|
|
292
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
266
293
|
|
|
267
294
|
## Roadmap
|
|
268
295
|
|
|
269
|
-
- [x] ~Lazy evaluation via the pyarrow Dataset interface [#93](https://github.com/
|
|
270
|
-
- [x] Support proper parallelism via proper partition handling on the rust/datafusion side. [#106](https://github.com/
|
|
271
|
-
- [x] Support core datafusion optimizations to scan less data, like [104](https://github.com/
|
|
272
|
-
- [x] Translate a single Zarr to a collection of tables [#85](https://github.com/
|
|
273
|
-
- [ ] Distributed beyond a single node through the DataFusion integration with Ray Datasets [#68](https://github.com/
|
|
274
|
-
- [ ] Demo: calculate Sea Surface Temperature from 1940 - Present in SQL [#36](https://github.com/
|
|
275
|
-
- [ ] Provide an option to integrate DataFusion directly to Zarr via Rust [#4](https://github.com/
|
|
276
|
-
- [ ] (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).
|
|
277
304
|
|
|
278
305
|
## Sponsors & Contributors
|
|
279
306
|
|
|
@@ -293,6 +320,9 @@ I want to give a special thanks to the following folks and institutions:
|
|
|
293
320
|
changes.
|
|
294
321
|
- Aman Kumar for spending a considerable amount of his GSoC internship
|
|
295
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.
|
|
296
326
|
|
|
297
327
|
|
|
298
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 —
|
|
@@ -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"):
|