]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Add dumper for Python int to numeric
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Thu, 6 May 2021 17:08:20 +0000 (19:08 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Thu, 6 May 2021 17:17:44 +0000 (19:17 +0200)
psycopg3/psycopg3/types/numeric.py
psycopg3_c/psycopg3_c/types/numeric.pyx
tests/fix_faker.py
tests/types/test_numeric.py

index fb9b6fea23ee620b8c1a41b9e78b6081d420bdf3..e2c1214df0046321a4e8a102f9951379d891a148 100644 (file)
@@ -5,6 +5,7 @@ Adapers for numeric types.
 # Copyright (C) 2020-2021 The Psycopg Team
 
 import struct
+from math import log
 from typing import Any, Callable, DefaultDict, Dict, Tuple, Union, cast
 from decimal import Decimal, DefaultContext, Context
 
@@ -177,12 +178,33 @@ class Int8BinaryDumper(Int8Dumper):
         return _pack_int8(obj)
 
 
+# Ratio between number of bits required to store a number and number of pg
+# decimal digits required.
+BIT_PER_PGDIGIT = log(2) / log(10_000)
+
+
 class IntNumericBinaryDumper(IntNumericDumper):
 
     format = Format.BINARY
 
-    def dump(self, obj: int) -> bytes:
-        raise NotImplementedError("binary decimal dump not implemented yet")
+    def dump(self, obj: int) -> bytearray:
+        ndigits = int(obj.bit_length() * BIT_PER_PGDIGIT) + 1
+        out = bytearray(b"\x00\x00" * (ndigits + 4))
+        if obj < 0:
+            sign = NUMERIC_NEG
+            obj = -obj
+        else:
+            sign = NUMERIC_POS
+
+        out[:8] = _pack_numeric_head(ndigits, ndigits - 1, sign, 0)
+        i = 8 + (ndigits - 1) * 2
+        while obj:
+            rem = obj % 10_000
+            obj //= 10_000
+            out[i : i + 2] = _pack_uint2(rem)
+            i -= 2
+
+        return out
 
 
 class OidBinaryDumper(OidDumper):
index 11786afc3006cb92dd68b614cbc5eb2f652ef41b..d835f46e5622d391ed8076657ea9d57fa09380dd 100644 (file)
@@ -161,6 +161,14 @@ cdef class Int8BinaryDumper(CDumper):
         return sizeof(int64_t)
 
 
+# Ratio between number of bits required to store a number and number of pg
+# decimal digits required.
+DEF BIT_PER_PGDIGIT = 0.07525749891599529  # log(2) / log(10_000)
+
+DEF NUMERIC_POS = 0x0000
+DEF NUMERIC_NEG = 0x4000
+
+
 @cython.final
 cdef class IntNumericBinaryDumper(CDumper):
 
@@ -170,7 +178,35 @@ cdef class IntNumericBinaryDumper(CDumper):
         self.oid = oids.NUMERIC_OID
 
     cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
-        raise NotImplementedError("binary decimal dump not implemented yet")
+        # Calculate the number of PG digits required to store the number
+        cdef uint16_t ndigits
+        ndigits = <uint16_t>((<int>obj.bit_length()) * BIT_PER_PGDIGIT) + 1
+
+        cdef uint16_t sign = NUMERIC_POS
+        if obj < 0:
+            sign = NUMERIC_NEG
+            obj = -obj
+
+        cdef Py_ssize_t length = sizeof(uint16_t) * (ndigits + 4)
+        cdef uint16_t *buf
+        buf = <uint16_t *><void *>CDumper.ensure_size(rv, offset, length)
+        buf[0] = endian.htobe16(ndigits)
+        buf[1] = endian.htobe16(ndigits - 1)  # weight
+        buf[2] = endian.htobe16(sign)
+        buf[3] = 0  # dscale
+
+        cdef int i = 4 + ndigits - 1
+        cdef uint16_t rem
+        while obj:
+            rem = obj % 10000
+            obj //= 10000
+            buf[i] = endian.htobe16(rem)
+            i -= 1
+        while i > 3:
+            buf[i] = 0
+            i -= 1
+
+        return length
 
 
 cdef class IntDumper(_NumberDumper):
index b5b6f20e50e021ff11d096ffe9b84c1c2ef9b82e..6770b554bebd35c7e405323830c1280154635b40 100644 (file)
@@ -272,7 +272,7 @@ class Faker:
             assert got == want
 
     def make_int(self, spec):
-        return randrange(-(1 << 63), 1 << 63)
+        return randrange(-(1 << 90), 1 << 90)
 
     def make_Int2(self, spec):
         return spec(randrange(-(1 << 15), 1 << 15))
index 3f6f93f822b14bd5f7341accc4d22935ea4305f2..8fc9ab42f727dab29d44e431ce1caf7ff01607a7 100644 (file)
@@ -58,13 +58,16 @@ def test_dump_int(conn, val, expr, fmt_in):
 )
 @pytest.mark.parametrize("fmt_in", [Format.AUTO, Format.TEXT, Format.BINARY])
 def test_dump_int_subtypes(conn, val, expr, fmt_in):
-    if fmt_in in (Format.AUTO, Format.BINARY) and "numeric" in expr:
-        pytest.xfail("binary numeric not implemented")
     cur = conn.cursor()
     cur.execute(f"select pg_typeof({expr}) = pg_typeof(%{fmt_in})", (val,))
     assert cur.fetchone()[0] is True
-    cur.execute(f"select {expr} = %{fmt_in}", (val,))
-    assert cur.fetchone()[0] is True
+    cur.execute(
+        f"select {expr} = %(v){fmt_in}, {expr}::text, %(v){fmt_in}::text",
+        {"v": val},
+    )
+    ok, want, got = cur.fetchone()
+    assert got == want
+    assert ok
 
 
 @pytest.mark.parametrize("fmt_in", [Format.AUTO, Format.TEXT, Format.BINARY])
@@ -89,6 +92,10 @@ def test_dump_enum(conn, fmt_in):
         (-42, b" -42"),
         (int(2 ** 63 - 1), b"9223372036854775807"),
         (int(-(2 ** 63)), b" -9223372036854775808"),
+        (int(2 ** 63), b"9223372036854775808"),
+        (int(-(2 ** 63 + 1)), b" -9223372036854775809"),
+        (int(2 ** 100), b"1267650600228229401496703205376"),
+        (int(-(2 ** 100)), b" -1267650600228229401496703205376"),
     ],
 )
 def test_quote_int(conn, val, expr):