summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-08-10 21:35:59 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-08-10 21:40:46 -0400
commit39adada25d71ac33d2a468cc19eb61c0fdfa9280 (patch)
treed109bc998adfb31e54d2c40094a306d8e73762ca
parentb914e9123848be8c39ff5a876911528446a2946f (diff)
downloadsqlalchemy-39adada25d71ac33d2a468cc19eb61c0fdfa9280.tar.gz
- with pg JSON types, indexed/keyed access *always* returns json/jsonb;
e.g. it never is automatically cast to the type of element in the structure. therefore we really don't need the concept of index_map, it's overkill. Replace it with a simple callback in Indexable where each type does its own setup_getitem(), and we're done. Add a convenience "index_type" attribute to HSTORE and an "astext_type" attribute to JSON/JSONB..
-rw-r--r--lib/sqlalchemy/dialects/postgresql/array.py55
-rw-r--r--lib/sqlalchemy/dialects/postgresql/hstore.py24
-rw-r--r--lib/sqlalchemy/dialects/postgresql/json.py32
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py143
-rw-r--r--test/dialect/postgresql/test_types.py53
5 files changed, 49 insertions, 258 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/array.py b/lib/sqlalchemy/dialects/postgresql/array.py
index 5e3bcceb4..8c63b43ce 100644
--- a/lib/sqlalchemy/dialects/postgresql/array.py
+++ b/lib/sqlalchemy/dialects/postgresql/array.py
@@ -171,8 +171,7 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
})
Multi-dimensional array index support is provided automatically based on
- either the value specified for the :paramref:`.ARRAY.dimensions` parameter,
- or more specifically the :paramref:`.ARRAY.index_map` parameter.
+ either the value specified for the :paramref:`.ARRAY.dimensions` parameter.
E.g. an :class:`.ARRAY` with dimensions set to 2 would return an expression
of type :class:`.ARRAY` for a single index operation::
@@ -216,6 +215,17 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
"""Define comparison operations for :class:`.ARRAY`."""
+ def _setup_getitem(self, index):
+ if isinstance(index, slice):
+ return_type = self.type
+ elif self.type.dimensions is None or self.type.dimensions == 1:
+ return_type = self.type.item_type
+ else:
+ adapt_kw = {'dimensions': self.type.dimensions - 1}
+ return_type = self.type.adapt(self.type.__class__, **adapt_kw)
+
+ return operators.getitem, index, return_type
+
def any(self, other, operator=operators.eq):
"""Return ``other operator ANY (array)`` clause.
@@ -297,14 +307,8 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
comparator_factory = Comparator
- def _type_for_index(self, index):
- adapt_kw = {}
- if self.dimensions is not None:
- adapt_kw['dimensions'] = self.dimensions - 1
- return super(ARRAY, self)._type_for_index(index, adapt_kw)
-
def __init__(self, item_type, as_tuple=False, dimensions=None,
- zero_indexes=False, index_map=None):
+ zero_indexes=False):
"""Construct an ARRAY.
E.g.::
@@ -338,25 +342,6 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
.. versionadded:: 0.9.5
- :param index_map: type map used by the getitem operator, e.g.
- expressions like ``col[5]``. See :class:`.Indexable` for a
- description of how this map is configured.
-
- For the :class:`.ARRAY` class, this map if omitted is
- automatically configured based on the number of dimensions
- given, that is an :class:`.ARRAY` that specifies
- ``dimensions=3`` and a return type of ``String`` would
- generate an index_map of ``{ANY_KEY: {ANY_KEY: {ANY_KEY:
- String}}}``, so that an expression of ``col[x][y][z]`` would
- yield arrays until the last index, which yields a string
- expression.
-
- When the map and the dimensions argument is omitted, the map here
- assumes a 1-dimensional array and defaults to
- ``{ANY_KEY: <array type}``.
-
- ..versionadded:: 1.1
-
"""
if isinstance(item_type, ARRAY):
@@ -368,20 +353,6 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
self.as_tuple = as_tuple
self.dimensions = dimensions
self.zero_indexes = zero_indexes
- if index_map is not None:
- self.index_map = index_map
- elif self.dimensions is not None:
- self.index_map = d = {
- ARRAY.SLICE_TYPE: ARRAY.SAME_TYPE
- }
- for i in range(self.dimensions - 1):
- d[ARRAY.ANY_KEY] = d = {}
- d[ARRAY.ANY_KEY] = self.item_type
- else:
- self.index_map = {
- ARRAY.ANY_KEY: self.item_type,
- ARRAY.SLICE_TYPE: ARRAY.SAME_TYPE
- }
@property
def hashable(self):
diff --git a/lib/sqlalchemy/dialects/postgresql/hstore.py b/lib/sqlalchemy/dialects/postgresql/hstore.py
index 0a6327407..63d392632 100644
--- a/lib/sqlalchemy/dialects/postgresql/hstore.py
+++ b/lib/sqlalchemy/dialects/postgresql/hstore.py
@@ -118,23 +118,19 @@ class HSTORE(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
__visit_name__ = 'HSTORE'
hashable = False
+ index_type = sqltypes.Text()
- index_map = {sqltypes.Indexable.ANY_KEY: sqltypes.Text()}
+ def __init__(self, index_type=None):
+ """Construct a new :class:`.HSTORE`.
- def __init__(self, none_as_null=False, index_map=None):
- """Construct an :class:`.HSTORE` type.
+ :param index_type: the type that should be used for indexed values.
+ Defaults to :class:`.types.Text`.
- :param index_map: type map used by the getitem operator, e.g.
- expressions like ``col['somekey']``. See :class:`.Indexable` for a
- description of how this map is configured. For :class:`.HSTORE`,
- the index_map defaults to ``{ANY_KEY: Text}``, as all hstore
- elements in Postgresql are of type text.
+ .. versionadded:: 1.1.0
- .. versionadded: 1.1
-
- """
- if index_map is not None:
- self.index_map = index_map
+ """
+ if index_type is not None:
+ self.index_type = index_type
class Comparator(
sqltypes.Indexable.Comparator, sqltypes.Concatenable.Comparator):
@@ -170,7 +166,7 @@ class HSTORE(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
CONTAINED_BY, other, result_type=sqltypes.Boolean)
def _setup_getitem(self, index):
- return INDEX, index, self.type._type_for_index(index)
+ return INDEX, index, self.type.index_type
def defined(self, key):
"""Boolean expression. Test for presence of a non-NULL value for
diff --git a/lib/sqlalchemy/dialects/postgresql/json.py b/lib/sqlalchemy/dialects/postgresql/json.py
index 8ab49db81..dcd42c48c 100644
--- a/lib/sqlalchemy/dialects/postgresql/json.py
+++ b/lib/sqlalchemy/dialects/postgresql/json.py
@@ -106,10 +106,7 @@ class JSON(sqltypes.Indexable, sqltypes.TypeEngine):
Index operations return an expression object whose type defaults to
:class:`.JSON` by default, so that further JSON-oriented instructions
- may be called upon the result type. The return type of index operations
- can be customized on a per-key basis using the :paramref:`.JSON.index_map`
- parameter; see :class:`.Indexable` for background on how to set up
- index maps.
+ may be called upon the result type.
The :class:`.JSON` type, when used with the SQLAlchemy ORM, does not
detect in-place mutations to the structure. In order to detect these, the
@@ -142,8 +139,9 @@ class JSON(sqltypes.Indexable, sqltypes.TypeEngine):
__visit_name__ = 'JSON'
hashable = False
+ astext_type = sqltypes.Text()
- def __init__(self, none_as_null=False, index_map=None):
+ def __init__(self, none_as_null=False, astext_type=None):
"""Construct a :class:`.JSON` type.
:param none_as_null: if True, persist the value ``None`` as a
@@ -157,18 +155,16 @@ class JSON(sqltypes.Indexable, sqltypes.TypeEngine):
.. versionchanged:: 0.9.8 - Added ``none_as_null``, and :func:`.null`
is now supported in order to persist a NULL value.
- :param index_map: type map used by the getitem operator, e.g.
- expressions like ``col[5]``. See :class:`.Indexable` for a
- description of how this map is configured. The index_map
- for the :class:`.JSON` and :class:`.JSONB` types defaults to
- ``{ANY_KEY: SAME_TYPE}``.
+ :param astext_type: the type to use for the
+ :attr:`.JSON.Comparator.astext`
+ accessor on indexed attributes. Defaults to :class:`.types.Text`.
- .. versionadded: 1.1
+ .. versionadded:: 1.1.0
"""
self.none_as_null = none_as_null
- if index_map is not None:
- self.index_map = index_map
+ if astext_type is not None:
+ self.astext_type = astext_type
class Comparator(
sqltypes.Indexable.Comparator, sqltypes.Concatenable.Comparator):
@@ -195,24 +191,18 @@ class JSON(sqltypes.Indexable, sqltypes.TypeEngine):
against = ASTEXT
return self.expr.left.operate(
- against, self.expr.right, result_type=sqltypes.Text)
+ against, self.expr.right, result_type=self.type.astext_type)
def _setup_getitem(self, index):
if not isinstance(index, util.string_types):
assert isinstance(index, collections.Sequence)
tokens = [util.text_type(elem) for elem in index]
index = "{%s}" % (", ".join(tokens))
-
- ret_type = self.type
- for tok in tokens:
- ret_type = ret_type._type_for_index(tok)
-
operator = PATHIDX
else:
operator = INDEX
- ret_type = self.type._type_for_index(index)
- return operator, index, ret_type
+ return operator, index, self.type
comparator_factory = Comparator
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index ea602a733..a9d8a1d16 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -79,87 +79,6 @@ class Indexable(object):
"""A mixin that marks a type as supporting indexing operations,
such as array or JSON structures.
- The key feature provided by :class:`.Indexable` is the index map feature.
- This allows SQL expressions that use the index operator to have
- an expected return type based on the structure. For example, if
- a JSON type expects to return json structures with the keys "a" and "b",
- and the value referred to by "a" is an integer, and the value referred
- to by "b" is itself a JSON structure with keys "c" and "d" referring
- to String values, an index map for this schema would look like the
- following::
-
- # assume JSON is an Indexable subclass
-
- type = JSON(index_map={
- "a": Integer,
- "b": {
- "c": String,
- "d": String
- }
- }
- )
-
- An expression that uses this type would indicate the following return
- types::
-
- column('x', type)['a'] # returns Integer
- column('x', type)['b'] # returns JSON(index_map={"c": String, "d": String})
- column('x', type)['b']['c'] # returns String
- column('x', type)['unknown'] # raises an error
-
- To indicate any key returned, the symbol :attr:`.Indexable.ANY_KEY` may
- be used::
-
- type = JSON(index_map={
- JSON.ANY_KEY: {
- "x": Integer,
- "y": Integer
- }
- }
- )
-
- The above structure would be appropriate for a list of two-element mappings,
- where any value in the first position, including SQL expressions, returns
- the structure::
-
- column('x', type)[5]['x'] # returns Integer
- column('x', type)[column('y')]['x'] # returns Integer
-
- To indicate a recursive structure, e.g. where a match should just
- return the current type, use the symbol :attr:`.Indexable.SAME_TYPE`::
-
- type = JSON(index_map={
- JSON.ANY_KEY: JSON.SAME_TYPE
- }
- )
-
- The above structure would cause all index expressions to keep returning
- the same JSON structure repeatedly. Types like the built in PostgreSQL
- :class:`.postgresql.JSON` type use the above map by default.
-
- For an array-like structure, the index_map would typically refer
- to the number of dimensions known to be in the array. E.g. if we
- have a two-dimensional array of integers, the index_map assuming an
- :class:`.Indexable` subclass ARRAY would be::
-
- type = ARRAY(index_map={
- ARRAY.ANY_KEY: {
- ARRAY.ANY_KEY: Integer
- }
- }
- )
-
- Above, a single-index operation returns another array::
-
- column('x', type)[5] # returns ARRAY of Integer
-
- whereas calling upon two dimensions returns Integer::
-
- column('x', type)[5][7] # returns Integer
-
- The above ARRAY structure is used by the built-in PostgreSQL
- :class:`.postgresql.ARRAY` type, when configured with a specific
- number of dimensions.
.. versionadded:: 1.1.0
@@ -170,70 +89,10 @@ class Indexable(object):
"""if True, Python zero-based indexes should be interpreted as one-based
on the SQL expression side."""
- ANY_KEY = util.symbol('ANY_KEY')
- """Symbol indicating a match for any index or key in an index_map structure.
-
- .. seealso::
-
- :class:`.Indexable`
-
- """
-
- SLICE_TYPE = util.symbol('SLICE_TYPE')
- """Symbol indicating a match for an index sent as a slice in
- an index_map structure.
-
- .. seealso::
-
- :class:`.Indexable`
-
- """
-
- SAME_TYPE = util.symbol('SAME_TYPE')
- """Symbol indicating that this same type should be returned upon
- a match in an index_map structure.
-
- .. seealso::
-
- :class:`.Indexable`
-
- """
-
- index_map = util.immutabledict({
- ANY_KEY: SAME_TYPE
- })
-
- def _type_for_index(self, index, adapt_kw=util.immutabledict()):
- """Given a getitem index, look in the index_map to see if
- a known type is set up for this value.
-
- May return a new type which would contain a fragment of
- the index map at that point.
-
- """
- map_ = self.index_map
- if isinstance(index, slice):
- index = Indexable.SLICE_TYPE
-
- if index in map_:
- new_type = map_[index]
- else:
- try:
- new_type = map_[Indexable.ANY_KEY]
- except KeyError:
- raise exc.InvalidRequestError(
- "Key not handled by type declaration: %s" % index)
- if isinstance(new_type, collections.Mapping):
- return self.adapt(self.__class__, index_map=new_type, **adapt_kw)
- elif new_type is Indexable.SAME_TYPE:
- return self
- else:
- return new_type
-
class Comparator(TypeEngine.Comparator):
def _setup_getitem(self, index):
- return operators.getitem, index, self.type._type_for_index(index)
+ raise NotImplementedError()
def __getitem__(self, index):
operator, adjusted_right_expr, result_type = \
diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py
index 8bf872295..d03186322 100644
--- a/test/dialect/postgresql/test_types.py
+++ b/test/dialect/postgresql/test_types.py
@@ -828,25 +828,6 @@ class ArrayTest(fixtures.TablesTest, AssertsExecutionResults):
), True
)
- def test_array_index_map_generic(self):
- col = column('x', postgresql.ARRAY(Integer))
- is_(
- col[5].type._type_affinity, Integer
- )
-
- def test_array_index_map_unbounded(self):
- col = column(
- 'x',
- postgresql.ARRAY(
- Integer, index_map={
- postgresql.ARRAY.ANY_KEY: postgresql.ARRAY.SAME_TYPE}))
- is_(
- col[5].type._type_affinity, postgresql.ARRAY
- )
- is_(
- col[5][6][7][8].type._type_affinity, postgresql.ARRAY
- )
-
def test_array_index_map_dimensions(self):
col = column('x', postgresql.ARRAY(Integer, dimensions=3))
is_(
@@ -1418,7 +1399,7 @@ class HStoreTest(AssertsCompiledSQL, fixtures.TestBase):
class MyType(types.UserDefinedType):
pass
- col = column('x', HSTORE(index_map={HSTORE.ANY_KEY: MyType}))
+ col = column('x', HSTORE(index_type=MyType))
is_(col['foo'].type.__class__, MyType)
@@ -2144,9 +2125,7 @@ class JSONTest(AssertsCompiledSQL, fixtures.TestBase):
)
def test_path_typing(self):
- col = column('x', JSON(index_map={
- 'q': {'p': {'r': Integer, JSON.ANY_KEY: String}}
- }))
+ col = column('x', JSON())
is_(
col['q'].type._type_affinity, JSON
)
@@ -2159,27 +2138,23 @@ class JSONTest(AssertsCompiledSQL, fixtures.TestBase):
is_(
col[('q', 'p')].type._type_affinity, JSON
)
+
+ def test_custom_astext_type(self):
+ class MyType(types.UserDefinedType):
+ pass
+
+ col = column('x', JSON(astext_type=MyType))
+
is_(
- col['q']['p']['r'].type._type_affinity, Integer
- )
- is_(
- col[('q', 'p', 'r')].type._type_affinity, Integer
- )
- is_(
- col['q']['p']['j'].type._type_affinity, String
+ col['q'].astext.type.__class__, MyType
)
+
is_(
- col[('q', 'p', 'j')].type._type_affinity, String
+ col[('q', 'p')].astext.type.__class__, MyType
)
- def test_path_typing_unknown_key(self):
- col = column('x', JSON(index_map={
- 'q': Integer
- }))
- assert_raises_message(
- sa.exc.InvalidRequestError,
- "Key not handled by type declaration: j",
- operators.getitem, col, 'j'
+ is_(
+ col['q']['p'].astext.type.__class__, MyType
)
def test_where_getitem_as_text(self):