summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-11-17 22:58:23 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2012-11-17 22:58:23 -0500
commitcda08307bedd4c0aadb49457f0912103179eadca (patch)
tree74279773a19263abf7c92a76bfa0dbac43e62cd4
parent713a1d3b241d23873ac8c2edeb81e073afd2db87 (diff)
downloadsqlalchemy-cda08307bedd4c0aadb49457f0912103179eadca.tar.gz
- hstore documentation, migration
- don't need a custom exception here, just use ValueError
-rw-r--r--doc/build/changelog/migration_08.rst38
-rw-r--r--doc/build/core/types.rst1
-rw-r--r--doc/build/dialects/postgresql.rst5
-rw-r--r--lib/sqlalchemy/dialects/postgresql/__init__.py5
-rw-r--r--lib/sqlalchemy/dialects/postgresql/hstore.py97
-rw-r--r--test/dialect/test_postgresql.py40
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)