]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
pg8000: Support range and multirange types
authorTony Locke <tlocke@tlocke.org.uk>
Mon, 19 Jun 2023 20:32:18 +0000 (16:32 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 19 Jun 2023 23:38:05 +0000 (19:38 -0400)
The pg8000 dialect now supports RANGE and MULTIRANGE datatypes, using the
existing RANGE API described at :ref:`postgresql_ranges`.  Range and
multirange types are supported in the pg8000 driver from version 1.29.8.
Pull request courtesy Tony Locke.

Fixes: #9965
Closes: #9966
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/9966
Pull-request-sha: b48dc3ef9cbb267fe945d08ca38240c68b716f79

Change-Id: I61778fe35f9edbf93f440136e6667847bcec4b9c

doc/build/changelog/unreleased_20/9965.rst [new file with mode: 0644]
doc/build/dialects/postgresql.rst
lib/sqlalchemy/dialects/postgresql/pg8000.py
test/dialect/postgresql/test_types.py
test/requirements.py

diff --git a/doc/build/changelog/unreleased_20/9965.rst b/doc/build/changelog/unreleased_20/9965.rst
new file mode 100644 (file)
index 0000000..f719771
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: usecase, postgresql
+    :tickets: 9965
+
+    The pg8000 dialect now supports RANGE and MULTIRANGE datatypes, using the
+    existing RANGE API described at :ref:`postgresql_ranges`.  Range and
+    multirange types are supported in the pg8000 driver from version 1.29.8.
+    Pull request courtesy Tony Locke.
index 53659fb54e99c4dad8da54b110d34c4ac30ce855..71a017ee06a424068ca5505a8f9c70341dc4be34 100644 (file)
@@ -144,8 +144,12 @@ E.g.::
 Range and Multirange Types
 --------------------------
 
-PostgreSQL range and multirange types are supported for the psycopg2,
-psycopg, and asyncpg dialects.
+PostgreSQL range and multirange types are supported for the
+psycopg, pg8000 and asyncpg dialects; the psycopg2 dialect supports the
+range types only.
+
+.. versionadded:: 2.0.17 Added range and multirange support for the pg8000
+   dialect.  pg8000 1.29.8 or greater is required.
 
 Data values being passed to the database may be passed as string
 values or by using the :class:`_postgresql.Range` data object.
@@ -223,9 +227,16 @@ Multiranges
 Multiranges are supported by PostgreSQL 14 and above.  SQLAlchemy's
 multirange datatypes deal in lists of :class:`_postgresql.Range` types.
 
-.. versionadded:: 2.0 Added support for MULTIRANGE datatypes.   In contrast
-   to the ``psycopg`` multirange feature, SQLAlchemy's adaptation represents
-   a multirange datatype as a list of :class:`_postgresql.Range` objects.
+Multiranges are supported on the psycopg, asyncpg, and pg8000 dialects
+**only**.  The psycopg2 dialect, which is SQLAlchemy's default ``postgresql``
+dialect, **does not** support multirange datatypes.
+
+.. versionadded:: 2.0 Added support for MULTIRANGE datatypes.
+   SQLAlchemy represents a multirange value as a list of
+   :class:`_postgresql.Range` objects.
+
+.. versionadded:: 2.0.17 Added multirange support for the pg8000 dialect.
+   pg8000 1.29.8 or greater is required.
 
 The example below illustrates use of the :class:`_postgresql.TSMULTIRANGE`
 datatype::
index a32d375c7bf3df376bac099ef73bbfea1ae803dd..7536f89e590a6bd7fae6d872eac39546d1550fc9 100644 (file)
@@ -94,6 +94,7 @@ of the :ref:`psycopg2 <psycopg2_isolation_level>` dialect:
 import decimal
 import re
 
+from . import ranges
 from .array import ARRAY as PGARRAY
 from .base import _DECIMAL_TYPES
 from .base import _FLOAT_TYPES
@@ -251,6 +252,70 @@ class _PGOIDVECTOR(_SpaceVector, OIDVECTOR):
     pass
 
 
+class _Pg8000Range(ranges.AbstractRangeImpl):
+    def bind_processor(self, dialect):
+        pg8000_Range = dialect.dbapi.Range
+
+        def to_range(value):
+            if isinstance(value, ranges.Range):
+                value = pg8000_Range(
+                    value.lower, value.upper, value.bounds, value.empty
+                )
+            return value
+
+        return to_range
+
+    def result_processor(self, dialect, coltype):
+        def to_range(value):
+            if value is not None:
+                value = ranges.Range(
+                    value.lower,
+                    value.upper,
+                    bounds=value.bounds,
+                    empty=value.is_empty,
+                )
+            return value
+
+        return to_range
+
+
+class _Pg8000MultiRange(ranges.AbstractMultiRangeImpl):
+    def bind_processor(self, dialect):
+        pg8000_Range = dialect.dbapi.Range
+
+        def to_multirange(value):
+            if isinstance(value, list):
+                mr = []
+                for v in value:
+                    if isinstance(v, ranges.Range):
+                        mr.append(
+                            pg8000_Range(v.lower, v.upper, v.bounds, v.empty)
+                        )
+                    else:
+                        mr.append(v)
+                return mr
+            else:
+                return value
+
+        return to_multirange
+
+    def result_processor(self, dialect, coltype):
+        def to_multirange(value):
+            if value is None:
+                return None
+
+            mr = []
+            for v in value:
+                mr.append(
+                    ranges.Range(
+                        v.lower, v.upper, bounds=v.bounds, empty=v.is_empty
+                    )
+                )
+            return mr
+
+        return to_multirange
+
+
 _server_side_id = util.counter()
 
 
@@ -383,6 +448,18 @@ class PGDialect_pg8000(PGDialect):
             sqltypes.Enum: _PGEnum,
             sqltypes.ARRAY: _PGARRAY,
             OIDVECTOR: _PGOIDVECTOR,
+            ranges.INT4RANGE: _Pg8000Range,
+            ranges.INT8RANGE: _Pg8000Range,
+            ranges.NUMRANGE: _Pg8000Range,
+            ranges.DATERANGE: _Pg8000Range,
+            ranges.TSRANGE: _Pg8000Range,
+            ranges.TSTZRANGE: _Pg8000Range,
+            ranges.INT4MULTIRANGE: _Pg8000MultiRange,
+            ranges.INT8MULTIRANGE: _Pg8000MultiRange,
+            ranges.NUMMULTIRANGE: _Pg8000MultiRange,
+            ranges.DATEMULTIRANGE: _Pg8000MultiRange,
+            ranges.TSMULTIRANGE: _Pg8000MultiRange,
+            ranges.TSTZMULTIRANGE: _Pg8000MultiRange,
         },
     )
 
