From 3ac8647480925f81b97814606297ab3abb397fa7 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Mon, 19 Sep 2022 02:20:38 +0100 Subject: [PATCH] fix: manage dumpers returning None in nested dumpers --- psycopg/psycopg/types/composite.py | 4 +++- psycopg/psycopg/types/json.py | 6 +++++- psycopg/psycopg/types/range.py | 4 +++- tests/types/test_composite.py | 18 ++++++++++++++++++ tests/types/test_multirange.py | 13 +++++++++++++ tests/types/test_range.py | 15 +++++++++++++++ 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/psycopg/psycopg/types/composite.py b/psycopg/psycopg/types/composite.py index d3f41b675..174727cfb 100644 --- a/psycopg/psycopg/types/composite.py +++ b/psycopg/psycopg/types/composite.py @@ -97,7 +97,9 @@ class SequenceDumper(RecursiveDumper): dumper = self._tx.get_dumper(item, PyFormat.from_pq(self.format)) ad = dumper.dump(item) - if not ad: + if ad is None: + ad = b"" + elif not ad: ad = b'""' elif self._re_needs_quotes.search(ad): ad = b'"' + self._re_esc.sub(rb"\1\1", ad) + b'"' diff --git a/psycopg/psycopg/types/json.py b/psycopg/psycopg/types/json.py index 0f5651e71..7ac2be63f 100644 --- a/psycopg/psycopg/types/json.py +++ b/psycopg/psycopg/types/json.py @@ -172,7 +172,11 @@ class JsonbBinaryDumper(_JsonDumper): oid = _oids.JSONB_OID def dump(self, obj: Any) -> Optional[Buffer]: - return b"\x01" + super().dump(obj) + obj_bytes = super().dump(obj) + if obj_bytes is not None: + return b"\x01" + obj_bytes + else: + return None class _JsonLoader(Loader): diff --git a/psycopg/psycopg/types/range.py b/psycopg/psycopg/types/range.py index c3d472621..dba34139c 100644 --- a/psycopg/psycopg/types/range.py +++ b/psycopg/psycopg/types/range.py @@ -372,7 +372,9 @@ def dump_range_text(obj: Range[Any], dump: DumpFunc) -> Buffer: def dump_item(item: Any) -> Buffer: ad = dump(item) - if not ad: + if ad is None: + return b"" + elif not ad: return b'""' elif _re_needs_quotes.search(ad): return b'"' + _re_esc.sub(rb"\1\1", ad) + b'"' diff --git a/tests/types/test_composite.py b/tests/types/test_composite.py index 2a2a3a878..b0433f19b 100644 --- a/tests/types/test_composite.py +++ b/tests/types/test_composite.py @@ -9,6 +9,7 @@ from psycopg.types.composite import TupleDumper, TupleBinaryDumper from ..utils import eur from ..fix_crdb import is_crdb, crdb_skip_message +from ..test_adapt import StrNoneDumper, StrNoneBinaryDumper pytestmark = pytest.mark.crdb_skip("composite") @@ -69,6 +70,23 @@ def test_dump_tuple(conn, rec, obj): assert res == obj +def test_dump_tuple_null(conn): + cur = conn.cursor() + cur.execute( + """ + drop type if exists tmptype; + create type tmptype as (f1 text, f2 text); + """ + ) + info = CompositeInfo.fetch(conn, "tmptype") + register_composite(info, conn) + conn.adapters.register_dumper(str, StrNoneDumper) + conn.adapters.register_dumper(str, StrNoneBinaryDumper) + + res = conn.execute("select %s::tmptype", [("foo", "")]).fetchone()[0] + assert res == ("foo", None) + + @pytest.mark.parametrize("fmt_out", pq.Format) def test_load_all_chars(conn, fmt_out): cur = conn.cursor(binary=fmt_out) diff --git a/tests/types/test_multirange.py b/tests/types/test_multirange.py index 65ff0fe0e..2b0fecac1 100644 --- a/tests/types/test_multirange.py +++ b/tests/types/test_multirange.py @@ -14,6 +14,7 @@ from psycopg.types.multirange import register_multirange from ..utils import eur from .test_range import create_test_range +from ..test_adapt import StrNoneDumper, StrNoneBinaryDumper pytestmark = [ pytest.mark.pg(">= 14"), @@ -409,6 +410,18 @@ def test_dump_custom_empty(conn, testmr): assert cur.fetchone()[0] is True +@pytest.mark.parametrize("fmt_in", PyFormat) +def test_dump_custom_none(conn, fmt_in): + info = MultirangeInfo.fetch(conn, "testmultirange") + register_multirange(info, conn) + conn.adapters.register_dumper(str, StrNoneDumper) + conn.adapters.register_dumper(str, StrNoneBinaryDumper) + + r = Multirange[str]() + cur = conn.execute("select '{}'::testmultirange = %s", (r,)) + assert cur.fetchone()[0] is True + + @pytest.mark.parametrize("fmt_out", pq.Format) def test_load_custom_empty(conn, testmr, fmt_out): info = MultirangeInfo.fetch(conn, "testmultirange") diff --git a/tests/types/test_range.py b/tests/types/test_range.py index b1026d302..08b2387ab 100644 --- a/tests/types/test_range.py +++ b/tests/types/test_range.py @@ -12,6 +12,7 @@ from psycopg.types.range import Range, RangeInfo, register_range from ..utils import eur from ..fix_crdb import is_crdb, crdb_skip_message +from ..test_adapt import StrNoneDumper, StrNoneBinaryDumper pytestmark = pytest.mark.crdb_skip("range") @@ -325,6 +326,20 @@ def test_dump_custom_empty(conn, testrange): assert cur.fetchone()[0] is True +@pytest.mark.parametrize("fmt_in", PyFormat) +def test_dump_custom_null(conn, testrange, fmt_in): + info = RangeInfo.fetch(conn, "testrange") + register_range(info, conn) + conn.adapters.register_dumper(str, StrNoneDumper) + conn.adapters.register_dumper(str, StrNoneBinaryDumper) + + r = Range[str]("", "foo") + cur = conn.execute(f"select %{fmt_in.value}::testrange", (r,)) + r1 = cur.fetchone()[0] + assert r1.lower is None + assert r1.upper == "foo" + + def test_dump_quoting(conn, testrange): info = RangeInfo.fetch(conn, "testrange") register_range(info, conn) -- 2.47.2