Skip to content

Commit 51e35ce

Browse files
committed
feat: allow empty structs
working towards ibis-project#8289 I'm not sure how useful empty structs are, since it seems like only bigquery, dask, and pandas actually support them. But still, if you stay in ibis-land, perhaps it is useful. ie for doing type manipulations, or maybe you only use them for intermediate calculations? 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
1 parent 5bef96a commit 51e35ce

File tree

5 files changed

+42
-5
lines changed

5 files changed

+42
-5
lines changed

ibis/backends/tests/errors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,14 @@
110110
from psycopg2.errors import OperationalError as PsycoPg2OperationalError
111111
from psycopg2.errors import ProgrammingError as PsycoPg2ProgrammingError
112112
from psycopg2.errors import SyntaxError as PsycoPg2SyntaxError
113+
from psycopg2.errors import UndefinedFunction as PsycoPg2UndefinedFunction
113114
from psycopg2.errors import UndefinedObject as PsycoPg2UndefinedObject
114115
except ImportError:
115116
PsycoPg2SyntaxError = PsycoPg2IndeterminateDatatype = (
116117
PsycoPg2InvalidTextRepresentation
117118
) = PsycoPg2DivisionByZero = PsycoPg2InternalError = PsycoPg2ProgrammingError = (
118119
PsycoPg2OperationalError
119-
) = PsycoPg2UndefinedObject = None
120+
) = PsycoPg2UndefinedFunction = PsycoPg2UndefinedObject = None
120121

121122
try:
122123
from pymysql.err import NotSupportedError as MySQLNotSupportedError

ibis/backends/tests/test_struct.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414
import ibis.expr.datatypes as dt
1515
from ibis import util
1616
from ibis.backends.tests.errors import (
17+
ClickHouseDatabaseError,
18+
DuckDBParserException,
1719
PolarsColumnNotFoundError,
20+
PolarsComputeError,
1821
PsycoPg2InternalError,
1922
PsycoPg2SyntaxError,
23+
PsycoPg2UndefinedFunction,
2024
Py4JJavaError,
2125
PySparkAnalysisException,
26+
TrinoUserError,
2227
)
2328
from ibis.common.exceptions import IbisError
2429

@@ -29,6 +34,20 @@
2934
]
3035

3136

37+
@pytest.mark.notimpl("clickhouse", raises=ClickHouseDatabaseError)
38+
@pytest.mark.notimpl("duckdb", raises=DuckDBParserException)
39+
@pytest.mark.notimpl("flink", raises=Py4JJavaError)
40+
@pytest.mark.notimpl("polars", raises=PolarsComputeError)
41+
@pytest.mark.notimpl("postgres", raises=PsycoPg2UndefinedFunction)
42+
@pytest.mark.notimpl("pyspark", raises=Py4JJavaError)
43+
@pytest.mark.notimpl("risingwave", raises=PsycoPg2InternalError)
44+
@pytest.mark.notimpl("trino", raises=TrinoUserError)
45+
def test_struct_factory_empty(con):
46+
s = ibis.struct({})
47+
result = con.execute(s)
48+
assert result == {}
49+
50+
3251
@pytest.mark.notimpl(["dask"])
3352
@pytest.mark.parametrize(
3453
("field", "expected"),

ibis/expr/datatypes/value.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ def infer(value: Any) -> dt.DataType:
4242
@infer.register(collections.OrderedDict)
4343
def infer_struct(value: Mapping[str, Any]) -> dt.Struct:
4444
"""Infer the [`Struct`](./datatypes.qmd#ibis.expr.datatypes.Struct) type of `value`."""
45-
if not value:
46-
raise TypeError("Empty struct type not supported")
4745
fields = {name: infer(val) for name, val in value.items()}
4846
return dt.Struct(fields)
4947

ibis/expr/operations/structs.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from public import public
66

7+
import ibis.expr.datashape as ds
78
import ibis.expr.datatypes as dt
89
import ibis.expr.rules as rlz
910
from ibis.common.annotations import ValidationError, attribute
@@ -38,8 +39,6 @@ class StructColumn(Value):
3839
names: VarTuple[str]
3940
values: VarTuple[Value]
4041

41-
shape = rlz.shape_like("values")
42-
4342
def __init__(self, names, values):
4443
if len(names) != len(values):
4544
raise ValidationError(
@@ -52,3 +51,9 @@ def __init__(self, names, values):
5251
def dtype(self) -> dt.DataType:
5352
dtypes = (value.dtype for value in self.values)
5453
return dt.Struct.from_tuples(zip(self.names, dtypes))
54+
55+
@attribute
56+
def shape(self) -> ds.DataShape:
57+
if len(self.values) == 0:
58+
return ds.scalar
59+
return rlz.highest_precedence_shape(self.values)

ibis/tests/expr/test_struct.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import pytest
66

77
import ibis
8+
import ibis.expr.datashape as ds
9+
import ibis.expr.datatypes as dt
810
import ibis.expr.operations as ops
911
import ibis.expr.types as ir
1012
from ibis import _
13+
from ibis.common.annotations import ValidationError
1114
from ibis.expr.tests.test_newrels import join_tables
1215
from ibis.tests.util import assert_pickle_roundtrip
1316

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

2427

28+
@pytest.mark.parametrize("val", [{}, []])
29+
@pytest.mark.parametrize("typ", [None, "struct<>", dt.Struct.from_tuples([])])
30+
def test_struct_factory_empty(val, typ):
31+
with pytest.raises(ValidationError):
32+
ibis.struct(val, type="struct<a: float64, b: float64>")
33+
s = ibis.struct(val, type=typ)
34+
assert s.names == tuple()
35+
assert s.type() == dt.Struct.from_tuples([])
36+
assert s.op().shape == ds.scalar
37+
38+
2539
def test_struct_operations():
2640
value = OrderedDict(
2741
[

0 commit comments

Comments
 (0)