diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-08-10 21:35:59 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-08-10 21:40:46 -0400 |
commit | 39adada25d71ac33d2a468cc19eb61c0fdfa9280 (patch) | |
tree | d109bc998adfb31e54d2c40094a306d8e73762ca | |
parent | b914e9123848be8c39ff5a876911528446a2946f (diff) | |
download | sqlalchemy-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.py | 55 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/hstore.py | 24 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/json.py | 32 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 143 | ||||
-rw-r--r-- | test/dialect/postgresql/test_types.py | 53 |
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): |