Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/dialects/postgresql»
This commit is contained in:
338
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/dml.py
Normal file
338
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/dml.py
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# dialects/postgresql/dml.py
|
||||||
|
# Copyright (C) 2005-2026 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 annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from . import ext
|
||||||
|
from .._typing import _OnConflictConstraintT
|
||||||
|
from .._typing import _OnConflictIndexElementsT
|
||||||
|
from .._typing import _OnConflictIndexWhereT
|
||||||
|
from .._typing import _OnConflictSetT
|
||||||
|
from .._typing import _OnConflictWhereT
|
||||||
|
from ... import util
|
||||||
|
from ...sql import coercions
|
||||||
|
from ...sql import roles
|
||||||
|
from ...sql import schema
|
||||||
|
from ...sql._typing import _DMLTableArgument
|
||||||
|
from ...sql.base import _exclusive_against
|
||||||
|
from ...sql.base import _generative
|
||||||
|
from ...sql.base import ColumnCollection
|
||||||
|
from ...sql.base import ReadOnlyColumnCollection
|
||||||
|
from ...sql.dml import Insert as StandardInsert
|
||||||
|
from ...sql.elements import ClauseElement
|
||||||
|
from ...sql.elements import ColumnElement
|
||||||
|
from ...sql.elements import KeyedColumnElement
|
||||||
|
from ...sql.elements import TextClause
|
||||||
|
from ...sql.expression import alias
|
||||||
|
from ...util.typing import Self
|
||||||
|
|
||||||
|
__all__ = ("Insert", "insert")
|
||||||
|
|
||||||
|
|
||||||
|
def insert(table: _DMLTableArgument) -> Insert:
|
||||||
|
"""Construct a PostgreSQL-specific variant :class:`_postgresql.Insert`
|
||||||
|
construct.
|
||||||
|
|
||||||
|
.. container:: inherited_member
|
||||||
|
|
||||||
|
The :func:`sqlalchemy.dialects.postgresql.insert` function creates
|
||||||
|
a :class:`sqlalchemy.dialects.postgresql.Insert`. This class is based
|
||||||
|
on the dialect-agnostic :class:`_sql.Insert` construct which may
|
||||||
|
be constructed using the :func:`_sql.insert` function in
|
||||||
|
SQLAlchemy Core.
|
||||||
|
|
||||||
|
The :class:`_postgresql.Insert` construct includes additional methods
|
||||||
|
:meth:`_postgresql.Insert.on_conflict_do_update`,
|
||||||
|
:meth:`_postgresql.Insert.on_conflict_do_nothing`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return Insert(table)
|
||||||
|
|
||||||
|
|
||||||
|
class Insert(StandardInsert):
|
||||||
|
"""PostgreSQL-specific implementation of INSERT.
|
||||||
|
|
||||||
|
Adds methods for PG-specific syntaxes such as ON CONFLICT.
|
||||||
|
|
||||||
|
The :class:`_postgresql.Insert` object is created using the
|
||||||
|
:func:`sqlalchemy.dialects.postgresql.insert` function.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
stringify_dialect = "postgresql"
|
||||||
|
inherit_cache = False
|
||||||
|
|
||||||
|
@util.memoized_property
|
||||||
|
def excluded(
|
||||||
|
self,
|
||||||
|
) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
|
||||||
|
"""Provide the ``excluded`` namespace for an ON CONFLICT statement
|
||||||
|
|
||||||
|
PG's ON CONFLICT clause allows reference to the row that would
|
||||||
|
be inserted, known as ``excluded``. This attribute provides
|
||||||
|
all columns in this row to be referenceable.
|
||||||
|
|
||||||
|
.. tip:: The :attr:`_postgresql.Insert.excluded` attribute is an
|
||||||
|
instance of :class:`_expression.ColumnCollection`, which provides
|
||||||
|
an interface the same as that of the :attr:`_schema.Table.c`
|
||||||
|
collection described at :ref:`metadata_tables_and_columns`.
|
||||||
|
With this collection, ordinary names are accessible like attributes
|
||||||
|
(e.g. ``stmt.excluded.some_column``), but special names and
|
||||||
|
dictionary method names should be accessed using indexed access,
|
||||||
|
such as ``stmt.excluded["column name"]`` or
|
||||||
|
``stmt.excluded["values"]``. See the docstring for
|
||||||
|
:class:`_expression.ColumnCollection` for further examples.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`postgresql_insert_on_conflict` - example of how
|
||||||
|
to use :attr:`_expression.Insert.excluded`
|
||||||
|
|
||||||
|
"""
|
||||||
|
return alias(self.table, name="excluded").columns
|
||||||
|
|
||||||
|
_on_conflict_exclusive = _exclusive_against(
|
||||||
|
"_post_values_clause",
|
||||||
|
msgs={
|
||||||
|
"_post_values_clause": "This Insert construct already has "
|
||||||
|
"an ON CONFLICT clause established"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@_generative
|
||||||
|
@_on_conflict_exclusive
|
||||||
|
def on_conflict_do_update(
|
||||||
|
self,
|
||||||
|
constraint: _OnConflictConstraintT = None,
|
||||||
|
index_elements: _OnConflictIndexElementsT = None,
|
||||||
|
index_where: _OnConflictIndexWhereT = None,
|
||||||
|
set_: _OnConflictSetT = None,
|
||||||
|
where: _OnConflictWhereT = None,
|
||||||
|
) -> Self:
|
||||||
|
r"""
|
||||||
|
Specifies a DO UPDATE SET action for ON CONFLICT clause.
|
||||||
|
|
||||||
|
Either the ``constraint`` or ``index_elements`` argument is
|
||||||
|
required, but only one of these can be specified.
|
||||||
|
|
||||||
|
:param constraint:
|
||||||
|
The name of a unique or exclusion constraint on the table,
|
||||||
|
or the constraint object itself if it has a .name attribute.
|
||||||
|
|
||||||
|
:param index_elements:
|
||||||
|
A sequence consisting of string column names, :class:`_schema.Column`
|
||||||
|
objects, or other column expression objects that will be used
|
||||||
|
to infer a target index.
|
||||||
|
|
||||||
|
:param index_where:
|
||||||
|
Additional WHERE criterion that can be used to infer a
|
||||||
|
conditional target index.
|
||||||
|
|
||||||
|
:param set\_:
|
||||||
|
A dictionary or other mapping object
|
||||||
|
where the keys are either names of columns in the target table,
|
||||||
|
or :class:`_schema.Column` objects or other ORM-mapped columns
|
||||||
|
matching that of the target table, and expressions or literals
|
||||||
|
as values, specifying the ``SET`` actions to take.
|
||||||
|
|
||||||
|
.. versionadded:: 1.4 The
|
||||||
|
:paramref:`_postgresql.Insert.on_conflict_do_update.set_`
|
||||||
|
parameter supports :class:`_schema.Column` objects from the target
|
||||||
|
:class:`_schema.Table` as keys.
|
||||||
|
|
||||||
|
.. warning:: This dictionary does **not** take into account
|
||||||
|
Python-specified default UPDATE values or generation functions,
|
||||||
|
e.g. those specified using :paramref:`_schema.Column.onupdate`.
|
||||||
|
These values will not be exercised for an ON CONFLICT style of
|
||||||
|
UPDATE, unless they are manually specified in the
|
||||||
|
:paramref:`.Insert.on_conflict_do_update.set_` dictionary.
|
||||||
|
|
||||||
|
:param where:
|
||||||
|
Optional argument. An expression object representing a ``WHERE``
|
||||||
|
clause that restricts the rows affected by ``DO UPDATE SET``. Rows not
|
||||||
|
meeting the ``WHERE`` condition will not be updated (effectively a
|
||||||
|
``DO NOTHING`` for those rows).
|
||||||
|
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`postgresql_insert_on_conflict`
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._post_values_clause = OnConflictDoUpdate(
|
||||||
|
constraint, index_elements, index_where, set_, where
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@_generative
|
||||||
|
@_on_conflict_exclusive
|
||||||
|
def on_conflict_do_nothing(
|
||||||
|
self,
|
||||||
|
constraint: _OnConflictConstraintT = None,
|
||||||
|
index_elements: _OnConflictIndexElementsT = None,
|
||||||
|
index_where: _OnConflictIndexWhereT = None,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Specifies a DO NOTHING action for ON CONFLICT clause.
|
||||||
|
|
||||||
|
The ``constraint`` and ``index_elements`` arguments
|
||||||
|
are optional, but only one of these can be specified.
|
||||||
|
|
||||||
|
:param constraint:
|
||||||
|
The name of a unique or exclusion constraint on the table,
|
||||||
|
or the constraint object itself if it has a .name attribute.
|
||||||
|
|
||||||
|
:param index_elements:
|
||||||
|
A sequence consisting of string column names, :class:`_schema.Column`
|
||||||
|
objects, or other column expression objects that will be used
|
||||||
|
to infer a target index.
|
||||||
|
|
||||||
|
:param index_where:
|
||||||
|
Additional WHERE criterion that can be used to infer a
|
||||||
|
conditional target index.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`postgresql_insert_on_conflict`
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._post_values_clause = OnConflictDoNothing(
|
||||||
|
constraint, index_elements, index_where
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class OnConflictClause(ClauseElement):
|
||||||
|
stringify_dialect = "postgresql"
|
||||||
|
|
||||||
|
constraint_target: Optional[str]
|
||||||
|
inferred_target_elements: Optional[List[Union[str, schema.Column[Any]]]]
|
||||||
|
inferred_target_whereclause: Optional[
|
||||||
|
Union[ColumnElement[Any], TextClause]
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
constraint: _OnConflictConstraintT = None,
|
||||||
|
index_elements: _OnConflictIndexElementsT = None,
|
||||||
|
index_where: _OnConflictIndexWhereT = None,
|
||||||
|
):
|
||||||
|
if constraint is not None:
|
||||||
|
if not isinstance(constraint, str) and isinstance(
|
||||||
|
constraint,
|
||||||
|
(schema.Constraint, ext.ExcludeConstraint),
|
||||||
|
):
|
||||||
|
constraint = getattr(constraint, "name") or constraint
|
||||||
|
|
||||||
|
if constraint is not None:
|
||||||
|
if index_elements is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"'constraint' and 'index_elements' are mutually exclusive"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(constraint, str):
|
||||||
|
self.constraint_target = constraint
|
||||||
|
self.inferred_target_elements = None
|
||||||
|
self.inferred_target_whereclause = None
|
||||||
|
elif isinstance(constraint, schema.Index):
|
||||||
|
index_elements = constraint.expressions
|
||||||
|
index_where = constraint.dialect_options["postgresql"].get(
|
||||||
|
"where"
|
||||||
|
)
|
||||||
|
elif isinstance(constraint, ext.ExcludeConstraint):
|
||||||
|
index_elements = constraint.columns
|
||||||
|
index_where = constraint.where
|
||||||
|
else:
|
||||||
|
index_elements = constraint.columns
|
||||||
|
index_where = constraint.dialect_options["postgresql"].get(
|
||||||
|
"where"
|
||||||
|
)
|
||||||
|
|
||||||
|
if index_elements is not None:
|
||||||
|
self.constraint_target = None
|
||||||
|
self.inferred_target_elements = [
|
||||||
|
coercions.expect(roles.DDLConstraintColumnRole, column)
|
||||||
|
for column in index_elements
|
||||||
|
]
|
||||||
|
|
||||||
|
self.inferred_target_whereclause = (
|
||||||
|
coercions.expect(
|
||||||
|
(
|
||||||
|
roles.StatementOptionRole
|
||||||
|
if isinstance(constraint, ext.ExcludeConstraint)
|
||||||
|
else roles.WhereHavingRole
|
||||||
|
),
|
||||||
|
index_where,
|
||||||
|
)
|
||||||
|
if index_where is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
elif constraint is None:
|
||||||
|
self.constraint_target = self.inferred_target_elements = (
|
||||||
|
self.inferred_target_whereclause
|
||||||
|
) = None
|
||||||
|
|
||||||
|
|
||||||
|
class OnConflictDoNothing(OnConflictClause):
|
||||||
|
__visit_name__ = "on_conflict_do_nothing"
|
||||||
|
|
||||||
|
|
||||||
|
class OnConflictDoUpdate(OnConflictClause):
|
||||||
|
__visit_name__ = "on_conflict_do_update"
|
||||||
|
|
||||||
|
update_values_to_set: List[Tuple[Union[schema.Column[Any], str], Any]]
|
||||||
|
update_whereclause: Optional[ColumnElement[Any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
constraint: _OnConflictConstraintT = None,
|
||||||
|
index_elements: _OnConflictIndexElementsT = None,
|
||||||
|
index_where: _OnConflictIndexWhereT = None,
|
||||||
|
set_: _OnConflictSetT = None,
|
||||||
|
where: _OnConflictWhereT = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
constraint=constraint,
|
||||||
|
index_elements=index_elements,
|
||||||
|
index_where=index_where,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.inferred_target_elements is None
|
||||||
|
and self.constraint_target is None
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Either constraint or index_elements, "
|
||||||
|
"but not both, must be specified unless DO NOTHING"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(set_, dict):
|
||||||
|
if not set_:
|
||||||
|
raise ValueError("set parameter dictionary must not be empty")
|
||||||
|
elif isinstance(set_, ColumnCollection):
|
||||||
|
set_ = dict(set_)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"set parameter must be a non-empty dictionary "
|
||||||
|
"or a ColumnCollection such as the `.c.` collection "
|
||||||
|
"of a Table object"
|
||||||
|
)
|
||||||
|
self.update_values_to_set = [
|
||||||
|
(coercions.expect(roles.DMLColumnRole, key), value)
|
||||||
|
for key, value in set_.items()
|
||||||
|
]
|
||||||
|
self.update_whereclause = (
|
||||||
|
coercions.expect(roles.WhereHavingRole, where)
|
||||||
|
if where is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
546
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/ext.py
Normal file
546
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/ext.py
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# dialects/postgresql/ext.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import overload
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from . import types
|
||||||
|
from .array import ARRAY
|
||||||
|
from ...sql import coercions
|
||||||
|
from ...sql import elements
|
||||||
|
from ...sql import expression
|
||||||
|
from ...sql import functions
|
||||||
|
from ...sql import roles
|
||||||
|
from ...sql import schema
|
||||||
|
from ...sql.schema import ColumnCollectionConstraint
|
||||||
|
from ...sql.sqltypes import TEXT
|
||||||
|
from ...sql.visitors import InternalTraversal
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...sql._typing import _ColumnExpressionArgument
|
||||||
|
from ...sql._typing import _DDLColumnArgument
|
||||||
|
from ...sql.elements import ClauseElement
|
||||||
|
from ...sql.elements import ColumnElement
|
||||||
|
from ...sql.operators import OperatorType
|
||||||
|
from ...sql.selectable import FromClause
|
||||||
|
from ...sql.visitors import _CloneCallableType
|
||||||
|
from ...sql.visitors import _TraverseInternalsType
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound=Any)
|
||||||
|
|
||||||
|
|
||||||
|
class aggregate_order_by(expression.ColumnElement[_T]):
|
||||||
|
"""Represent a PostgreSQL aggregate order by expression.
|
||||||
|
|
||||||
|
E.g.::
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import aggregate_order_by
|
||||||
|
|
||||||
|
expr = func.array_agg(aggregate_order_by(table.c.a, table.c.b.desc()))
|
||||||
|
stmt = select(expr)
|
||||||
|
|
||||||
|
would represent the expression:
|
||||||
|
|
||||||
|
.. sourcecode:: sql
|
||||||
|
|
||||||
|
SELECT array_agg(a ORDER BY b DESC) FROM table;
|
||||||
|
|
||||||
|
Similarly::
|
||||||
|
|
||||||
|
expr = func.string_agg(
|
||||||
|
table.c.a, aggregate_order_by(literal_column("','"), table.c.a)
|
||||||
|
)
|
||||||
|
stmt = select(expr)
|
||||||
|
|
||||||
|
Would represent:
|
||||||
|
|
||||||
|
.. sourcecode:: sql
|
||||||
|
|
||||||
|
SELECT string_agg(a, ',' ORDER BY a) FROM table;
|
||||||
|
|
||||||
|
.. versionchanged:: 1.2.13 - the ORDER BY argument may be multiple terms
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:class:`_functions.array_agg`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__visit_name__ = "aggregate_order_by"
|
||||||
|
|
||||||
|
stringify_dialect = "postgresql"
|
||||||
|
_traverse_internals: _TraverseInternalsType = [
|
||||||
|
("target", InternalTraversal.dp_clauseelement),
|
||||||
|
("type", InternalTraversal.dp_type),
|
||||||
|
("order_by", InternalTraversal.dp_clauseelement),
|
||||||
|
]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: ColumnElement[_T],
|
||||||
|
*order_by: _ColumnExpressionArgument[Any],
|
||||||
|
): ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: _ColumnExpressionArgument[_T],
|
||||||
|
*order_by: _ColumnExpressionArgument[Any],
|
||||||
|
): ...
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: _ColumnExpressionArgument[_T],
|
||||||
|
*order_by: _ColumnExpressionArgument[Any],
|
||||||
|
):
|
||||||
|
self.target: ClauseElement = coercions.expect(
|
||||||
|
roles.ExpressionElementRole, target
|
||||||
|
)
|
||||||
|
self.type = self.target.type
|
||||||
|
|
||||||
|
_lob = len(order_by)
|
||||||
|
self.order_by: ClauseElement
|
||||||
|
if _lob == 0:
|
||||||
|
raise TypeError("at least one ORDER BY element is required")
|
||||||
|
elif _lob == 1:
|
||||||
|
self.order_by = coercions.expect(
|
||||||
|
roles.ExpressionElementRole, order_by[0]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.order_by = elements.ClauseList(
|
||||||
|
*order_by, _literal_as_text_role=roles.ExpressionElementRole
|
||||||
|
)
|
||||||
|
|
||||||
|
def self_group(
|
||||||
|
self, against: Optional[OperatorType] = None
|
||||||
|
) -> ClauseElement:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_children(self, **kwargs: Any) -> Iterable[ClauseElement]:
|
||||||
|
return self.target, self.order_by
|
||||||
|
|
||||||
|
def _copy_internals(
|
||||||
|
self, clone: _CloneCallableType = elements._clone, **kw: Any
|
||||||
|
) -> None:
|
||||||
|
self.target = clone(self.target, **kw)
|
||||||
|
self.order_by = clone(self.order_by, **kw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _from_objects(self) -> List[FromClause]:
|
||||||
|
return self.target._from_objects + self.order_by._from_objects
|
||||||
|
|
||||||
|
|
||||||
|
class ExcludeConstraint(ColumnCollectionConstraint):
|
||||||
|
"""A table-level EXCLUDE constraint.
|
||||||
|
|
||||||
|
Defines an EXCLUDE constraint as described in the `PostgreSQL
|
||||||
|
documentation`__.
|
||||||
|
|
||||||
|
__ https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
__visit_name__ = "exclude_constraint"
|
||||||
|
|
||||||
|
where = None
|
||||||
|
inherit_cache = False
|
||||||
|
|
||||||
|
create_drop_stringify_dialect = "postgresql"
|
||||||
|
|
||||||
|
@elements._document_text_coercion(
|
||||||
|
"where",
|
||||||
|
":class:`.ExcludeConstraint`",
|
||||||
|
":paramref:`.ExcludeConstraint.where`",
|
||||||
|
)
|
||||||
|
def __init__(
|
||||||
|
self, *elements: Tuple[_DDLColumnArgument, str], **kw: Any
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
Create an :class:`.ExcludeConstraint` object.
|
||||||
|
|
||||||
|
E.g.::
|
||||||
|
|
||||||
|
const = ExcludeConstraint(
|
||||||
|
(Column("period"), "&&"),
|
||||||
|
(Column("group"), "="),
|
||||||
|
where=(Column("group") != "some group"),
|
||||||
|
ops={"group": "my_operator_class"},
|
||||||
|
)
|
||||||
|
|
||||||
|
The constraint is normally embedded into the :class:`_schema.Table`
|
||||||
|
construct
|
||||||
|
directly, or added later using :meth:`.append_constraint`::
|
||||||
|
|
||||||
|
some_table = Table(
|
||||||
|
"some_table",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True),
|
||||||
|
Column("period", TSRANGE()),
|
||||||
|
Column("group", String),
|
||||||
|
)
|
||||||
|
|
||||||
|
some_table.append_constraint(
|
||||||
|
ExcludeConstraint(
|
||||||
|
(some_table.c.period, "&&"),
|
||||||
|
(some_table.c.group, "="),
|
||||||
|
where=some_table.c.group != "some group",
|
||||||
|
name="some_table_excl_const",
|
||||||
|
ops={"group": "my_operator_class"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
The exclude constraint defined in this example requires the
|
||||||
|
``btree_gist`` extension, that can be created using the
|
||||||
|
command ``CREATE EXTENSION btree_gist;``.
|
||||||
|
|
||||||
|
:param \*elements:
|
||||||
|
|
||||||
|
A sequence of two tuples of the form ``(column, operator)`` where
|
||||||
|
"column" is either a :class:`_schema.Column` object, or a SQL
|
||||||
|
expression element (e.g. ``func.int8range(table.from, table.to)``)
|
||||||
|
or the name of a column as string, and "operator" is a string
|
||||||
|
containing the operator to use (e.g. `"&&"` or `"="`).
|
||||||
|
|
||||||
|
In order to specify a column name when a :class:`_schema.Column`
|
||||||
|
object is not available, while ensuring
|
||||||
|
that any necessary quoting rules take effect, an ad-hoc
|
||||||
|
:class:`_schema.Column` or :func:`_expression.column`
|
||||||
|
object should be used.
|
||||||
|
The ``column`` may also be a string SQL expression when
|
||||||
|
passed as :func:`_expression.literal_column` or
|
||||||
|
:func:`_expression.text`
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
Optional, the in-database name of this constraint.
|
||||||
|
|
||||||
|
:param deferrable:
|
||||||
|
Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when
|
||||||
|
issuing DDL for this constraint.
|
||||||
|
|
||||||
|
:param initially:
|
||||||
|
Optional string. If set, emit INITIALLY <value> when issuing DDL
|
||||||
|
for this constraint.
|
||||||
|
|
||||||
|
:param info: Optional data dictionary which will be populated into the
|
||||||
|
:attr:`.SchemaItem.info` attribute of this object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.50
|
||||||
|
|
||||||
|
:param using:
|
||||||
|
Optional string. If set, emit USING <index_method> when issuing DDL
|
||||||
|
for this constraint. Defaults to 'gist'.
|
||||||
|
|
||||||
|
:param where:
|
||||||
|
Optional SQL expression construct or literal SQL string.
|
||||||
|
If set, emit WHERE <predicate> when issuing DDL
|
||||||
|
for this constraint.
|
||||||
|
|
||||||
|
:param ops:
|
||||||
|
Optional dictionary. Used to define operator classes for the
|
||||||
|
elements; works the same way as that of the
|
||||||
|
:ref:`postgresql_ops <postgresql_operator_classes>`
|
||||||
|
parameter specified to the :class:`_schema.Index` construct.
|
||||||
|
|
||||||
|
.. versionadded:: 1.3.21
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`postgresql_operator_classes` - general description of how
|
||||||
|
PostgreSQL operator classes are specified.
|
||||||
|
|
||||||
|
"""
|
||||||
|
columns = []
|
||||||
|
render_exprs = []
|
||||||
|
self.operators = {}
|
||||||
|
|
||||||
|
expressions, operators = zip(*elements)
|
||||||
|
|
||||||
|
for (expr, column, strname, add_element), operator in zip(
|
||||||
|
coercions.expect_col_expression_collection(
|
||||||
|
roles.DDLConstraintColumnRole, expressions
|
||||||
|
),
|
||||||
|
operators,
|
||||||
|
):
|
||||||
|
if add_element is not None:
|
||||||
|
columns.append(add_element)
|
||||||
|
|
||||||
|
name = column.name if column is not None else strname
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
# backwards compat
|
||||||
|
self.operators[name] = operator
|
||||||
|
|
||||||
|
render_exprs.append((expr, name, operator))
|
||||||
|
|
||||||
|
self._render_exprs = render_exprs
|
||||||
|
|
||||||
|
ColumnCollectionConstraint.__init__(
|
||||||
|
self,
|
||||||
|
*columns,
|
||||||
|
name=kw.get("name"),
|
||||||
|
deferrable=kw.get("deferrable"),
|
||||||
|
initially=kw.get("initially"),
|
||||||
|
info=kw.get("info"),
|
||||||
|
)
|
||||||
|
self.using = kw.get("using", "gist")
|
||||||
|
where = kw.get("where")
|
||||||
|
if where is not None:
|
||||||
|
self.where = coercions.expect(roles.StatementOptionRole, where)
|
||||||
|
|
||||||
|
self.ops = kw.get("ops", {})
|
||||||
|
|
||||||
|
def _set_parent(self, table, **kw):
|
||||||
|
super()._set_parent(table)
|
||||||
|
|
||||||
|
self._render_exprs = [
|
||||||
|
(
|
||||||
|
expr if not isinstance(expr, str) else table.c[expr],
|
||||||
|
name,
|
||||||
|
operator,
|
||||||
|
)
|
||||||
|
for expr, name, operator in (self._render_exprs)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _copy(self, target_table=None, **kw):
|
||||||
|
elements = [
|
||||||
|
(
|
||||||
|
schema._copy_expression(expr, self.parent, target_table),
|
||||||
|
operator,
|
||||||
|
)
|
||||||
|
for expr, _, operator in self._render_exprs
|
||||||
|
]
|
||||||
|
c = self.__class__(
|
||||||
|
*elements,
|
||||||
|
name=self.name,
|
||||||
|
deferrable=self.deferrable,
|
||||||
|
initially=self.initially,
|
||||||
|
where=self.where,
|
||||||
|
using=self.using,
|
||||||
|
)
|
||||||
|
c.dispatch._update(self.dispatch)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def array_agg(*arg, **kw):
|
||||||
|
"""PostgreSQL-specific form of :class:`_functions.array_agg`, ensures
|
||||||
|
return type is :class:`_postgresql.ARRAY` and not
|
||||||
|
the plain :class:`_types.ARRAY`, unless an explicit ``type_``
|
||||||
|
is passed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
kw["_default_array_type"] = ARRAY
|
||||||
|
return functions.func.array_agg(*arg, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class _regconfig_fn(functions.GenericFunction[_T]):
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
args = list(args)
|
||||||
|
if len(args) > 1:
|
||||||
|
initial_arg = coercions.expect(
|
||||||
|
roles.ExpressionElementRole,
|
||||||
|
args.pop(0),
|
||||||
|
name=getattr(self, "name", None),
|
||||||
|
apply_propagate_attrs=self,
|
||||||
|
type_=types.REGCONFIG,
|
||||||
|
)
|
||||||
|
initial_arg = [initial_arg]
|
||||||
|
else:
|
||||||
|
initial_arg = []
|
||||||
|
|
||||||
|
addtl_args = [
|
||||||
|
coercions.expect(
|
||||||
|
roles.ExpressionElementRole,
|
||||||
|
c,
|
||||||
|
name=getattr(self, "name", None),
|
||||||
|
apply_propagate_attrs=self,
|
||||||
|
)
|
||||||
|
for c in args
|
||||||
|
]
|
||||||
|
super().__init__(*(initial_arg + addtl_args), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class to_tsvector(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``to_tsvector`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_postgresql.TSVECTOR`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.to_tsvector` will be used automatically when invoking
|
||||||
|
``sqlalchemy.func.to_tsvector()``, ensuring the correct argument and return
|
||||||
|
type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = types.TSVECTOR
|
||||||
|
|
||||||
|
|
||||||
|
class to_tsquery(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``to_tsquery`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_postgresql.TSQUERY`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.to_tsquery` will be used automatically when invoking
|
||||||
|
``sqlalchemy.func.to_tsquery()``, ensuring the correct argument and return
|
||||||
|
type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = types.TSQUERY
|
||||||
|
|
||||||
|
|
||||||
|
class plainto_tsquery(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``plainto_tsquery`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_postgresql.TSQUERY`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.plainto_tsquery` will be used automatically when
|
||||||
|
invoking ``sqlalchemy.func.plainto_tsquery()``, ensuring the correct
|
||||||
|
argument and return type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = types.TSQUERY
|
||||||
|
|
||||||
|
|
||||||
|
class phraseto_tsquery(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``phraseto_tsquery`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_postgresql.TSQUERY`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.phraseto_tsquery` will be used automatically when
|
||||||
|
invoking ``sqlalchemy.func.phraseto_tsquery()``, ensuring the correct
|
||||||
|
argument and return type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = types.TSQUERY
|
||||||
|
|
||||||
|
|
||||||
|
class websearch_to_tsquery(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``websearch_to_tsquery`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_postgresql.TSQUERY`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.websearch_to_tsquery` will be used automatically when
|
||||||
|
invoking ``sqlalchemy.func.websearch_to_tsquery()``, ensuring the correct
|
||||||
|
argument and return type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = types.TSQUERY
|
||||||
|
|
||||||
|
|
||||||
|
class ts_headline(_regconfig_fn):
|
||||||
|
"""The PostgreSQL ``ts_headline`` SQL function.
|
||||||
|
|
||||||
|
This function applies automatic casting of the REGCONFIG argument
|
||||||
|
to use the :class:`_postgresql.REGCONFIG` datatype automatically,
|
||||||
|
and applies a return type of :class:`_types.TEXT`.
|
||||||
|
|
||||||
|
Assuming the PostgreSQL dialect has been imported, either by invoking
|
||||||
|
``from sqlalchemy.dialects import postgresql``, or by creating a PostgreSQL
|
||||||
|
engine using ``create_engine("postgresql...")``,
|
||||||
|
:class:`_postgresql.ts_headline` will be used automatically when invoking
|
||||||
|
``sqlalchemy.func.ts_headline()``, ensuring the correct argument and return
|
||||||
|
type handlers are used at compile and execution time.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0rc1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
inherit_cache = True
|
||||||
|
type = TEXT
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
args = list(args)
|
||||||
|
|
||||||
|
# parse types according to
|
||||||
|
# https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE
|
||||||
|
if len(args) < 2:
|
||||||
|
# invalid args; don't do anything
|
||||||
|
has_regconfig = False
|
||||||
|
elif (
|
||||||
|
isinstance(args[1], elements.ColumnElement)
|
||||||
|
and args[1].type._type_affinity is types.TSQUERY
|
||||||
|
):
|
||||||
|
# tsquery is second argument, no regconfig argument
|
||||||
|
has_regconfig = False
|
||||||
|
else:
|
||||||
|
has_regconfig = True
|
||||||
|
|
||||||
|
if has_regconfig:
|
||||||
|
initial_arg = coercions.expect(
|
||||||
|
roles.ExpressionElementRole,
|
||||||
|
args.pop(0),
|
||||||
|
apply_propagate_attrs=self,
|
||||||
|
name=getattr(self, "name", None),
|
||||||
|
type_=types.REGCONFIG,
|
||||||
|
)
|
||||||
|
initial_arg = [initial_arg]
|
||||||
|
else:
|
||||||
|
initial_arg = []
|
||||||
|
|
||||||
|
addtl_args = [
|
||||||
|
coercions.expect(
|
||||||
|
roles.ExpressionElementRole,
|
||||||
|
c,
|
||||||
|
name=getattr(self, "name", None),
|
||||||
|
apply_propagate_attrs=self,
|
||||||
|
)
|
||||||
|
for c in args
|
||||||
|
]
|
||||||
|
super().__init__(*(initial_arg + addtl_args), **kwargs)
|
||||||
405
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/hstore.py
Normal file
405
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/hstore.py
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
# dialects/postgresql/hstore.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .array import ARRAY
|
||||||
|
from .operators import CONTAINED_BY
|
||||||
|
from .operators import CONTAINS
|
||||||
|
from .operators import GETITEM
|
||||||
|
from .operators import HAS_ALL
|
||||||
|
from .operators import HAS_ANY
|
||||||
|
from .operators import HAS_KEY
|
||||||
|
from ... import types as sqltypes
|
||||||
|
from ...sql import functions as sqlfunc
|
||||||
|
|
||||||
|
__all__ = ("HSTORE", "hstore")
|
||||||
|
|
||||||
|
|
||||||
|
class HSTORE(sqltypes.Indexable, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||||
|
"""Represent the PostgreSQL HSTORE type.
|
||||||
|
|
||||||
|
The :class:`.HSTORE` type stores dictionaries containing strings, e.g.::
|
||||||
|
|
||||||
|
data_table = Table(
|
||||||
|
"data_table",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True),
|
||||||
|
Column("data", HSTORE),
|
||||||
|
)
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
data_table.insert(), data={"key1": "value1", "key2": "value2"}
|
||||||
|
)
|
||||||
|
|
||||||
|
:class:`.HSTORE` provides for a wide range of operations, including:
|
||||||
|
|
||||||
|
* Index operations::
|
||||||
|
|
||||||
|
data_table.c.data["some key"] == "some value"
|
||||||
|
|
||||||
|
* Containment operations::
|
||||||
|
|
||||||
|
data_table.c.data.has_key("some key")
|
||||||
|
|
||||||
|
data_table.c.data.has_all(["one", "two", "three"])
|
||||||
|
|
||||||
|
* Concatenation::
|
||||||
|
|
||||||
|
data_table.c.data + {"k1": "v1"}
|
||||||
|
|
||||||
|
For a full list of special methods see
|
||||||
|
:class:`.HSTORE.comparator_factory`.
|
||||||
|
|
||||||
|
.. container:: topic
|
||||||
|
|
||||||
|
**Detecting Changes in HSTORE columns when using the ORM**
|
||||||
|
|
||||||
|
For usage with the SQLAlchemy ORM, it may be desirable to combine the
|
||||||
|
usage of :class:`.HSTORE` with :class:`.MutableDict` dictionary now
|
||||||
|
part of the :mod:`sqlalchemy.ext.mutable` extension. This extension
|
||||||
|
will allow "in-place" changes to the dictionary, e.g. addition of new
|
||||||
|
keys or replacement/removal of existing keys to/from the current
|
||||||
|
dictionary, to produce events which will be detected by the unit of
|
||||||
|
work::
|
||||||
|
|
||||||
|
from sqlalchemy.ext.mutable import MutableDict
|
||||||
|
|
||||||
|
|
||||||
|
class MyClass(Base):
|
||||||
|
__tablename__ = "data_table"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
data = Column(MutableDict.as_mutable(HSTORE))
|
||||||
|
|
||||||
|
|
||||||
|
my_object = session.query(MyClass).one()
|
||||||
|
|
||||||
|
# in-place mutation, requires Mutable extension
|
||||||
|
# in order for the ORM to detect
|
||||||
|
my_object.data["some_key"] = "some value"
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
When the :mod:`sqlalchemy.ext.mutable` extension is not used, the ORM
|
||||||
|
will not be alerted to any changes to the contents of an existing
|
||||||
|
dictionary, unless that dictionary value is re-assigned to the
|
||||||
|
HSTORE-attribute itself, thus generating a change event.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:class:`.hstore` - render the PostgreSQL ``hstore()`` function.
|
||||||
|
|
||||||
|
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
__visit_name__ = "HSTORE"
|
||||||
|
hashable = False
|
||||||
|
text_type = sqltypes.Text()
|
||||||
|
|
||||||
|
def __init__(self, text_type=None):
|
||||||
|
"""Construct a new :class:`.HSTORE`.
|
||||||
|
|
||||||
|
:param text_type: the type that should be used for indexed values.
|
||||||
|
Defaults to :class:`_types.Text`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if text_type is not None:
|
||||||
|
self.text_type = text_type
|
||||||
|
|
||||||
|
class Comparator(
|
||||||
|
sqltypes.Indexable.Comparator, sqltypes.Concatenable.Comparator
|
||||||
|
):
|
||||||
|
"""Define comparison operations for :class:`.HSTORE`."""
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_getitem(self, index):
|
||||||
|
return GETITEM, index, self.type.text_type
|
||||||
|
|
||||||
|
def defined(self, key):
|
||||||
|
"""Boolean expression. Test for presence of a non-NULL value for
|
||||||
|
the key. Note that the key may be a SQLA expression.
|
||||||
|
"""
|
||||||
|
return _HStoreDefinedFunction(self.expr, key)
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
"""HStore expression. Returns the contents of this hstore with the
|
||||||
|
given key deleted. Note that the key may be a SQLA expression.
|
||||||
|
"""
|
||||||
|
if isinstance(key, dict):
|
||||||
|
key = _serialize_hstore(key)
|
||||||
|
return _HStoreDeleteFunction(self.expr, key)
|
||||||
|
|
||||||
|
def slice(self, array):
|
||||||
|
"""HStore expression. Returns a subset of an hstore defined by
|
||||||
|
array of keys.
|
||||||
|
"""
|
||||||
|
return _HStoreSliceFunction(self.expr, array)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
"""Text array expression. Returns array of keys."""
|
||||||
|
return _HStoreKeysFunction(self.expr)
|
||||||
|
|
||||||
|
def vals(self):
|
||||||
|
"""Text array expression. Returns array of values."""
|
||||||
|
return _HStoreValsFunction(self.expr)
|
||||||
|
|
||||||
|
def array(self):
|
||||||
|
"""Text array expression. Returns array of alternating keys and
|
||||||
|
values.
|
||||||
|
"""
|
||||||
|
return _HStoreArrayFunction(self.expr)
|
||||||
|
|
||||||
|
def matrix(self):
|
||||||
|
"""Text array expression. Returns array of [key, value] pairs."""
|
||||||
|
return _HStoreMatrixFunction(self.expr)
|
||||||
|
|
||||||
|
comparator_factory = Comparator
|
||||||
|
|
||||||
|
def bind_processor(self, dialect):
|
||||||
|
# note that dialect-specific types like that of psycopg and
|
||||||
|
# psycopg2 will override this method to allow driver-level conversion
|
||||||
|
# instead, see _PsycopgHStore
|
||||||
|
def process(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return _serialize_hstore(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return process
|
||||||
|
|
||||||
|
def result_processor(self, dialect, coltype):
|
||||||
|
# note that dialect-specific types like that of psycopg and
|
||||||
|
# psycopg2 will override this method to allow driver-level conversion
|
||||||
|
# instead, see _PsycopgHStore
|
||||||
|
def process(value):
|
||||||
|
if value is not None:
|
||||||
|
return _parse_hstore(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return process
|
||||||
|
|
||||||
|
|
||||||
|
class hstore(sqlfunc.GenericFunction):
|
||||||
|
"""Construct an hstore value within a SQL expression using the
|
||||||
|
PostgreSQL ``hstore()`` function.
|
||||||
|
|
||||||
|
The :class:`.hstore` function accepts one or two arguments as described
|
||||||
|
in the PostgreSQL documentation.
|
||||||
|
|
||||||
|
E.g.::
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import array, hstore
|
||||||
|
|
||||||
|
select(hstore("key1", "value1"))
|
||||||
|
|
||||||
|
select(
|
||||||
|
hstore(
|
||||||
|
array(["key1", "key2", "key3"]),
|
||||||
|
array(["value1", "value2", "value3"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:class:`.HSTORE` - the PostgreSQL ``HSTORE`` datatype.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
type = HSTORE
|
||||||
|
name = "hstore"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreDefinedFunction(sqlfunc.GenericFunction):
|
||||||
|
type = sqltypes.Boolean
|
||||||
|
name = "defined"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreDeleteFunction(sqlfunc.GenericFunction):
|
||||||
|
type = HSTORE
|
||||||
|
name = "delete"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreSliceFunction(sqlfunc.GenericFunction):
|
||||||
|
type = HSTORE
|
||||||
|
name = "slice"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreKeysFunction(sqlfunc.GenericFunction):
|
||||||
|
type = ARRAY(sqltypes.Text)
|
||||||
|
name = "akeys"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreValsFunction(sqlfunc.GenericFunction):
|
||||||
|
type = ARRAY(sqltypes.Text)
|
||||||
|
name = "avals"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreArrayFunction(sqlfunc.GenericFunction):
|
||||||
|
type = ARRAY(sqltypes.Text)
|
||||||
|
name = "hstore_to_array"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
class _HStoreMatrixFunction(sqlfunc.GenericFunction):
|
||||||
|
type = ARRAY(sqltypes.Text)
|
||||||
|
name = "hstore_to_matrix"
|
||||||
|
inherit_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# parsing. note that none of this is used with the psycopg2 backend,
|
||||||
|
# which provides its own native extensions.
|
||||||
|
#
|
||||||
|
|
||||||
|
# My best guess at the parsing rules of hstore literals, since no formal
|
||||||
|
# grammar is given. This is mostly reverse engineered from PG's input parser
|
||||||
|
# behavior.
|
||||||
|
HSTORE_PAIR_RE = re.compile(
|
||||||
|
r"""
|
||||||
|
(
|
||||||
|
"(?P<key> (\\ . | [^"\\])* )" # Quoted key
|
||||||
|
)
|
||||||
|
[ ]* => [ ]* # Pair operator, optional adjoining whitespace
|
||||||
|
(
|
||||||
|
(?P<value_null> NULL ) # NULL value
|
||||||
|
| "(?P<value> (\\ . | [^"\\])* )" # Quoted value
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
HSTORE_DELIMITER_RE = re.compile(
|
||||||
|
r"""
|
||||||
|
[ ]* , [ ]*
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_error(hstore_str, pos):
|
||||||
|
"""format an unmarshalling error."""
|
||||||
|
|
||||||
|
ctx = 20
|
||||||
|
hslen = len(hstore_str)
|
||||||
|
|
||||||
|
parsed_tail = hstore_str[max(pos - ctx - 1, 0) : min(pos, hslen)]
|
||||||
|
residual = hstore_str[min(pos, hslen) : min(pos + ctx + 1, hslen)]
|
||||||
|
|
||||||
|
if len(parsed_tail) > ctx:
|
||||||
|
parsed_tail = "[...]" + parsed_tail[1:]
|
||||||
|
if len(residual) > ctx:
|
||||||
|
residual = residual[:-1] + "[...]"
|
||||||
|
|
||||||
|
return "After %r, could not parse residual at position %d: %r" % (
|
||||||
|
parsed_tail,
|
||||||
|
pos,
|
||||||
|
residual,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_hstore(hstore_str):
|
||||||
|
"""Parse an hstore from its literal string representation.
|
||||||
|
|
||||||
|
Attempts to approximate PG's hstore input parsing rules as closely as
|
||||||
|
possible. Although currently this is not strictly necessary, since the
|
||||||
|
current implementation of hstore's output syntax is stricter than what it
|
||||||
|
accepts as input, the documentation makes no guarantees that will always
|
||||||
|
be the case.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
pos = 0
|
||||||
|
pair_match = HSTORE_PAIR_RE.match(hstore_str)
|
||||||
|
|
||||||
|
while pair_match is not None:
|
||||||
|
key = pair_match.group("key").replace(r"\"", '"').replace("\\\\", "\\")
|
||||||
|
if pair_match.group("value_null"):
|
||||||
|
value = None
|
||||||
|
else:
|
||||||
|
value = (
|
||||||
|
pair_match.group("value")
|
||||||
|
.replace(r"\"", '"')
|
||||||
|
.replace("\\\\", "\\")
|
||||||
|
)
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
pos += pair_match.end()
|
||||||
|
|
||||||
|
delim_match = HSTORE_DELIMITER_RE.match(hstore_str[pos:])
|
||||||
|
if delim_match is not None:
|
||||||
|
pos += delim_match.end()
|
||||||
|
|
||||||
|
pair_match = HSTORE_PAIR_RE.match(hstore_str[pos:])
|
||||||
|
|
||||||
|
if pos != len(hstore_str):
|
||||||
|
raise ValueError(_parse_error(hstore_str, pos))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_hstore(val):
|
||||||
|
"""Serialize a dictionary into an hstore literal. Keys and values must
|
||||||
|
both be strings (except None for values).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def esc(s, position):
|
||||||
|
if position == "value" and s is None:
|
||||||
|
return "NULL"
|
||||||
|
elif isinstance(s, str):
|
||||||
|
return '"%s"' % s.replace("\\", "\\\\").replace('"', r"\"")
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"%r in %s position is not a string." % (s, position)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ", ".join(
|
||||||
|
"%s=>%s" % (esc(k, "key"), esc(v, "value")) for k, v in val.items()
|
||||||
|
)
|
||||||
404
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/json.py
Normal file
404
venv/Lib/site-packages/sqlalchemy/dialects/postgresql/json.py
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
# dialects/postgresql/json.py
|
||||||
|
# Copyright (C) 2005-2026 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 annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .array import ARRAY
|
||||||
|
from .array import array as _pg_array
|
||||||
|
from .operators import ASTEXT
|
||||||
|
from .operators import CONTAINED_BY
|
||||||
|
from .operators import CONTAINS
|
||||||
|
from .operators import DELETE_PATH
|
||||||
|
from .operators import HAS_ALL
|
||||||
|
from .operators import HAS_ANY
|
||||||
|
from .operators import HAS_KEY
|
||||||
|
from .operators import JSONPATH_ASTEXT
|
||||||
|
from .operators import PATH_EXISTS
|
||||||
|
from .operators import PATH_MATCH
|
||||||
|
from ... import types as sqltypes
|
||||||
|
from ...sql import cast
|
||||||
|
from ...sql._typing import _T
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...engine.interfaces import Dialect
|
||||||
|
from ...sql.elements import ColumnElement
|
||||||
|
from ...sql.operators import OperatorType
|
||||||
|
from ...sql.type_api import _BindProcessorType
|
||||||
|
from ...sql.type_api import _LiteralProcessorType
|
||||||
|
from ...sql.type_api import TypeEngine
|
||||||
|
|
||||||
|
__all__ = ("JSON", "JSONB")
|
||||||
|
|
||||||
|
|
||||||
|
class JSONPathType(sqltypes.JSON.JSONPathType):
|
||||||
|
def _processor(
|
||||||
|
self, dialect: Dialect, super_proc: Optional[Callable[[Any], Any]]
|
||||||
|
) -> Callable[[Any], Any]:
|
||||||
|
def process(value: Any) -> Any:
|
||||||
|
if isinstance(value, str):
|
||||||
|
# If it's already a string assume that it's in json path
|
||||||
|
# format. This allows using cast with json paths literals
|
||||||
|
return value
|
||||||
|
elif value:
|
||||||
|
# If it's already a string assume that it's in json path
|
||||||
|
# format. This allows using cast with json paths literals
|
||||||
|
value = "{%s}" % (", ".join(map(str, value)))
|
||||||
|
else:
|
||||||
|
value = "{}"
|
||||||
|
if super_proc:
|
||||||
|
value = super_proc(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
return process
|
||||||
|
|
||||||
|
def bind_processor(self, dialect: Dialect) -> _BindProcessorType[Any]:
|
||||||
|
return self._processor(dialect, self.string_bind_processor(dialect)) # type: ignore[return-value] # noqa: E501
|
||||||
|
|
||||||
|
def literal_processor(
|
||||||
|
self, dialect: Dialect
|
||||||
|
) -> _LiteralProcessorType[Any]:
|
||||||
|
return self._processor(dialect, self.string_literal_processor(dialect)) # type: ignore[return-value] # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
class JSONPATH(JSONPathType):
|
||||||
|
"""JSON Path Type.
|
||||||
|
|
||||||
|
This is usually required to cast literal values to json path when using
|
||||||
|
json search like function, such as ``jsonb_path_query_array`` or
|
||||||
|
``jsonb_path_exists``::
|
||||||
|
|
||||||
|
stmt = sa.select(
|
||||||
|
sa.func.jsonb_path_query_array(
|
||||||
|
table.c.jsonb_col, cast("$.address.id", JSONPATH)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__visit_name__ = "JSONPATH"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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+psycopg2://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`
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
render_bind_cast = True
|
||||||
|
astext_type: TypeEngine[str] = sqltypes.Text()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
none_as_null: bool = False,
|
||||||
|
astext_type: Optional[TypeEngine[str]] = 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()})
|
||||||
|
|
||||||
|
.. 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`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().__init__(none_as_null=none_as_null)
|
||||||
|
if astext_type is not None:
|
||||||
|
self.astext_type = astext_type
|
||||||
|
|
||||||
|
class Comparator(sqltypes.JSON.Comparator[_T]):
|
||||||
|
"""Define comparison operations for :class:`_types.JSON`."""
|
||||||
|
|
||||||
|
type: JSON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def astext(self) -> ColumnElement[str]:
|
||||||
|
"""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( # type: ignore[no-any-return]
|
||||||
|
JSONPATH_ASTEXT,
|
||||||
|
self.expr.right,
|
||||||
|
result_type=self.type.astext_type,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.expr.left.operate( # type: ignore[no-any-return]
|
||||||
|
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`,
|
||||||
|
:meth:`.JSONB.Comparator.contained_by`,
|
||||||
|
:meth:`.JSONB.Comparator.delete_path`,
|
||||||
|
:meth:`.JSONB.Comparator.path_exists` and
|
||||||
|
:meth:`.JSONB.Comparator.path_match`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:class:`_types.JSON`
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
**For applications that have indexes against JSONB subscript
|
||||||
|
expressions**
|
||||||
|
|
||||||
|
SQLAlchemy 2.0.42 made a change in how the subscript operation for
|
||||||
|
:class:`.JSONB` is rendered, from ``-> 'element'`` to ``['element']``,
|
||||||
|
for PostgreSQL versions greater than 14. This change caused an
|
||||||
|
unintended side effect for indexes that were created against
|
||||||
|
expressions that use subscript notation, e.g.
|
||||||
|
``Index("ix_entity_json_ab_text", data["a"]["b"].astext)``. If these
|
||||||
|
indexes were generated with the older syntax e.g. ``((entity.data ->
|
||||||
|
'a') ->> 'b')``, they will not be used by the PostgreSQL query planner
|
||||||
|
when a query is made using SQLAlchemy 2.0.42 or higher on PostgreSQL
|
||||||
|
versions 14 or higher. This occurs because the new text will resemble
|
||||||
|
``(entity.data['a'] ->> 'b')`` which will fail to produce the exact
|
||||||
|
textual syntax match required by the PostgreSQL query planner.
|
||||||
|
Therefore, for users upgrading to SQLAlchemy 2.0.42 or higher, existing
|
||||||
|
indexes that were created against :class:`.JSONB` expressions that use
|
||||||
|
subscripting would need to be dropped and re-created in order for them
|
||||||
|
to work with the new query syntax, e.g. an expression like
|
||||||
|
``((entity.data -> 'a') ->> 'b')`` would become ``(entity.data['a'] ->>
|
||||||
|
'b')``.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ticket:`12868` - discussion of this issue
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__visit_name__ = "JSONB"
|
||||||
|
|
||||||
|
def coerce_compared_value(
|
||||||
|
self, op: Optional[OperatorType], value: Any
|
||||||
|
) -> TypeEngine[Any]:
|
||||||
|
if op in (PATH_MATCH, PATH_EXISTS):
|
||||||
|
return JSON.JSONPathType()
|
||||||
|
else:
|
||||||
|
return super().coerce_compared_value(op, value)
|
||||||
|
|
||||||
|
class Comparator(JSON.Comparator[_T]):
|
||||||
|
"""Define comparison operations for :class:`_types.JSON`."""
|
||||||
|
|
||||||
|
type: JSONB
|
||||||
|
|
||||||
|
def has_key(self, other: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test for presence of a key (equivalent of
|
||||||
|
the ``?`` operator). Note that the key may be a SQLA expression.
|
||||||
|
"""
|
||||||
|
return self.operate(HAS_KEY, other, result_type=sqltypes.Boolean)
|
||||||
|
|
||||||
|
def has_all(self, other: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test for presence of all keys in jsonb
|
||||||
|
(equivalent of the ``?&`` operator)
|
||||||
|
"""
|
||||||
|
return self.operate(HAS_ALL, other, result_type=sqltypes.Boolean)
|
||||||
|
|
||||||
|
def has_any(self, other: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test for presence of any key in jsonb
|
||||||
|
(equivalent of the ``?|`` operator)
|
||||||
|
"""
|
||||||
|
return self.operate(HAS_ANY, other, result_type=sqltypes.Boolean)
|
||||||
|
|
||||||
|
def contains(self, other: Any, **kwargs: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test if keys (or array) are a superset
|
||||||
|
of/contained the keys of the argument jsonb expression
|
||||||
|
(equivalent of the ``@>`` operator).
|
||||||
|
|
||||||
|
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: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test if keys are a proper subset of the
|
||||||
|
keys of the argument jsonb expression
|
||||||
|
(equivalent of the ``<@`` operator).
|
||||||
|
"""
|
||||||
|
return self.operate(
|
||||||
|
CONTAINED_BY, other, result_type=sqltypes.Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_path(
|
||||||
|
self, array: Union[List[str], _pg_array[str]]
|
||||||
|
) -> ColumnElement[JSONB]:
|
||||||
|
"""JSONB expression. Deletes field or array element specified in
|
||||||
|
the argument array (equivalent of the ``#-`` operator).
|
||||||
|
|
||||||
|
The input may be a list of strings that will be coerced to an
|
||||||
|
``ARRAY`` or an instance of :meth:`_postgres.array`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if not isinstance(array, _pg_array):
|
||||||
|
array = _pg_array(array)
|
||||||
|
right_side = cast(array, ARRAY(sqltypes.TEXT))
|
||||||
|
return self.operate(DELETE_PATH, right_side, result_type=JSONB)
|
||||||
|
|
||||||
|
def path_exists(self, other: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test for presence of item given by the
|
||||||
|
argument JSONPath expression (equivalent of the ``@?`` operator).
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self.operate(
|
||||||
|
PATH_EXISTS, other, result_type=sqltypes.Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
def path_match(self, other: Any) -> ColumnElement[bool]:
|
||||||
|
"""Boolean expression. Test if JSONPath predicate given by the
|
||||||
|
argument JSONPath expression matches
|
||||||
|
(equivalent of the ``@@`` operator).
|
||||||
|
|
||||||
|
Only the first item of the result is taken into account.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self.operate(
|
||||||
|
PATH_MATCH, other, result_type=sqltypes.Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
comparator_factory = Comparator
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
# dialects/postgresql/named_types.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from ... import schema
|
||||||
|
from ... import util
|
||||||
|
from ...sql import coercions
|
||||||
|
from ...sql import elements
|
||||||
|
from ...sql import roles
|
||||||
|
from ...sql import sqltypes
|
||||||
|
from ...sql import type_api
|
||||||
|
from ...sql.base import _NoArg
|
||||||
|
from ...sql.ddl import InvokeCreateDDLBase
|
||||||
|
from ...sql.ddl import InvokeDropDDLBase
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...sql._typing import _CreateDropBind
|
||||||
|
from ...sql._typing import _TypeEngineArgument
|
||||||
|
|
||||||
|
|
||||||
|
class NamedType(schema.SchemaVisitable, sqltypes.TypeEngine):
|
||||||
|
"""Base for named types."""
|
||||||
|
|
||||||
|
__abstract__ = True
|
||||||
|
DDLGenerator: Type[NamedTypeGenerator]
|
||||||
|
DDLDropper: Type[NamedTypeDropper]
|
||||||
|
create_type: bool
|
||||||
|
|
||||||
|
def create(
|
||||||
|
self, bind: _CreateDropBind, checkfirst: bool = True, **kw: Any
|
||||||
|
) -> None:
|
||||||
|
"""Emit ``CREATE`` DDL for this type.
|
||||||
|
|
||||||
|
:param bind: a connectable :class:`_engine.Engine`,
|
||||||
|
:class:`_engine.Connection`, or similar object to emit
|
||||||
|
SQL.
|
||||||
|
:param checkfirst: if ``True``, a query against
|
||||||
|
the PG catalog will be first performed to see
|
||||||
|
if the type does not exist already before
|
||||||
|
creating.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bind._run_ddl_visitor(self.DDLGenerator, self, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def drop(
|
||||||
|
self, bind: _CreateDropBind, checkfirst: bool = True, **kw: Any
|
||||||
|
) -> None:
|
||||||
|
"""Emit ``DROP`` DDL for this type.
|
||||||
|
|
||||||
|
:param bind: a connectable :class:`_engine.Engine`,
|
||||||
|
:class:`_engine.Connection`, or similar object to emit
|
||||||
|
SQL.
|
||||||
|
:param checkfirst: if ``True``, a query against
|
||||||
|
the PG catalog will be first performed to see
|
||||||
|
if the type actually exists before dropping.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bind._run_ddl_visitor(self.DDLDropper, self, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def _check_for_name_in_memos(
|
||||||
|
self, checkfirst: bool, kw: Dict[str, Any]
|
||||||
|
) -> bool:
|
||||||
|
"""Look in the 'ddl runner' for 'memos', then
|
||||||
|
note our name in that collection.
|
||||||
|
|
||||||
|
This to ensure a particular named type is operated
|
||||||
|
upon only once within any kind of create/drop
|
||||||
|
sequence without relying upon "checkfirst".
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.create_type:
|
||||||
|
return True
|
||||||
|
if "_ddl_runner" in kw:
|
||||||
|
ddl_runner = kw["_ddl_runner"]
|
||||||
|
type_name = f"pg_{self.__visit_name__}"
|
||||||
|
if type_name in ddl_runner.memo:
|
||||||
|
existing = ddl_runner.memo[type_name]
|
||||||
|
else:
|
||||||
|
existing = ddl_runner.memo[type_name] = set()
|
||||||
|
present = (self.schema, self.name) in existing
|
||||||
|
existing.add((self.schema, self.name))
|
||||||
|
return present
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_table_create(
|
||||||
|
self,
|
||||||
|
target: Any,
|
||||||
|
bind: _CreateDropBind,
|
||||||
|
checkfirst: bool = False,
|
||||||
|
**kw: Any,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
checkfirst
|
||||||
|
or (
|
||||||
|
not self.metadata
|
||||||
|
and not kw.get("_is_metadata_operation", False)
|
||||||
|
)
|
||||||
|
) and not self._check_for_name_in_memos(checkfirst, kw):
|
||||||
|
self.create(bind=bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def _on_table_drop(
|
||||||
|
self,
|
||||||
|
target: Any,
|
||||||
|
bind: _CreateDropBind,
|
||||||
|
checkfirst: bool = False,
|
||||||
|
**kw: Any,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
not self.metadata
|
||||||
|
and not kw.get("_is_metadata_operation", False)
|
||||||
|
and not self._check_for_name_in_memos(checkfirst, kw)
|
||||||
|
):
|
||||||
|
self.drop(bind=bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def _on_metadata_create(
|
||||||
|
self,
|
||||||
|
target: Any,
|
||||||
|
bind: _CreateDropBind,
|
||||||
|
checkfirst: bool = False,
|
||||||
|
**kw: Any,
|
||||||
|
) -> None:
|
||||||
|
if not self._check_for_name_in_memos(checkfirst, kw):
|
||||||
|
self.create(bind=bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def _on_metadata_drop(
|
||||||
|
self,
|
||||||
|
target: Any,
|
||||||
|
bind: _CreateDropBind,
|
||||||
|
checkfirst: bool = False,
|
||||||
|
**kw: Any,
|
||||||
|
) -> None:
|
||||||
|
if not self._check_for_name_in_memos(checkfirst, kw):
|
||||||
|
self.drop(bind=bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
|
||||||
|
class NamedTypeGenerator(InvokeCreateDDLBase):
|
||||||
|
def __init__(self, dialect, connection, checkfirst=False, **kwargs):
|
||||||
|
super().__init__(connection, **kwargs)
|
||||||
|
self.checkfirst = checkfirst
|
||||||
|
|
||||||
|
def _can_create_type(self, type_):
|
||||||
|
if not self.checkfirst:
|
||||||
|
return True
|
||||||
|
|
||||||
|
effective_schema = self.connection.schema_for_object(type_)
|
||||||
|
return not self.connection.dialect.has_type(
|
||||||
|
self.connection, type_.name, schema=effective_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NamedTypeDropper(InvokeDropDDLBase):
|
||||||
|
def __init__(self, dialect, connection, checkfirst=False, **kwargs):
|
||||||
|
super().__init__(connection, **kwargs)
|
||||||
|
self.checkfirst = checkfirst
|
||||||
|
|
||||||
|
def _can_drop_type(self, type_):
|
||||||
|
if not self.checkfirst:
|
||||||
|
return True
|
||||||
|
|
||||||
|
effective_schema = self.connection.schema_for_object(type_)
|
||||||
|
return self.connection.dialect.has_type(
|
||||||
|
self.connection, type_.name, schema=effective_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EnumGenerator(NamedTypeGenerator):
|
||||||
|
def visit_enum(self, enum):
|
||||||
|
if not self._can_create_type(enum):
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.with_ddl_events(enum):
|
||||||
|
self.connection.execute(CreateEnumType(enum))
|
||||||
|
|
||||||
|
|
||||||
|
class EnumDropper(NamedTypeDropper):
|
||||||
|
def visit_enum(self, enum):
|
||||||
|
if not self._can_drop_type(enum):
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.with_ddl_events(enum):
|
||||||
|
self.connection.execute(DropEnumType(enum))
|
||||||
|
|
||||||
|
|
||||||
|
class ENUM(NamedType, type_api.NativeForEmulated, sqltypes.Enum):
|
||||||
|
"""PostgreSQL ENUM type.
|
||||||
|
|
||||||
|
This is a subclass of :class:`_types.Enum` which includes
|
||||||
|
support for PG's ``CREATE TYPE`` and ``DROP TYPE``.
|
||||||
|
|
||||||
|
When the builtin type :class:`_types.Enum` is used and the
|
||||||
|
:paramref:`.Enum.native_enum` flag is left at its default of
|
||||||
|
True, the PostgreSQL backend will use a :class:`_postgresql.ENUM`
|
||||||
|
type as the implementation, so the special create/drop rules
|
||||||
|
will be used.
|
||||||
|
|
||||||
|
The create/drop behavior of ENUM is necessarily intricate, due to the
|
||||||
|
awkward relationship the ENUM type has in relationship to the
|
||||||
|
parent table, in that it may be "owned" by just a single table, or
|
||||||
|
may be shared among many tables.
|
||||||
|
|
||||||
|
When using :class:`_types.Enum` or :class:`_postgresql.ENUM`
|
||||||
|
in an "inline" fashion, the ``CREATE TYPE`` and ``DROP TYPE`` is emitted
|
||||||
|
corresponding to when the :meth:`_schema.Table.create` and
|
||||||
|
:meth:`_schema.Table.drop`
|
||||||
|
methods are called::
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
"sometable",
|
||||||
|
metadata,
|
||||||
|
Column("some_enum", ENUM("a", "b", "c", name="myenum")),
|
||||||
|
)
|
||||||
|
|
||||||
|
table.create(engine) # will emit CREATE ENUM and CREATE TABLE
|
||||||
|
table.drop(engine) # will emit DROP TABLE and DROP ENUM
|
||||||
|
|
||||||
|
To use a common enumerated type between multiple tables, the best
|
||||||
|
practice is to declare the :class:`_types.Enum` or
|
||||||
|
:class:`_postgresql.ENUM` independently, and associate it with the
|
||||||
|
:class:`_schema.MetaData` object itself::
|
||||||
|
|
||||||
|
my_enum = ENUM("a", "b", "c", name="myenum", metadata=metadata)
|
||||||
|
|
||||||
|
t1 = Table("sometable_one", metadata, Column("some_enum", myenum))
|
||||||
|
|
||||||
|
t2 = Table("sometable_two", metadata, Column("some_enum", myenum))
|
||||||
|
|
||||||
|
When this pattern is used, care must still be taken at the level
|
||||||
|
of individual table creates. Emitting CREATE TABLE without also
|
||||||
|
specifying ``checkfirst=True`` will still cause issues::
|
||||||
|
|
||||||
|
t1.create(engine) # will fail: no such type 'myenum'
|
||||||
|
|
||||||
|
If we specify ``checkfirst=True``, the individual table-level create
|
||||||
|
operation will check for the ``ENUM`` and create if not exists::
|
||||||
|
|
||||||
|
# will check if enum exists, and emit CREATE TYPE if not
|
||||||
|
t1.create(engine, checkfirst=True)
|
||||||
|
|
||||||
|
When using a metadata-level ENUM type, the type will always be created
|
||||||
|
and dropped if either the metadata-wide create/drop is called::
|
||||||
|
|
||||||
|
metadata.create_all(engine) # will emit CREATE TYPE
|
||||||
|
metadata.drop_all(engine) # will emit DROP TYPE
|
||||||
|
|
||||||
|
The type can also be created and dropped directly::
|
||||||
|
|
||||||
|
my_enum.create(engine)
|
||||||
|
my_enum.drop(engine)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
native_enum = True
|
||||||
|
DDLGenerator = EnumGenerator
|
||||||
|
DDLDropper = EnumDropper
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*enums,
|
||||||
|
name: Union[str, _NoArg, None] = _NoArg.NO_ARG,
|
||||||
|
create_type: bool = True,
|
||||||
|
**kw,
|
||||||
|
):
|
||||||
|
"""Construct an :class:`_postgresql.ENUM`.
|
||||||
|
|
||||||
|
Arguments are the same as that of
|
||||||
|
:class:`_types.Enum`, but also including
|
||||||
|
the following parameters.
|
||||||
|
|
||||||
|
:param create_type: Defaults to True.
|
||||||
|
Indicates that ``CREATE TYPE`` should be
|
||||||
|
emitted, after optionally checking for the
|
||||||
|
presence of the type, when the parent
|
||||||
|
table is being created; and additionally
|
||||||
|
that ``DROP TYPE`` is called when the table
|
||||||
|
is dropped. When ``False``, no check
|
||||||
|
will be performed and no ``CREATE TYPE``
|
||||||
|
or ``DROP TYPE`` is emitted, unless
|
||||||
|
:meth:`~.postgresql.ENUM.create`
|
||||||
|
or :meth:`~.postgresql.ENUM.drop`
|
||||||
|
are called directly.
|
||||||
|
Setting to ``False`` is helpful
|
||||||
|
when invoking a creation scheme to a SQL file
|
||||||
|
without access to the actual database -
|
||||||
|
the :meth:`~.postgresql.ENUM.create` and
|
||||||
|
:meth:`~.postgresql.ENUM.drop` methods can
|
||||||
|
be used to emit SQL to a target bind.
|
||||||
|
|
||||||
|
"""
|
||||||
|
native_enum = kw.pop("native_enum", None)
|
||||||
|
if native_enum is False:
|
||||||
|
util.warn(
|
||||||
|
"the native_enum flag does not apply to the "
|
||||||
|
"sqlalchemy.dialects.postgresql.ENUM datatype; this type "
|
||||||
|
"always refers to ENUM. Use sqlalchemy.types.Enum for "
|
||||||
|
"non-native enum."
|
||||||
|
)
|
||||||
|
self.create_type = create_type
|
||||||
|
if name is not _NoArg.NO_ARG:
|
||||||
|
kw["name"] = name
|
||||||
|
super().__init__(*enums, **kw)
|
||||||
|
|
||||||
|
def coerce_compared_value(self, op, value):
|
||||||
|
super_coerced_type = super().coerce_compared_value(op, value)
|
||||||
|
if (
|
||||||
|
super_coerced_type._type_affinity
|
||||||
|
is type_api.STRINGTYPE._type_affinity
|
||||||
|
):
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return super_coerced_type
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __test_init__(cls):
|
||||||
|
return cls(name="name")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def adapt_emulated_to_native(cls, impl, **kw):
|
||||||
|
"""Produce a PostgreSQL native :class:`_postgresql.ENUM` from plain
|
||||||
|
:class:`.Enum`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
kw.setdefault("validate_strings", impl.validate_strings)
|
||||||
|
kw.setdefault("name", impl.name)
|
||||||
|
kw.setdefault("schema", impl.schema)
|
||||||
|
kw.setdefault("inherit_schema", impl.inherit_schema)
|
||||||
|
kw.setdefault("metadata", impl.metadata)
|
||||||
|
kw.setdefault("_create_events", False)
|
||||||
|
kw.setdefault("values_callable", impl.values_callable)
|
||||||
|
kw.setdefault("omit_aliases", impl._omit_aliases)
|
||||||
|
kw.setdefault("_adapted_from", impl)
|
||||||
|
if type_api._is_native_for_emulated(impl.__class__):
|
||||||
|
kw.setdefault("create_type", impl.create_type)
|
||||||
|
|
||||||
|
return cls(**kw)
|
||||||
|
|
||||||
|
def create(self, bind: _CreateDropBind, checkfirst: bool = True) -> None:
|
||||||
|
"""Emit ``CREATE TYPE`` for this
|
||||||
|
:class:`_postgresql.ENUM`.
|
||||||
|
|
||||||
|
If the underlying dialect does not support
|
||||||
|
PostgreSQL CREATE TYPE, no action is taken.
|
||||||
|
|
||||||
|
:param bind: a connectable :class:`_engine.Engine`,
|
||||||
|
:class:`_engine.Connection`, or similar object to emit
|
||||||
|
SQL.
|
||||||
|
:param checkfirst: if ``True``, a query against
|
||||||
|
the PG catalog will be first performed to see
|
||||||
|
if the type does not exist already before
|
||||||
|
creating.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not bind.dialect.supports_native_enum:
|
||||||
|
return
|
||||||
|
|
||||||
|
super().create(bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def drop(self, bind: _CreateDropBind, checkfirst: bool = True) -> None:
|
||||||
|
"""Emit ``DROP TYPE`` for this
|
||||||
|
:class:`_postgresql.ENUM`.
|
||||||
|
|
||||||
|
If the underlying dialect does not support
|
||||||
|
PostgreSQL DROP TYPE, no action is taken.
|
||||||
|
|
||||||
|
:param bind: a connectable :class:`_engine.Engine`,
|
||||||
|
:class:`_engine.Connection`, or similar object to emit
|
||||||
|
SQL.
|
||||||
|
:param checkfirst: if ``True``, a query against
|
||||||
|
the PG catalog will be first performed to see
|
||||||
|
if the type actually exists before dropping.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not bind.dialect.supports_native_enum:
|
||||||
|
return
|
||||||
|
|
||||||
|
super().drop(bind, checkfirst=checkfirst)
|
||||||
|
|
||||||
|
def get_dbapi_type(self, dbapi: ModuleType) -> None:
|
||||||
|
"""dont return dbapi.STRING for ENUM in PostgreSQL, since that's
|
||||||
|
a different type"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DomainGenerator(NamedTypeGenerator):
|
||||||
|
def visit_DOMAIN(self, domain):
|
||||||
|
if not self._can_create_type(domain):
|
||||||
|
return
|
||||||
|
with self.with_ddl_events(domain):
|
||||||
|
self.connection.execute(CreateDomainType(domain))
|
||||||
|
|
||||||
|
|
||||||
|
class DomainDropper(NamedTypeDropper):
|
||||||
|
def visit_DOMAIN(self, domain):
|
||||||
|
if not self._can_drop_type(domain):
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.with_ddl_events(domain):
|
||||||
|
self.connection.execute(DropDomainType(domain))
|
||||||
|
|
||||||
|
|
||||||
|
class DOMAIN(NamedType, sqltypes.SchemaType):
|
||||||
|
r"""Represent the DOMAIN PostgreSQL type.
|
||||||
|
|
||||||
|
A domain is essentially a data type with optional constraints
|
||||||
|
that restrict the allowed set of values. E.g.::
|
||||||
|
|
||||||
|
PositiveInt = DOMAIN("pos_int", Integer, check="VALUE > 0", not_null=True)
|
||||||
|
|
||||||
|
UsPostalCode = DOMAIN(
|
||||||
|
"us_postal_code",
|
||||||
|
Text,
|
||||||
|
check="VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'",
|
||||||
|
)
|
||||||
|
|
||||||
|
See the `PostgreSQL documentation`__ for additional details
|
||||||
|
|
||||||
|
__ https://www.postgresql.org/docs/current/sql-createdomain.html
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
DDLGenerator = DomainGenerator
|
||||||
|
DDLDropper = DomainDropper
|
||||||
|
|
||||||
|
__visit_name__ = "DOMAIN"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
data_type: _TypeEngineArgument[Any],
|
||||||
|
*,
|
||||||
|
collation: Optional[str] = None,
|
||||||
|
default: Union[elements.TextClause, str, None] = None,
|
||||||
|
constraint_name: Optional[str] = None,
|
||||||
|
not_null: Optional[bool] = None,
|
||||||
|
check: Union[elements.TextClause, str, None] = None,
|
||||||
|
create_type: bool = True,
|
||||||
|
**kw: Any,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Construct a DOMAIN.
|
||||||
|
|
||||||
|
:param name: the name of the domain
|
||||||
|
:param data_type: The underlying data type of the domain.
|
||||||
|
This can include array specifiers.
|
||||||
|
:param collation: An optional collation for the domain.
|
||||||
|
If no collation is specified, the underlying data type's default
|
||||||
|
collation is used. The underlying type must be collatable if
|
||||||
|
``collation`` is specified.
|
||||||
|
:param default: The DEFAULT clause specifies a default value for
|
||||||
|
columns of the domain data type. The default should be a string
|
||||||
|
or a :func:`_expression.text` value.
|
||||||
|
If no default value is specified, then the default value is
|
||||||
|
the null value.
|
||||||
|
:param constraint_name: An optional name for a constraint.
|
||||||
|
If not specified, the backend generates a name.
|
||||||
|
:param not_null: Values of this domain are prevented from being null.
|
||||||
|
By default domain are allowed to be null. If not specified
|
||||||
|
no nullability clause will be emitted.
|
||||||
|
:param check: CHECK clause specify integrity constraint or test
|
||||||
|
which values of the domain must satisfy. A constraint must be
|
||||||
|
an expression producing a Boolean result that can use the key
|
||||||
|
word VALUE to refer to the value being tested.
|
||||||
|
Differently from PostgreSQL, only a single check clause is
|
||||||
|
currently allowed in SQLAlchemy.
|
||||||
|
:param schema: optional schema name
|
||||||
|
:param metadata: optional :class:`_schema.MetaData` object which
|
||||||
|
this :class:`_postgresql.DOMAIN` will be directly associated
|
||||||
|
:param create_type: Defaults to True.
|
||||||
|
Indicates that ``CREATE TYPE`` should be emitted, after optionally
|
||||||
|
checking for the presence of the type, when the parent table is
|
||||||
|
being created; and additionally that ``DROP TYPE`` is called
|
||||||
|
when the table is dropped.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.data_type = type_api.to_instance(data_type)
|
||||||
|
self.default = default
|
||||||
|
self.collation = collation
|
||||||
|
self.constraint_name = constraint_name
|
||||||
|
self.not_null = bool(not_null)
|
||||||
|
if check is not None:
|
||||||
|
check = coercions.expect(roles.DDLExpressionRole, check)
|
||||||
|
self.check = check
|
||||||
|
self.create_type = create_type
|
||||||
|
super().__init__(name=name, **kw)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __test_init__(cls):
|
||||||
|
return cls("name", sqltypes.Integer)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateEnumType(schema._CreateDropBase):
|
||||||
|
__visit_name__ = "create_enum_type"
|
||||||
|
|
||||||
|
|
||||||
|
class DropEnumType(schema._CreateDropBase):
|
||||||
|
__visit_name__ = "drop_enum_type"
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDomainType(schema._CreateDropBase):
|
||||||
|
"""Represent a CREATE DOMAIN statement."""
|
||||||
|
|
||||||
|
__visit_name__ = "create_domain_type"
|
||||||
|
|
||||||
|
|
||||||
|
class DropDomainType(schema._CreateDropBase):
|
||||||
|
"""Represent a DROP DOMAIN statement."""
|
||||||
|
|
||||||
|
__visit_name__ = "drop_domain_type"
|
||||||
Reference in New Issue
Block a user