diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-11-17 22:58:23 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-11-17 22:58:23 -0500 |
commit | cda08307bedd4c0aadb49457f0912103179eadca (patch) | |
tree | 74279773a19263abf7c92a76bfa0dbac43e62cd4 | |
parent | 713a1d3b241d23873ac8c2edeb81e073afd2db87 (diff) | |
download | sqlalchemy-cda08307bedd4c0aadb49457f0912103179eadca.tar.gz |
- hstore documentation, migration
- don't need a custom exception here, just use ValueError
-rw-r--r-- | doc/build/changelog/migration_08.rst | 38 | ||||
-rw-r--r-- | doc/build/core/types.rst | 1 | ||||
-rw-r--r-- | doc/build/dialects/postgresql.rst | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/__init__.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/hstore.py | 97 | ||||
-rw-r--r-- | test/dialect/test_postgresql.py | 40 |
6 files changed, 141 insertions, 45 deletions
diff --git a/doc/build/changelog/migration_08.rst b/doc/build/changelog/migration_08.rst index c4f34f6a7..7d4cc4991 100644 --- a/doc/build/changelog/migration_08.rst +++ b/doc/build/changelog/migration_08.rst @@ -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: diff --git a/doc/build/core/types.rst b/doc/build/core/types.rst index 745752f0e..6a7463141 100644 --- a/doc/build/core/types.rst +++ b/doc/build/core/types.rst @@ -753,7 +753,6 @@ Base Type API :members: :show-inheritance: - .. autoclass:: Concatenable :members: :inherited-members: diff --git a/doc/build/dialects/postgresql.rst b/doc/build/dialects/postgresql.rst index 3943f865a..ac89ab123 100644 --- a/doc/build/dialects/postgresql.rst +++ b/doc/build/dialects/postgresql.rst @@ -50,6 +50,11 @@ construction arguments, are as follows: :show-inheritance: .. autoclass:: HSTORE + :members: + :show-inheritance: + +.. autoclass:: hstore + :members: :show-inheritance: .. autoclass:: INET diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index 2a1a07cbd..04334fa78 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -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' ) diff --git a/lib/sqlalchemy/dialects/postgresql/hstore.py b/lib/sqlalchemy/dialects/postgresql/hstore.py index ee5dec168..d7a161274 100644 --- a/lib/sqlalchemy/dialects/postgresql/hstore.py +++ b/lib/sqlalchemy/dialects/postgresql/hstore.py @@ -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:: + + data_table.c.data['some key'] == 'some value' + + * Containment operations:: + + data_table.c.data.has_key('some key') - * :meth:`.HSTORE.comparatopr_factory.has_all` + data_table.c.data.has_all(['one', 'two', 'three']) - * :meth:`.HSTORE.comparatopr_factory.defined` + * 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 :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:: - 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. + :class:`.HSTORE` - the Postgresql ``HSTORE`` datatype. """ type = HSTORE diff --git a/test/dialect/test_postgresql.py b/test/dialect/test_postgresql.py index 25b6bcb54..40d8d0c79 100644 --- a/test/dialect/test_postgresql.py +++ b/test/dialect/test_postgresql.py @@ -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) |