From fd3303e30ae0aebd1fed5243298a15527beeb924 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Sat, 29 Jun 2024 11:52:43 -0800 Subject: [PATCH] test: check that empty structs are disallowed See https://github.com/ibis-project/ibis/pull/9310 for a discussion of whether empty structs should be allowed or disallowed. We settled on disallowing them for now, since pyarrow is like the only backend to support them, and it is definitely easier to change our mind and allow them later rather than disallow them later. Perhaps if you stay in ibis-land, emptry structs are useful ie for doing type manipulations, or maybe you only use them for intermediate calculations? But until we see this concrete use case, don't worry about it. --- ibis/expr/datatypes/core.py | 12 +++++++++++- ibis/expr/datatypes/value.py | 2 -- ibis/tests/expr/test_struct.py | 7 +++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ibis/expr/datatypes/core.py b/ibis/expr/datatypes/core.py index 2a2cfa4eae789..decb29500e34b 100644 --- a/ibis/expr/datatypes/core.py +++ b/ibis/expr/datatypes/core.py @@ -831,13 +831,23 @@ def _pretty_piece(self) -> str: @public class Struct(Parametric, MapSet): - """Structured values.""" + """A nested type with ordered fields of any type, analogous to a dataclass in Python. + + eg a Struct might have a field a of type int64 and a field b of type string. + """ fields: FrozenOrderedDict[str, DataType] scalar = "StructScalar" column = "StructColumn" + def __init__(self, fields, nullable=True): + # We could do this validation in a type annotation, but I thought this + # was common enough to warrant a nicer error message. + if not fields: + raise TypeError("Structs require at least one field") + super().__init__(fields=fields, nullable=nullable) + @classmethod def from_tuples( cls, pairs: Iterable[tuple[str, str | DataType]], nullable: bool = True diff --git a/ibis/expr/datatypes/value.py b/ibis/expr/datatypes/value.py index ad40f9047ecf3..dceccf048d6ab 100644 --- a/ibis/expr/datatypes/value.py +++ b/ibis/expr/datatypes/value.py @@ -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) diff --git a/ibis/tests/expr/test_struct.py b/ibis/tests/expr/test_struct.py index de297a4e4a586..6c87da2102958 100644 --- a/ibis/tests/expr/test_struct.py +++ b/ibis/tests/expr/test_struct.py @@ -22,6 +22,13 @@ def s(): return ibis.table(dict(a="struct"), name="s") +@pytest.mark.parametrize("val", [{}, []]) +@pytest.mark.parametrize("typ", [None, "struct<>"]) +def test_struct_factory_empty(val, typ): + with pytest.raises(TypeError): + ibis.struct(val, type=typ) + + def test_struct_operations(): value = OrderedDict( [