- hstore documentation, migration

- don't need a custom exception here, just use ValueError
This commit is contained in:
Mike Bayer
2012-11-17 22:58:23 -05:00
parent 713a1d3b24
commit cda08307be
6 changed files with 141 additions and 45 deletions
+37 -1
View File
@@ -762,10 +762,46 @@ against a particular target selectable::
:meth:`.Select.correlate_except`
Postgresql HSTORE type
----------------------
Support for Postgresql's ``HSTORE`` type is now available as
:class:`.postgresql.HSTORE`. This type makes great usage
of the new operator system to provide a full range of operators
for HSTORE types, including index access, concatenation,
and containment methods such as
:meth:`~.HSTORE.comparator_factory.has_key`,
:meth:`~.HSTORE.comparator_factory.has_any`, and
:meth:`~.HSTORE.comparator_factory.matrix`::
from sqlalchemy.dialects.postgresql import HSTORE
data = Table('data_table', metadata,
Column('id', Integer, primary_key=True),
Column('hstore_data', HSTORE)
)
engine.execute(
select([data.c.hstore_data['some_key']])
).scalar()
engine.execute(
select([data.c.hstore_data.matrix()])
).scalar()
.. seealso::
:class:`.postgresql.HSTORE`
:class:`.postgresql.hstore`
:ticket:`2606`
Enhanced Postgresql ARRAY type
------------------------------
The ``postgresql.ARRAY`` type will accept an optional
The :class:`.postgresql.ARRAY` type will accept an optional
"dimension" argument, pinning it to a fixed number of
dimensions and greatly improving efficiency when retrieving
results:
-1
View File
@@ -753,7 +753,6 @@ Base Type API
:members:
:show-inheritance:
.. autoclass:: Concatenable
:members:
:inherited-members:
+5
View File
@@ -50,6 +50,11 @@ construction arguments, are as follows:
:show-inheritance:
.. autoclass:: HSTORE
:members:
:show-inheritance:
.. autoclass:: hstore
:members:
:show-inheritance:
.. autoclass:: INET
@@ -12,12 +12,11 @@ from .base import \
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, array
from .hstore import HSTORE, hstore, HStoreSyntaxError
from .hstore import HSTORE, hstore
__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', 'array', 'HSTORE', 'hstore',
'HStoreSyntaxError'
'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'array', 'HSTORE', 'hstore'
)
+60 -37
View File
@@ -10,9 +10,8 @@ from .base import ARRAY
from ... import types as sqltypes
from ...sql import functions as sqlfunc
from ...sql.operators import custom_op
from ...exc import SQLAlchemyError
__all__ = ('HStoreSyntaxError', 'HSTORE', 'hstore')
__all__ = ('HSTORE', 'hstore')
# My best guess at the parsing rules of hstore literals, since no formal
# grammar is given. This is mostly reverse engineered from PG's input parser
@@ -33,28 +32,23 @@ HSTORE_DELIMITER_RE = re.compile(r"""
""", re.VERBOSE)
class HStoreSyntaxError(SQLAlchemyError):
"""Indicates an error unmarshalling an hstore value."""
def __init__(self, hstore_str, pos):
self.hstore_str = hstore_str
self.pos = pos
def _parse_error(hstore_str, pos):
"""format an unmarshalling error."""
ctx = 20
hslen = len(hstore_str)
ctx = 20
hslen = len(hstore_str)
parsed_tail = hstore_str[max(pos - ctx - 1, 0):min(pos, hslen)]
residual = hstore_str[min(pos, hslen):min(pos + ctx + 1, hslen)]
parsed_tail = hstore_str[max(pos - ctx - 1, 0):min(pos, hslen)]
residual = hstore_str[min(pos, hslen):min(pos + ctx + 1, hslen)]
if len(parsed_tail) > ctx:
parsed_tail = '[...]' + parsed_tail[1:]
if len(residual) > ctx:
residual = residual[:-1] + '[...]'
if len(parsed_tail) > ctx:
parsed_tail = '[...]' + parsed_tail[1:]
if len(residual) > ctx:
residual = residual[:-1] + '[...]'
super(HStoreSyntaxError, self).__init__(
"After %r, could not parse residual at position %d: %r" %
(parsed_tail, pos, residual)
)
return "After %r, could not parse residual at position %d: %r" % (
parsed_tail, pos, residual)
def _parse_hstore(hstore_str):
@@ -66,7 +60,7 @@ def _parse_hstore(hstore_str):
accepts as input, the documentation makes no guarantees that will always
be the case.
Throws HStoreSyntaxError if parsing fails.
"""
result = {}
@@ -90,7 +84,7 @@ def _parse_hstore(hstore_str):
pair_match = HSTORE_PAIR_RE.match(hstore_str[pos:])
if pos != len(hstore_str):
raise HStoreSyntaxError(hstore_str, pos)
raise ValueError(_parse_error(hstore_str, pos))
return result
@@ -131,11 +125,21 @@ class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine):
:class:`.HSTORE` provides for a wide range of operations, including:
* :meth:`.HSTORE.comparatopr_factory.has_key`
* Index operations::
* :meth:`.HSTORE.comparatopr_factory.has_all`
data_table.c.data['some key'] == 'some value'
* :meth:`.HSTORE.comparatopr_factory.defined`
* Containment operations::
data_table.c.data.has_key('some key')
data_table.c.data.has_all(['one', 'two', 'three'])
* Concatenation::
data_table.c.data + {"k1": "v1"}
For a full list of special methods see :class:`.HSTORE.comparator_factory`.
For usage with the SQLAlchemy ORM, it may be desirable to combine
the usage of :class:`.HSTORE` with the :mod:`sqlalchemy.ext.mutable`
@@ -158,11 +162,20 @@ class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine):
session.commit()
.. versionadded:: 0.8
.. seealso::
:class:`.hstore` - render the Postgresql ``hstore()`` function.
"""
__visit_name__ = 'HSTORE'
class comparator_factory(sqltypes.TypeEngine.Comparator):
"""Define comparison operations for :class:`.HSTORE`."""
def has_key(self, other):
"""Boolean expression. Test for presence of a key. Note that the
key may be a SQLA expression.
@@ -251,15 +264,6 @@ class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine):
return op, sqltypes.Text
return op, other_comparator.type
#@util.memoized_property
#@property
#def _expression_adaptations(self):
# return {
# operators.getitem: {
# sqltypes.String: sqltypes.String
# },
# }
def bind_processor(self, dialect):
def process(value):
if isinstance(value, dict):
@@ -278,11 +282,30 @@ class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine):
class hstore(sqlfunc.GenericFunction):
"""Construct an hstore on the server side using the hstore function.
"""Construct an hstore value within a SQL expression using the
Postgresql ``hstore()`` function.
The single argument or a pair of arguments are evaluated as SQLAlchemy
expressions, so both may contain columns, function calls, or any other
valid SQL expressions which evaluate to text or array.
The :class:`.hstore` function accepts one or two arguments as described
in the Postgresql documentation.
E.g.::
from sqlalchemy.dialects.postgresql import array, hstore
select([hstore('key1', 'value1')])
select([
hstore(
array(['key1', 'key2', 'key3']),
array(['value1', 'value2', 'value3'])
)
])
.. versionadded:: 0.8
.. seealso::
:class:`.HSTORE` - the Postgresql ``HSTORE`` datatype.
"""
type = HSTORE
+37 -3
View File
@@ -17,7 +17,7 @@ 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
from sqlalchemy.dialects.postgresql import HSTORE, hstore, array, ARRAY
from sqlalchemy.util.compat import decimal
from sqlalchemy.testing.util import round_decimal
from sqlalchemy.sql import table, column
@@ -2755,6 +2755,20 @@ class HStoreTest(fixtures.TestBase):
'"key2"=>"value2", "key1"=>"value1"'
)
def test_parse_error(self):
from sqlalchemy.engine import default
dialect = default.DefaultDialect()
proc = self.test_table.c.hash.type._cached_result_processor(
dialect, None)
assert_raises_message(
ValueError,
r'''After '\[\.\.\.\], "key1"=>"value1", ', could not parse '''
'''residual at position 36: 'crapcrapcrap, "key3"\[\.\.\.\]''',
proc,
'"key2"=>"value2", "key1"=>"value1", '
'crapcrapcrap, "key3"=>"value3"'
)
def test_result_deserialize_default(self):
from sqlalchemy.engine import default
@@ -2954,9 +2968,8 @@ class HStoreTest(fixtures.TestBase):
)
class HStoreRoundTripTest(fixtures.TablesTest):
#__only_on__ = 'postgresql'
__requires__ = 'hstore',
__dialect__ = postgresql.dialect()
__dialect__ = 'postgresql'
@classmethod
def define_tables(cls, metadata):
@@ -3025,3 +3038,24 @@ class HStoreRoundTripTest(fixtures.TablesTest):
select([data_table.c.data]).where(data_table.c.data['k1'] == 'r3v1')
).first()
eq_(result, ({'k1': 'r3v1', 'k2': 'r3v2'},))
def _test_fixed_round_trip(self, engine):
s = select([
hstore(
array(['key1', 'key2', 'key3']),
array(['value1', 'value2', 'value3'])
)
])
eq_(
engine.scalar(s),
{"key1": "value1", "key2": "value2", "key3": "value3"}
)
def test_fixed_round_trip_python(self):
engine = self._non_native_engine()
self._test_fixed_round_trip(engine)
@testing.only_on("postgresql+psycopg2")
def test_fixed_round_trip_native(self):
engine = testing.db
self._test_fixed_round_trip(engine)