You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							328 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
	
	
							328 lines
						
					
					
						
							10 KiB
						
					
					
				# postgresql/json.py
 | 
						|
# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
 | 
						|
# <see AUTHORS file>
 | 
						|
#
 | 
						|
# This module is part of SQLAlchemy and is released under
 | 
						|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
 | 
						|
from __future__ import absolute_import
 | 
						|
 | 
						|
from ... import types as sqltypes
 | 
						|
from ... import util
 | 
						|
from ...sql import operators
 | 
						|
 | 
						|
 | 
						|
__all__ = ("JSON", "JSONB")
 | 
						|
 | 
						|
idx_precedence = operators._PRECEDENCE[operators.json_getitem_op]
 | 
						|
 | 
						|
ASTEXT = operators.custom_op(
 | 
						|
    "->>",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
JSONPATH_ASTEXT = operators.custom_op(
 | 
						|
    "#>>",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
HAS_KEY = operators.custom_op(
 | 
						|
    "?",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
HAS_ALL = operators.custom_op(
 | 
						|
    "?&",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
HAS_ANY = operators.custom_op(
 | 
						|
    "?|",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
CONTAINS = operators.custom_op(
 | 
						|
    "@>",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
CONTAINED_BY = operators.custom_op(
 | 
						|
    "<@",
 | 
						|
    precedence=idx_precedence,
 | 
						|
    natural_self_precedent=True,
 | 
						|
    eager_grouping=True,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
class JSONPathType(sqltypes.JSON.JSONPathType):
 | 
						|
    def bind_processor(self, dialect):
 | 
						|
        super_proc = self.string_bind_processor(dialect)
 | 
						|
 | 
						|
        def process(value):
 | 
						|
            assert isinstance(value, util.collections_abc.Sequence)
 | 
						|
            tokens = [util.text_type(elem) for elem in value]
 | 
						|
            value = "{%s}" % (", ".join(tokens))
 | 
						|
            if super_proc:
 | 
						|
                value = super_proc(value)
 | 
						|
            return value
 | 
						|
 | 
						|
        return process
 | 
						|
 | 
						|
    def literal_processor(self, dialect):
 | 
						|
        super_proc = self.string_literal_processor(dialect)
 | 
						|
 | 
						|
        def process(value):
 | 
						|
            assert isinstance(value, util.collections_abc.Sequence)
 | 
						|
            tokens = [util.text_type(elem) for elem in value]
 | 
						|
            value = "{%s}" % (", ".join(tokens))
 | 
						|
            if super_proc:
 | 
						|
                value = super_proc(value)
 | 
						|
            return value
 | 
						|
 | 
						|
        return process
 | 
						|
 | 
						|
 | 
						|
class JSON(sqltypes.JSON):
 | 
						|
    """Represent the PostgreSQL JSON type.
 | 
						|
 | 
						|
    :class:`_postgresql.JSON` is used automatically whenever the base
 | 
						|
    :class:`_types.JSON` datatype is used against a PostgreSQL backend,
 | 
						|
    however base :class:`_types.JSON` datatype does not provide Python
 | 
						|
    accessors for PostgreSQL-specific comparison methods such as
 | 
						|
    :meth:`_postgresql.JSON.Comparator.astext`; additionally, to use
 | 
						|
    PostgreSQL ``JSONB``, the :class:`_postgresql.JSONB` datatype should
 | 
						|
    be used explicitly.
 | 
						|
 | 
						|
    .. seealso::
 | 
						|
 | 
						|
        :class:`_types.JSON` - main documentation for the generic
 | 
						|
        cross-platform JSON datatype.
 | 
						|
 | 
						|
    The operators provided by the PostgreSQL version of :class:`_types.JSON`
 | 
						|
    include:
 | 
						|
 | 
						|
    * Index operations (the ``->`` operator)::
 | 
						|
 | 
						|
        data_table.c.data['some key']
 | 
						|
 | 
						|
        data_table.c.data[5]
 | 
						|
 | 
						|
 | 
						|
    * Index operations returning text (the ``->>`` operator)::
 | 
						|
 | 
						|
        data_table.c.data['some key'].astext == 'some value'
 | 
						|
 | 
						|
      Note that equivalent functionality is available via the
 | 
						|
      :attr:`.JSON.Comparator.as_string` accessor.
 | 
						|
 | 
						|
    * Index operations with CAST
 | 
						|
      (equivalent to ``CAST(col ->> ['some key'] AS <type>)``)::
 | 
						|
 | 
						|
        data_table.c.data['some key'].astext.cast(Integer) == 5
 | 
						|
 | 
						|
      Note that equivalent functionality is available via the
 | 
						|
      :attr:`.JSON.Comparator.as_integer` and similar accessors.
 | 
						|
 | 
						|
    * Path index operations (the ``#>`` operator)::
 | 
						|
 | 
						|
        data_table.c.data[('key_1', 'key_2', 5, ..., 'key_n')]
 | 
						|
 | 
						|
    * Path index operations returning text (the ``#>>`` operator)::
 | 
						|
 | 
						|
        data_table.c.data[('key_1', 'key_2', 5, ..., 'key_n')].astext == 'some value'
 | 
						|
 | 
						|
    .. versionchanged:: 1.1  The :meth:`_expression.ColumnElement.cast`
 | 
						|
       operator on
 | 
						|
       JSON objects now requires that the :attr:`.JSON.Comparator.astext`
 | 
						|
       modifier be called explicitly, if the cast works only from a textual
 | 
						|
       string.
 | 
						|
 | 
						|
    Index operations return an expression object whose type defaults to
 | 
						|
    :class:`_types.JSON` by default,
 | 
						|
    so that further JSON-oriented instructions
 | 
						|
    may be called upon the result type.
 | 
						|
 | 
						|
    Custom serializers and deserializers are specified at the dialect level,
 | 
						|
    that is using :func:`_sa.create_engine`.  The reason for this is that when
 | 
						|
    using psycopg2, the DBAPI only allows serializers at the per-cursor
 | 
						|
    or per-connection level.   E.g.::
 | 
						|
 | 
						|
        engine = create_engine("postgresql://scott:tiger@localhost/test",
 | 
						|
                                json_serializer=my_serialize_fn,
 | 
						|
                                json_deserializer=my_deserialize_fn
 | 
						|
                        )
 | 
						|
 | 
						|
    When using the psycopg2 dialect, the json_deserializer is registered
 | 
						|
    against the database using ``psycopg2.extras.register_default_json``.
 | 
						|
 | 
						|
    .. seealso::
 | 
						|
 | 
						|
        :class:`_types.JSON` - Core level JSON type
 | 
						|
 | 
						|
        :class:`_postgresql.JSONB`
 | 
						|
 | 
						|
    .. versionchanged:: 1.1 :class:`_postgresql.JSON` is now a PostgreSQL-
 | 
						|
       specific specialization of the new :class:`_types.JSON` type.
 | 
						|
 | 
						|
    """  # noqa
 | 
						|
 | 
						|
    astext_type = sqltypes.Text()
 | 
						|
 | 
						|
    def __init__(self, none_as_null=False, astext_type=None):
 | 
						|
        """Construct a :class:`_types.JSON` type.
 | 
						|
 | 
						|
        :param none_as_null: if True, persist the value ``None`` as a
 | 
						|
         SQL NULL value, not the JSON encoding of ``null``.   Note that
 | 
						|
         when this flag is False, the :func:`.null` construct can still
 | 
						|
         be used to persist a NULL value::
 | 
						|
 | 
						|
             from sqlalchemy import null
 | 
						|
             conn.execute(table.insert(), data=null())
 | 
						|
 | 
						|
         .. versionchanged:: 0.9.8 - Added ``none_as_null``, and :func:`.null`
 | 
						|
            is now supported in order to persist a NULL value.
 | 
						|
 | 
						|
         .. seealso::
 | 
						|
 | 
						|
              :attr:`_types.JSON.NULL`
 | 
						|
 | 
						|
        :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
 | 
						|
 | 
						|
        """
 | 
						|
        super(JSON, self).__init__(none_as_null=none_as_null)
 | 
						|
        if astext_type is not None:
 | 
						|
            self.astext_type = astext_type
 | 
						|
 | 
						|
    class Comparator(sqltypes.JSON.Comparator):
 | 
						|
        """Define comparison operations for :class:`_types.JSON`."""
 | 
						|
 | 
						|
        @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:`_expression.ColumnElement.cast`
 | 
						|
 | 
						|
            """
 | 
						|
            if isinstance(self.expr.right.type, sqltypes.JSON.JSONPathType):
 | 
						|
                return self.expr.left.operate(
 | 
						|
                    JSONPATH_ASTEXT,
 | 
						|
                    self.expr.right,
 | 
						|
                    result_type=self.type.astext_type,
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                return self.expr.left.operate(
 | 
						|
                    ASTEXT, self.expr.right, result_type=self.type.astext_type
 | 
						|
                )
 | 
						|
 | 
						|
    comparator_factory = Comparator
 | 
						|
 | 
						|
 | 
						|
class JSONB(JSON):
 | 
						|
    """Represent the PostgreSQL JSONB type.
 | 
						|
 | 
						|
    The :class:`_postgresql.JSONB` type stores arbitrary JSONB format data,
 | 
						|
    e.g.::
 | 
						|
 | 
						|
        data_table = Table('data_table', metadata,
 | 
						|
            Column('id', Integer, primary_key=True),
 | 
						|
            Column('data', JSONB)
 | 
						|
        )
 | 
						|
 | 
						|
        with engine.connect() as conn:
 | 
						|
            conn.execute(
 | 
						|
                data_table.insert(),
 | 
						|
                data = {"key1": "value1", "key2": "value2"}
 | 
						|
            )
 | 
						|
 | 
						|
    The :class:`_postgresql.JSONB` type includes all operations provided by
 | 
						|
    :class:`_types.JSON`, including the same behaviors for indexing
 | 
						|
    operations.
 | 
						|
    It also adds additional operators specific to JSONB, including
 | 
						|
    :meth:`.JSONB.Comparator.has_key`, :meth:`.JSONB.Comparator.has_all`,
 | 
						|
    :meth:`.JSONB.Comparator.has_any`, :meth:`.JSONB.Comparator.contains`,
 | 
						|
    and :meth:`.JSONB.Comparator.contained_by`.
 | 
						|
 | 
						|
    Like the :class:`_types.JSON` type, the :class:`_postgresql.JSONB`
 | 
						|
    type does not detect
 | 
						|
    in-place changes when used with the ORM, unless the
 | 
						|
    :mod:`sqlalchemy.ext.mutable` extension is used.
 | 
						|
 | 
						|
    Custom serializers and deserializers
 | 
						|
    are shared with the :class:`_types.JSON` class,
 | 
						|
    using the ``json_serializer``
 | 
						|
    and ``json_deserializer`` keyword arguments.  These must be specified
 | 
						|
    at the dialect level using :func:`_sa.create_engine`.  When using
 | 
						|
    psycopg2, the serializers are associated with the jsonb type using
 | 
						|
    ``psycopg2.extras.register_default_jsonb`` on a per-connection basis,
 | 
						|
    in the same way that ``psycopg2.extras.register_default_json`` is used
 | 
						|
    to register these handlers with the json type.
 | 
						|
 | 
						|
    .. versionadded:: 0.9.7
 | 
						|
 | 
						|
    .. seealso::
 | 
						|
 | 
						|
        :class:`_types.JSON`
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    __visit_name__ = "JSONB"
 | 
						|
 | 
						|
    class Comparator(JSON.Comparator):
 | 
						|
        """Define comparison operations for :class:`_types.JSON`."""
 | 
						|
 | 
						|
        def has_key(self, other):
 | 
						|
            """Boolean expression.  Test for presence of a key.  Note that the
 | 
						|
            key may be a SQLA expression.
 | 
						|
            """
 | 
						|
            return self.operate(HAS_KEY, other, result_type=sqltypes.Boolean)
 | 
						|
 | 
						|
        def has_all(self, other):
 | 
						|
            """Boolean expression.  Test for presence of all keys in jsonb"""
 | 
						|
            return self.operate(HAS_ALL, other, result_type=sqltypes.Boolean)
 | 
						|
 | 
						|
        def has_any(self, other):
 | 
						|
            """Boolean expression.  Test for presence of any key in jsonb"""
 | 
						|
            return self.operate(HAS_ANY, other, result_type=sqltypes.Boolean)
 | 
						|
 | 
						|
        def contains(self, other, **kwargs):
 | 
						|
            """Boolean expression.  Test if keys (or array) are a superset
 | 
						|
            of/contained the keys of the argument jsonb expression.
 | 
						|
 | 
						|
            kwargs may be ignored by this operator but are required for API
 | 
						|
            conformance.
 | 
						|
            """
 | 
						|
            return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
 | 
						|
 | 
						|
        def contained_by(self, other):
 | 
						|
            """Boolean expression.  Test if keys are a proper subset of the
 | 
						|
            keys of the argument jsonb expression.
 | 
						|
            """
 | 
						|
            return self.operate(
 | 
						|
                CONTAINED_BY, other, result_type=sqltypes.Boolean
 | 
						|
            )
 | 
						|
 | 
						|
    comparator_factory = Comparator
 |