Basic type support for the new range types in postgres 9.2

This commit is contained in:
Chris Withers
2013-05-19 08:50:06 +01:00
parent 1652491cc6
commit 70edfa229c
5 changed files with 211 additions and 2 deletions
@@ -13,11 +13,14 @@ from .base import \
INET, CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME, \
DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect, array, Any, All
from .hstore import HSTORE, hstore
from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \
TSTZRANGE
__all__ = (
'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC',
'FLOAT', 'REAL', 'INET', 'CIDR', 'UUID', 'BIT', 'MACADDR',
'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN',
'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE',
'hstore'
'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE',
'TSRANGE', 'TSTZRANGE'
)
@@ -1150,6 +1150,24 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
def visit_HSTORE(self, type_):
return "HSTORE"
def visit_INT4RANGE(self, type_):
return "INT4RANGE"
def visit_INT8RANGE(self, type_):
return "INT8RANGE"
def visit_NUMRANGE(self, type_):
return "NUMRANGE"
def visit_DATERANGE(self, type_):
return "DATERANGE"
def visit_TSRANGE(self, type_):
return "TSRANGE"
def visit_TSTZRANGE(self, type_):
return "TSTZRANGE"
def visit_datetime(self, type_):
return self.visit_TIMESTAMP(type_)
@@ -0,0 +1,51 @@
# Copyright (C) 2013 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
from .base import ischema_names
from ... import types as sqltypes
__all__ = ('INT4RANGE', 'INT8RANGE', 'NUMRANGE')
class INT4RANGE(sqltypes.TypeEngine):
"Represent the Postgresql INT4RANGE type."
__visit_name__ = 'INT4RANGE'
ischema_names['int4range'] = INT4RANGE
class INT8RANGE(sqltypes.TypeEngine):
"Represent the Postgresql INT8RANGE type."
__visit_name__ = 'INT8RANGE'
ischema_names['int8range'] = INT8RANGE
class NUMRANGE(sqltypes.TypeEngine):
"Represent the Postgresql NUMRANGE type."
__visit_name__ = 'NUMRANGE'
ischema_names['numrange'] = NUMRANGE
class DATERANGE(sqltypes.TypeEngine):
"Represent the Postgresql DATERANGE type."
__visit_name__ = 'DATERANGE'
ischema_names['daterange'] = DATERANGE
class TSRANGE(sqltypes.TypeEngine):
"Represent the Postgresql TSRANGE type."
__visit_name__ = 'TSRANGE'
ischema_names['tsrange'] = TSRANGE
class TSTZRANGE(sqltypes.TypeEngine):
"Represent the Postgresql TSTZRANGE type."
__visit_name__ = 'TSTZRANGE'
ischema_names['tstzrange'] = TSTZRANGE
+123 -1
View File
@@ -17,7 +17,8 @@ from sqlalchemy import Table, Column, select, MetaData, text, Integer, \
from sqlalchemy.orm import Session, mapper, aliased
from sqlalchemy import exc, schema, types
from sqlalchemy.dialects.postgresql import base as postgresql
from sqlalchemy.dialects.postgresql import HSTORE, hstore, array
from sqlalchemy.dialects.postgresql import HSTORE, hstore, array, \
INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, TSTZRANGE
import decimal
from sqlalchemy import util
from sqlalchemy.testing.util import round_decimal
@@ -3232,3 +3233,124 @@ class HStoreRoundTripTest(fixtures.TablesTest):
def test_unicode_round_trip_native(self):
engine = testing.db
self._test_unicode_round_trip(engine)
class _RangeTypeMixin(object):
__requires__ = 'range_types',
__dialect__ = 'postgresql+psycopg2'
@property
def extras(self):
# done this way so we don't get ImportErrors with
# older psycopg2 versions.
from psycopg2 import extras
return extras
@classmethod
def define_tables(cls, metadata):
# no reason ranges shouldn't be primary keys,
# so lets just use them as such
Table('data_table', metadata,
Column('range', cls._col_type, primary_key=True),
)
def test_actual_type(self):
eq_(str(self._col_type()), self._col_str)
def test_reflect(self):
from sqlalchemy import inspect
insp = inspect(testing.db)
cols = insp.get_columns('data_table')
assert isinstance(cols[0]['type'], self._col_type)
def _assert_data(self):
data = testing.db.execute(
select([self.tables.data_table.c.range])
).fetchall()
eq_(data, [(self._data_obj(), )])
def test_insert_obj(self):
testing.db.engine.execute(
self.tables.data_table.insert(),
{'range': self._data_obj()}
)
self._assert_data()
def test_insert_text(self):
testing.db.engine.execute(
self.tables.data_table.insert(),
{'range': self._data_str}
)
self._assert_data()
class Int4RangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = INT4RANGE
_col_str = 'INT4RANGE'
_data_str = '[1,2)'
def _data_obj(self):
return self.extras.NumericRange(1, 2)
class Int8RangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = INT8RANGE
_col_str = 'INT8RANGE'
_data_str = '[9223372036854775806,9223372036854775807)'
def _data_obj(self):
return self.extras.NumericRange(
9223372036854775806, 9223372036854775807
)
class NumRangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = NUMRANGE
_col_str = 'NUMRANGE'
_data_str = '[1.0,2.0)'
def _data_obj(self):
return self.extras.NumericRange(
decimal.Decimal('1.0'), decimal.Decimal('2.0')
)
class DateRangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = DATERANGE
_col_str = 'DATERANGE'
_data_str = '[2013-03-23,2013-03-24)'
def _data_obj(self):
return self.extras.DateRange(
datetime.date(2013, 3, 23), datetime.date(2013, 3, 24)
)
class DateTimeRangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = TSRANGE
_col_str = 'TSRANGE'
_data_str = '[2013-03-23 14:30,2013-03-23 23:30)'
def _data_obj(self):
return self.extras.DateTimeRange(
datetime.datetime(2013, 3, 23, 14, 30),
datetime.datetime(2013, 3, 23, 23, 30)
)
class DateTimeTZRangeTests(_RangeTypeMixin, fixtures.TablesTest):
_col_type = TSTZRANGE
_col_str = 'TSTZRANGE'
# make sure we use one, steady timestamp with timezone pair
# for all parts of all these tests
_tstzs = None
def tstzs(self):
if self._tstzs is None:
lower = testing.db.connect().scalar(
func.current_timestamp().select()
)
upper = lower+datetime.timedelta(1)
self._tstzs = (lower, upper)
return self._tstzs
@property
def _data_str(self):
return '[%s,%s)' % self.tstzs()
def _data_obj(self):
return self.extras.DateTimeTZRange(*self.tstzs())
+15
View File
@@ -601,6 +601,21 @@ class DefaultRequirements(SuiteRequirements):
return only_if(check_hstore)
@property
def range_types(self):
def check_range_types():
if not against("postgresql+psycopg2"):
return False
try:
self.db.execute("select '[1,2)'::int4range;")
# only supported in psycopg 2.5+
from psycopg2.extras import NumericRange
return True
except:
return False
return only_if(check_range_types)
@property
def sqlite(self):
return skip_if(lambda: not self._has_sqlite())