]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- hstore documentation, migration
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 18 Nov 2012 03:58:23 +0000 (22:58 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 18 Nov 2012 03:58:23 +0000 (22:58 -0500)
- don't need a custom exception here, just use ValueError

doc/build/changelog/migration_08.rst
doc/build/core/types.rst
doc/build/dialects/postgresql.rst
lib/sqlalchemy/dialects/postgresql/__init__.py
lib/sqlalchemy/dialects/postgresql/hstore.py
test/dialect/test_postgresql.py

index c4f34f6a7ec6bf8281eaab2f4bbd2ad9195da7cb..7d4cc4991e4730e4ed93c39b98629158b4111d42 100644 (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:
index 745752f0e2cdad890af9bf11fbe2566ede024b7a..6a74631414f0dd54f42e5fc22955931f79fb93a8 100644 (file)
@@ -753,7 +753,6 @@ Base Type API
    :members:
    :show-inheritance:
 
-
 .. autoclass:: Concatenable
    :members:
    :inherited-members:
index 3943f865a1113e269f6c39f10cf2865bf93dc9ab..ac89ab1234546c617f8a315dc7ac00fd5bf7c38b 100644 (file)
@@ -50,6 +50,11 @@ construction arguments, are as follows:
     :show-inheritance:
 
 .. autoclass:: HSTORE
+    :members:
+    :show-inheritance:
+
+.. autoclass:: hstore
+    :members:
     :show-inheritance:
 
 .. autoclass:: INET
index 2a1a07cbd7955e77ebca670330e8931035e7b370..04334fa7899ec20b956fe83aafe2fdfca25938c3 100644 (file)
@@ -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'
 )
index ee5dec168e83ee59f02d814e6b08082af9e2fc55..d7a1612749bfca4a4acc47a80309f688d5f176be 100644 (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::
+
+        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
index 25b6bcb54a4292dfb786098ec3cf5d337e5de2a5..40d8d0c79b3155b0a8aeeab1bbb02fc14cd18da8 100644 (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)