]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Only convert Range for sqlalchemy Range object
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 21 Oct 2022 16:37:04 +0000 (12:37 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 21 Oct 2022 16:37:04 +0000 (12:37 -0400)
Refined the new approach to range objects described at :ref:`change_7156`
to accommodate driver-specific range and multirange objects, to better
accommodate both legacy code as well as when passing results from raw SQL
result sets back into new range or multirange expressions.

Fixes: #8690
Change-Id: I7e62c47067f695c6380ad0fe2fe19deaf33594d1

doc/build/changelog/changelog_20.rst
doc/build/changelog/unreleased_20/8690.rst [new file with mode: 0644]
doc/build/changelog/whatsnew_20.rst
lib/sqlalchemy/dialects/postgresql/asyncpg.py
lib/sqlalchemy/dialects/postgresql/psycopg.py
lib/sqlalchemy/dialects/postgresql/psycopg2.py
test/dialect/postgresql/test_types.py

index 19427c9b48de716b0e69a616000e02695daf1708..e3470f02878aa1a7e5e3d939d5e5d214c0ac3bae 100644 (file)
 
         .. seealso::
 
+            :ref:`change_7156`
+
             :ref:`postgresql_ranges`
 
     .. change::
diff --git a/doc/build/changelog/unreleased_20/8690.rst b/doc/build/changelog/unreleased_20/8690.rst
new file mode 100644 (file)
index 0000000..d4ae25b
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: usecase, postgresql
+    :tickets: 8690
+
+    Refined the new approach to range objects described at :ref:`change_7156`
+    to accommodate driver-specific range and multirange objects, to better
+    accommodate both legacy code as well as when passing results from raw SQL
+    result sets back into new range or multirange expressions.
index 04c1be3e0a8d38c2188b09a5a6456b6868988d0a..18ca8aeda816237ee7ee8e4001e87574f313a8b3 100644 (file)
@@ -1849,6 +1849,29 @@ the :meth:`_types.TypeEngine.with_variant` method as follows::
         Column("value", Float(5).with_variant(oracle.FLOAT(16), "oracle")),
     )
 
+.. _change_7156:
+
+New RANGE / MULTIRANGE support and changes for PostgreSQL backends
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+RANGE / MULTIRANGE support has been fully implemented for psycopg2, psycopg3,
+and asyncpg dialects.  The new support uses a new SQLAlchemy-specific
+:class:`_postgresql.Range` object that is agnostic of the different backends
+and does not require the use of backend-specific imports or extension
+steps.  For multirange support, lists of :class:`_postgresql.Range`
+objects are used.
+
+Code that used the previous psycopg2-specific types should be modified
+to use :class:`_postgresql.Range`, which presents a compatible interface.
+
+See the documentation at :ref:`postgresql_ranges` for background on
+using the new feature.
+
+
+.. seealso::
+
+    :ref:`postgresql_ranges`
+
 .. _change_7086:
 
 ``match()`` operator on PostgreSQL uses ``plainto_tsquery()`` rather than ``to_tsquery()``
index 96bac59d95a0c84c68edd4eb46395909a16236c1..cd161d28e0ae0be90e318955003b00a871d0bec9 100644 (file)
@@ -293,13 +293,11 @@ class AsyncpgCHAR(sqltypes.CHAR):
 
 class _AsyncpgRange(ranges.AbstractRangeImpl):
     def bind_processor(self, dialect):
-        Range = dialect.dbapi.asyncpg.Range
-
-        NoneType = type(None)
+        asyncpg_Range = dialect.dbapi.asyncpg.Range
 
         def to_range(value):
