SolarManager unter Versionsverwaltung

Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 20:46:59 +02:00
co-authored by Claude Opus 5
commit 79843aa2ae
968 changed files with 261182 additions and 0 deletions
View File
+636
View File
@@ -0,0 +1,636 @@
# Copyright (c) 2020, 2023, 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 also distributed 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 included with
# MySQL.
#
# 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 Any, Dict, Generator, Iterator, List, Optional, 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
try:
import mysql.connector
from mysql.connector.connection import MySQLConnection
from mysql.connector.connection_cext import CMySQLConnection
from mysql.connector.conversion import MySQLConverter
from mysql.connector.cursor import MySQLCursor
from mysql.connector.cursor_cext import CMySQLCursor
from mysql.connector.custom_types import HexLiteral
from mysql.connector.pooling import PooledMySQLConnection
from mysql.connector.types import (
ParamsDictType,
ParamsSequenceOrDictType,
ParamsSequenceType,
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
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)
value = value.astimezone(timezone.utc).replace(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: Union[MySQLCursor, CMySQLCursor]) -> None:
self.cursor: Union[MySQLCursor, CMySQLCursor] = cursor
@staticmethod
def _adapt_execute_args_dict(args: ParamsDictType) -> ParamsDictType:
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[ParamsSequenceType],
) -> Optional[ParamsSequenceType]:
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[ParamsSequenceOrDictType] = None
) -> Optional[Generator[Union[MySQLCursor, CMySQLCursor], 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: Union[
Tuple[ParamsSequenceOrDictType, ...],
List[ParamsSequenceOrDictType],
],
) -> Optional[Generator[Union[MySQLCursor, CMySQLCursor], 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")
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, MySQLConnection, CMySQLConnection]:
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(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
@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.get_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)
+106
View File
@@ -0,0 +1,106 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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)
+45
View File
@@ -0,0 +1,45 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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",
]
+33
View File
@@ -0,0 +1,33 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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"]
+50
View File
@@ -0,0 +1,50 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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)
+461
View File
@@ -0,0 +1,461 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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
+104
View File
@@ -0,0 +1,104 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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
from typing import Optional
from django.conf import settings
from django.db.backends.mysql.operations import (
DatabaseOperations as MySQLDatabaseOperations,
)
from django.utils import 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 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 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)
+59
View File
@@ -0,0 +1,59 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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
+33
View File
@@ -0,0 +1,33 @@
# Copyright (c) 2020, 2022, 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 also distributed 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 included with
# MySQL.
#
# 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"]