From: Tony Locke Date: Mon, 19 Jun 2023 20:32:18 +0000 (-0400) Subject: pg8000: Support range and multirange types X-Git-Tag: rel_2_0_17~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=26dea369c7c985db4db8c504fdeb0bf00c1edeac;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git pg8000: Support range and multirange types 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 --- diff --git a/doc/build/changelog/unreleased_20/9965.rst b/doc/build/changelog/unreleased_20/9965.rst new file mode 100644 index 0000000000..f71977171c --- /dev/null +++ b/doc/build/changelog/unreleased_20/9965.rst @@ -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. diff --git a/doc/build/dialects/postgresql.rst b/doc/build/dialects/postgresql.rst index 53659fb54e..71a017ee06 100644 --- a/doc/build/dialects/postgresql.rst +++ b/doc/build/dialects/postgresql.rst @@ -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:: diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index a32d375c7b..7536f89e59 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -94,6 +94,7 @@ of the :ref:`psycopg2 ` 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, }, ) diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py index b813885337..ab417e8813 100644 --- a/test/dialect/postgresql/test_types.py +++ b/test/dialect/postgresql/test_types.py @@ -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( diff --git a/test/requirements.py b/test/requirements.py index 4e57ff7292..61cb139338 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -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):