-            if not isinstance(value, (str, NoneType)):
-                value = Range(
+            if isinstance(value, ranges.Range):
+                value = asyncpg_Range(
                     value.lower,
                     value.upper,
                     lower_inc=value.bounds[0] == "[",
@@ -328,7 +326,7 @@ class _AsyncpgRange(ranges.AbstractRangeImpl):
 
 class _AsyncpgMultiRange(ranges.AbstractMultiRangeImpl):
     def bind_processor(self, dialect):
-        Range = dialect.dbapi.asyncpg.Range
+        asyncpg_Range = dialect.dbapi.asyncpg.Range
 
         NoneType = type(None)
 
@@ -337,8 +335,8 @@ class _AsyncpgMultiRange(ranges.AbstractMultiRangeImpl):
                 return value
 
             def to_range(value):
-                if not isinstance(value, (str, NoneType)):
-                    value = Range(
+                if isinstance(value, ranges.Range):
+                    value = asyncpg_Range(
                         value.lower,
                         value.upper,
                         lower_inc=value.bounds[0] == "[",
index 7ca274e2c779bb4def5022b356fbe54ad1937910..400c3186ec7ee50e5248951e334f2e04389e3e10 100644 (file)
@@ -164,13 +164,11 @@ class _PGBoolean(sqltypes.Boolean):
 
 class _PsycopgRange(ranges.AbstractRangeImpl):
     def bind_processor(self, dialect):
-        Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
-
-        NoneType = type(None)
+        psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
 
         def to_range(value):
-            if not isinstance(value, (str, NoneType)):
-                value = Range(
+            if isinstance(value, ranges.Range):
+                value = psycopg_Range(
                     value.lower, value.upper, value.bounds, value.empty
                 )
             return value
@@ -193,18 +191,20 @@ class _PsycopgRange(ranges.AbstractRangeImpl):
 
 class _PsycopgMultiRange(ranges.AbstractMultiRangeImpl):
     def bind_processor(self, dialect):
-        Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
-        Multirange = cast(PGDialect_psycopg, dialect)._psycopg_Multirange
+        psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
+        psycopg_Multirange = cast(
+            PGDialect_psycopg, dialect
+        )._psycopg_Multirange
 
         NoneType = type(None)
 
         def to_range(value):
-            if isinstance(value, (str, NoneType)):
+            if isinstance(value, (str, NoneType, psycopg_Multirange)):
                 return value
 
-            return Multirange(
+            return psycopg_Multirange(
                 [
-                    Range(
+                    psycopg_Range(
                         element.lower,
                         element.upper,
                         element.bounds,
index 350f4b61690b5e3525b2605bde18d4231ffbaa3a..4a8df5b5e8961976d6d530cc28d4b05e533d6f25 100644 (file)
@@ -503,16 +503,14 @@ class _Psycopg2Range(ranges.AbstractRangeImpl):
     _psycopg2_range_cls = "none"
 
     def bind_processor(self, dialect):
-        Range = getattr(
+        psycopg2_Range = getattr(
             cast(PGDialect_psycopg2, dialect)._psycopg2_extras,
             self._psycopg2_range_cls,
         )
 
-        NoneType = type(None)
-
         def to_range(value):
-            if not isinstance(value, (str, NoneType)):
-                value = Range(
+            if isinstance(value, ranges.Range):
+                value = psycopg2_Range(
                     value.lower, value.upper, value.bounds, value.empty
                 )
             return value
index 1f93a40235f70cfdd9c7877f73719a108b184f37..91eada9a81d66724134abac73d46e353997c8ec3 100644 (file)
@@ -3891,6 +3891,23 @@ class _RangeTypeRoundTrip(fixtures.TablesTest):
         cols = insp.get_columns("data_table")
         assert isinstance(cols[0]["type"], self._col_type)
 
+    def test_textual_round_trip_w_dialect_type(self, connection):
+        """test #8690"""
+        data_table = self.tables.data_table
+
+        data_obj = self._data_obj()
+        connection.execute(
+            self.tables.data_table.insert(), {"range": data_obj}
+        )
+
+        q1 = text("SELECT range from data_table")
+        v = connection.scalar(q1)
+
+        q2 = select(data_table).where(data_table.c.range == v)
+        v2 = connection.scalar(q2)
+
+        eq_(data_obj, v2)
+
     def _assert_data(self, conn):
         data = conn.execute(select(self.tables.data_table.c.range)).fetchall()
         eq_(data, [(self._data_obj(),)])
@@ -4348,6 +4365,23 @@ class _MultiRangeTypeRoundTrip(fixtures.TablesTest):
         data = conn.execute(select(self.tables.data_table.c.range)).fetchall()
         eq_(data, [(self._data_obj(),)])
 
+    def test_textual_round_trip_w_dialect_type(self, connection):
+        """test #8690"""
+        data_table = self.tables.data_table
+
+        data_obj = self._data_obj()
+        connection.execute(
+            self.tables.data_table.insert(), {"range": data_obj}
+        )
+
+        q1 = text("SELECT range from data_table")
+        v = connection.scalar(q1)
+
+        q2 = select(data_table).where(data_table.c.range == v)
+        v2 = connection.scalar(q2)
+
+        eq_(data_obj, v2)
+
     def test_insert_obj(self, connection):
         connection.execute(
             self.tables.data_table.insert(), {"range": self._data_obj()}