summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-08-10 10:06:27 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-08-10 10:06:27 -0400
commit020a7b643f254afdeba530c27fe4453c74dad825 (patch)
tree4b86ef5a536dbae722a7d9e5396e0a90a287131e
parentb0c88dd7711463e052bb9b8414199720cde62296 (diff)
downloadsqlalchemy-020a7b643f254afdeba530c27fe4453c74dad825.tar.gz
dev
-rw-r--r--lib/sqlalchemy/dialects/postgresql/__init__.py6
-rw-r--r--lib/sqlalchemy/dialects/postgresql/array.py27
-rw-r--r--lib/sqlalchemy/dialects/postgresql/json.py182
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py17
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):