From eeff036db61377b8159757e6cc2a2d83d85bf69e Mon Sep 17 00:00:00 2001 From: zeeeeb Date: Tue, 28 Jun 2022 19:05:08 -0400 Subject: [PATCH] fixes: #7156 - Adds support for PostgreSQL MultiRange type This adds functionality for PostgreSQL MultiRange type, as discussed in Issue #7156. As far as I can tell, only psycopg provides a [Multirange adaptation](https://www.psycopg.org/psycopg3/docs/basic/pgtypes.html#multirange-adaptation). Psycopg2 only supports a [Range adaptation/data type](https://www.psycopg.org/psycopg3/docs/basic/pgtypes.html#multirange-adaptation). This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [ ] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [x] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. Closes: #7816 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/7816 Pull-request-sha: 7e9e0c858dcdb58d4fcca24964ef8d58d1842d41 Change-Id: I345e0f58f534ac37709a7a4627b6de8ddd8fa89e --- doc/build/changelog/unreleased_20/7156.rst | 7 + doc/build/dialects/postgresql.rst | 66 +++ .../dialects/postgresql/__init__.py | 12 + lib/sqlalchemy/dialects/postgresql/base.py | 24 + lib/sqlalchemy/dialects/postgresql/ranges.py | 36 ++ test/dialect/postgresql/test_dialect.py | 4 +- test/dialect/postgresql/test_types.py | 494 +++++++++++++++++- test/requirements.py | 12 +- 8 files changed, 647 insertions(+), 8 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/7156.rst diff --git a/doc/build/changelog/unreleased_20/7156.rst b/doc/build/changelog/unreleased_20/7156.rst new file mode 100644 index 0000000000..76a27ccc1f --- /dev/null +++ b/doc/build/changelog/unreleased_20/7156.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: postgresql, usecase + :tickets: 7156 + + Adds support for PostgreSQL MultiRange types, introduced in PostgreSQL 14. + Note that this feature currently only tested with `psycopg` and depends on + the `psycopg.types.range` extension module. \ No newline at end of file diff --git a/doc/build/dialects/postgresql.rst b/doc/build/dialects/postgresql.rst index 7cd413e25e..b3755c2cde 100644 --- a/doc/build/dialects/postgresql.rst +++ b/doc/build/dialects/postgresql.rst @@ -170,6 +170,72 @@ For example: during=DateTimeRange(datetime(2013, 3, 23), None) ) +MultiRange Types +~~~~~~~~~~~~~~~~ + +The new MultiRange column types found in PostgreSQL 14 onwards are +catered for by the following types: + +.. autoclass:: INT4MULTIRANGE + + +.. autoclass:: INT8MULTIRANGE + + +.. autoclass:: NUMMULTIRANGE + + +.. autoclass:: DATEMULTIRANGE + + +.. autoclass:: TSMULTIRANGE + + +.. autoclass:: TSTZMULTIRANGE + + +The types above get most of their functionality from the following +mixin: + +.. autoclass:: sqlalchemy.dialects.postgresql.ranges.RangeOperators + :members: + +.. warning:: + + The multirange type DDL support should work with any PostgreSQL DBAPI + driver, however the data types returned may vary. The feature is + currently developed against the psycopg driver, and is known to + work with the range types specific to the `psycopg.types.range` + extension module. + +When instantiating models that use these column types, you should pass +whatever data type is expected by the DBAPI driver you're using for +the column type. + +For example: + +.. code-block:: python + # Note: Multirange type currently only tested against the psycopg + # driver, hence the use here. + from psycopg.types.range import Range + from pscyopg.types.multirange import Multirange + from sqlalchemy.dialects.postgresql import TSMULTIRANGE + + class RoomBooking(Base): + + __tablename__ = 'room_booking' + + room = Column(Integer(), primary_key=True) + during = Column(TSMULTIRANGE()) + + booking = RoomBooking( + room=101, + during=Multirange([ + Range(datetime(2013, 3, 23), datetime(2014, 3, 22)), + Range(datetime(2015, 1, 1), None) + ]) + + PostgreSQL Constraint Types --------------------------- diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index 62195f59e6..baafdb1811 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -47,11 +47,17 @@ from .named_types import DropDomainType from .named_types import DropEnumType from .named_types import ENUM from .named_types import NamedType +from .ranges import DATEMULTIRANGE from .ranges import DATERANGE +from .ranges import INT4MULTIRANGE from .ranges import INT4RANGE +from .ranges import INT8MULTIRANGE from .ranges import INT8RANGE +from .ranges import NUMMULTIRANGE from .ranges import NUMRANGE +from .ranges import TSMULTIRANGE from .ranges import TSRANGE +from .ranges import TSTZMULTIRANGE from .ranges import TSTZRANGE from .types import BIT from .types import BYTEA @@ -110,9 +116,15 @@ __all__ = ( "INT8RANGE", "NUMRANGE", "DATERANGE", + "INT4MULTIRANGE", + "INT8MULTIRANGE", + "NUMMULTIRANGE", + "DATEMULTIRANGE", "TSVECTOR", "TSRANGE", "TSTZRANGE", + "TSMULTIRANGE", + "TSTZMULTIRANGE", "JSON", "JSONB", "Any", diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 8b89cdee20..efb4dd547f 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1656,6 +1656,12 @@ ischema_names = { "daterange": _ranges.DATERANGE, "tsrange": _ranges.TSRANGE, "tstzrange": _ranges.TSTZRANGE, + "int4multirange": _ranges.INT4MULTIRANGE, + "int8multirange": _ranges.INT8MULTIRANGE, + "nummultirange": _ranges.NUMMULTIRANGE, + "datemultirange": _ranges.DATEMULTIRANGE, + "tsmultirange": _ranges.TSMULTIRANGE, + "tstzmultirange": _ranges.TSTZMULTIRANGE, "integer": INTEGER, "bigint": BIGINT, "smallint": SMALLINT, @@ -2500,6 +2506,24 @@ class PGTypeCompiler(compiler.GenericTypeCompiler): def visit_JSONB(self, type_, **kw): return "JSONB" + def visit_INT4MULTIRANGE(self, type_, **kw): + return "INT4MULTIRANGE" + + def visit_INT8MULTIRANGE(self, type_, **kw): + return "INT8MULTIRANGE" + + def visit_NUMMULTIRANGE(self, type_, **kw): + return "NUMMULTIRANGE" + + def visit_DATEMULTIRANGE(self, type_, **kw): + return "DATEMULTIRANGE" + + def visit_TSMULTIRANGE(self, type_, **kw): + return "TSMULTIRANGE" + + def visit_TSTZMULTIRANGE(self, type_, **kw): + return "TSTZMULTIRANGE" + def visit_INT4RANGE(self, type_, **kw): return "INT4RANGE" diff --git a/lib/sqlalchemy/dialects/postgresql/ranges.py b/lib/sqlalchemy/dialects/postgresql/ranges.py index 81431ad597..4f010abf13 100644 --- a/lib/sqlalchemy/dialects/postgresql/ranges.py +++ b/lib/sqlalchemy/dialects/postgresql/ranges.py @@ -138,3 +138,39 @@ class TSTZRANGE(RangeOperators, sqltypes.TypeEngine): """Represent the PostgreSQL TSTZRANGE type.""" __visit_name__ = "TSTZRANGE" + + +class INT4MULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL INT4MULTIRANGE type.""" + + __visit_name__ = "INT4MULTIRANGE" + + +class INT8MULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL INT8MULTIRANGE type.""" + + __visit_name__ = "INT8MULTIRANGE" + + +class NUMMULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL NUMMULTIRANGE type.""" + + __visit_name__ = "NUMMULTIRANGE" + + +class DATEMULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL DATEMULTIRANGE type.""" + + __visit_name__ = "DATEMULTIRANGE" + + +class TSMULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL TSRANGE type.""" + + __visit_name__ = "TSMULTIRANGE" + + +class TSTZMULTIRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the PostgreSQL TSTZRANGE type.""" + + __visit_name__ = "TSTZMULTIRANGE" diff --git a/test/dialect/postgresql/test_dialect.py b/test/dialect/postgresql/test_dialect.py index 1ffd82ae4d..9cbb0bca7a 100644 --- a/test/dialect/postgresql/test_dialect.py +++ b/test/dialect/postgresql/test_dialect.py @@ -1207,7 +1207,7 @@ class MiscBackendTest( dbapi_conn.rollback() eq_(val, "off") - @testing.requires.psycopg_compatibility + @testing.requires.any_psycopg_compatibility def test_psycopg_non_standard_err(self): # note that psycopg2 is sometimes called psycopg2cffi # depending on platform @@ -1230,7 +1230,7 @@ class MiscBackendTest( assert isinstance(exception, exc.OperationalError) @testing.requires.no_coverage - @testing.requires.psycopg_compatibility + @testing.requires.any_psycopg_compatibility def test_notice_logging(self): log = logging.getLogger("sqlalchemy.dialects.postgresql") buf = logging.handlers.BufferingHandler(100) diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py index 41bd1f5e7b..f774300e68 100644 --- a/test/dialect/postgresql/test_types.py +++ b/test/dialect/postgresql/test_types.py @@ -1,4 +1,5 @@ # coding: utf-8 +from collections import defaultdict import datetime import decimal from enum import Enum as _PY_Enum @@ -37,18 +38,24 @@ from sqlalchemy import Unicode from sqlalchemy import util from sqlalchemy.dialects import postgresql from sqlalchemy.dialects.postgresql import array +from sqlalchemy.dialects.postgresql import DATEMULTIRANGE from sqlalchemy.dialects.postgresql import DATERANGE from sqlalchemy.dialects.postgresql import DOMAIN from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.dialects.postgresql import HSTORE from sqlalchemy.dialects.postgresql import hstore +from sqlalchemy.dialects.postgresql import INT4MULTIRANGE from sqlalchemy.dialects.postgresql import INT4RANGE +from sqlalchemy.dialects.postgresql import INT8MULTIRANGE from sqlalchemy.dialects.postgresql import INT8RANGE from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import NamedType +from sqlalchemy.dialects.postgresql import NUMMULTIRANGE from sqlalchemy.dialects.postgresql import NUMRANGE +from sqlalchemy.dialects.postgresql import TSMULTIRANGE from sqlalchemy.dialects.postgresql import TSRANGE +from sqlalchemy.dialects.postgresql import TSTZMULTIRANGE from sqlalchemy.dialects.postgresql import TSTZRANGE from sqlalchemy.exc import CompileError from sqlalchemy.orm import declarative_base @@ -2650,7 +2657,7 @@ class ArrayEnum(fixtures.TestBase): testing.combinations( sqltypes.ARRAY, postgresql.ARRAY, - (_ArrayOfEnum, testing.requires.psycopg_compatibility), + (_ArrayOfEnum, testing.requires.any_psycopg_compatibility), argnames="array_cls", )(fn) ) @@ -3701,7 +3708,7 @@ class _RangeTypeCompilation(AssertsCompiledSQL, fixtures.TestBase): class _RangeTypeRoundTrip(fixtures.TablesTest): - __requires__ = "range_types", "psycopg_compatibility" + __requires__ = "range_types", "any_psycopg_compatibility" __backend__ = True def extras(self): @@ -3934,6 +3941,489 @@ class DateTimeTZRangeRoundTripTest(_DateTimeTZRangeTests, _RangeTypeRoundTrip): pass +class _MultiRangeTypeCompilation(AssertsCompiledSQL, fixtures.TestBase): + __dialect__ = "postgresql" + + # operator tests + + @classmethod + def setup_test_class(cls): + table = Table( + "data_table", + MetaData(), + Column("multirange", cls._col_type, primary_key=True), + ) + cls.col = table.c.multirange + + def _test_clause(self, colclause, expected, type_): + self.assert_compile(colclause, expected) + is_(colclause.type._type_affinity, type_._type_affinity) + + def test_where_equal(self): + self._test_clause( + self.col == self._data_str(), + "data_table.multirange = %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_where_not_equal(self): + self._test_clause( + self.col != self._data_str(), + "data_table.multirange <> %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_where_is_null(self): + self._test_clause( + self.col == None, + "data_table.multirange IS NULL", + sqltypes.BOOLEANTYPE, + ) + + def test_where_is_not_null(self): + self._test_clause( + self.col != None, + "data_table.multirange IS NOT NULL", + sqltypes.BOOLEANTYPE, + ) + + def test_where_less_than(self): + self._test_clause( + self.col < self._data_str(), + "data_table.multirange < %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_where_greater_than(self): + self._test_clause( + self.col > self._data_str(), + "data_table.multirange > %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_where_less_than_or_equal(self): + self._test_clause( + self.col <= self._data_str(), + "data_table.multirange <= %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_where_greater_than_or_equal(self): + self._test_clause( + self.col >= self._data_str(), + "data_table.multirange >= %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_contains(self): + self._test_clause( + self.col.contains(self._data_str()), + "data_table.multirange @> %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_contained_by(self): + self._test_clause( + self.col.contained_by(self._data_str()), + "data_table.multirange <@ %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_overlaps(self): + self._test_clause( + self.col.overlaps(self._data_str()), + "data_table.multirange && %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_strictly_left_of(self): + self._test_clause( + self.col << self._data_str(), + "data_table.multirange << %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + self._test_clause( + self.col.strictly_left_of(self._data_str()), + "data_table.multirange << %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_strictly_right_of(self): + self._test_clause( + self.col >> self._data_str(), + "data_table.multirange >> %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + self._test_clause( + self.col.strictly_right_of(self._data_str()), + "data_table.multirange >> %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_not_extend_right_of(self): + self._test_clause( + self.col.not_extend_right_of(self._data_str()), + "data_table.multirange &< %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_not_extend_left_of(self): + self._test_clause( + self.col.not_extend_left_of(self._data_str()), + "data_table.multirange &> %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_adjacent_to(self): + self._test_clause( + self.col.adjacent_to(self._data_str()), + "data_table.multirange -|- %(multirange_1)s", + sqltypes.BOOLEANTYPE, + ) + + def test_union(self): + self._test_clause( + self.col + self.col, + "data_table.multirange + data_table.multirange", + self.col.type, + ) + + def test_intersection(self): + self._test_clause( + self.col * self.col, + "data_table.multirange * data_table.multirange", + self.col.type, + ) + + def test_different(self): + self._test_clause( + self.col - self.col, + "data_table.multirange - data_table.multirange", + self.col.type, + ) + + +class _MultiRangeTypeRoundTrip(fixtures.TablesTest): + __requires__ = "range_types", "psycopg_only_compatibility" + __backend__ = True + + def extras(self): + # done this way so we don't get ImportErrors with + # older psycopg2 versions. + if testing.against("postgresql+psycopg"): + from psycopg.types.range import Range + from psycopg.types.multirange import Multirange + + class psycopg_extras: + def __init__(self): + self.data = defaultdict( + lambda: Range, Multirange=Multirange + ) + + def __getattr__(self, name): + return self.data[name] + + extras = psycopg_extras() + else: + assert False, "Unsupported MultiRange Dialect" + return extras + + @classmethod + def define_tables(cls, metadata): + # no reason ranges shouldn't be primary keys, + # so lets just use them as such + table = Table( + "data_table", + metadata, + Column("range", cls._col_type, primary_key=True), + ) + cls.col = table.c.range + + def test_actual_type(self): + eq_(str(self._col_type()), self._col_str) + + def test_reflect(self, connection): + from sqlalchemy import inspect + + insp = inspect(connection) + cols = insp.get_columns("data_table") + assert isinstance(cols[0]["type"], self._col_type) + + def _assert_data(self, conn): + data = conn.execute(select(self.tables.data_table.c.range)).fetchall() + eq_(data, [(self._data_obj(),)]) + + def test_insert_obj(self, connection): + connection.execute( + self.tables.data_table.insert(), {"range": self._data_obj()} + ) + self._assert_data(connection) + + def test_insert_text(self, connection): + connection.execute( + self.tables.data_table.insert(), {"range": self._data_str()} + ) + self._assert_data(connection) + + def test_union_result(self, connection): + # insert + connection.execute( + self.tables.data_table.insert(), {"range": self._data_str()} + ) + # select + range_ = self.tables.data_table.c.range + data = connection.execute(select(range_ + range_)).fetchall() + eq_(data, [(self._data_obj(),)]) + + def test_intersection_result(self, connection): + # insert + connection.execute( + self.tables.data_table.insert(), {"range": self._data_str()} + ) + # select + range_ = self.tables.data_table.c.range + data = connection.execute(select(range_ * range_)).fetchall() + eq_(data, [(self._data_obj(),)]) + + def test_difference_result(self, connection): + # insert + connection.execute( + self.tables.data_table.insert(), {"range": self._data_str()} + ) + # select + range_ = self.tables.data_table.c.range + data = connection.execute(select(range_ - range_)).fetchall() + eq_(data, [(self.extras().Multirange(),)]) + + +class _Int4MultiRangeTests: + + _col_type = INT4MULTIRANGE + _col_str = "INT4MULTIRANGE" + + def _data_str(self): + return "{[1,2), [3, 5), [9, 12)}" + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range(1, 2), + self.extras().Range(3, 5), + self.extras().Range(9, 12), + ] + ) + + +class _Int8MultiRangeTests: + + _col_type = INT8MULTIRANGE + _col_str = "INT8MULTIRANGE" + + def _data_str(self): + return ( + "{[9223372036854775801,9223372036854775803)," + + "[9223372036854775805,9223372036854775807)}" + ) + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range(9223372036854775801, 9223372036854775803), + self.extras().Range(9223372036854775805, 9223372036854775807), + ] + ) + + +class _NumMultiRangeTests: + + _col_type = NUMMULTIRANGE + _col_str = "NUMMULTIRANGE" + + def _data_str(self): + return "{[1.0,2.0), [3.0, 5.0), [9.0, 12.0)}" + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range( + decimal.Decimal("1.0"), decimal.Decimal("2.0") + ), + self.extras().Range( + decimal.Decimal("3.0"), decimal.Decimal("5.0") + ), + self.extras().Range( + decimal.Decimal("9.0"), decimal.Decimal("12.0") + ), + ] + ) + + +class _DateMultiRangeTests: + + _col_type = DATEMULTIRANGE + _col_str = "DATEMULTIRANGE" + + def _data_str(self): + return "{[2013-03-23,2013-03-24), [2014-05-23,2014-05-24)}" + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range( + datetime.date(2013, 3, 23), datetime.date(2013, 3, 24) + ), + self.extras().Range( + datetime.date(2014, 5, 23), datetime.date(2014, 5, 24) + ), + ] + ) + + +class _DateTimeMultiRangeTests: + + _col_type = TSMULTIRANGE + _col_str = "TSMULTIRANGE" + + def _data_str(self): + return ( + "{[2013-03-23 14:30,2013-03-23 23:30)," + + "[2014-05-23 14:30,2014-05-23 23:30)}" + ) + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range( + datetime.datetime(2013, 3, 23, 14, 30), + datetime.datetime(2013, 3, 23, 23, 30), + ), + self.extras().Range( + datetime.datetime(2014, 5, 23, 14, 30), + datetime.datetime(2014, 5, 23, 23, 30), + ), + ] + ) + + +class _DateTimeTZMultiRangeTests: + + _col_type = TSTZMULTIRANGE + _col_str = "TSTZMULTIRANGE" + + # make sure we use one, steady timestamp with timezone pair + # for all parts of all these tests + _tstzs = None + _tstzs_delta = None + + def tstzs(self): + if self._tstzs is None: + with testing.db.connect() as connection: + lower = connection.scalar(func.current_timestamp().select()) + upper = lower + datetime.timedelta(1) + self._tstzs = (lower, upper) + return self._tstzs + + def tstzs_delta(self): + if self._tstzs_delta is None: + with testing.db.connect() as connection: + lower = connection.scalar( + func.current_timestamp().select() + ) + datetime.timedelta(3) + upper = lower + datetime.timedelta(2) + self._tstzs_delta = (lower, upper) + return self._tstzs_delta + + def _data_str(self): + tstzs_lower, tstzs_upper = self.tstzs() + tstzs_delta_lower, tstzs_delta_upper = self.tstzs_delta() + return "{{[{tl},{tu}), [{tdl},{tdu})}}".format( + tl=tstzs_lower, + tu=tstzs_upper, + tdl=tstzs_delta_lower, + tdu=tstzs_delta_upper, + ) + + def _data_obj(self): + return self.extras().Multirange( + [ + self.extras().Range(*self.tstzs()), + self.extras().Range(*self.tstzs_delta()), + ] + ) + + +class Int4MultiRangeCompilationTest( + _Int4MultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class Int4MultiRangeRoundTripTest( + _Int4MultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + +class Int8MultiRangeCompilationTest( + _Int8MultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class Int8MultiRangeRoundTripTest( + _Int8MultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + +class NumMultiRangeCompilationTest( + _NumMultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class NumMultiRangeRoundTripTest( + _NumMultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + +class DateMultiRangeCompilationTest( + _DateMultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class DateMultiRangeRoundTripTest( + _DateMultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + +class DateTimeMultiRangeCompilationTest( + _DateTimeMultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class DateTimeMultiRangeRoundTripTest( + _DateTimeMultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + +class DateTimeTZMultiRangeCompilationTest( + _DateTimeTZMultiRangeTests, _MultiRangeTypeCompilation +): + pass + + +class DateTimeTZRMultiangeRoundTripTest( + _DateTimeTZMultiRangeTests, _MultiRangeTypeRoundTrip +): + pass + + class JSONTest(AssertsCompiledSQL, fixtures.TestBase): __dialect__ = "postgresql" diff --git a/test/requirements.py b/test/requirements.py index c7c5beed94..8cd586efd4 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -1354,7 +1354,7 @@ class DefaultRequirements(SuiteRequirements): @property def range_types(self): def check_range_types(config): - if not self.psycopg_compatibility.enabled: + if not self.any_psycopg_compatibility.enabled: return False try: with config.db.connect() as conn: @@ -1414,14 +1414,14 @@ class DefaultRequirements(SuiteRequirements): @property def native_hstore(self): - return self.psycopg_compatibility + return self.any_psycopg_compatibility @property def psycopg2_compatibility(self): return only_on(["postgresql+psycopg2", "postgresql+psycopg2cffi"]) @property - def psycopg_compatibility(self): + def any_psycopg_compatibility(self): return only_on( [ "postgresql+psycopg2", @@ -1430,9 +1430,13 @@ class DefaultRequirements(SuiteRequirements): ] ) + @property + def psycopg_only_compatibility(self): + return only_on(["postgresql+psycopg"]) + @property def psycopg_or_pg8000_compatibility(self): - return only_on([self.psycopg_compatibility, "postgresql+pg8000"]) + return only_on([self.any_psycopg_compatibility, "postgresql+pg8000"]) @property def percent_schema_names(self): -- 2.47.2