Primer commit del proyecto RSS
This commit is contained in:
commit
27c9515d29
1568 changed files with 252311 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
653
venv/lib/python3.12/site-packages/mysql/connector/django/base.py
Normal file
653
venv/lib/python3.12/site-packages/mysql/connector/django/base.py
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
# Copyright (c) 2020, 2025, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
# mypy: disable-error-code="override"
|
||||
|
||||
"""Django database Backend using MySQL Connector/Python.
|
||||
|
||||
This Django database backend is heavily based on the MySQL backend from Django.
|
||||
|
||||
Changes include:
|
||||
* Support for microseconds (MySQL 5.6.3 and later)
|
||||
* Using INFORMATION_SCHEMA where possible
|
||||
* Using new defaults for, for example SQL_AUTO_IS_NULL
|
||||
|
||||
Requires and comes with MySQL Connector/Python v8.0.22 and later:
|
||||
http://dev.mysql.com/downloads/connector/python/
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from datetime import datetime, time
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import IntegrityError
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.utils import dateparse, timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from mysql.connector.types import MySQLConvertibleType
|
||||
|
||||
try:
|
||||
import mysql.connector
|
||||
|
||||
from mysql.connector.conversion import MySQLConverter
|
||||
from mysql.connector.custom_types import HexLiteral
|
||||
from mysql.connector.pooling import PooledMySQLConnection
|
||||
from mysql.connector.types import ParamsSequenceOrDictType, RowType, StrOrBytes
|
||||
except ImportError as err:
|
||||
raise ImproperlyConfigured(f"Error loading mysql.connector module: {err}") from err
|
||||
|
||||
try:
|
||||
from _mysql_connector import datetime_to_mysql
|
||||
except ImportError:
|
||||
HAVE_CEXT = False
|
||||
else:
|
||||
HAVE_CEXT = True
|
||||
|
||||
from .client import DatabaseClient
|
||||
from .creation import DatabaseCreation
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import DatabaseIntrospection
|
||||
from .operations import DatabaseOperations
|
||||
from .schema import DatabaseSchemaEditor
|
||||
from .validation import DatabaseValidation
|
||||
|
||||
Error = mysql.connector.Error
|
||||
DatabaseError = mysql.connector.DatabaseError
|
||||
NotSupportedError = mysql.connector.NotSupportedError
|
||||
OperationalError = mysql.connector.OperationalError
|
||||
ProgrammingError = mysql.connector.ProgrammingError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mysql.connector.abstracts import MySQLConnectionAbstract, MySQLCursorAbstract
|
||||
|
||||
|
||||
def adapt_datetime_with_timezone_support(value: datetime) -> StrOrBytes:
|
||||
"""Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL."""
|
||||
if settings.USE_TZ:
|
||||
if timezone.is_naive(value):
|
||||
warnings.warn(
|
||||
f"MySQL received a naive datetime ({value})"
|
||||
" while time zone support is active.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
default_timezone = timezone.get_default_timezone()
|
||||
value = timezone.make_aware(value, default_timezone)
|
||||
# pylint: disable=no-member
|
||||
value = value.astimezone(timezone.utc).replace( # type: ignore[attr-defined]
|
||||
tzinfo=None
|
||||
)
|
||||
if HAVE_CEXT:
|
||||
mysql_datetime: bytes = datetime_to_mysql(value)
|
||||
return mysql_datetime
|
||||
return value.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
|
||||
class CursorWrapper:
|
||||
"""Wrapper around MySQL Connector/Python's cursor class.
|
||||
|
||||
The cursor class is defined by the options passed to MySQL
|
||||
Connector/Python. If buffered option is True in those options,
|
||||
MySQLCursorBuffered will be used.
|
||||
"""
|
||||
|
||||
codes_for_integrityerror = (
|
||||
1048, # Column cannot be null
|
||||
1690, # BIGINT UNSIGNED value is out of range
|
||||
3819, # CHECK constraint is violated
|
||||
4025, # CHECK constraint failed
|
||||
)
|
||||
|
||||
def __init__(self, cursor: "MySQLCursorAbstract") -> None:
|
||||
self.cursor: "MySQLCursorAbstract" = cursor
|
||||
|
||||
@staticmethod
|
||||
def _adapt_execute_args_dict(
|
||||
args: Dict[str, MySQLConvertibleType],
|
||||
) -> Dict[str, MySQLConvertibleType]:
|
||||
if not args:
|
||||
return args
|
||||
new_args = dict(args)
|
||||
for key, value in args.items():
|
||||
if isinstance(value, datetime):
|
||||
new_args[key] = adapt_datetime_with_timezone_support(value)
|
||||
|
||||
return new_args
|
||||
|
||||
@staticmethod
|
||||
def _adapt_execute_args(
|
||||
args: Optional[Sequence[MySQLConvertibleType]],
|
||||
) -> Optional[Sequence[MySQLConvertibleType]]:
|
||||
if not args:
|
||||
return args
|
||||
new_args = list(args)
|
||||
for i, arg in enumerate(args):
|
||||
if isinstance(arg, datetime):
|
||||
new_args[i] = adapt_datetime_with_timezone_support(arg)
|
||||
|
||||
return tuple(new_args)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
query: str,
|
||||
args: Optional[
|
||||
Union[Sequence[MySQLConvertibleType], Dict[str, MySQLConvertibleType]]
|
||||
] = None,
|
||||
) -> Optional[Generator["MySQLCursorAbstract", None, None]]:
|
||||
"""Executes the given operation
|
||||
|
||||
This wrapper method around the execute()-method of the cursor is
|
||||
mainly needed to re-raise using different exceptions.
|
||||
"""
|
||||
new_args: Optional[ParamsSequenceOrDictType] = None
|
||||
if isinstance(args, dict):
|
||||
new_args = self._adapt_execute_args_dict(args)
|
||||
else:
|
||||
new_args = self._adapt_execute_args(args)
|
||||
try:
|
||||
return self.cursor.execute(query, new_args)
|
||||
except mysql.connector.OperationalError as exc:
|
||||
if exc.args[0] in self.codes_for_integrityerror:
|
||||
raise IntegrityError(*tuple(exc.args)) from None
|
||||
raise
|
||||
|
||||
def executemany(
|
||||
self,
|
||||
query: str,
|
||||
args: Sequence[
|
||||
Union[Sequence[MySQLConvertibleType], Dict[str, MySQLConvertibleType]]
|
||||
],
|
||||
) -> Optional[Generator["MySQLCursorAbstract", None, None]]:
|
||||
"""Executes the given operation
|
||||
|
||||
This wrapper method around the executemany()-method of the cursor is
|
||||
mainly needed to re-raise using different exceptions.
|
||||
"""
|
||||
try:
|
||||
return self.cursor.executemany(query, args)
|
||||
except mysql.connector.OperationalError as exc:
|
||||
if exc.args[0] in self.codes_for_integrityerror:
|
||||
raise IntegrityError(*tuple(exc.args)) from None
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr: Any) -> Any:
|
||||
"""Return an attribute of wrapped cursor"""
|
||||
return getattr(self.cursor, attr)
|
||||
|
||||
def __iter__(self) -> Iterator[RowType]:
|
||||
"""Return an iterator over wrapped cursor"""
|
||||
return iter(self.cursor)
|
||||
|
||||
|
||||
class DatabaseWrapper(BaseDatabaseWrapper): # pylint: disable=abstract-method
|
||||
"""Represent a database connection."""
|
||||
|
||||
vendor = "mysql"
|
||||
# This dictionary maps Field objects to their associated MySQL column
|
||||
# types, as strings. Column-type strings can contain format strings; they'll
|
||||
# be interpolated against the values of Field.__dict__ before being output.
|
||||
# If a column type is set to None, it won't be included in the output.
|
||||
data_types = {
|
||||
"AutoField": "integer AUTO_INCREMENT",
|
||||
"BigAutoField": "bigint AUTO_INCREMENT",
|
||||
"BinaryField": "longblob",
|
||||
"BooleanField": "bool",
|
||||
"CharField": "varchar(%(max_length)s)",
|
||||
"DateField": "date",
|
||||
"DateTimeField": "datetime(6)",
|
||||
"DecimalField": "numeric(%(max_digits)s, %(decimal_places)s)",
|
||||
"DurationField": "bigint",
|
||||
"FileField": "varchar(%(max_length)s)",
|
||||
"FilePathField": "varchar(%(max_length)s)",
|
||||
"FloatField": "double precision",
|
||||
"IntegerField": "integer",
|
||||
"BigIntegerField": "bigint",
|
||||
"IPAddressField": "char(15)",
|
||||
"GenericIPAddressField": "char(39)",
|
||||
"JSONField": "json",
|
||||
"NullBooleanField": "bool",
|
||||
"OneToOneField": "integer",
|
||||
"PositiveBigIntegerField": "bigint UNSIGNED",
|
||||
"PositiveIntegerField": "integer UNSIGNED",
|
||||
"PositiveSmallIntegerField": "smallint UNSIGNED",
|
||||
"SlugField": "varchar(%(max_length)s)",
|
||||
"SmallAutoField": "smallint AUTO_INCREMENT",
|
||||
"SmallIntegerField": "smallint",
|
||||
"TextField": "longtext",
|
||||
"TimeField": "time(6)",
|
||||
"UUIDField": "char(32)",
|
||||
}
|
||||
|
||||
# For these data types:
|
||||
# - MySQL < 8.0.13 doesn't accept default values and
|
||||
# implicitly treat them as nullable
|
||||
# - all versions of MySQL doesn't support full width database
|
||||
# indexes
|
||||
_limited_data_types = (
|
||||
"tinyblob",
|
||||
"blob",
|
||||
"mediumblob",
|
||||
"longblob",
|
||||
"tinytext",
|
||||
"text",
|
||||
"mediumtext",
|
||||
"longtext",
|
||||
"json",
|
||||
)
|
||||
|
||||
operators = {
|
||||
"exact": "= %s",
|
||||
"iexact": "LIKE %s",
|
||||
"contains": "LIKE BINARY %s",
|
||||
"icontains": "LIKE %s",
|
||||
"regex": "REGEXP BINARY %s",
|
||||
"iregex": "REGEXP %s",
|
||||
"gt": "> %s",
|
||||
"gte": ">= %s",
|
||||
"lt": "< %s",
|
||||
"lte": "<= %s",
|
||||
"startswith": "LIKE BINARY %s",
|
||||
"endswith": "LIKE BINARY %s",
|
||||
"istartswith": "LIKE %s",
|
||||
"iendswith": "LIKE %s",
|
||||
}
|
||||
|
||||
# The patterns below are used to generate SQL pattern lookup clauses when
|
||||
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
||||
# or the result of a bilateral transformation).
|
||||
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
||||
# escaped on database side.
|
||||
#
|
||||
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
||||
# the LIKE operator.
|
||||
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
|
||||
pattern_ops = {
|
||||
"contains": "LIKE BINARY CONCAT('%%', {}, '%%')",
|
||||
"icontains": "LIKE CONCAT('%%', {}, '%%')",
|
||||
"startswith": "LIKE BINARY CONCAT({}, '%%')",
|
||||
"istartswith": "LIKE CONCAT({}, '%%')",
|
||||
"endswith": "LIKE BINARY CONCAT('%%', {})",
|
||||
"iendswith": "LIKE CONCAT('%%', {})",
|
||||
}
|
||||
|
||||
isolation_level: Optional[str] = None
|
||||
isolation_levels = {
|
||||
"read uncommitted",
|
||||
"read committed",
|
||||
"repeatable read",
|
||||
"serializable",
|
||||
}
|
||||
|
||||
Database = mysql.connector
|
||||
SchemaEditorClass = DatabaseSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
client_class = DatabaseClient
|
||||
creation_class = DatabaseCreation
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = DatabaseIntrospection
|
||||
ops_class = DatabaseOperations
|
||||
validation_class = DatabaseValidation
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
options = self.settings_dict.get("OPTIONS")
|
||||
if options:
|
||||
self._use_pure = options.get("use_pure", not HAVE_CEXT)
|
||||
converter_class = options.get(
|
||||
"converter_class",
|
||||
DjangoMySQLConverter,
|
||||
)
|
||||
if not issubclass(converter_class, DjangoMySQLConverter):
|
||||
raise ProgrammingError(
|
||||
"Converter class should be a subclass of "
|
||||
"mysql.connector.django.base.DjangoMySQLConverter"
|
||||
)
|
||||
self.converter = converter_class()
|
||||
else:
|
||||
self.converter = DjangoMySQLConverter()
|
||||
self._use_pure = not HAVE_CEXT
|
||||
|
||||
def __getattr__(self, attr: str) -> bool:
|
||||
if attr.startswith("mysql_is"):
|
||||
return False
|
||||
raise AttributeError
|
||||
|
||||
def get_connection_params(self) -> Dict[str, Any]:
|
||||
kwargs = {
|
||||
"charset": "utf8",
|
||||
"use_unicode": True,
|
||||
"buffered": False,
|
||||
"consume_results": True,
|
||||
}
|
||||
|
||||
settings_dict = self.settings_dict
|
||||
|
||||
if settings_dict["USER"]:
|
||||
kwargs["user"] = settings_dict["USER"]
|
||||
if settings_dict["NAME"]:
|
||||
kwargs["database"] = settings_dict["NAME"]
|
||||
if settings_dict["PASSWORD"]:
|
||||
kwargs["passwd"] = settings_dict["PASSWORD"]
|
||||
if settings_dict["HOST"].startswith("/"):
|
||||
kwargs["unix_socket"] = settings_dict["HOST"]
|
||||
elif settings_dict["HOST"]:
|
||||
kwargs["host"] = settings_dict["HOST"]
|
||||
if settings_dict["PORT"]:
|
||||
kwargs["port"] = int(settings_dict["PORT"])
|
||||
if settings_dict.get("OPTIONS", {}).get("init_command"):
|
||||
kwargs["init_command"] = settings_dict["OPTIONS"]["init_command"]
|
||||
|
||||
# Raise exceptions for database warnings if DEBUG is on
|
||||
kwargs["raise_on_warnings"] = settings.DEBUG
|
||||
|
||||
kwargs["client_flags"] = [
|
||||
# Need potentially affected rows on UPDATE
|
||||
mysql.connector.constants.ClientFlag.FOUND_ROWS,
|
||||
]
|
||||
|
||||
try:
|
||||
options = settings_dict["OPTIONS"].copy()
|
||||
isolation_level = options.pop("isolation_level", None)
|
||||
if isolation_level:
|
||||
isolation_level = isolation_level.lower()
|
||||
if isolation_level not in self.isolation_levels:
|
||||
valid_levels = ", ".join(
|
||||
f"'{level}'" for level in sorted(self.isolation_levels)
|
||||
)
|
||||
raise ImproperlyConfigured(
|
||||
f"Invalid transaction isolation level '{isolation_level}' "
|
||||
f"specified.\nUse one of {valid_levels}, or None."
|
||||
)
|
||||
self.isolation_level = isolation_level
|
||||
kwargs.update(options)
|
||||
except KeyError:
|
||||
# OPTIONS missing is OK
|
||||
pass
|
||||
return kwargs
|
||||
|
||||
def get_new_connection(
|
||||
self, conn_params: Dict[str, Any]
|
||||
) -> Union[PooledMySQLConnection, "MySQLConnectionAbstract"]:
|
||||
if "converter_class" not in conn_params:
|
||||
conn_params["converter_class"] = DjangoMySQLConverter
|
||||
cnx = mysql.connector.connect(**conn_params)
|
||||
|
||||
return cnx
|
||||
|
||||
def init_connection_state(self) -> None:
|
||||
assignments = []
|
||||
if self.features.is_sql_auto_is_null_enabled: # type: ignore[attr-defined]
|
||||
# SQL_AUTO_IS_NULL controls whether an AUTO_INCREMENT column on
|
||||
# a recently inserted row will return when the field is tested
|
||||
# for NULL. Disabling this brings this aspect of MySQL in line
|
||||
# with SQL standards.
|
||||
assignments.append("SET SQL_AUTO_IS_NULL = 0")
|
||||
|
||||
if self.isolation_level:
|
||||
assignments.append(
|
||||
"SET SESSION TRANSACTION ISOLATION LEVEL "
|
||||
f"{self.isolation_level.upper()}"
|
||||
)
|
||||
|
||||
if assignments:
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("; ".join(assignments))
|
||||
|
||||
if "AUTOCOMMIT" in self.settings_dict:
|
||||
try:
|
||||
self.set_autocommit(self.settings_dict["AUTOCOMMIT"])
|
||||
except AttributeError:
|
||||
self._set_autocommit(self.settings_dict["AUTOCOMMIT"])
|
||||
|
||||
def create_cursor(self, name: Any = None) -> CursorWrapper:
|
||||
cursor = self.connection.cursor()
|
||||
return CursorWrapper(cursor)
|
||||
|
||||
def _rollback(self) -> None:
|
||||
try:
|
||||
BaseDatabaseWrapper._rollback(self) # type: ignore[attr-defined]
|
||||
except NotSupportedError:
|
||||
pass
|
||||
|
||||
def _set_autocommit(self, autocommit: bool) -> None:
|
||||
with self.wrap_database_errors:
|
||||
self.connection.autocommit = autocommit
|
||||
|
||||
def disable_constraint_checking(self) -> bool:
|
||||
"""
|
||||
Disable foreign key checks, primarily for use in adding rows with
|
||||
forward references. Always return True to indicate constraint checks
|
||||
need to be re-enabled.
|
||||
"""
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("SET foreign_key_checks=0")
|
||||
return True
|
||||
|
||||
def enable_constraint_checking(self) -> None:
|
||||
"""
|
||||
Re-enable foreign key checks after they have been disabled.
|
||||
"""
|
||||
# Override needs_rollback in case constraint_checks_disabled is
|
||||
# nested inside transaction.atomic.
|
||||
self.needs_rollback, needs_rollback = False, self.needs_rollback
|
||||
try:
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("SET foreign_key_checks=1")
|
||||
finally:
|
||||
self.needs_rollback = needs_rollback
|
||||
|
||||
def check_constraints(self, table_names: Optional[List[str]] = None) -> None:
|
||||
"""
|
||||
Check each table name in `table_names` for rows with invalid foreign
|
||||
key references. This method is intended to be used in conjunction with
|
||||
`disable_constraint_checking()` and `enable_constraint_checking()`, to
|
||||
determine if rows with invalid references were entered while constraint
|
||||
checks were off.
|
||||
"""
|
||||
with self.cursor() as cursor:
|
||||
if table_names is None:
|
||||
table_names = self.introspection.table_names(cursor)
|
||||
for table_name in table_names:
|
||||
primary_key_column_name = self.introspection.get_primary_key_column(
|
||||
cursor, table_name
|
||||
)
|
||||
if not primary_key_column_name:
|
||||
continue
|
||||
key_columns = self.introspection.get_key_columns( # type: ignore[attr-defined]
|
||||
cursor, table_name
|
||||
)
|
||||
for (
|
||||
column_name,
|
||||
referenced_table_name,
|
||||
referenced_column_name,
|
||||
) in key_columns:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT REFERRING.`{primary_key_column_name}`,
|
||||
REFERRING.`{column_name}`
|
||||
FROM `{table_name}` as REFERRING
|
||||
LEFT JOIN `{referenced_table_name}` as REFERRED
|
||||
ON (
|
||||
REFERRING.`{column_name}` =
|
||||
REFERRED.`{referenced_column_name}`
|
||||
)
|
||||
WHERE REFERRING.`{column_name}` IS NOT NULL
|
||||
AND REFERRED.`{referenced_column_name}` IS NULL
|
||||
"""
|
||||
)
|
||||
for bad_row in cursor.fetchall():
|
||||
raise IntegrityError(
|
||||
f"The row in table '{table_name}' with primary "
|
||||
f"key '{bad_row[0]}' has an invalid foreign key: "
|
||||
f"{table_name}.{column_name} contains a value "
|
||||
f"'{bad_row[1]}' that does not have a "
|
||||
f"corresponding value in "
|
||||
f"{referenced_table_name}."
|
||||
f"{referenced_column_name}."
|
||||
)
|
||||
|
||||
def is_usable(self) -> bool:
|
||||
try:
|
||||
self.connection.ping()
|
||||
except Error:
|
||||
return False
|
||||
return True
|
||||
|
||||
@cached_property # type: ignore[misc]
|
||||
@staticmethod
|
||||
def display_name() -> str:
|
||||
"""Display name."""
|
||||
return "MySQL"
|
||||
|
||||
@cached_property
|
||||
def data_type_check_constraints(self) -> Dict[str, str]:
|
||||
"""Mapping of Field objects to their SQL for CHECK constraints."""
|
||||
if self.features.supports_column_check_constraints:
|
||||
check_constraints = {
|
||||
"PositiveBigIntegerField": "`%(column)s` >= 0",
|
||||
"PositiveIntegerField": "`%(column)s` >= 0",
|
||||
"PositiveSmallIntegerField": "`%(column)s` >= 0",
|
||||
}
|
||||
return check_constraints
|
||||
return {}
|
||||
|
||||
@cached_property
|
||||
def mysql_server_data(self) -> Dict[str, Any]:
|
||||
"""Return MySQL server data."""
|
||||
with self.temporary_connection() as cursor:
|
||||
# Select some server variables and test if the time zone
|
||||
# definitions are installed. CONVERT_TZ returns NULL if 'UTC'
|
||||
# timezone isn't loaded into the mysql.time_zone table.
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT VERSION(),
|
||||
@@sql_mode,
|
||||
@@default_storage_engine,
|
||||
@@sql_auto_is_null,
|
||||
@@lower_case_table_names,
|
||||
CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
|
||||
"""
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return {
|
||||
"version": row[0],
|
||||
"sql_mode": row[1],
|
||||
"default_storage_engine": row[2],
|
||||
"sql_auto_is_null": bool(row[3]),
|
||||
"lower_case_table_names": bool(row[4]),
|
||||
"has_zoneinfo_database": bool(row[5]),
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def mysql_server_info(self) -> Any:
|
||||
"""Return MySQL version."""
|
||||
with self.temporary_connection() as cursor:
|
||||
cursor.execute("SELECT VERSION()")
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
@cached_property
|
||||
def mysql_version(self) -> Tuple[int, ...]:
|
||||
"""Return MySQL version."""
|
||||
config = self.get_connection_params()
|
||||
with mysql.connector.connect(**config) as conn:
|
||||
server_version: Tuple[int, ...] = conn.server_version
|
||||
return server_version
|
||||
|
||||
@cached_property
|
||||
def sql_mode(self) -> Set[str]:
|
||||
"""Return SQL mode."""
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute("SELECT @@sql_mode")
|
||||
sql_mode = cursor.fetchone()
|
||||
return set(sql_mode[0].split(",") if sql_mode else ())
|
||||
|
||||
@property
|
||||
def use_pure(self) -> bool:
|
||||
"""Return True if pure Python version is being used."""
|
||||
ans: bool = self._use_pure
|
||||
return ans
|
||||
|
||||
|
||||
class DjangoMySQLConverter(MySQLConverter):
|
||||
"""Custom converter for Django."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@staticmethod
|
||||
def _time_to_python(value: bytes, dsc: Any = None) -> Optional[time]:
|
||||
"""Return MySQL TIME data type as datetime.time()
|
||||
|
||||
Returns datetime.time()
|
||||
"""
|
||||
return dateparse.parse_time(value.decode("utf-8"))
|
||||
|
||||
@staticmethod
|
||||
def _datetime_to_python(value: bytes, dsc: Any = None) -> Optional[datetime]:
|
||||
"""Connector/Python always returns naive datetime.datetime
|
||||
|
||||
Connector/Python always returns naive timestamps since MySQL has
|
||||
no time zone support.
|
||||
|
||||
- A naive datetime is a datetime that doesn't know its own timezone.
|
||||
|
||||
Django needs a non-naive datetime, but in this method we don't need
|
||||
to make a datetime value time zone aware since Django itself at some
|
||||
point will make it aware (at least in versions 3.2.16 and 4.1.2) when
|
||||
USE_TZ=True. This may change in a future release, we need to keep an
|
||||
eye on this behaviour.
|
||||
|
||||
Returns datetime.datetime()
|
||||
"""
|
||||
return MySQLConverter._datetime_to_python(value) if value else None
|
||||
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def _safestring_to_mysql(self, value: str) -> Union[bytes, HexLiteral]:
|
||||
return self._str_to_mysql(value)
|
||||
|
||||
def _safetext_to_mysql(self, value: str) -> Union[bytes, HexLiteral]:
|
||||
return self._str_to_mysql(value)
|
||||
|
||||
def _safebytes_to_mysql(self, value: bytes) -> bytes:
|
||||
return self._bytes_to_mysql(value)
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""Database Client."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from django.db.backends.base.client import BaseDatabaseClient
|
||||
|
||||
|
||||
class DatabaseClient(BaseDatabaseClient):
|
||||
"""Encapsulate backend-specific methods for opening a client shell."""
|
||||
|
||||
executable_name = "mysql"
|
||||
|
||||
@classmethod
|
||||
def settings_to_cmd_args_env(
|
||||
cls, settings_dict: Dict[str, Any], parameters: Optional[Iterable[str]] = None
|
||||
) -> Tuple[List[str], Optional[Dict[str, Any]]]:
|
||||
args = [cls.executable_name]
|
||||
|
||||
db = settings_dict["OPTIONS"].get("database", settings_dict["NAME"])
|
||||
user = settings_dict["OPTIONS"].get("user", settings_dict["USER"])
|
||||
passwd = settings_dict["OPTIONS"].get("password", settings_dict["PASSWORD"])
|
||||
host = settings_dict["OPTIONS"].get("host", settings_dict["HOST"])
|
||||
port = settings_dict["OPTIONS"].get("port", settings_dict["PORT"])
|
||||
ssl_ca = settings_dict["OPTIONS"].get("ssl_ca")
|
||||
ssl_cert = settings_dict["OPTIONS"].get("ssl_cert")
|
||||
ssl_key = settings_dict["OPTIONS"].get("ssl_key")
|
||||
defaults_file = settings_dict["OPTIONS"].get("read_default_file")
|
||||
charset = settings_dict["OPTIONS"].get("charset")
|
||||
|
||||
# --defaults-file should always be the first option
|
||||
if defaults_file:
|
||||
args.append(f"--defaults-file={defaults_file}")
|
||||
|
||||
# Load any custom init_commands. We always force SQL_MODE to TRADITIONAL
|
||||
init_command = settings_dict["OPTIONS"].get("init_command", "")
|
||||
args.append(f"--init-command=SET @@session.SQL_MODE=TRADITIONAL;{init_command}")
|
||||
|
||||
if user:
|
||||
args.append(f"--user={user}")
|
||||
if passwd:
|
||||
args.append(f"--password={passwd}")
|
||||
|
||||
if host:
|
||||
if "/" in host:
|
||||
args.append(f"--socket={host}")
|
||||
else:
|
||||
args.append(f"--host={host}")
|
||||
|
||||
if port:
|
||||
args.append(f"--port={port}")
|
||||
|
||||
if db:
|
||||
args.append(f"--database={db}")
|
||||
|
||||
if ssl_ca:
|
||||
args.append(f"--ssl-ca={ssl_ca}")
|
||||
if ssl_cert:
|
||||
args.append(f"--ssl-cert={ssl_cert}")
|
||||
if ssl_key:
|
||||
args.append(f"--ssl-key={ssl_key}")
|
||||
|
||||
if charset:
|
||||
args.append(f"--default-character-set={charset}")
|
||||
|
||||
if parameters:
|
||||
args.extend(parameters)
|
||||
|
||||
return args, None
|
||||
|
||||
def runshell(self, parameters: Optional[Iterable[str]] = None) -> None:
|
||||
args, env = self.settings_to_cmd_args_env(
|
||||
self.connection.settings_dict, parameters
|
||||
)
|
||||
env = {**os.environ, **env} if env else None
|
||||
subprocess.run(args, env=env, check=True)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""SQL Compiler classes."""
|
||||
|
||||
from django.db.backends.mysql.compiler import (
|
||||
SQLAggregateCompiler,
|
||||
SQLCompiler,
|
||||
SQLDeleteCompiler,
|
||||
SQLInsertCompiler,
|
||||
SQLUpdateCompiler,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SQLAggregateCompiler",
|
||||
"SQLCompiler",
|
||||
"SQLDeleteCompiler",
|
||||
"SQLInsertCompiler",
|
||||
"SQLUpdateCompiler",
|
||||
]
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""Backend specific database creation."""
|
||||
|
||||
from django.db.backends.mysql.creation import DatabaseCreation
|
||||
|
||||
__all__ = ["DatabaseCreation"]
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""Database Features."""
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(MySQLDatabaseFeatures):
|
||||
"""Database Features Specification class."""
|
||||
|
||||
empty_fetchmany_value: List[Any] = []
|
||||
|
||||
@cached_property
|
||||
def can_introspect_check_constraints(self) -> bool: # type: ignore[override]
|
||||
"""Check if backend support introspection CHECK of constraints."""
|
||||
return self.connection.mysql_version >= (8, 0, 16)
|
||||
|
||||
@cached_property
|
||||
def supports_microsecond_precision(self) -> bool:
|
||||
"""Check if backend support microsecond precision."""
|
||||
return self.connection.mysql_version >= (5, 6, 3)
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
# mypy: disable-error-code="override,attr-defined,call-arg"
|
||||
|
||||
"""Database Introspection."""
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
import sqlparse
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.db.backends.base.introspection import (
|
||||
BaseDatabaseIntrospection,
|
||||
FieldInfo as BaseFieldInfo,
|
||||
TableInfo,
|
||||
)
|
||||
from django.db.models import Index
|
||||
from django.utils.datastructures import OrderedSet
|
||||
|
||||
from mysql.connector.constants import FieldType
|
||||
|
||||
# from .base import CursorWrapper produces a circular import error,
|
||||
# avoiding importing CursorWrapper explicitly, using a documented
|
||||
# trick; write the imports inside if TYPE_CHECKING: so that they
|
||||
# are not executed at runtime.
|
||||
# Ref: https://buildmedia.readthedocs.org/media/pdf/mypy/stable/mypy.pdf [page 42]
|
||||
if TYPE_CHECKING:
|
||||
# CursorWraper is used exclusively for type hinting
|
||||
from mysql.connector.django.base import CursorWrapper
|
||||
|
||||
# Based on my investigation, named tuples to
|
||||
# comply with mypy need to define a static list or tuple
|
||||
# for field_names (second argument). In this case, the field
|
||||
# names are created dynamically for FieldInfo which triggers
|
||||
# a mypy error. The solution is not straightforward since
|
||||
# FieldInfo attributes are Django version dependent. Code
|
||||
# refactory is needed to fix this issue.
|
||||
FieldInfo = namedtuple( # type: ignore[misc]
|
||||
"FieldInfo",
|
||||
BaseFieldInfo._fields + ("extra", "is_unsigned", "has_json_constraint"),
|
||||
)
|
||||
if DJANGO_VERSION < (3, 2, 0):
|
||||
InfoLine = namedtuple(
|
||||
"InfoLine",
|
||||
"col_name data_type max_len num_prec num_scale extra column_default "
|
||||
"is_unsigned",
|
||||
)
|
||||
else:
|
||||
InfoLine = namedtuple( # type: ignore[no-redef]
|
||||
"InfoLine",
|
||||
"col_name data_type max_len num_prec num_scale extra column_default "
|
||||
"collation is_unsigned",
|
||||
)
|
||||
|
||||
|
||||
class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||
"""Encapsulate backend-specific introspection utilities."""
|
||||
|
||||
data_types_reverse = {
|
||||
FieldType.BLOB: "TextField",
|
||||
FieldType.DECIMAL: "DecimalField",
|
||||
FieldType.NEWDECIMAL: "DecimalField",
|
||||
FieldType.DATE: "DateField",
|
||||
FieldType.DATETIME: "DateTimeField",
|
||||
FieldType.DOUBLE: "FloatField",
|
||||
FieldType.FLOAT: "FloatField",
|
||||
FieldType.INT24: "IntegerField",
|
||||
FieldType.LONG: "IntegerField",
|
||||
FieldType.LONGLONG: "BigIntegerField",
|
||||
FieldType.SHORT: "SmallIntegerField",
|
||||
FieldType.STRING: "CharField",
|
||||
FieldType.TIME: "TimeField",
|
||||
FieldType.TIMESTAMP: "DateTimeField",
|
||||
FieldType.TINY: "IntegerField",
|
||||
FieldType.TINY_BLOB: "TextField",
|
||||
FieldType.MEDIUM_BLOB: "TextField",
|
||||
FieldType.LONG_BLOB: "TextField",
|
||||
FieldType.VAR_STRING: "CharField",
|
||||
}
|
||||
|
||||
def get_field_type(self, data_type: str, description: FieldInfo) -> str:
|
||||
field_type = super().get_field_type(data_type, description) # type: ignore[arg-type]
|
||||
if "auto_increment" in description.extra:
|
||||
if field_type == "IntegerField":
|
||||
return "AutoField"
|
||||
if field_type == "BigIntegerField":
|
||||
return "BigAutoField"
|
||||
if field_type == "SmallIntegerField":
|
||||
return "SmallAutoField"
|
||||
if description.is_unsigned:
|
||||
if field_type == "BigIntegerField":
|
||||
return "PositiveBigIntegerField"
|
||||
if field_type == "IntegerField":
|
||||
return "PositiveIntegerField"
|
||||
if field_type == "SmallIntegerField":
|
||||
return "PositiveSmallIntegerField"
|
||||
# JSON data type is an alias for LONGTEXT in MariaDB, use check
|
||||
# constraints clauses to introspect JSONField.
|
||||
if description.has_json_constraint:
|
||||
return "JSONField"
|
||||
return field_type
|
||||
|
||||
def get_table_list(self, cursor: "CursorWrapper") -> List[TableInfo]:
|
||||
"""Return a list of table and view names in the current database."""
|
||||
cursor.execute("SHOW FULL TABLES")
|
||||
return [
|
||||
TableInfo(row[0], {"BASE TABLE": "t", "VIEW": "v"}.get(row[1]))
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
def get_table_description(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> List[FieldInfo]:
|
||||
"""
|
||||
Return a description of the table with the DB-API cursor.description
|
||||
interface."
|
||||
"""
|
||||
json_constraints: Dict[Any, Any] = {}
|
||||
# A default collation for the given table.
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT table_collation
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = %s
|
||||
""",
|
||||
[table_name],
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
default_column_collation = row[0] if row else ""
|
||||
# information_schema database gives more accurate results for some figures:
|
||||
# - varchar length returned by cursor.description is an internal length,
|
||||
# not visible length (#5725)
|
||||
# - precision and scale (for decimal fields) (#5014)
|
||||
# - auto_increment is not available in cursor.description
|
||||
if DJANGO_VERSION < (3, 2, 0):
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
column_name, data_type, character_maximum_length,
|
||||
numeric_precision, numeric_scale, extra, column_default,
|
||||
CASE
|
||||
WHEN column_type LIKE '%% unsigned' THEN 1
|
||||
ELSE 0
|
||||
END AS is_unsigned
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = %s AND table_schema = DATABASE()
|
||||
""",
|
||||
[table_name],
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
column_name, data_type, character_maximum_length,
|
||||
numeric_precision, numeric_scale, extra, column_default,
|
||||
CASE
|
||||
WHEN collation_name = %s THEN NULL
|
||||
ELSE collation_name
|
||||
END AS collation_name,
|
||||
CASE
|
||||
WHEN column_type LIKE '%% unsigned' THEN 1
|
||||
ELSE 0
|
||||
END AS is_unsigned
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = %s AND table_schema = DATABASE()
|
||||
""",
|
||||
[default_column_collation, table_name],
|
||||
)
|
||||
field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()}
|
||||
|
||||
cursor.execute(
|
||||
f"SELECT * FROM {self.connection.ops.quote_name(table_name)} LIMIT 1"
|
||||
)
|
||||
|
||||
def to_int(i: Any) -> Optional[int]:
|
||||
return int(i) if i is not None else i
|
||||
|
||||
fields = []
|
||||
for line in cursor.description:
|
||||
info = field_info[line[0]]
|
||||
if DJANGO_VERSION < (3, 2, 0):
|
||||
fields.append(
|
||||
FieldInfo(
|
||||
*line[:3],
|
||||
to_int(info.max_len) or line[3],
|
||||
to_int(info.num_prec) or line[4],
|
||||
to_int(info.num_scale) or line[5],
|
||||
line[6],
|
||||
info.column_default,
|
||||
info.extra,
|
||||
info.is_unsigned,
|
||||
line[0] in json_constraints,
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields.append(
|
||||
FieldInfo(
|
||||
*line[:3],
|
||||
to_int(info.max_len) or line[3],
|
||||
to_int(info.num_prec) or line[4],
|
||||
to_int(info.num_scale) or line[5],
|
||||
line[6],
|
||||
info.column_default,
|
||||
info.collation,
|
||||
info.extra,
|
||||
info.is_unsigned,
|
||||
line[0] in json_constraints,
|
||||
)
|
||||
)
|
||||
return fields
|
||||
|
||||
def get_indexes(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> Dict[int, Dict[str, bool]]:
|
||||
"""Return indexes from table."""
|
||||
cursor.execute(f"SHOW INDEX FROM {self.connection.ops.quote_name(table_name)}")
|
||||
# Do a two-pass search for indexes: on first pass check which indexes
|
||||
# are multicolumn, on second pass check which single-column indexes
|
||||
# are present.
|
||||
rows = list(cursor.fetchall())
|
||||
multicol_indexes = set()
|
||||
for row in rows:
|
||||
if row[3] > 1:
|
||||
multicol_indexes.add(row[2])
|
||||
indexes: Dict[int, Dict[str, bool]] = {}
|
||||
for row in rows:
|
||||
if row[2] in multicol_indexes:
|
||||
continue
|
||||
if row[4] not in indexes:
|
||||
indexes[row[4]] = {"primary_key": False, "unique": False}
|
||||
# It's possible to have the unique and PK constraints in
|
||||
# separate indexes.
|
||||
if row[2] == "PRIMARY":
|
||||
indexes[row[4]]["primary_key"] = True
|
||||
if not row[1]:
|
||||
indexes[row[4]]["unique"] = True
|
||||
return indexes
|
||||
|
||||
def get_primary_key_column(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Returns the name of the primary key column for the given table
|
||||
"""
|
||||
for column in self.get_indexes(cursor, table_name).items():
|
||||
if column[1]["primary_key"]:
|
||||
return column[0]
|
||||
return None
|
||||
|
||||
def get_sequences(
|
||||
self, cursor: "CursorWrapper", table_name: str, table_fields: Any = ()
|
||||
) -> List[Dict[str, str]]:
|
||||
for field_info in self.get_table_description(cursor, table_name):
|
||||
if "auto_increment" in field_info.extra:
|
||||
# MySQL allows only one auto-increment column per table.
|
||||
return [{"table": table_name, "column": field_info.name}]
|
||||
return []
|
||||
|
||||
def get_relations(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> Dict[str, Tuple[str, str]]:
|
||||
"""
|
||||
Return a dictionary of {field_name: (field_name_other_table, other_table)}
|
||||
representing all relationships to the given table.
|
||||
"""
|
||||
constraints = self.get_key_columns(cursor, table_name)
|
||||
relations = {}
|
||||
for my_fieldname, other_table, other_field in constraints:
|
||||
relations[my_fieldname] = (other_field, other_table)
|
||||
return relations
|
||||
|
||||
def get_key_columns(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Return a list of (column_name, referenced_table_name, referenced_column_name)
|
||||
for all key columns in the given table.
|
||||
"""
|
||||
key_columns: List[Any] = []
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name, referenced_table_name, referenced_column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = %s
|
||||
AND table_schema = DATABASE()
|
||||
AND referenced_table_name IS NOT NULL
|
||||
AND referenced_column_name IS NOT NULL""",
|
||||
[table_name],
|
||||
)
|
||||
key_columns.extend(cursor.fetchall())
|
||||
return key_columns
|
||||
|
||||
def get_storage_engine(self, cursor: "CursorWrapper", table_name: str) -> str:
|
||||
"""
|
||||
Retrieve the storage engine for a given table. Return the default
|
||||
storage engine if the table doesn't exist.
|
||||
"""
|
||||
cursor.execute(
|
||||
"SELECT engine FROM information_schema.tables WHERE table_name = %s",
|
||||
[table_name],
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
# pylint: disable=protected-access
|
||||
if not result:
|
||||
return self.connection.features._mysql_storage_engine
|
||||
# pylint: enable=protected-access
|
||||
return result[0]
|
||||
|
||||
def _parse_constraint_columns(
|
||||
self, check_clause: Any, columns: Set[str]
|
||||
) -> OrderedSet:
|
||||
check_columns: OrderedSet = OrderedSet()
|
||||
statement = sqlparse.parse(check_clause)[0]
|
||||
tokens = (token for token in statement.flatten() if not token.is_whitespace)
|
||||
for token in tokens:
|
||||
if (
|
||||
token.ttype == sqlparse.tokens.Name
|
||||
and self.connection.ops.quote_name(token.value) == token.value
|
||||
and token.value[1:-1] in columns
|
||||
):
|
||||
check_columns.add(token.value[1:-1])
|
||||
return check_columns
|
||||
|
||||
def get_constraints(
|
||||
self, cursor: "CursorWrapper", table_name: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve any constraints or keys (unique, pk, fk, check, index) across
|
||||
one or more columns.
|
||||
"""
|
||||
constraints: Dict[str, Any] = {}
|
||||
# Get the actual constraint names and columns
|
||||
name_query = """
|
||||
SELECT kc.`constraint_name`, kc.`column_name`,
|
||||
kc.`referenced_table_name`, kc.`referenced_column_name`
|
||||
FROM information_schema.key_column_usage AS kc
|
||||
WHERE
|
||||
kc.table_schema = DATABASE() AND
|
||||
kc.table_name = %s
|
||||
ORDER BY kc.`ordinal_position`
|
||||
"""
|
||||
cursor.execute(name_query, [table_name])
|
||||
for constraint, column, ref_table, ref_column in cursor.fetchall():
|
||||
if constraint not in constraints:
|
||||
constraints[constraint] = {
|
||||
"columns": OrderedSet(),
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"check": False,
|
||||
"foreign_key": (ref_table, ref_column) if ref_column else None,
|
||||
}
|
||||
if self.connection.features.supports_index_column_ordering:
|
||||
constraints[constraint]["orders"] = []
|
||||
constraints[constraint]["columns"].add(column)
|
||||
# Now get the constraint types
|
||||
type_query = """
|
||||
SELECT c.constraint_name, c.constraint_type
|
||||
FROM information_schema.table_constraints AS c
|
||||
WHERE
|
||||
c.table_schema = DATABASE() AND
|
||||
c.table_name = %s
|
||||
"""
|
||||
cursor.execute(type_query, [table_name])
|
||||
for constraint, kind in cursor.fetchall():
|
||||
if kind.lower() == "primary key":
|
||||
constraints[constraint]["primary_key"] = True
|
||||
constraints[constraint]["unique"] = True
|
||||
elif kind.lower() == "unique":
|
||||
constraints[constraint]["unique"] = True
|
||||
# Add check constraints.
|
||||
if self.connection.features.can_introspect_check_constraints:
|
||||
unnamed_constraints_index = 0
|
||||
columns = {
|
||||
info.name for info in self.get_table_description(cursor, table_name)
|
||||
}
|
||||
type_query = """
|
||||
SELECT cc.constraint_name, cc.check_clause
|
||||
FROM
|
||||
information_schema.check_constraints AS cc,
|
||||
information_schema.table_constraints AS tc
|
||||
WHERE
|
||||
cc.constraint_schema = DATABASE() AND
|
||||
tc.table_schema = cc.constraint_schema AND
|
||||
cc.constraint_name = tc.constraint_name AND
|
||||
tc.constraint_type = 'CHECK' AND
|
||||
tc.table_name = %s
|
||||
"""
|
||||
cursor.execute(type_query, [table_name])
|
||||
for constraint, check_clause in cursor.fetchall():
|
||||
constraint_columns = self._parse_constraint_columns(
|
||||
check_clause, columns
|
||||
)
|
||||
# Ensure uniqueness of unnamed constraints. Unnamed unique
|
||||
# and check columns constraints have the same name as
|
||||
# a column.
|
||||
if set(constraint_columns) == {constraint}:
|
||||
unnamed_constraints_index += 1
|
||||
constraint = f"__unnamed_constraint_{unnamed_constraints_index}__"
|
||||
constraints[constraint] = {
|
||||
"columns": constraint_columns,
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"index": False,
|
||||
"check": True,
|
||||
"foreign_key": None,
|
||||
}
|
||||
# Now add in the indexes
|
||||
cursor.execute(f"SHOW INDEX FROM {self.connection.ops.quote_name(table_name)}")
|
||||
for _, _, index, _, column, order, type_ in [
|
||||
x[:6] + (x[10],) for x in cursor.fetchall()
|
||||
]:
|
||||
if index not in constraints:
|
||||
constraints[index] = {
|
||||
"columns": OrderedSet(),
|
||||
"primary_key": False,
|
||||
"unique": False,
|
||||
"check": False,
|
||||
"foreign_key": None,
|
||||
}
|
||||
if self.connection.features.supports_index_column_ordering:
|
||||
constraints[index]["orders"] = []
|
||||
constraints[index]["index"] = True
|
||||
constraints[index]["type"] = (
|
||||
Index.suffix if type_ == "BTREE" else type_.lower()
|
||||
)
|
||||
constraints[index]["columns"].add(column)
|
||||
if self.connection.features.supports_index_column_ordering:
|
||||
constraints[index]["orders"].append("DESC" if order == "D" else "ASC")
|
||||
# Convert the sorted sets to lists
|
||||
for constraint in constraints.values():
|
||||
constraint["columns"] = list(constraint["columns"])
|
||||
return constraints
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
# mypy: disable-error-code="override,attr-defined"
|
||||
|
||||
"""Database Operations."""
|
||||
|
||||
from datetime import datetime, time, timezone
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.backends.mysql.operations import (
|
||||
DatabaseOperations as MySQLDatabaseOperations,
|
||||
)
|
||||
from django.utils import timezone as django_timezone
|
||||
|
||||
try:
|
||||
from _mysql_connector import datetime_to_mysql, time_to_mysql
|
||||
except ImportError:
|
||||
HAVE_CEXT = False
|
||||
else:
|
||||
HAVE_CEXT = True
|
||||
|
||||
|
||||
class DatabaseOperations(MySQLDatabaseOperations):
|
||||
"""Database Operations class."""
|
||||
|
||||
compiler_module = "mysql.connector.django.compiler"
|
||||
|
||||
def regex_lookup(self, lookup_type: str) -> str:
|
||||
"""Return the string to use in a query when performing regular
|
||||
expression lookup."""
|
||||
if self.connection.mysql_version < (8, 0, 0):
|
||||
if lookup_type == "regex":
|
||||
return "%s REGEXP BINARY %s"
|
||||
return "%s REGEXP %s"
|
||||
|
||||
match_option = "c" if lookup_type == "regex" else "i"
|
||||
return f"REGEXP_LIKE(%s, %s, '{match_option}')"
|
||||
|
||||
def adapt_datetimefield_value(self, value: Optional[datetime]) -> Optional[bytes]:
|
||||
"""Transform a datetime value to an object compatible with what is
|
||||
expected by the backend driver for datetime columns."""
|
||||
return self.value_to_db_datetime(value)
|
||||
|
||||
def value_to_db_datetime(self, value: Optional[datetime]) -> Optional[bytes]:
|
||||
"""Convert value to MySQL DATETIME."""
|
||||
ans: Optional[bytes] = None
|
||||
if value is None:
|
||||
return ans
|
||||
# MySQL doesn't support tz-aware times
|
||||
if django_timezone.is_aware(value):
|
||||
if settings.USE_TZ:
|
||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
raise ValueError("MySQL backend does not support timezone-aware times")
|
||||
if not self.connection.features.supports_microsecond_precision:
|
||||
value = value.replace(microsecond=0)
|
||||
if not self.connection.use_pure:
|
||||
return datetime_to_mysql(value)
|
||||
return self.connection.converter.to_mysql(value)
|
||||
|
||||
def adapt_timefield_value(self, value: Optional[time]) -> Optional[bytes]:
|
||||
"""Transform a time value to an object compatible with what is expected
|
||||
by the backend driver for time columns."""
|
||||
return self.value_to_db_time(value)
|
||||
|
||||
def value_to_db_time(self, value: Optional[time]) -> Optional[bytes]:
|
||||
"""Convert value to MySQL TIME."""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# MySQL doesn't support tz-aware times
|
||||
if django_timezone.is_aware(value):
|
||||
raise ValueError("MySQL backend does not support timezone-aware times")
|
||||
|
||||
if not self.connection.use_pure:
|
||||
return time_to_mysql(value)
|
||||
return self.connection.converter.to_mysql(value)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
# mypy: disable-error-code="override"
|
||||
|
||||
"""Database schema editor."""
|
||||
from typing import Any
|
||||
|
||||
from django.db.backends.mysql.schema import (
|
||||
DatabaseSchemaEditor as MySQLDatabaseSchemaEditor,
|
||||
)
|
||||
|
||||
|
||||
class DatabaseSchemaEditor(MySQLDatabaseSchemaEditor):
|
||||
"""This class is responsible for emitting schema-changing statements to the
|
||||
databases.
|
||||
"""
|
||||
|
||||
def quote_value(self, value: Any) -> Any:
|
||||
"""Quote value."""
|
||||
self.connection.ensure_connection()
|
||||
if isinstance(value, str):
|
||||
value = value.replace("%", "%%")
|
||||
quoted = self.connection.connection.converter.escape(value)
|
||||
if isinstance(value, str) and isinstance(quoted, bytes):
|
||||
quoted = quoted.decode()
|
||||
return quoted
|
||||
|
||||
def prepare_default(self, value: Any) -> Any:
|
||||
"""Implement the required abstract method.
|
||||
|
||||
MySQL has requires_literal_defaults=False, therefore return the value.
|
||||
"""
|
||||
return value
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License, version 2.0, as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is designed to work with certain software (including
|
||||
# but not limited to OpenSSL) that is licensed under separate terms,
|
||||
# as designated in a particular file or component or in included license
|
||||
# documentation. The authors of MySQL hereby grant you an
|
||||
# additional permission to link the program and your derivative works
|
||||
# with the separately licensed software that they have either included with
|
||||
# the program or referenced in the documentation.
|
||||
#
|
||||
# Without limiting anything contained in the foregoing, this file,
|
||||
# which is part of MySQL Connector/Python, is also subject to the
|
||||
# Universal FOSS Exception, version 1.0, a copy of which can be found at
|
||||
# http://oss.oracle.com/licenses/universal-foss-exception.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License, version 2.0, for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""Backend specific database validation."""
|
||||
|
||||
from django.db.backends.mysql.validation import DatabaseValidation
|
||||
|
||||
__all__ = ["DatabaseValidation"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue