Skip to content

Commit 50b9f52

Browse files
committed
fix: changes
1 parent 289bedb commit 50b9f52

5 files changed

Lines changed: 177 additions & 361 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ and dim-coordinate dtype are recovered from the registered Dataset
148148
automatically.
149149

150150
For filtered queries that return only part of the original extent, pass
151-
`sparse_extent="template"` to reindex back to the full grid with NaN
151+
`sparsity="template"` to reindex back to the full grid with NaN
152152
fills:
153153

154154
```python
155155
out = ctx.sql(
156156
'SELECT * FROM "air" WHERE lat > 50'
157-
).to_dataset(sparse_extent="template")
157+
).to_dataset(sparsity="template")
158158
# Full lat range restored; cells with lat <= 50 are NaN.
159159
```
160160

tests/test_ds.py

Lines changed: 66 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
cases, ``dimension_columns`` auto-inference vs explicit.
99
* Template-based metadata recovery (var attrs / encoding, dataset
1010
attrs, non-dim coords, dim-coord dtype).
11-
* Sparse-extent handling and edge cases (null dim rows, fill_value
11+
* Sparsity handling and edge cases (null dim rows, fill_value
1212
dtype behavior, vectorized indexer fallback).
1313
"""
1414

@@ -63,7 +63,7 @@ def _clear_encoding(ds: xr.Dataset) -> xr.Dataset:
6363
"""Strip ``encoding`` from a Dataset and all its variables.
6464
6565
Round-trip identity tests should not be coupled to encoding choices,
66-
since ``_apply_template`` deliberately drops dtype-bound keys.
66+
since ``apply_template`` deliberately drops dtype-bound keys.
6767
"""
6868
ds = ds.copy()
6969
for v in ds.variables.values():
@@ -315,26 +315,32 @@ def test_lazy_select_star_round_trip_equality(air_dataset_small):
315315
np.testing.assert_array_equal(actual["air"].values, expected["air"].values)
316316

317317

318-
def test_aggregation_uses_eager_path(air_dataset_small):
319-
"""Aggregation queries materialize once via the eager path.
318+
def test_aggregation_uses_lazy_backend(air_dataset_small):
319+
"""Aggregation queries return a lazy Dataset just like SELECT *.
320320
321-
No assertion on the data type of variable._data here; xarray may
322-
wrap eager numpy arrays in NumpyIndexingAdapter. The contract is:
323-
values match the source and slicing afterwards is local.
321+
Pushdown and laziness are orthogonal: an aggregation can't push the
322+
request indexer into a useful filter, but the result is still
323+
streamed via execute_stream on first access. The user-visible
324+
contract is values match the source's ``mean(dim="time")``.
324325
"""
325326
ctx = XarrayContext()
326327
ctx.from_dataset("air", air_dataset_small)
327328
out = ctx.sql(
328329
"SELECT lat, lon, AVG(air) AS air_avg FROM air GROUP BY lat, lon"
329330
).to_dataset(dimension_columns=["lat", "lon"])
330-
underlying = out["air_avg"].variable._data
331331
from xarray_sql.ds import SQLBackendArray
332332

333-
# Aggregation -> not a SQLBackendArray.
334-
if hasattr(underlying, "array"):
335-
assert not isinstance(underlying.array, SQLBackendArray)
336-
else:
337-
assert not isinstance(underlying, SQLBackendArray)
333+
inner = out["air_avg"].variable._data
334+
underlying = inner.array if hasattr(inner, "array") else inner
335+
assert isinstance(underlying, SQLBackendArray)
336+
expected = (
337+
air_dataset_small.compute()
338+
.sortby(["lat", "lon"])
339+
.mean(dim="time")["air"]
340+
.values
341+
)
342+
actual = out.sortby(["lat", "lon"])["air_avg"].values
343+
np.testing.assert_allclose(actual, expected)
338344

339345

340346
def test_lazy_outer_indexer_array(air_dataset_small):
@@ -378,12 +384,12 @@ def test_lazy_compute_returns_eager(air_dataset_small):
378384

379385

380386
# ---------------------------------------------------------------------------
381-
# Sparse-extent handling and edge cases
387+
# Sparsity handling and edge cases
382388
# ---------------------------------------------------------------------------
383389

384390

385-
def test_sparse_extent_result_default_filters_lazy(air_dataset_small):
386-
"""Default sparse_extent='result' keeps only filtered coords (lazy path)."""
391+
def test_sparsity_result_default_filters_lazy(air_dataset_small):
392+
"""Default sparsity='result' keeps only filtered coords (lazy path)."""
387393
ctx = XarrayContext()
388394
ctx.from_dataset("air", air_dataset_small)
389395
threshold = float(air_dataset_small["lat"].values[5])
@@ -392,13 +398,13 @@ def test_sparse_extent_result_default_filters_lazy(air_dataset_small):
392398
assert out.sizes["lat"] < air_dataset_small.sizes["lat"]
393399

394400

395-
def test_sparse_extent_template_full_grid(air_dataset_small):
396-
"""sparse_extent='template' reindexes to the full grid with NaN fills."""
401+
def test_sparsity_template_full_grid(air_dataset_small):
402+
"""sparsity='template' reindexes to the full grid with NaN fills."""
397403
ctx = XarrayContext()
398404
ctx.from_dataset("air", air_dataset_small)
399405
threshold = float(air_dataset_small["lat"].values[5])
400406
out = ctx.sql(f"SELECT * FROM air WHERE lat > {threshold}").to_dataset(
401-
sparse_extent="template"
407+
sparsity="template"
402408
)
403409
assert out.sizes["lat"] == air_dataset_small.sizes["lat"]
404410
lat_vals = out["lat"].values
@@ -410,29 +416,32 @@ def test_sparse_extent_template_full_grid(air_dataset_small):
410416
assert not np.isnan(above.values).any()
411417

412418

413-
def test_sparse_extent_template_requires_template(air_dataset_small):
414-
"""No resolvable template -> sparse_extent='template' raises."""
415-
from xarray_sql.ds import _eager_to_xarray
416-
417-
df = pd.DataFrame([(0, 0, 1.0), (1, 1, 2.0)], columns=["lat", "lon", "v"])
419+
def test_sparsity_template_requires_template(air_dataset_small):
420+
"""No resolvable template -> sparsity='template' raises."""
421+
other = air_dataset_small.copy()
422+
ctx = XarrayContext()
423+
# Two registrations so auto-resolve returns None.
424+
ctx.from_dataset("a", air_dataset_small)
425+
ctx.from_dataset("b", other)
418426
with pytest.raises(ValueError, match="requires template= to be supplied"):
419-
_eager_to_xarray(
420-
df, dimension_columns=["lat", "lon"], sparse_extent="template"
427+
ctx.sql("SELECT * FROM a").to_dataset(
428+
dimension_columns=["time", "lat", "lon"],
429+
sparsity="template",
421430
)
422431

423432

424-
def test_sparse_extent_invalid_value_raises():
425-
from xarray_sql.ds import _eager_to_xarray
426-
427-
df = pd.DataFrame([(0, 0, 1.0)], columns=["lat", "lon", "v"])
428-
with pytest.raises(ValueError, match="sparse_extent must be"):
429-
_eager_to_xarray(
430-
df, dimension_columns=["lat", "lon"], sparse_extent="bogus"
433+
def test_sparsity_invalid_value_raises(air_dataset_small):
434+
ctx = XarrayContext()
435+
ctx.from_dataset("air", air_dataset_small)
436+
with pytest.raises(ValueError, match="sparsity must be"):
437+
ctx.sql("SELECT * FROM air").to_dataset(
438+
dimension_columns=["time", "lat", "lon"],
439+
sparsity="bogus", # type: ignore[arg-type]
431440
)
432441

433442

434-
def test_sparse_extent_template_with_aggregation(air_dataset_small):
435-
"""sparse_extent='template' on an aggregation respects dimension_columns subset."""
443+
def test_sparsity_template_with_aggregation(air_dataset_small):
444+
"""sparsity='template' on an aggregation respects dimension_columns subset."""
436445
ctx = XarrayContext()
437446
ctx.from_dataset("air", air_dataset_small)
438447
threshold = float(air_dataset_small["lat"].values[5])
@@ -443,7 +452,7 @@ def test_sparse_extent_template_with_aggregation(air_dataset_small):
443452
WHERE lat > {threshold}
444453
GROUP BY lat, lon
445454
"""
446-
).to_dataset(dimension_columns=["lat", "lon"], sparse_extent="template")
455+
).to_dataset(dimension_columns=["lat", "lon"], sparsity="template")
447456
assert out.sizes["lat"] == air_dataset_small.sizes["lat"]
448457
assert "time" not in out.dims
449458
below_mask = out["lat"].values <= threshold
@@ -460,60 +469,45 @@ def test_fill_value_int_upcasts_to_float():
460469
ctx = XarrayContext()
461470
ctx.from_dataset("t", ds)
462471
out = ctx.sql("SELECT * FROM t WHERE lat > 0").to_dataset(
463-
sparse_extent="template"
472+
sparsity="template"
464473
)
465474
assert np.issubdtype(out["v"].dtype, np.floating)
466475
assert np.isnan(out["v"].sel(lat=0).values).all()
467476

468477

469-
def test_fill_value_custom_preserves_int():
478+
def test_fill_value_custom_preserves_int(air_dataset_small):
470479
"""Passing a typed sentinel preserves the data var's int dtype."""
471-
from xarray_sql.ds import _eager_to_xarray
472-
473-
df = pd.DataFrame(
474-
[(1, 10, 100), (1, 11, 101), (2, 10, 200), (2, 11, 201)],
475-
columns=["lat", "lon", "v"],
476-
)
477-
template = xr.Dataset(
478-
{"v": (("lat", "lon"), np.zeros((3, 2), dtype=np.int64))},
480+
# Build a small int-valued Dataset, register, filter out part of the
481+
# extent, and reindex back with an int fill_value via sparsity.
482+
source = xr.Dataset(
483+
{
484+
"v": (
485+
("lat", "lon"),
486+
np.arange(6, dtype=np.int64).reshape(3, 2) + 1,
487+
),
488+
},
479489
coords={"lat": [0, 1, 2], "lon": [10, 11]},
480-
)
481-
out = _eager_to_xarray(
482-
df,
483-
dimension_columns=["lat", "lon"],
484-
template=template,
485-
sparse_extent="template",
486-
fill_value=-1,
490+
).chunk({"lat": 3})
491+
ctx = XarrayContext()
492+
ctx.from_dataset("t", source)
493+
out = ctx.sql("SELECT * FROM t WHERE lat > 0").to_dataset(
494+
sparsity="template", fill_value=-1
487495
)
488496
assert np.issubdtype(out["v"].dtype, np.integer)
489-
assert out["v"].sel(lat=0, lon=10).item() == -1
490-
assert out["v"].sel(lat=2, lon=11).item() == 201
491-
492-
493-
def test_drop_null_dim_rows_warns_once():
494-
"""Rows with NaN dim values are dropped with exactly one warning."""
495-
from xarray_sql.ds import _eager_to_xarray
496-
497-
df = pd.DataFrame(
498-
[(0, 0, 1.0), (np.nan, 0, 2.0), (1, 0, 3.0)],
499-
columns=["lat", "lon", "v"],
500-
)
501-
with pytest.warns(UserWarning, match="Dropping 1 row"):
502-
out = _eager_to_xarray(df, dimension_columns=["lat", "lon"])
503-
assert out.sizes["lat"] == 2
504-
assert out.sizes["lon"] == 1
497+
assert (out["v"].sel(lat=0).values == -1).all()
498+
assert out["v"].sel(lat=2, lon=11).item() == 6
505499

506500

507-
def test_sparse_extent_template_then_metadata(air_dataset_small):
508-
"""sparse_extent='template' composes with template metadata recovery."""
501+
def test_sparsity_template_then_metadata(air_dataset_small):
502+
"""sparsity='template' composes with template metadata recovery."""
509503
ds = air_dataset_small.copy()
510504
ds.attrs = {"src": "tmpl"}
511505
ds["air"].attrs = {"units": "K"}
512506
ctx = XarrayContext()
513507
ctx.from_dataset("air", ds)
514508
threshold = float(ds["lat"].values[5])
515509
out = ctx.sql(f"SELECT * FROM air WHERE lat > {threshold}").to_dataset(
516-
sparse_extent="template"
510+
sparsity="template"
517511
)
518512
assert out.attrs == {"src": "tmpl"}
519513
assert out["air"].attrs == {"units": "K"}

xarray_sql/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from . import cftime
22
from .df import from_map
3-
from .ds import XarrayDataFrame
3+
from .ds import XarrayDataFrame, apply_template
44
from .reader import read_xarray, read_xarray_table
55
from .sql import XarrayContext
66

77
__all__ = [
88
"cftime",
99
"XarrayContext",
1010
"XarrayDataFrame",
11+
"apply_template",
1112
"read_xarray_table",
1213
"read_xarray",
1314
"from_map", # deprecated

0 commit comments

Comments
 (0)