xarray_sql 0.3.2__tar.gz → 0.4.0__tar.gz

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