xarray_sql 0.3.3__tar.gz → 0.4.0__tar.gz

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