Skip to content

Commit

Permalink
feat: allow empty structs
Browse files Browse the repository at this point in the history
working towards ibis-project#8289

I'm not sure how useful empty structs are, since it seems like
not many backends actually support them.
But still, if you stay in ibis-land, perhaps it is useful.
Not that hard for us to support it, so why not.

I'm not sure of the history of the specific disallowment
that I am removing from the type inference.

Relevant context:

- ibis-project#8876
- https://github.com/ibis-project/ibis/issues?q=empty+struct
  • Loading branch information
NickCrews committed Jun 4, 2024
1 parent 5bef96a commit a20bfab
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 4 deletions.
10 changes: 10 additions & 0 deletions ibis/backends/tests/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import ibis.expr.datatypes as dt
from ibis import util
from ibis.backends.tests.errors import (
DuckDBParserException,
PolarsColumnNotFoundError,
PolarsComputeError,
PsycoPg2InternalError,
PsycoPg2SyntaxError,
Py4JJavaError,
Expand All @@ -29,6 +31,14 @@
]


@pytest.mark.notimpl("duckdb", raises=DuckDBParserException)
@pytest.mark.notimpl("polars", raises=PolarsComputeError)
def test_struct_factory_empty(con):
s = ibis.struct({})
result = con.execute(s)
assert result == {}


@pytest.mark.notimpl(["dask"])
@pytest.mark.parametrize(
("field", "expected"),
Expand Down
2 changes: 0 additions & 2 deletions ibis/expr/datatypes/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ def infer(value: Any) -> dt.DataType:
@infer.register(collections.OrderedDict)
def infer_struct(value: Mapping[str, Any]) -> dt.Struct:
"""Infer the [`Struct`](./datatypes.qmd#ibis.expr.datatypes.Struct) type of `value`."""
if not value:
raise TypeError("Empty struct type not supported")
fields = {name: infer(val) for name, val in value.items()}
return dt.Struct(fields)

Expand Down
9 changes: 7 additions & 2 deletions ibis/expr/operations/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from public import public

import ibis.expr.datashape as ds
import ibis.expr.datatypes as dt
import ibis.expr.rules as rlz
from ibis.common.annotations import ValidationError, attribute
Expand Down Expand Up @@ -38,8 +39,6 @@ class StructColumn(Value):
names: VarTuple[str]
values: VarTuple[Value]

shape = rlz.shape_like("values")

def __init__(self, names, values):
if len(names) != len(values):
raise ValidationError(
Expand All @@ -52,3 +51,9 @@ def __init__(self, names, values):
def dtype(self) -> dt.DataType:
dtypes = (value.dtype for value in self.values)
return dt.Struct.from_tuples(zip(self.names, dtypes))

@attribute
def shape(self) -> ds.DataShape:
if len(self.values) == 0:
return ds.scalar
return rlz.highest_precedence_shape(self.values)
14 changes: 14 additions & 0 deletions ibis/tests/expr/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import pytest

import ibis
import ibis.expr.datashape as ds
import ibis.expr.datatypes as dt
import ibis.expr.operations as ops
import ibis.expr.types as ir
from ibis import _
from ibis.common.annotations import ValidationError
from ibis.expr.tests.test_newrels import join_tables
from ibis.tests.util import assert_pickle_roundtrip

Expand All @@ -22,6 +25,17 @@ def s():
return ibis.table(dict(a="struct<f: float, g: string>"), name="s")


@pytest.mark.parametrize("val", [{}, []])
@pytest.mark.parametrize("typ", [None, "struct<>", dt.Struct.from_tuples([])])
def test_struct_factory_empty(val, typ):
with pytest.raises(ValidationError):
ibis.struct(val, type="struct<a: float64, b: float64>")
s = ibis.struct(val, type=typ)
assert s.names == tuple()
assert s.type() == dt.Struct.from_tuples([])
assert s.op().shape == ds.scalar


def test_struct_operations():
value = OrderedDict(
[
Expand Down

0 comments on commit a20bfab

Please sign in to comment.