diff --git a/ibis/backends/duckdb/compiler.py b/ibis/backends/duckdb/compiler.py index d2f8280adb597..d3f6ca6dabc19 100644 --- a/ibis/backends/duckdb/compiler.py +++ b/ibis/backends/duckdb/compiler.py @@ -199,6 +199,12 @@ def visit_ArrayZip(self, op, *, arg): any_arg_null = sg.or_(*(arr.is_(NULL) for arr in arg)) return self.if_(any_arg_null, NULL, zipped_arrays) + def visit_Map(self, op, *, keys, values): + # workaround for https://github.com/ibis-project/ibis/issues/8632 + regular = self.f.map(keys, values) + either_null = sg.or_(keys.is_(NULL), values.is_(NULL)) + return self.if_(either_null, NULL, regular) + def visit_MapGet(self, op, *, arg, key, default): return self.f.ifnull( self.f.list_extract(self.f.element_at(arg, key), 1), default diff --git a/ibis/backends/duckdb/tests/test_client.py b/ibis/backends/duckdb/tests/test_client.py index 6f82c48c2f841..c89dac9a1e871 100644 --- a/ibis/backends/duckdb/tests/test_client.py +++ b/ibis/backends/duckdb/tests/test_client.py @@ -276,3 +276,12 @@ def test_invalid_connect(tmp_path): url = f"duckdb://{tmp_path}?read_only=invalid_value" with pytest.raises(ValueError): ibis.connect(url) + + +@pytest.mark.xfail( + raises=duckdb.BinderException, + reason="https://github.com/ibis-project/ibis/issues/8632", +) +def test_map_null_workaround_xfail(): + conn = duckdb.connect() + conn.sql("SELECT MAP([0, 1, 2], NULL::INT[]);").fetchone() diff --git a/ibis/backends/postgres/compiler.py b/ibis/backends/postgres/compiler.py index 7d794984375b3..12b89829a3f92 100644 --- a/ibis/backends/postgres/compiler.py +++ b/ibis/backends/postgres/compiler.py @@ -330,7 +330,10 @@ def visit_ToJSONArray(self, op, *, arg): ) def visit_Map(self, op, *, keys, values): - return self.f.map(self.f.array(*keys), self.f.array(*values)) + # map(["a", "b"], NULL) results in {"a": NULL, "b": NULL} in regular postgres, + # so we need to modify it to return NULL instead + regular = self.f.map(keys, values) + return self.if_(values.is_(NULL), NULL, regular) def visit_MapLength(self, op, *, arg): return self.f.cardinality(self.f.akeys(arg)) diff --git a/ibis/backends/tests/test_map.py b/ibis/backends/tests/test_map.py index a80945653325f..5762a2e92674c 100644 --- a/ibis/backends/tests/test_map.py +++ b/ibis/backends/tests/test_map.py @@ -26,6 +26,24 @@ ] +@pytest.mark.notyet("clickhouse", reason="nested types can't be NULL") +@pytest.mark.broken(["pandas", "dask"], reason="TypeError: iteration over a 0-d array") +@pytest.mark.parametrize( + "kv", + [ + param((["a", "b"], None), id="null_values"), + param((None, ["c", "d"]), id="null_keys"), + param((None, None), id="null_both"), + ], +) +def test_map_nulls(con, kv): + k, v = kv + k = ibis.literal(k, type="array") + v = ibis.literal(v, type="array") + m = ibis.map(k, v) + assert con.execute(m) is None + + @pytest.mark.notimpl(["pandas", "dask"]) def test_map_table(backend): table = backend.map