xarray_sql 0.3.3__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.3 → xarray_sql-0.4.0rc1}/Cargo.lock +1 -1
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/Cargo.toml +1 -1
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/PKG-INFO +56 -28
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/README.md +49 -25
- xarray_sql-0.4.0rc1/benchmarks/duckdb_pushdown.py +103 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/01_ndvi.py +4 -4
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/02_climatology.py +7 -5
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/03_zonal_mean.py +8 -6
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/04_anomaly.py +7 -5
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/05_forecast_skill.py +4 -4
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/06_zonal_vector.py +8 -6
- 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.3 → xarray_sql-0.4.0rc1}/docs/examples.md +12 -1
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/docs/geospatial.md +135 -24
- xarray_sql-0.4.0rc1/docs/limitations.md +157 -0
- xarray_sql-0.4.0rc1/docs/performance.md +245 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/pyproject.toml +14 -2
- 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.3 → xarray_sql-0.4.0rc1}/tests/test_df.py +84 -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.3.3 → 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.3 → 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.3 → xarray_sql-0.4.0rc1}/xarray_sql/cftime.py +6 -4
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/xarray_sql/df.py +86 -6
- {xarray_sql-0.3.3 → 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.3.3 → xarray_sql-0.4.0rc1}/xarray_sql/proj.py +5 -5
- xarray_sql-0.4.0rc1/xarray_sql/roundtrip.py +495 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/xarray_sql/sql.py +4 -18
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/zensical.toml +23 -8
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/.gitignore +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/AGENTS.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/LICENSE +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/07_reproject_udf.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/08_regrid_weights.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/09_warp.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/README.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/_harness.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/perf_summary.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/run_all.sh +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/benchmarks/geospatial/run_perf.sh +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/docs/assets/logo.svg +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/docs/contributing.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/docs/index.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/docs/reference/xarray_sql.md +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/src/lib.rs +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/__init__.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/conftest.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_cft.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_ds.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_proj.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_reader.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_stats.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/tests/test_to_dataset_perf.py +0 -0
- {xarray_sql-0.3.3 → xarray_sql-0.4.0rc1}/xarray_sql/core.py +0 -0
- {xarray_sql-0.3.3 → 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,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,21 @@ 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
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/engines/) for the support matrix, DuckDB/Polars details,
|
|
131
|
+
and the lazy chunked round-trip.
|
|
132
|
+
|
|
110
133
|
## A bigger example: ARCO-ERA5
|
|
111
134
|
|
|
112
135
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -179,6 +202,8 @@ result = ctx.sql('''
|
|
|
179
202
|
# | 775 | -2.3064649711534457 |
|
|
180
203
|
# +-------+----------------------+
|
|
181
204
|
|
|
205
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
206
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
182
207
|
ctx.sql('''
|
|
183
208
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
184
209
|
FROM era5.surface
|
|
@@ -186,8 +211,6 @@ ctx.sql('''
|
|
|
186
211
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
187
212
|
GROUP BY latitude, longitude
|
|
188
213
|
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
214
|
''').to_dataset(template=ds)
|
|
192
215
|
# <xarray.Dataset> Size: 8MB
|
|
193
216
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -204,7 +227,7 @@ ctx.sql('''
|
|
|
204
227
|
```
|
|
205
228
|
|
|
206
229
|
_(A runnable version of this example lives at
|
|
207
|
-
[`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).)_
|
|
208
231
|
|
|
209
232
|
## Why build this?
|
|
210
233
|
|
|
@@ -237,6 +260,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
237
260
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
238
261
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
239
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.
|
|
240
266
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
241
267
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
242
268
|
|
|
@@ -268,8 +294,8 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
268
294
|
Every case matches its array reference. The headline finding: these operations
|
|
269
295
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
270
296
|
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](
|
|
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/).
|
|
273
299
|
|
|
274
300
|
## Why does this work?
|
|
275
301
|
|
|
@@ -277,15 +303,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
277
303
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
278
304
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
279
305
|
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
|
|
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).
|
|
282
309
|
|
|
283
310
|
## What are the current limitations?
|
|
284
311
|
|
|
285
|
-
|
|
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
|
|
286
314
|
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](
|
|
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 😉.
|
|
289
317
|
|
|
290
318
|
## What would a deeper integration look like?
|
|
291
319
|
|
|
@@ -298,7 +326,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
298
326
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
299
327
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
300
328
|
and BigQuery. More thoughts on this
|
|
301
|
-
in [#4](https://github.com/
|
|
329
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
302
330
|
|
|
303
331
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
304
332
|
|
|
@@ -308,18 +336,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
308
336
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
309
337
|
|
|
310
338
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
311
|
-
- [DuckDB-Zarr](https://github.com/
|
|
339
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
312
340
|
|
|
313
341
|
## Roadmap
|
|
314
342
|
|
|
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/
|
|
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).
|
|
323
351
|
|
|
324
352
|
## Sponsors & Contributors
|
|
325
353
|
|
|
@@ -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,21 @@ 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
|
+
See [Engines](https://xqlsystems.github.io/xarray-sql/engines/) for the support matrix, DuckDB/Polars details,
|
|
81
|
+
and the lazy chunked round-trip.
|
|
82
|
+
|
|
64
83
|
## A bigger example: ARCO-ERA5
|
|
65
84
|
|
|
66
85
|
The same interface scales to cloud-native datasets with hundreds of variables,
|
|
@@ -133,6 +152,8 @@ result = ctx.sql('''
|
|
|
133
152
|
# | 775 | -2.3064649711534457 |
|
|
134
153
|
# +-------+----------------------+
|
|
135
154
|
|
|
155
|
+
# `latitude`/`longitude` are inferred from the registered table's surviving
|
|
156
|
+
# dims; `template` is kept only to recover metadata (attrs, encoding).
|
|
136
157
|
ctx.sql('''
|
|
137
158
|
SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
|
|
138
159
|
FROM era5.surface
|
|
@@ -140,8 +161,6 @@ ctx.sql('''
|
|
|
140
161
|
AND TIMESTAMP '2020-01-01 05:00:00'
|
|
141
162
|
GROUP BY latitude, longitude
|
|
142
163
|
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
164
|
''').to_dataset(template=ds)
|
|
146
165
|
# <xarray.Dataset> Size: 8MB
|
|
147
166
|
# Dimensions: (latitude: 721, longitude: 1440)
|
|
@@ -158,7 +177,7 @@ ctx.sql('''
|
|
|
158
177
|
```
|
|
159
178
|
|
|
160
179
|
_(A runnable version of this example lives at
|
|
161
|
-
[`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).)_
|
|
162
181
|
|
|
163
182
|
## Why build this?
|
|
164
183
|
|
|
@@ -191,6 +210,9 @@ pure DataFusion and PyArrow, but works with the same principle!
|
|
|
191
210
|
_2026 update_: Instead of `from_map()`, we create a way to translate Xarray chunks
|
|
192
211
|
into Arrow RecordBatches. We pass a Python callback into a DataFusion `TableProvider`
|
|
193
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.
|
|
194
216
|
Ultimately, the initial insight of the `pivot()` function -- that any ndarray can be
|
|
195
217
|
translated into a 2D table -- underlies this performant query mechanism.
|
|
196
218
|
|
|
@@ -222,8 +244,8 @@ against an xarray/array reference** to floating-point tolerance:
|
|
|
222
244
|
Every case matches its array reference. The headline finding: these operations
|
|
223
245
|
are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window
|
|
224
246
|
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](
|
|
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/).
|
|
227
249
|
|
|
228
250
|
## Why does this work?
|
|
229
251
|
|
|
@@ -231,15 +253,17 @@ Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in
|
|
|
231
253
|
chunks and represented contiguously in memory. It is only a matter of metadata
|
|
232
254
|
that breaks them up into ndarrays. `pivot()`, which uses `to_dataframe()`,
|
|
233
255
|
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
|
|
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).
|
|
236
259
|
|
|
237
260
|
## What are the current limitations?
|
|
238
261
|
|
|
239
|
-
|
|
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
|
|
240
264
|
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](
|
|
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 😉.
|
|
243
267
|
|
|
244
268
|
## What would a deeper integration look like?
|
|
245
269
|
|
|
@@ -252,7 +276,7 @@ a [virtual](https://fsspec.github.io/kerchunk/)
|
|
|
252
276
|
filesystem for parquet that would internally map to Zarr. Raster-backed virtual
|
|
253
277
|
parquet would open up integrations to numerous tools like dask, pyarrow, duckdb,
|
|
254
278
|
and BigQuery. More thoughts on this
|
|
255
|
-
in [#4](https://github.com/
|
|
279
|
+
in [#4](https://github.com/xqlsystems/xarray-sql/issues/4).
|
|
256
280
|
|
|
257
281
|
_2025 update_: Something like this is being built across a few projects! The ones I know about are:
|
|
258
282
|
|
|
@@ -262,18 +286,18 @@ _2025 update_: Something like this is being built across a few projects! The one
|
|
|
262
286
|
_2026 update_: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:
|
|
263
287
|
|
|
264
288
|
- [Zarr-Datafusion](https://lib.rs/crates/zarr-datafusion)
|
|
265
|
-
- [DuckDB-Zarr](https://github.com/
|
|
289
|
+
- [DuckDB-Zarr](https://github.com/xqlsystems/duckdb-zarr)
|
|
266
290
|
|
|
267
291
|
## Roadmap
|
|
268
292
|
|
|
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/
|
|
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).
|
|
277
301
|
|
|
278
302
|
## Sponsors & Contributors
|
|
279
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).
|
|
@@ -49,8 +49,7 @@ import numpy as np
|
|
|
49
49
|
import pandas as pd
|
|
50
50
|
import xarray as xr
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
from _engines import EngineContext
|
|
54
53
|
from _harness import (
|
|
55
54
|
CaseSkipped,
|
|
56
55
|
assert_grid_close,
|
|
@@ -144,7 +143,8 @@ def main() -> None:
|
|
|
144
143
|
f"leads × 2 models)"
|
|
145
144
|
)
|
|
146
145
|
|
|
147
|
-
ctx =
|
|
146
|
+
ctx = EngineContext()
|
|
147
|
+
print(f" engine: {ctx.flavor}")
|
|
148
148
|
# chunks here is the Arrow batch (partition) size each table streams in, not a
|
|
149
149
|
# filter — no data is dropped. Both windows are small, so one partition each is
|
|
150
150
|
# fastest (fewer partitions = fewer Python→Arrow round-trips for the same
|
|
@@ -172,7 +172,7 @@ def main() -> None:
|
|
|
172
172
|
show_sql(sql)
|
|
173
173
|
|
|
174
174
|
for _ in measured("SQL RMSE by (model, lead) — lazy JOIN"):
|
|
175
|
-
got = ctx.
|
|
175
|
+
got = ctx.sql_to_dataset(sql, dims=["model", "lead"]).rmse
|
|
176
176
|
|
|
177
177
|
for _ in measured("xarray reference"):
|
|
178
178
|
ref = _reference_rmse(forecasts, truth)
|