xarray_sql 0.3.2__tar.gz → 0.4.0rc1__tar.gz

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