index b8138853377e3be6818acbeea9f35adea7b84d21..ab417e8813a2820c587f150904c952ac903db67b 100644 (file)
@@ -4636,7 +4636,7 @@ class _RangeTypeRoundTrip(_RangeComparisonFixtures, fixtures.TablesTest):
         )
         self._assert_data(connection)
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_insert_text(self, connection):
         connection.execute(
             self.tables.data_table.insert(), {"range": self._data_str()}
@@ -4653,7 +4653,7 @@ class _RangeTypeRoundTrip(_RangeComparisonFixtures, fixtures.TablesTest):
         data = connection.execute(select(range_ + range_)).fetchall()
         eq_(data, [(self._data_obj(),)])
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_union_result_text(self, connection):
         # insert
         connection.execute(
@@ -4674,7 +4674,7 @@ class _RangeTypeRoundTrip(_RangeComparisonFixtures, fixtures.TablesTest):
         data = connection.execute(select(range_ * range_)).fetchall()
         eq_(data, [(self._data_obj(),)])
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_intersection_result_text(self, connection):
         # insert
         connection.execute(
@@ -4695,7 +4695,7 @@ class _RangeTypeRoundTrip(_RangeComparisonFixtures, fixtures.TablesTest):
         data = connection.execute(select(range_ - range_)).fetchall()
         eq_(data, [(self._data_obj().__class__(empty=True),)])
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_difference_result_text(self, connection):
         # insert
         connection.execute(
@@ -5146,14 +5146,14 @@ class _MultiRangeTypeRoundTrip(fixtures.TablesTest):
         )
         self._assert_data(connection)
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_insert_text(self, connection):
         connection.execute(
             self.tables.data_table.insert(), {"range": self._data_str()}
         )
         self._assert_data(connection)
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_union_result_text(self, connection):
         # insert
         connection.execute(
@@ -5164,7 +5164,7 @@ class _MultiRangeTypeRoundTrip(fixtures.TablesTest):
         data = connection.execute(select(range_ + range_)).fetchall()
         eq_(data, [(self._data_obj(),)])
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_intersection_result_text(self, connection):
         # insert
         connection.execute(
@@ -5175,7 +5175,7 @@ class _MultiRangeTypeRoundTrip(fixtures.TablesTest):
         data = connection.execute(select(range_ * range_)).fetchall()
         eq_(data, [(self._data_obj(),)])
 
-    @testing.requires.any_psycopg_compatibility
+    @testing.requires.psycopg_or_pg8000_compatibility
     def test_difference_result_text(self, connection):
         # insert
         connection.execute(
index 4e57ff7292a587ebd4cee9bc647ee039503fc6db..61cb139338c22cf12fccb2fd38466f7d6cd92a5e 100644 (file)
@@ -1484,11 +1484,13 @@ class DefaultRequirements(SuiteRequirements):
 
     @property
     def range_types(self):
-        return only_on(["+psycopg2", "+psycopg", "+asyncpg"])
+        return only_on(["+psycopg2", "+psycopg", "+asyncpg", "+pg8000"])
 
     @property
     def multirange_types(self):
-        return only_on(["+psycopg", "+asyncpg"]) + only_on("postgresql >= 14")
+        return only_on(["+psycopg", "+asyncpg", "+pg8000"]) + only_on(
+            "postgresql >= 14"
+        )
 
     @property
     def async_dialect(self):