diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-08-10 10:06:27 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-08-10 10:06:27 -0400 |
commit | 020a7b643f254afdeba530c27fe4453c74dad825 (patch) | |
tree | 4b86ef5a536dbae722a7d9e5396e0a90a287131e | |
parent | b0c88dd7711463e052bb9b8414199720cde62296 (diff) | |
download | sqlalchemy-020a7b643f254afdeba530c27fe4453c74dad825.tar.gz |
dev
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/__init__.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/array.py | 27 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/json.py | 182 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 17 |
4 files changed, 114 insertions, 118 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index a0ffbbfbc..46f45a340 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -16,7 +16,7 @@ from .base import \ CreateEnumType from .constraints import ExcludeConstraint from .hstore import HSTORE, hstore -from .json import JSON, JSONElement, JSONB +from .json import JSON, JSONB from .array import array, ARRAY, Any, All from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \ @@ -28,6 +28,6 @@ __all__ = ( 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN', 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE', 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE', - 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONB', 'JSONElement', - 'DropEnumType', 'CreateEnumType' + 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONB', + 'DropEnumType', 'CreateEnumType', 'ExcludeConstraint' ) diff --git a/lib/sqlalchemy/dialects/postgresql/array.py b/lib/sqlalchemy/dialects/postgresql/array.py index 5c8dad211..c5bcdebd0 100644 --- a/lib/sqlalchemy/dialects/postgresql/array.py +++ b/lib/sqlalchemy/dialects/postgresql/array.py @@ -109,6 +109,11 @@ class array(expression.Tuple): return self +CONTAINS = operators.custom_op("@>", precedence=5) +CONTAINED_BY = operators.custom_op("<@", precedence=5) +OVERLAP = operators.custom_op("&&", precedence=5) + + class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine): """Postgresql ARRAY type. @@ -195,7 +200,8 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine): """ __visit_name__ = 'ARRAY' - class Comparator(sqltypes.Concatenable.Comparator): + class Comparator( + sqltypes.Indexable.Comparator, sqltypes.Concatenable.Comparator): """Define comparison operations for :class:`.ARRAY`.""" @@ -263,26 +269,29 @@ class ARRAY(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine): """Boolean expression. Test if elements are a superset of the elements of the argument array expression. """ - return self.expr.op('@>')(other) + return self.operate(CONTAINS, other) def contained_by(self, other): """Boolean expression. Test if elements are a proper subset of the elements of the argument array expression. """ - return self.expr.op('<@')(other) + return self.operate(CONTAINED_BY, other) def overlap(self, other): """Boolean expression. Test if array has elements in common with an argument array expression. """ - return self.expr.op('&&')(other) + return self.operate(OVERLAP, other) + + def _index_map_type(self, right_comparator): + return self.type def _adapt_expression(self, op, other_comparator): - if isinstance(op, operators.custom_op): - if op.opstring in ['@>', '<@', '&&']: - return op, sqltypes.Boolean - return sqltypes.Concatenable.Comparator.\ - _adapt_expression(self, op, other_comparator) + if op in (CONTAINS, CONTAINED_BY, OVERLAP): + return op, sqltypes.Boolean + else: + return super(ARRAY.Comparator, self).\ + _adapt_expression(op, other_comparator) comparator_factory = Comparator diff --git a/lib/sqlalchemy/dialects/postgresql/json.py b/lib/sqlalchemy/dialects/postgresql/json.py index 6f4ac4ac9..8b1a70820 100644 --- a/lib/sqlalchemy/dialects/postgresql/json.py +++ b/lib/sqlalchemy/dialects/postgresql/json.py @@ -6,113 +6,40 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from __future__ import absolute_import +import collections import json from .base import ischema_names from ... import types as sqltypes -from ...sql.operators import custom_op -from ... import sql -from ...sql import elements, default_comparator +from ...sql import operators +from ...sql import elements from ... import util -__all__ = ('JSON', 'JSONElement', 'JSONB') +__all__ = ('JSON', 'JSONB') -class JSONElement(elements.IndexExpression): - """Represents accessing an element of a :class:`.JSON` value. +# json : returns json +INDEX = operators.custom_op( + "->", precedence=5, natural_self_precedent=True +) - The :class:`.JSONElement` is produced whenever using the Python index - operator on an expression that has the type :class:`.JSON`:: +# path operator: returns json +PATHIDX = operators.custom_op( + "#>", precedence=5, natural_self_precedent=True +) - expr = mytable.c.json_data['some_key'] +# json + astext: returns text +ASTEXT = operators.custom_op( + "->>", precedence=5, natural_self_precedent=True +) - The expression typically compiles to a JSON access such as ``col -> key``. - Modifiers are then available for typing behavior, including - :meth:`.JSONElement.cast` and :attr:`.JSONElement.astext`. - - """ - - INDEX = custom_op( - "->", precedence=5, natural_self_precedent=True - ) - ARRAYIDX = custom_op( - "#>", precedence=5, natural_self_precedent=True - ) - ASTEXT = custom_op( - "->>", precedence=5, natural_self_precedent=True - ) - ASTEXT_ARRAYIDX = custom_op( - "#>>", precedence=5, natural_self_precedent=True - ) - - _ASTEXT_OPS = set([ASTEXT, ASTEXT_ARRAYIDX]) - _ARRIDX_OPS = set([ARRAYIDX, ASTEXT_ARRAYIDX]) - - def __init__(self, left, right, operator, result_type=None): - if hasattr(right, '__iter__') and \ - not isinstance(right, util.string_types): - right = "{%s}" % ( - ", ".join(util.text_type(elem) for elem in right)) - - if operator is self.INDEX: - operator = self.ARRAYIDX - elif operator is self.ASTEXT: - operator = self.ASTEXT_ARRAYIDX - - self._json_opstring = operator.opstring - self._astext = operator in self._ASTEXT_OPS - self._isarrayidx = operator in self._ARRIDX_OPS - - right = default_comparator._check_literal( - left, operator, right) - super(JSONElement, self).__init__( - left, right, operator, type_=result_type) - - @property - def astext(self): - """Convert this :class:`.JSONElement` to use the 'astext' operator - when evaluated. - - E.g.:: - - select([data_table.c.data['some key'].astext]) - - .. seealso:: - - :meth:`.JSONElement.cast` - - """ - if self._astext: - return self - else: - return JSONElement( - self.left, - self.right, - self.ASTEXT_ARRAYIDX if self.operator is self.ARRAYIDX - else self.ASTEXT, - result_type=sqltypes.String(convert_unicode=True) - ) - - def cast(self, type_): - """Convert this :class:`.JSONElement` to apply both the 'astext' operator - as well as an explicit type cast when evaluated. - - E.g.:: - - select([data_table.c.data['some key'].cast(Integer)]) - - .. seealso:: - - :attr:`.JSONElement.astext` - - """ - if not self._astext: - return self.astext.cast(type_) - else: - return sql.cast(self, type_) +# path operator + astext: returns text +ASTEXT_PATHIDX = operators.custom_op( + "#>>", precedence=5, natural_self_precedent=True +) -class JSON(sqltypes.TypeEngine): +class JSON(sqltypes.Indexable, sqltypes.TypeEngine): """Represent the Postgresql JSON type. The :class:`.JSON` type stores arbitrary JSON format data, e.g.:: @@ -201,22 +128,67 @@ class JSON(sqltypes.TypeEngine): """ self.none_as_null = none_as_null - class comparator_factory(sqltypes.Concatenable.Comparator): + class Comparator(sqltypes.Concatenable.Comparator): """Define comparison operations for :class:`.JSON`.""" - def __getitem__(self, other): - """Get the value at a given key.""" + def __init__(self, expr, astext=False, aspath=False): + super(JSON.comparator_factory, self).__init__(expr) + self._astext = astext + self._aspath = aspath - return JSONElement( - self.expr, other, JSONElement.INDEX, - result_type=self.expr.type) + def _clone(self, astext=False, aspath=False): + return self.__class__( + self.expr, + astext=self._astext or astext, + aspath=self._aspath or aspath) + + @property + def astext(self): + """On an indexed expression, use the "astext" (e.g. "->>") + conversion when rendered in SQL. + + E.g.:: + + select([data_table.c.data['some key'].astext]) + + .. seealso:: + + :meth:`.ColumnElement.cast` + + """ + if self._astext: + return self + else: + return self._clone(astext=True) + + def __getitem__(self, index): + if isinstance(index, collections.Sequence): + index = "{%s}" % ( + ", ".join(util.text_type(elem) for elem in index)) + aspath = True + else: + aspath = False + return self.operate(operators.getitem, index, aspath=aspath) def _adapt_expression(self, op, other_comparator): - if isinstance(op, custom_op): - if op.opstring == '->': - return op, sqltypes.Text - return sqltypes.Concatenable.Comparator.\ - _adapt_expression(self, op, other_comparator) + if op is operators.getitem: + if self._astext: + if self._aspath: + return ASTEXT_PATHIDX, sqltypes.Text + else: + return ASTEXT, sqltypes.Text + else: + if self._aspath: + # TODO: consult index map + return PATHIDX, self.type + else: + # TODO: consult index map + return INDEX, self.type + else: + return super(JSON.comparator_factory, self)._adapt_expression( + op, other_comparator) + + comparator_factory = Comparator def bind_processor(self, dialect): json_serializer = dialect._json_serializer or json.dumps diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index fcc44fe74..1f6228959 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -68,7 +68,8 @@ class Concatenable(object): )): return operators.concat_op, self.expr.type else: - return op, self.expr.type + return super(Concatenable.Comparator)._adapt_expression( + op, other_comparator) comparator_factory = Comparator @@ -83,6 +84,20 @@ class Indexable(object): """if True, Python zero-based indexes should be interpreted as one-based on the SQL expression side.""" + class Comparator(TypeEngine.Comparator): + + def _index_map_type(self, right_comparator): + raise NotImplementedError() + + def _adapt_expression(self, op, other_comparator): + if op is operators.getitem: + return op, self._index_map_type(other_comparator) + else: + return super(Indexable.Comparator)._adapt_expression( + op, other_comparator) + + comparator_factory = Comparator + class String(Concatenable, TypeEngine): |