: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:
:members:
:show-inheritance:
-
.. autoclass:: Concatenable
:members:
:inherited-members:
:show-inheritance:
.. autoclass:: HSTORE
+ :members:
+ :show-inheritance:
+
+.. autoclass:: hstore
+ :members:
:show-inheritance:
.. autoclass:: INET
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'
)
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
""", 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):
accepts as input, the documentation makes no guarantees that will always
be the case.
- Throws HStoreSyntaxError if parsing fails.
+
"""
result = {}
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
: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`
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.
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):
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
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
'"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
)
class HStoreRoundTripTest(fixtures.TablesTest):
- #__only_on__ = 'postgresql'
__requires__ = 'hstore',
- __dialect__ = postgresql.dialect()
+ __dialect__ = 'postgresql'
@classmethod
def define_tables(cls, metadata):
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)