This commit is contained in:
@@ -1,27 +1,24 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Protocol
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import __version__
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import schema
|
||||
from sqlalchemy import sql
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.engine import url
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.schema import CheckConstraint
|
||||
from sqlalchemy.schema import Column
|
||||
from sqlalchemy.schema import ForeignKeyConstraint
|
||||
@@ -29,33 +26,31 @@ from sqlalchemy.sql import visitors
|
||||
from sqlalchemy.sql.base import DialectKWArgs
|
||||
from sqlalchemy.sql.elements import BindParameter
|
||||
from sqlalchemy.sql.elements import ColumnClause
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.elements import UnaryExpression
|
||||
from sqlalchemy.sql.naming import _NONE_NAME as _NONE_NAME # type: ignore[attr-defined] # noqa: E501
|
||||
from sqlalchemy.sql.visitors import traverse
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy import ClauseElement
|
||||
from sqlalchemy import Identity
|
||||
from sqlalchemy import Index
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.engine import Dialect
|
||||
from sqlalchemy.engine import Transaction
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql.base import ColumnCollection
|
||||
from sqlalchemy.sql.compiler import SQLCompiler
|
||||
from sqlalchemy.sql.dml import Insert
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.schema import SchemaItem
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
from sqlalchemy.sql.selectable import TableClause
|
||||
|
||||
_CE = TypeVar("_CE", bound=Union["ColumnElement[Any]", "SchemaItem"])
|
||||
|
||||
|
||||
class _CompilerProtocol(Protocol):
|
||||
def __call__(self, element: Any, compiler: Any, **kw: Any) -> str: ...
|
||||
|
||||
|
||||
def _safe_int(value: str) -> Union[int, str]:
|
||||
try:
|
||||
return int(value)
|
||||
@@ -66,65 +61,90 @@ def _safe_int(value: str) -> Union[int, str]:
|
||||
_vers = tuple(
|
||||
[_safe_int(x) for x in re.findall(r"(\d+|[abc]\d)", __version__)]
|
||||
)
|
||||
sqla_13 = _vers >= (1, 3)
|
||||
sqla_14 = _vers >= (1, 4)
|
||||
# https://docs.sqlalchemy.org/en/latest/changelog/changelog_14.html#change-0c6e0cc67dfe6fac5164720e57ef307d
|
||||
sqla_14_18 = _vers >= (1, 4, 18)
|
||||
sqla_14_26 = _vers >= (1, 4, 26)
|
||||
sqla_2 = _vers >= (2,)
|
||||
sqlalchemy_version = __version__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
try:
|
||||
from sqlalchemy.sql.naming import _NONE_NAME as _NONE_NAME
|
||||
except ImportError:
|
||||
from sqlalchemy.sql.elements import _NONE_NAME as _NONE_NAME # type: ignore # noqa: E501
|
||||
|
||||
def compiles(
|
||||
element: Type[ClauseElement], *dialects: str
|
||||
) -> Callable[[_CompilerProtocol], _CompilerProtocol]: ...
|
||||
|
||||
class _Unsupported:
|
||||
"Placeholder for unsupported SQLAlchemy classes"
|
||||
|
||||
|
||||
try:
|
||||
from sqlalchemy import Computed
|
||||
except ImportError:
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
class Computed(_Unsupported):
|
||||
pass
|
||||
|
||||
has_computed = False
|
||||
has_computed_reflection = False
|
||||
else:
|
||||
from sqlalchemy.ext.compiler import compiles # noqa: I100,I202
|
||||
has_computed = True
|
||||
has_computed_reflection = _vers >= (1, 3, 16)
|
||||
|
||||
try:
|
||||
from sqlalchemy import Identity
|
||||
except ImportError:
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
identity_has_dialect_kwargs = issubclass(schema.Identity, DialectKWArgs)
|
||||
class Identity(_Unsupported):
|
||||
pass
|
||||
|
||||
has_identity = False
|
||||
else:
|
||||
identity_has_dialect_kwargs = issubclass(Identity, DialectKWArgs)
|
||||
|
||||
def _get_identity_options_dict(
|
||||
identity: Union[Identity, schema.Sequence, None],
|
||||
dialect_kwargs: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
if identity is None:
|
||||
return {}
|
||||
elif identity_has_dialect_kwargs:
|
||||
assert hasattr(identity, "_as_dict")
|
||||
as_dict = identity._as_dict()
|
||||
if dialect_kwargs:
|
||||
assert isinstance(identity, DialectKWArgs)
|
||||
as_dict.update(identity.dialect_kwargs)
|
||||
else:
|
||||
as_dict = {}
|
||||
if isinstance(identity, schema.Identity):
|
||||
# always=None means something different than always=False
|
||||
as_dict["always"] = identity.always
|
||||
if identity.on_null is not None:
|
||||
as_dict["on_null"] = identity.on_null
|
||||
# attributes common to Identity and Sequence
|
||||
attrs = (
|
||||
"start",
|
||||
"increment",
|
||||
"minvalue",
|
||||
"maxvalue",
|
||||
"nominvalue",
|
||||
"nomaxvalue",
|
||||
"cycle",
|
||||
"cache",
|
||||
"order",
|
||||
)
|
||||
as_dict.update(
|
||||
{
|
||||
key: getattr(identity, key, None)
|
||||
for key in attrs
|
||||
if getattr(identity, key, None) is not None
|
||||
}
|
||||
)
|
||||
return as_dict
|
||||
def _get_identity_options_dict(
|
||||
identity: Union[Identity, schema.Sequence, None],
|
||||
dialect_kwargs: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
if identity is None:
|
||||
return {}
|
||||
elif identity_has_dialect_kwargs:
|
||||
as_dict = identity._as_dict() # type: ignore
|
||||
if dialect_kwargs:
|
||||
assert isinstance(identity, DialectKWArgs)
|
||||
as_dict.update(identity.dialect_kwargs)
|
||||
else:
|
||||
as_dict = {}
|
||||
if isinstance(identity, Identity):
|
||||
# always=None means something different than always=False
|
||||
as_dict["always"] = identity.always
|
||||
if identity.on_null is not None:
|
||||
as_dict["on_null"] = identity.on_null
|
||||
# attributes common to Identity and Sequence
|
||||
attrs = (
|
||||
"start",
|
||||
"increment",
|
||||
"minvalue",
|
||||
"maxvalue",
|
||||
"nominvalue",
|
||||
"nomaxvalue",
|
||||
"cycle",
|
||||
"cache",
|
||||
"order",
|
||||
)
|
||||
as_dict.update(
|
||||
{
|
||||
key: getattr(identity, key, None)
|
||||
for key in attrs
|
||||
if getattr(identity, key, None) is not None
|
||||
}
|
||||
)
|
||||
return as_dict
|
||||
|
||||
has_identity = True
|
||||
|
||||
if sqla_2:
|
||||
from sqlalchemy.sql.base import _NoneName
|
||||
@@ -133,6 +153,7 @@ else:
|
||||
|
||||
|
||||
_ConstraintName = Union[None, str, _NoneName]
|
||||
|
||||
_ConstraintNameDefined = Union[str, _NoneName]
|
||||
|
||||
|
||||
@@ -142,11 +163,15 @@ def constraint_name_defined(
|
||||
return name is _NONE_NAME or isinstance(name, (str, _NoneName))
|
||||
|
||||
|
||||
def constraint_name_string(name: _ConstraintName) -> TypeGuard[str]:
|
||||
def constraint_name_string(
|
||||
name: _ConstraintName,
|
||||
) -> TypeGuard[str]:
|
||||
return isinstance(name, str)
|
||||
|
||||
|
||||
def constraint_name_or_none(name: _ConstraintName) -> Optional[str]:
|
||||
def constraint_name_or_none(
|
||||
name: _ConstraintName,
|
||||
) -> Optional[str]:
|
||||
return name if constraint_name_string(name) else None
|
||||
|
||||
|
||||
@@ -176,10 +201,17 @@ def _ensure_scope_for_ddl(
|
||||
yield
|
||||
|
||||
|
||||
def url_render_as_string(url, hide_password=True):
|
||||
if sqla_14:
|
||||
return url.render_as_string(hide_password=hide_password)
|
||||
else:
|
||||
return url.__to_string__(hide_password=hide_password)
|
||||
|
||||
|
||||
def _safe_begin_connection_transaction(
|
||||
connection: Connection,
|
||||
) -> Transaction:
|
||||
transaction = connection.get_transaction()
|
||||
transaction = _get_connection_transaction(connection)
|
||||
if transaction:
|
||||
return transaction
|
||||
else:
|
||||
@@ -189,7 +221,7 @@ def _safe_begin_connection_transaction(
|
||||
def _safe_commit_connection_transaction(
|
||||
connection: Connection,
|
||||
) -> None:
|
||||
transaction = connection.get_transaction()
|
||||
transaction = _get_connection_transaction(connection)
|
||||
if transaction:
|
||||
transaction.commit()
|
||||
|
||||
@@ -197,7 +229,7 @@ def _safe_commit_connection_transaction(
|
||||
def _safe_rollback_connection_transaction(
|
||||
connection: Connection,
|
||||
) -> None:
|
||||
transaction = connection.get_transaction()
|
||||
transaction = _get_connection_transaction(connection)
|
||||
if transaction:
|
||||
transaction.rollback()
|
||||
|
||||
@@ -218,34 +250,70 @@ def _idx_table_bound_expressions(idx: Index) -> Iterable[ColumnElement[Any]]:
|
||||
|
||||
def _copy(schema_item: _CE, **kw) -> _CE:
|
||||
if hasattr(schema_item, "_copy"):
|
||||
return schema_item._copy(**kw)
|
||||
return schema_item._copy(**kw) # type: ignore[union-attr]
|
||||
else:
|
||||
return schema_item.copy(**kw) # type: ignore[union-attr]
|
||||
|
||||
|
||||
def _get_connection_transaction(
|
||||
connection: Connection,
|
||||
) -> Optional[Transaction]:
|
||||
if sqla_14:
|
||||
return connection.get_transaction()
|
||||
else:
|
||||
r = connection._root # type: ignore[attr-defined]
|
||||
return r._Connection__transaction
|
||||
|
||||
|
||||
def _create_url(*arg, **kw) -> url.URL:
|
||||
if hasattr(url.URL, "create"):
|
||||
return url.URL.create(*arg, **kw)
|
||||
else:
|
||||
return url.URL(*arg, **kw)
|
||||
|
||||
|
||||
def _connectable_has_table(
|
||||
connectable: Connection, tablename: str, schemaname: Union[str, None]
|
||||
) -> bool:
|
||||
return connectable.dialect.has_table(connectable, tablename, schemaname)
|
||||
if sqla_14:
|
||||
return inspect(connectable).has_table(tablename, schemaname)
|
||||
else:
|
||||
return connectable.dialect.has_table(
|
||||
connectable, tablename, schemaname
|
||||
)
|
||||
|
||||
|
||||
def _exec_on_inspector(inspector, statement, **params):
|
||||
with inspector._operation_context() as conn:
|
||||
return conn.execute(statement, params)
|
||||
if sqla_14:
|
||||
with inspector._operation_context() as conn:
|
||||
return conn.execute(statement, params)
|
||||
else:
|
||||
return inspector.bind.execute(statement, params)
|
||||
|
||||
|
||||
def _nullability_might_be_unset(metadata_column):
|
||||
from sqlalchemy.sql import schema
|
||||
if not sqla_14:
|
||||
return metadata_column.nullable
|
||||
else:
|
||||
from sqlalchemy.sql import schema
|
||||
|
||||
return metadata_column._user_defined_nullable is schema.NULL_UNSPECIFIED
|
||||
return (
|
||||
metadata_column._user_defined_nullable is schema.NULL_UNSPECIFIED
|
||||
)
|
||||
|
||||
|
||||
def _server_default_is_computed(*server_default) -> bool:
|
||||
return any(isinstance(sd, schema.Computed) for sd in server_default)
|
||||
if not has_computed:
|
||||
return False
|
||||
else:
|
||||
return any(isinstance(sd, Computed) for sd in server_default)
|
||||
|
||||
|
||||
def _server_default_is_identity(*server_default) -> bool:
|
||||
return any(isinstance(sd, schema.Identity) for sd in server_default)
|
||||
if not sqla_14:
|
||||
return False
|
||||
else:
|
||||
return any(isinstance(sd, Identity) for sd in server_default)
|
||||
|
||||
|
||||
def _table_for_constraint(constraint: Constraint) -> Table:
|
||||
@@ -266,6 +334,15 @@ def _columns_for_constraint(constraint):
|
||||
return list(constraint.columns)
|
||||
|
||||
|
||||
def _reflect_table(inspector: Inspector, table: Table) -> None:
|
||||
if sqla_14:
|
||||
return inspector.reflect_table(table, None)
|
||||
else:
|
||||
return inspector.reflecttable( # type: ignore[attr-defined]
|
||||
table, None
|
||||
)
|
||||
|
||||
|
||||
def _resolve_for_variant(type_, dialect):
|
||||
if _type_has_variants(type_):
|
||||
base_type, mapping = _get_variant_mapping(type_)
|
||||
@@ -274,7 +351,7 @@ def _resolve_for_variant(type_, dialect):
|
||||
return type_
|
||||
|
||||
|
||||
if hasattr(sqltypes.TypeEngine, "_variant_mapping"): # 2.0
|
||||
if hasattr(sqltypes.TypeEngine, "_variant_mapping"):
|
||||
|
||||
def _type_has_variants(type_):
|
||||
return bool(type_._variant_mapping)
|
||||
@@ -291,12 +368,7 @@ else:
|
||||
return type_.impl, type_.mapping
|
||||
|
||||
|
||||
def _fk_spec(constraint: ForeignKeyConstraint) -> Any:
|
||||
if TYPE_CHECKING:
|
||||
assert constraint.columns is not None
|
||||
assert constraint.elements is not None
|
||||
assert isinstance(constraint.parent, Table)
|
||||
|
||||
def _fk_spec(constraint):
|
||||
source_columns = [
|
||||
constraint.columns[key].name for key in constraint.column_keys
|
||||
]
|
||||
@@ -325,7 +397,7 @@ def _fk_spec(constraint: ForeignKeyConstraint) -> Any:
|
||||
|
||||
|
||||
def _fk_is_self_referential(constraint: ForeignKeyConstraint) -> bool:
|
||||
spec = constraint.elements[0]._get_colspec()
|
||||
spec = constraint.elements[0]._get_colspec() # type: ignore[attr-defined]
|
||||
tokens = spec.split(".")
|
||||
tokens.pop(-1) # colname
|
||||
tablekey = ".".join(tokens)
|
||||
@@ -337,13 +409,13 @@ def _is_type_bound(constraint: Constraint) -> bool:
|
||||
# this deals with SQLAlchemy #3260, don't copy CHECK constraints
|
||||
# that will be generated by the type.
|
||||
# new feature added for #3260
|
||||
return constraint._type_bound
|
||||
return constraint._type_bound # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _find_columns(clause):
|
||||
"""locate Column objects within the given expression."""
|
||||
|
||||
cols: Set[ColumnElement[Any]] = set()
|
||||
cols = set()
|
||||
traverse(clause, {}, {"column": cols.add})
|
||||
return cols
|
||||
|
||||
@@ -430,7 +502,7 @@ class _textual_index_element(sql.ColumnElement):
|
||||
self.fake_column = schema.Column(self.text.text, sqltypes.NULLTYPE)
|
||||
table.append_column(self.fake_column)
|
||||
|
||||
def get_children(self, **kw):
|
||||
def get_children(self):
|
||||
return [self.fake_column]
|
||||
|
||||
|
||||
@@ -452,44 +524,116 @@ def _render_literal_bindparam(
|
||||
return compiler.render_literal_bindparam(element, **kw)
|
||||
|
||||
|
||||
def _get_index_expressions(idx):
|
||||
return list(idx.expressions)
|
||||
|
||||
|
||||
def _get_index_column_names(idx):
|
||||
return [getattr(exp, "name", None) for exp in _get_index_expressions(idx)]
|
||||
|
||||
|
||||
def _column_kwargs(col: Column) -> Mapping:
|
||||
if sqla_13:
|
||||
return col.kwargs
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def _get_constraint_final_name(
|
||||
constraint: Union[Index, Constraint], dialect: Optional[Dialect]
|
||||
) -> Optional[str]:
|
||||
if constraint.name is None:
|
||||
return None
|
||||
assert dialect is not None
|
||||
# for SQLAlchemy 1.4 we would like to have the option to expand
|
||||
# the use of "deferred" names for constraints as well as to have
|
||||
# some flexibility with "None" name and similar; make use of new
|
||||
# SQLAlchemy API to return what would be the final compiled form of
|
||||
# the name for this dialect.
|
||||
return dialect.identifier_preparer.format_constraint(
|
||||
constraint, _alembic_quote=False
|
||||
)
|
||||
if sqla_14:
|
||||
# for SQLAlchemy 1.4 we would like to have the option to expand
|
||||
# the use of "deferred" names for constraints as well as to have
|
||||
# some flexibility with "None" name and similar; make use of new
|
||||
# SQLAlchemy API to return what would be the final compiled form of
|
||||
# the name for this dialect.
|
||||
return dialect.identifier_preparer.format_constraint(
|
||||
constraint, _alembic_quote=False
|
||||
)
|
||||
else:
|
||||
# prior to SQLAlchemy 1.4, work around quoting logic to get at the
|
||||
# final compiled name without quotes.
|
||||
if hasattr(constraint.name, "quote"):
|
||||
# might be quoted_name, might be truncated_name, keep it the
|
||||
# same
|
||||
quoted_name_cls: type = type(constraint.name)
|
||||
else:
|
||||
quoted_name_cls = quoted_name
|
||||
|
||||
new_name = quoted_name_cls(str(constraint.name), quote=False)
|
||||
constraint = constraint.__class__(name=new_name)
|
||||
|
||||
if isinstance(constraint, schema.Index):
|
||||
# name should not be quoted.
|
||||
d = dialect.ddl_compiler(dialect, None) # type: ignore[arg-type]
|
||||
return d._prepared_index_name( # type: ignore[attr-defined]
|
||||
constraint
|
||||
)
|
||||
else:
|
||||
# name should not be quoted.
|
||||
return dialect.identifier_preparer.format_constraint(constraint)
|
||||
|
||||
|
||||
def _constraint_is_named(
|
||||
constraint: Union[Constraint, Index], dialect: Optional[Dialect]
|
||||
) -> bool:
|
||||
if constraint.name is None:
|
||||
return False
|
||||
assert dialect is not None
|
||||
name = dialect.identifier_preparer.format_constraint(
|
||||
constraint, _alembic_quote=False
|
||||
)
|
||||
return name is not None
|
||||
if sqla_14:
|
||||
if constraint.name is None:
|
||||
return False
|
||||
assert dialect is not None
|
||||
name = dialect.identifier_preparer.format_constraint(
|
||||
constraint, _alembic_quote=False
|
||||
)
|
||||
return name is not None
|
||||
else:
|
||||
return constraint.name is not None
|
||||
|
||||
|
||||
def _is_mariadb(mysql_dialect: Dialect) -> bool:
|
||||
if sqla_14:
|
||||
return mysql_dialect.is_mariadb # type: ignore[attr-defined]
|
||||
else:
|
||||
return bool(
|
||||
mysql_dialect.server_version_info
|
||||
and mysql_dialect._is_mariadb # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
def _mariadb_normalized_version_info(mysql_dialect):
|
||||
return mysql_dialect._mariadb_normalized_version_info
|
||||
|
||||
|
||||
def _insert_inline(table: Union[TableClause, Table]) -> Insert:
|
||||
if sqla_14:
|
||||
return table.insert().inline()
|
||||
else:
|
||||
return table.insert(inline=True) # type: ignore[call-arg]
|
||||
|
||||
|
||||
if sqla_14:
|
||||
from sqlalchemy import create_mock_engine
|
||||
from sqlalchemy import select as _select
|
||||
else:
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
def create_mock_engine(url, executor, **kw): # type: ignore[misc]
|
||||
return create_engine(
|
||||
"postgresql://", strategy="mock", executor=executor
|
||||
)
|
||||
|
||||
def _select(*columns, **kw) -> Select: # type: ignore[no-redef]
|
||||
return sql.select(list(columns), **kw) # type: ignore[call-overload]
|
||||
|
||||
|
||||
def is_expression_index(index: Index) -> bool:
|
||||
expr: Any
|
||||
for expr in index.expressions:
|
||||
if is_expression(expr):
|
||||
while isinstance(expr, UnaryExpression):
|
||||
expr = expr.element
|
||||
if not isinstance(expr, ColumnClause) or expr.is_literal:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_expression(expr: Any) -> bool:
|
||||
while isinstance(expr, UnaryExpression):
|
||||
expr = expr.element
|
||||
if not isinstance(expr, ColumnClause) or expr.is_literal:
|
||||
return True
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user