xarray_sql 0.3.3__tar.gz → 0.4.0rc1__tar.gz

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