add new project
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
MySQL database backend for Django.
|
||||
|
||||
Requires mysqlclient: https://pypi.org/project/mysqlclient/
|
||||
"""
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import utils
|
||||
from django.db.backends import utils as backend_utils
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
try:
|
||||
import MySQLdb as Database
|
||||
except ImportError as err:
|
||||
raise ImproperlyConfigured(
|
||||
'Error loading MySQLdb module.\n'
|
||||
'Did you install mysqlclient?'
|
||||
) from err
|
||||
|
||||
from MySQLdb.constants import CLIENT, FIELD_TYPE # isort:skip
|
||||
from MySQLdb.converters import conversions # isort:skip
|
||||
|
||||
# Some of these import MySQLdb, so import them after checking if it's installed.
|
||||
from .client import DatabaseClient # isort:skip
|
||||
from .creation import DatabaseCreation # isort:skip
|
||||
from .features import DatabaseFeatures # isort:skip
|
||||
from .introspection import DatabaseIntrospection # isort:skip
|
||||
from .operations import DatabaseOperations # isort:skip
|
||||
from .schema import DatabaseSchemaEditor # isort:skip
|
||||
from .validation import DatabaseValidation # isort:skip
|
||||
|
||||
version = Database.version_info
|
||||
if version < (1, 3, 13):
|
||||
raise ImproperlyConfigured('mysqlclient 1.3.13 or newer is required; you have %s.' % Database.__version__)
|
||||
|
||||
|
||||
# MySQLdb returns TIME columns as timedelta -- they are more like timedelta in
|
||||
# terms of actual behavior as they are signed and include days -- and Django
|
||||
# expects time.
|
||||
django_conversions = {
|
||||
**conversions,
|
||||
**{FIELD_TYPE.TIME: backend_utils.typecast_time},
|
||||
}
|
||||
|
||||
# This should match the numerical portion of the version numbers (we can treat
|
||||
# versions like 5.0.24 and 5.0.24a as the same).
|
||||
server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})')
|
||||
|
||||
|
||||
class CursorWrapper:
|
||||
"""
|
||||
A thin wrapper around MySQLdb's normal cursor class that catches particular
|
||||
exception instances and reraises them with the correct types.
|
||||
|
||||
Implemented as a wrapper, rather than a subclass, so that it isn't stuck
|
||||
to the particular underlying representation returned by Connection.cursor().
|
||||
"""
|
||||
codes_for_integrityerror = (
|
||||
1048, # Column cannot be null
|
||||
1690, # BIGINT UNSIGNED value is out of range
|
||||
)
|
||||
|
||||
def __init__(self, cursor):
|
||||
self.cursor = cursor
|
||||
|
||||
def execute(self, query, args=None):
|
||||
try:
|
||||
# args is None means no string interpolation
|
||||
return self.cursor.execute(query, args)
|
||||
except Database.OperationalError as e:
|
||||
# Map some error codes to IntegrityError, since they seem to be
|
||||
# misclassified and Django would prefer the more logical place.
|
||||
if e.args[0] in self.codes_for_integrityerror:
|
||||
raise utils.IntegrityError(*tuple(e.args))
|
||||
raise
|
||||
|
||||
def executemany(self, query, args):
|
||||
try:
|
||||
return self.cursor.executemany(query, args)
|
||||
except Database.OperationalError as e:
|
||||
# Map some error codes to IntegrityError, since they seem to be
|
||||
# misclassified and Django would prefer the more logical place.
|
||||
if e.args[0] in self.codes_for_integrityerror:
|
||||
raise utils.IntegrityError(*tuple(e.args))
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.cursor, attr)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.cursor)
|
||||
|
||||
|
||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
vendor = 'mysql'
|
||||
display_name = '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)',
|
||||
'NullBooleanField': 'bool',
|
||||
'OneToOneField': 'integer',
|
||||
'PositiveIntegerField': 'integer UNSIGNED',
|
||||
'PositiveSmallIntegerField': 'smallint UNSIGNED',
|
||||
'SlugField': 'varchar(%(max_length)s)',
|
||||
'SmallIntegerField': 'smallint',
|
||||
'TextField': 'longtext',
|
||||
'TimeField': 'time(6)',
|
||||
'UUIDField': 'char(32)',
|
||||
}
|
||||
|
||||
# For these columns, MySQL doesn't:
|
||||
# - accept default values and implicitly treats these columns as nullable
|
||||
# - support a database index
|
||||
_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',
|
||||
'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_levels = {
|
||||
'read uncommitted',
|
||||
'read committed',
|
||||
'repeatable read',
|
||||
'serializable',
|
||||
}
|
||||
|
||||
Database = Database
|
||||
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 get_connection_params(self):
|
||||
kwargs = {
|
||||
'conv': django_conversions,
|
||||
'charset': 'utf8',
|
||||
}
|
||||
settings_dict = self.settings_dict
|
||||
if settings_dict['USER']:
|
||||
kwargs['user'] = settings_dict['USER']
|
||||
if settings_dict['NAME']:
|
||||
kwargs['db'] = 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'])
|
||||
# We need the number of potentially affected rows after an
|
||||
# "UPDATE", not the number of changed rows.
|
||||
kwargs['client_flag'] = CLIENT.FOUND_ROWS
|
||||
# Validate the transaction isolation level, if specified.
|
||||
options = settings_dict['OPTIONS'].copy()
|
||||
isolation_level = options.pop('isolation_level', 'read committed')
|
||||
if isolation_level:
|
||||
isolation_level = isolation_level.lower()
|
||||
if isolation_level not in self.isolation_levels:
|
||||
raise ImproperlyConfigured(
|
||||
"Invalid transaction isolation level '%s' specified.\n"
|
||||
"Use one of %s, or None." % (
|
||||
isolation_level,
|
||||
', '.join("'%s'" % s for s in sorted(self.isolation_levels))
|
||||
))
|
||||
self.isolation_level = isolation_level
|
||||
kwargs.update(options)
|
||||
return kwargs
|
||||
|
||||
def get_new_connection(self, conn_params):
|
||||
return Database.connect(**conn_params)
|
||||
|
||||
def init_connection_state(self):
|
||||
assignments = []
|
||||
if self.features.is_sql_auto_is_null_enabled:
|
||||
# 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 %s' % self.isolation_level.upper())
|
||||
|
||||
if assignments:
|
||||
with self.cursor() as cursor:
|
||||
cursor.execute('; '.join(assignments))
|
||||
|
||||
def create_cursor(self, name=None):
|
||||
cursor = self.connection.cursor()
|
||||
return CursorWrapper(cursor)
|
||||
|
||||
def _rollback(self):
|
||||
try:
|
||||
BaseDatabaseWrapper._rollback(self)
|
||||
except Database.NotSupportedError:
|
||||
pass
|
||||
|
||||
def _set_autocommit(self, autocommit):
|
||||
with self.wrap_database_errors:
|
||||
self.connection.autocommit(autocommit)
|
||||
|
||||
def disable_constraint_checking(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
self.cursor().execute('SET foreign_key_checks=0')
|
||||
return True
|
||||
|
||||
def enable_constraint_checking(self):
|
||||
"""
|
||||
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:
|
||||
self.cursor().execute('SET foreign_key_checks=1')
|
||||
finally:
|
||||
self.needs_rollback = needs_rollback
|
||||
|
||||
def check_constraints(self, table_names=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(
|
||||
"""
|
||||
SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
|
||||
LEFT JOIN `%s` as REFERRED
|
||||
ON (REFERRING.`%s` = REFERRED.`%s`)
|
||||
WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL
|
||||
""" % (
|
||||
primary_key_column_name, column_name, table_name,
|
||||
referenced_table_name, column_name, referenced_column_name,
|
||||
column_name, referenced_column_name,
|
||||
)
|
||||
)
|
||||
for bad_row in cursor.fetchall():
|
||||
raise utils.IntegrityError(
|
||||
"The row in table '%s' with primary key '%s' has an invalid "
|
||||
"foreign key: %s.%s contains a value '%s' that does not "
|
||||
"have a corresponding value in %s.%s."
|
||||
% (
|
||||
table_name, bad_row[0], table_name, column_name,
|
||||
bad_row[1], referenced_table_name, referenced_column_name,
|
||||
)
|
||||
)
|
||||
|
||||
def is_usable(self):
|
||||
try:
|
||||
self.connection.ping()
|
||||
except Database.Error:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@cached_property
|
||||
def mysql_server_info(self):
|
||||
with self.temporary_connection() as cursor:
|
||||
cursor.execute('SELECT VERSION()')
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
@cached_property
|
||||
def mysql_version(self):
|
||||
match = server_version_re.match(self.mysql_server_info)
|
||||
if not match:
|
||||
raise Exception('Unable to determine MySQL version from version string %r' % self.mysql_server_info)
|
||||
return tuple(int(x) for x in match.groups())
|
||||
|
||||
@cached_property
|
||||
def mysql_is_mariadb(self):
|
||||
# MariaDB isn't officially supported.
|
||||
return 'mariadb' in self.mysql_server_info.lower()
|
||||
@@ -0,0 +1,48 @@
|
||||
import subprocess
|
||||
|
||||
from django.db.backends.base.client import BaseDatabaseClient
|
||||
|
||||
|
||||
class DatabaseClient(BaseDatabaseClient):
|
||||
executable_name = 'mysql'
|
||||
|
||||
@classmethod
|
||||
def settings_to_cmd_args(cls, settings_dict):
|
||||
args = [cls.executable_name]
|
||||
db = settings_dict['OPTIONS'].get('db', settings_dict['NAME'])
|
||||
user = settings_dict['OPTIONS'].get('user', settings_dict['USER'])
|
||||
passwd = settings_dict['OPTIONS'].get('passwd', settings_dict['PASSWORD'])
|
||||
host = settings_dict['OPTIONS'].get('host', settings_dict['HOST'])
|
||||
port = settings_dict['OPTIONS'].get('port', settings_dict['PORT'])
|
||||
server_ca = settings_dict['OPTIONS'].get('ssl', {}).get('ca')
|
||||
client_cert = settings_dict['OPTIONS'].get('ssl', {}).get('cert')
|
||||
client_key = settings_dict['OPTIONS'].get('ssl', {}).get('key')
|
||||
defaults_file = settings_dict['OPTIONS'].get('read_default_file')
|
||||
# Seems to be no good way to set sql_mode with CLI.
|
||||
|
||||
if defaults_file:
|
||||
args += ["--defaults-file=%s" % defaults_file]
|
||||
if user:
|
||||
args += ["--user=%s" % user]
|
||||
if passwd:
|
||||
args += ["--password=%s" % passwd]
|
||||
if host:
|
||||
if '/' in host:
|
||||
args += ["--socket=%s" % host]
|
||||
else:
|
||||
args += ["--host=%s" % host]
|
||||
if port:
|
||||
args += ["--port=%s" % port]
|
||||
if server_ca:
|
||||
args += ["--ssl-ca=%s" % server_ca]
|
||||
if client_cert:
|
||||
args += ["--ssl-cert=%s" % client_cert]
|
||||
if client_key:
|
||||
args += ["--ssl-key=%s" % client_key]
|
||||
if db:
|
||||
args += [db]
|
||||
return args
|
||||
|
||||
def runshell(self):
|
||||
args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)
|
||||
subprocess.check_call(args)
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db.models.sql import compiler
|
||||
|
||||
|
||||
class SQLCompiler(compiler.SQLCompiler):
|
||||
def as_subquery_condition(self, alias, columns, compiler):
|
||||
qn = compiler.quote_name_unless_alias
|
||||
qn2 = self.connection.ops.quote_name
|
||||
sql, params = self.as_sql()
|
||||
return '(%s) IN (%s)' % (', '.join('%s.%s' % (qn(alias), qn2(column)) for column in columns), sql), params
|
||||
|
||||
|
||||
class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
|
||||
pass
|
||||
|
||||
|
||||
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
|
||||
pass
|
||||
|
||||
|
||||
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
|
||||
pass
|
||||
|
||||
|
||||
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
|
||||
pass
|
||||
@@ -0,0 +1,67 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from django.db.backends.base.creation import BaseDatabaseCreation
|
||||
|
||||
from .client import DatabaseClient
|
||||
|
||||
|
||||
class DatabaseCreation(BaseDatabaseCreation):
|
||||
|
||||
def sql_table_creation_suffix(self):
|
||||
suffix = []
|
||||
test_settings = self.connection.settings_dict['TEST']
|
||||
if test_settings['CHARSET']:
|
||||
suffix.append('CHARACTER SET %s' % test_settings['CHARSET'])
|
||||
if test_settings['COLLATION']:
|
||||
suffix.append('COLLATE %s' % test_settings['COLLATION'])
|
||||
return ' '.join(suffix)
|
||||
|
||||
def _execute_create_test_db(self, cursor, parameters, keepdb=False):
|
||||
try:
|
||||
super()._execute_create_test_db(cursor, parameters, keepdb)
|
||||
except Exception as e:
|
||||
if len(e.args) < 1 or e.args[0] != 1007:
|
||||
# All errors except "database exists" (1007) cancel tests.
|
||||
self.log('Got an error creating the test database: %s' % e)
|
||||
sys.exit(2)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _clone_test_db(self, suffix, verbosity, keepdb=False):
|
||||
source_database_name = self.connection.settings_dict['NAME']
|
||||
target_database_name = self.get_test_db_clone_settings(suffix)['NAME']
|
||||
test_db_params = {
|
||||
'dbname': self.connection.ops.quote_name(target_database_name),
|
||||
'suffix': self.sql_table_creation_suffix(),
|
||||
}
|
||||
with self._nodb_connection.cursor() as cursor:
|
||||
try:
|
||||
self._execute_create_test_db(cursor, test_db_params, keepdb)
|
||||
except Exception:
|
||||
if keepdb:
|
||||
# If the database should be kept, skip everything else.
|
||||
return
|
||||
try:
|
||||
if verbosity >= 1:
|
||||
self.log('Destroying old test database for alias %s...' % (
|
||||
self._get_database_display_str(verbosity, target_database_name),
|
||||
))
|
||||
cursor.execute('DROP DATABASE %(dbname)s' % test_db_params)
|
||||
self._execute_create_test_db(cursor, test_db_params, keepdb)
|
||||
except Exception as e:
|
||||
self.log('Got an error recreating the test database: %s' % e)
|
||||
sys.exit(2)
|
||||
self._clone_db(source_database_name, target_database_name)
|
||||
|
||||
def _clone_db(self, source_database_name, target_database_name):
|
||||
dump_args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)[1:]
|
||||
dump_args[-1] = source_database_name
|
||||
dump_cmd = ['mysqldump', '--routines', '--events'] + dump_args
|
||||
load_cmd = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)
|
||||
load_cmd[-1] = target_database_name
|
||||
|
||||
with subprocess.Popen(dump_cmd, stdout=subprocess.PIPE) as dump_proc:
|
||||
with subprocess.Popen(load_cmd, stdin=dump_proc.stdout, stdout=subprocess.DEVNULL):
|
||||
# Allow dump_proc to receive a SIGPIPE if the load process exits.
|
||||
dump_proc.stdout.close()
|
||||
@@ -0,0 +1,120 @@
|
||||
import operator
|
||||
|
||||
from django.db.backends.base.features import BaseDatabaseFeatures
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
empty_fetchmany_value = ()
|
||||
update_can_self_select = False
|
||||
allows_group_by_pk = True
|
||||
related_fields_match_type = True
|
||||
# MySQL doesn't support sliced subqueries with IN/ALL/ANY/SOME.
|
||||
allow_sliced_subqueries_with_in = False
|
||||
has_select_for_update = True
|
||||
supports_forward_references = False
|
||||
supports_regex_backreferencing = False
|
||||
supports_date_lookup_using_string = False
|
||||
can_introspect_autofield = True
|
||||
can_introspect_binary_field = False
|
||||
can_introspect_duration_field = False
|
||||
can_introspect_small_integer_field = True
|
||||
can_introspect_positive_integer_field = True
|
||||
introspected_boolean_field_type = 'IntegerField'
|
||||
supports_index_column_ordering = False
|
||||
supports_timezones = False
|
||||
requires_explicit_null_ordering_when_grouping = True
|
||||
allows_auto_pk_0 = False
|
||||
can_release_savepoints = True
|
||||
atomic_transactions = False
|
||||
supports_column_check_constraints = False
|
||||
supports_table_check_constraints = False
|
||||
can_clone_databases = True
|
||||
supports_temporal_subtraction = True
|
||||
supports_select_intersection = False
|
||||
supports_select_difference = False
|
||||
supports_slicing_ordering_in_compound = True
|
||||
supports_index_on_text_field = False
|
||||
has_case_insensitive_like = False
|
||||
create_test_procedure_without_params_sql = """
|
||||
CREATE PROCEDURE test_procedure ()
|
||||
BEGIN
|
||||
DECLARE V_I INTEGER;
|
||||
SET V_I = 1;
|
||||
END;
|
||||
"""
|
||||
create_test_procedure_with_int_param_sql = """
|
||||
CREATE PROCEDURE test_procedure (P_I INTEGER)
|
||||
BEGIN
|
||||
DECLARE V_I INTEGER;
|
||||
SET V_I = P_I;
|
||||
END;
|
||||
"""
|
||||
db_functions_convert_bytes_to_str = True
|
||||
# Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
|
||||
supported_explain_formats = {'JSON', 'TEXT', 'TRADITIONAL'}
|
||||
# Neither MySQL nor MariaDB support partial indexes.
|
||||
supports_partial_indexes = False
|
||||
|
||||
@cached_property
|
||||
def _mysql_storage_engine(self):
|
||||
"Internal method used in Django tests. Don't rely on this from your code"
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute("SELECT ENGINE FROM INFORMATION_SCHEMA.ENGINES WHERE SUPPORT = 'DEFAULT'")
|
||||
result = cursor.fetchone()
|
||||
return result[0]
|
||||
|
||||
@cached_property
|
||||
def can_introspect_foreign_keys(self):
|
||||
"Confirm support for introspected foreign keys"
|
||||
return self._mysql_storage_engine != 'MyISAM'
|
||||
|
||||
@cached_property
|
||||
def has_zoneinfo_database(self):
|
||||
# Test if the time zone definitions are installed.
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1")
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
@cached_property
|
||||
def is_sql_auto_is_null_enabled(self):
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute('SELECT @@SQL_AUTO_IS_NULL')
|
||||
result = cursor.fetchone()
|
||||
return result and result[0] == 1
|
||||
|
||||
@cached_property
|
||||
def supports_over_clause(self):
|
||||
if self.connection.mysql_is_mariadb:
|
||||
return self.connection.mysql_version >= (10, 2)
|
||||
return self.connection.mysql_version >= (8, 0, 2)
|
||||
|
||||
@cached_property
|
||||
def has_select_for_update_skip_locked(self):
|
||||
return not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (8, 0, 1)
|
||||
|
||||
has_select_for_update_nowait = property(operator.attrgetter('has_select_for_update_skip_locked'))
|
||||
|
||||
@cached_property
|
||||
def needs_explain_extended(self):
|
||||
# EXTENDED is deprecated (and not required) in MySQL 5.7.
|
||||
return not self.connection.mysql_is_mariadb and self.connection.mysql_version < (5, 7)
|
||||
|
||||
@cached_property
|
||||
def supports_transactions(self):
|
||||
"""
|
||||
All storage engines except MyISAM support transactions.
|
||||
"""
|
||||
return self._mysql_storage_engine != 'MyISAM'
|
||||
|
||||
@cached_property
|
||||
def ignores_table_name_case(self):
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute('SELECT @@LOWER_CASE_TABLE_NAMES')
|
||||
result = cursor.fetchone()
|
||||
return result and result[0] != 0
|
||||
|
||||
@cached_property
|
||||
def supports_default_in_lead_lag(self):
|
||||
# To be added in https://jira.mariadb.org/browse/MDEV-12981.
|
||||
return not self.connection.mysql_is_mariadb
|
||||
@@ -0,0 +1,209 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from MySQLdb.constants import FIELD_TYPE
|
||||
|
||||
from django.db.backends.base.introspection import (
|
||||
BaseDatabaseIntrospection, FieldInfo as BaseFieldInfo, TableInfo,
|
||||
)
|
||||
from django.db.models.indexes import Index
|
||||
from django.utils.datastructures import OrderedSet
|
||||
|
||||
FieldInfo = namedtuple('FieldInfo', BaseFieldInfo._fields + ('extra', 'is_unsigned'))
|
||||
InfoLine = namedtuple('InfoLine', 'col_name data_type max_len num_prec num_scale extra column_default is_unsigned')
|
||||
|
||||
|
||||
class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||
data_types_reverse = {
|
||||
FIELD_TYPE.BLOB: 'TextField',
|
||||
FIELD_TYPE.CHAR: 'CharField',
|
||||
FIELD_TYPE.DECIMAL: 'DecimalField',
|
||||
FIELD_TYPE.NEWDECIMAL: 'DecimalField',
|
||||
FIELD_TYPE.DATE: 'DateField',
|
||||
FIELD_TYPE.DATETIME: 'DateTimeField',
|
||||
FIELD_TYPE.DOUBLE: 'FloatField',
|
||||
FIELD_TYPE.FLOAT: 'FloatField',
|
||||
FIELD_TYPE.INT24: 'IntegerField',
|
||||
FIELD_TYPE.LONG: 'IntegerField',
|
||||
FIELD_TYPE.LONGLONG: 'BigIntegerField',
|
||||
FIELD_TYPE.SHORT: 'SmallIntegerField',
|
||||
FIELD_TYPE.STRING: 'CharField',
|
||||
FIELD_TYPE.TIME: 'TimeField',
|
||||
FIELD_TYPE.TIMESTAMP: 'DateTimeField',
|
||||
FIELD_TYPE.TINY: 'IntegerField',
|
||||
FIELD_TYPE.TINY_BLOB: 'TextField',
|
||||
FIELD_TYPE.MEDIUM_BLOB: 'TextField',
|
||||
FIELD_TYPE.LONG_BLOB: 'TextField',
|
||||
FIELD_TYPE.VAR_STRING: 'CharField',
|
||||
}
|
||||
|
||||
def get_field_type(self, data_type, description):
|
||||
field_type = super().get_field_type(data_type, description)
|
||||
if 'auto_increment' in description.extra:
|
||||
if field_type == 'IntegerField':
|
||||
return 'AutoField'
|
||||
elif field_type == 'BigIntegerField':
|
||||
return 'BigAutoField'
|
||||
if description.is_unsigned:
|
||||
if field_type == 'IntegerField':
|
||||
return 'PositiveIntegerField'
|
||||
elif field_type == 'SmallIntegerField':
|
||||
return 'PositiveSmallIntegerField'
|
||||
return field_type
|
||||
|
||||
def get_table_list(self, cursor):
|
||||
"""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, table_name):
|
||||
"""
|
||||
Return a description of the table with the DB-API cursor.description
|
||||
interface."
|
||||
"""
|
||||
# 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
|
||||
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])
|
||||
field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()}
|
||||
|
||||
cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
|
||||
|
||||
def to_int(i):
|
||||
return int(i) if i is not None else i
|
||||
|
||||
fields = []
|
||||
for line in cursor.description:
|
||||
info = field_info[line[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,
|
||||
))
|
||||
return fields
|
||||
|
||||
def get_sequences(self, cursor, table_name, table_fields=()):
|
||||
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, table_name):
|
||||
"""
|
||||
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, table_name):
|
||||
"""
|
||||
Return a list of (column_name, referenced_table_name, referenced_column_name)
|
||||
for all key columns in the given table.
|
||||
"""
|
||||
key_columns = []
|
||||
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, table_name):
|
||||
"""
|
||||
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()
|
||||
if not result:
|
||||
return self.connection.features._mysql_storage_engine
|
||||
return result[0]
|
||||
|
||||
def get_constraints(self, cursor, table_name):
|
||||
"""
|
||||
Retrieve any constraints or keys (unique, pk, fk, check, index) across
|
||||
one or more columns.
|
||||
"""
|
||||
constraints = {}
|
||||
# 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,
|
||||
}
|
||||
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
|
||||
# Now add in the indexes
|
||||
cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name))
|
||||
for table, non_unique, index, colseq, column, type_ in [x[:5] + (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,
|
||||
}
|
||||
constraints[index]['index'] = True
|
||||
constraints[index]['type'] = Index.suffix if type_ == 'BTREE' else type_.lower()
|
||||
constraints[index]['columns'].add(column)
|
||||
# Convert the sorted sets to lists
|
||||
for constraint in constraints.values():
|
||||
constraint['columns'] = list(constraint['columns'])
|
||||
return constraints
|
||||
@@ -0,0 +1,306 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.backends.base.operations import BaseDatabaseOperations
|
||||
from django.utils import timezone
|
||||
from django.utils.duration import duration_microseconds
|
||||
|
||||
|
||||
class DatabaseOperations(BaseDatabaseOperations):
|
||||
compiler_module = "django.db.backends.mysql.compiler"
|
||||
|
||||
# MySQL stores positive fields as UNSIGNED ints.
|
||||
integer_field_ranges = {
|
||||
**BaseDatabaseOperations.integer_field_ranges,
|
||||
'PositiveSmallIntegerField': (0, 65535),
|
||||
'PositiveIntegerField': (0, 4294967295),
|
||||
}
|
||||
cast_data_types = {
|
||||
'AutoField': 'signed integer',
|
||||
'BigAutoField': 'signed integer',
|
||||
'CharField': 'char(%(max_length)s)',
|
||||
'DecimalField': 'decimal(%(max_digits)s, %(decimal_places)s)',
|
||||
'TextField': 'char',
|
||||
'IntegerField': 'signed integer',
|
||||
'BigIntegerField': 'signed integer',
|
||||
'SmallIntegerField': 'signed integer',
|
||||
'PositiveIntegerField': 'unsigned integer',
|
||||
'PositiveSmallIntegerField': 'unsigned integer',
|
||||
}
|
||||
cast_char_field_without_max_length = 'char'
|
||||
explain_prefix = 'EXPLAIN'
|
||||
|
||||
def date_extract_sql(self, lookup_type, field_name):
|
||||
# https://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
|
||||
if lookup_type == 'week_day':
|
||||
# DAYOFWEEK() returns an integer, 1-7, Sunday=1.
|
||||
# Note: WEEKDAY() returns 0-6, Monday=0.
|
||||
return "DAYOFWEEK(%s)" % field_name
|
||||
elif lookup_type == 'week':
|
||||
# Override the value of default_week_format for consistency with
|
||||
# other database backends.
|
||||
# Mode 3: Monday, 1-53, with 4 or more days this year.
|
||||
return "WEEK(%s, 3)" % field_name
|
||||
elif lookup_type == 'iso_year':
|
||||
# Get the year part from the YEARWEEK function, which returns a
|
||||
# number as year * 100 + week.
|
||||
return "TRUNCATE(YEARWEEK(%s, 3), -2) / 100" % field_name
|
||||
else:
|
||||
# EXTRACT returns 1-53 based on ISO-8601 for the week number.
|
||||
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
||||
|
||||
def date_trunc_sql(self, lookup_type, field_name):
|
||||
fields = {
|
||||
'year': '%%Y-01-01',
|
||||
'month': '%%Y-%%m-01',
|
||||
} # Use double percents to escape.
|
||||
if lookup_type in fields:
|
||||
format_str = fields[lookup_type]
|
||||
return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str)
|
||||
elif lookup_type == 'quarter':
|
||||
return "MAKEDATE(YEAR(%s), 1) + INTERVAL QUARTER(%s) QUARTER - INTERVAL 1 QUARTER" % (
|
||||
field_name, field_name
|
||||
)
|
||||
elif lookup_type == 'week':
|
||||
return "DATE_SUB(%s, INTERVAL WEEKDAY(%s) DAY)" % (
|
||||
field_name, field_name
|
||||
)
|
||||
else:
|
||||
return "DATE(%s)" % (field_name)
|
||||
|
||||
def _convert_field_to_tz(self, field_name, tzname):
|
||||
if settings.USE_TZ:
|
||||
field_name = "CONVERT_TZ(%s, 'UTC', '%s')" % (field_name, tzname)
|
||||
return field_name
|
||||
|
||||
def datetime_cast_date_sql(self, field_name, tzname):
|
||||
field_name = self._convert_field_to_tz(field_name, tzname)
|
||||
return "DATE(%s)" % field_name
|
||||
|
||||
def datetime_cast_time_sql(self, field_name, tzname):
|
||||
field_name = self._convert_field_to_tz(field_name, tzname)
|
||||
return "TIME(%s)" % field_name
|
||||
|
||||
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||
field_name = self._convert_field_to_tz(field_name, tzname)
|
||||
return self.date_extract_sql(lookup_type, field_name)
|
||||
|
||||
def datetime_trunc_sql(self, lookup_type, field_name, tzname):
|
||||
field_name = self._convert_field_to_tz(field_name, tzname)
|
||||
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
|
||||
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
|
||||
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
|
||||
if lookup_type == 'quarter':
|
||||
return (
|
||||
"CAST(DATE_FORMAT(MAKEDATE(YEAR({field_name}), 1) + "
|
||||
"INTERVAL QUARTER({field_name}) QUARTER - " +
|
||||
"INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
|
||||
).format(field_name=field_name)
|
||||
if lookup_type == 'week':
|
||||
return (
|
||||
"CAST(DATE_FORMAT(DATE_SUB({field_name}, "
|
||||
"INTERVAL WEEKDAY({field_name}) DAY), "
|
||||
"'%%Y-%%m-%%d 00:00:00') AS DATETIME)"
|
||||
).format(field_name=field_name)
|
||||
try:
|
||||
i = fields.index(lookup_type) + 1
|
||||
except ValueError:
|
||||
sql = field_name
|
||||
else:
|
||||
format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]])
|
||||
sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
|
||||
return sql
|
||||
|
||||
def time_trunc_sql(self, lookup_type, field_name):
|
||||
fields = {
|
||||
'hour': '%%H:00:00',
|
||||
'minute': '%%H:%%i:00',
|
||||
'second': '%%H:%%i:%%s',
|
||||
} # Use double percents to escape.
|
||||
if lookup_type in fields:
|
||||
format_str = fields[lookup_type]
|
||||
return "CAST(DATE_FORMAT(%s, '%s') AS TIME)" % (field_name, format_str)
|
||||
else:
|
||||
return "TIME(%s)" % (field_name)
|
||||
|
||||
def date_interval_sql(self, timedelta):
|
||||
return 'INTERVAL %s MICROSECOND' % duration_microseconds(timedelta)
|
||||
|
||||
def format_for_duration_arithmetic(self, sql):
|
||||
return 'INTERVAL %s MICROSECOND' % sql
|
||||
|
||||
def force_no_ordering(self):
|
||||
"""
|
||||
"ORDER BY NULL" prevents MySQL from implicitly ordering by grouped
|
||||
columns. If no ordering would otherwise be applied, we don't want any
|
||||
implicit sorting going on.
|
||||
"""
|
||||
return [(None, ("NULL", [], False))]
|
||||
|
||||
def last_executed_query(self, cursor, sql, params):
|
||||
# With MySQLdb, cursor objects have an (undocumented) "_executed"
|
||||
# attribute where the exact query sent to the database is saved.
|
||||
# See MySQLdb/cursors.py in the source distribution.
|
||||
query = getattr(cursor, '_executed', None)
|
||||
if query is not None:
|
||||
query = query.decode(errors='replace')
|
||||
return query
|
||||
|
||||
def no_limit_value(self):
|
||||
# 2**64 - 1, as recommended by the MySQL documentation
|
||||
return 18446744073709551615
|
||||
|
||||
def quote_name(self, name):
|
||||
if name.startswith("`") and name.endswith("`"):
|
||||
return name # Quoting once is enough.
|
||||
return "`%s`" % name
|
||||
|
||||
def random_function_sql(self):
|
||||
return 'RAND()'
|
||||
|
||||
def sql_flush(self, style, tables, sequences, allow_cascade=False):
|
||||
# NB: The generated SQL below is specific to MySQL
|
||||
# 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
|
||||
# to clear all tables of all data
|
||||
if tables:
|
||||
sql = ['SET FOREIGN_KEY_CHECKS = 0;']
|
||||
for table in tables:
|
||||
sql.append('%s %s;' % (
|
||||
style.SQL_KEYWORD('TRUNCATE'),
|
||||
style.SQL_FIELD(self.quote_name(table)),
|
||||
))
|
||||
sql.append('SET FOREIGN_KEY_CHECKS = 1;')
|
||||
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
|
||||
return sql
|
||||
else:
|
||||
return []
|
||||
|
||||
def validate_autopk_value(self, value):
|
||||
# MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653.
|
||||
if value == 0:
|
||||
raise ValueError('The database backend does not accept 0 as a '
|
||||
'value for AutoField.')
|
||||
return value
|
||||
|
||||
def adapt_datetimefield_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Expression values are adapted by the database.
|
||||
if hasattr(value, 'resolve_expression'):
|
||||
return value
|
||||
|
||||
# MySQL doesn't support tz-aware datetimes
|
||||
if timezone.is_aware(value):
|
||||
if settings.USE_TZ:
|
||||
value = timezone.make_naive(value, self.connection.timezone)
|
||||
else:
|
||||
raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
|
||||
return str(value)
|
||||
|
||||
def adapt_timefield_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Expression values are adapted by the database.
|
||||
if hasattr(value, 'resolve_expression'):
|
||||
return value
|
||||
|
||||
# MySQL doesn't support tz-aware times
|
||||
if timezone.is_aware(value):
|
||||
raise ValueError("MySQL backend does not support timezone-aware times.")
|
||||
|
||||
return str(value)
|
||||
|
||||
def max_name_length(self):
|
||||
return 64
|
||||
|
||||
def bulk_insert_sql(self, fields, placeholder_rows):
|
||||
placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
|
||||
values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
|
||||
return "VALUES " + values_sql
|
||||
|
||||
def combine_expression(self, connector, sub_expressions):
|
||||
if connector == '^':
|
||||
return 'POW(%s)' % ','.join(sub_expressions)
|
||||
# Convert the result to a signed integer since MySQL's binary operators
|
||||
# return an unsigned integer.
|
||||
elif connector in ('&', '|', '<<'):
|
||||
return 'CONVERT(%s, SIGNED)' % connector.join(sub_expressions)
|
||||
elif connector == '>>':
|
||||
lhs, rhs = sub_expressions
|
||||
return 'FLOOR(%(lhs)s / POW(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs}
|
||||
return super().combine_expression(connector, sub_expressions)
|
||||
|
||||
def get_db_converters(self, expression):
|
||||
converters = super().get_db_converters(expression)
|
||||
internal_type = expression.output_field.get_internal_type()
|
||||
if internal_type in ['BooleanField', 'NullBooleanField']:
|
||||
converters.append(self.convert_booleanfield_value)
|
||||
elif internal_type == 'DateTimeField':
|
||||
if settings.USE_TZ:
|
||||
converters.append(self.convert_datetimefield_value)
|
||||
elif internal_type == 'UUIDField':
|
||||
converters.append(self.convert_uuidfield_value)
|
||||
return converters
|
||||
|
||||
def convert_booleanfield_value(self, value, expression, connection):
|
||||
if value in (0, 1):
|
||||
value = bool(value)
|
||||
return value
|
||||
|
||||
def convert_datetimefield_value(self, value, expression, connection):
|
||||
if value is not None:
|
||||
value = timezone.make_aware(value, self.connection.timezone)
|
||||
return value
|
||||
|
||||
def convert_uuidfield_value(self, value, expression, connection):
|
||||
if value is not None:
|
||||
value = uuid.UUID(value)
|
||||
return value
|
||||
|
||||
def binary_placeholder_sql(self, value):
|
||||
return '_binary %s' if value is not None and not hasattr(value, 'as_sql') else '%s'
|
||||
|
||||
def subtract_temporals(self, internal_type, lhs, rhs):
|
||||
lhs_sql, lhs_params = lhs
|
||||
rhs_sql, rhs_params = rhs
|
||||
if internal_type == 'TimeField':
|
||||
if self.connection.mysql_is_mariadb:
|
||||
# MariaDB includes the microsecond component in TIME_TO_SEC as
|
||||
# a decimal. MySQL returns an integer without microseconds.
|
||||
return 'CAST((TIME_TO_SEC(%(lhs)s) - TIME_TO_SEC(%(rhs)s)) * 1000000 AS SIGNED)' % {
|
||||
'lhs': lhs_sql, 'rhs': rhs_sql
|
||||
}, lhs_params + rhs_params
|
||||
return (
|
||||
"((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -"
|
||||
" (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))"
|
||||
) % {'lhs': lhs_sql, 'rhs': rhs_sql}, lhs_params * 2 + rhs_params * 2
|
||||
else:
|
||||
return "TIMESTAMPDIFF(MICROSECOND, %s, %s)" % (rhs_sql, lhs_sql), rhs_params + lhs_params
|
||||
|
||||
def explain_query_prefix(self, format=None, **options):
|
||||
# Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
|
||||
if format and format.upper() == 'TEXT':
|
||||
format = 'TRADITIONAL'
|
||||
prefix = super().explain_query_prefix(format, **options)
|
||||
if format:
|
||||
prefix += ' FORMAT=%s' % format
|
||||
if self.connection.features.needs_explain_extended and format is None:
|
||||
# EXTENDED and FORMAT are mutually exclusive options.
|
||||
prefix += ' EXTENDED'
|
||||
return prefix
|
||||
|
||||
def regex_lookup(self, lookup_type):
|
||||
# REGEXP BINARY doesn't work correctly in MySQL 8+ and REGEXP_LIKE
|
||||
# doesn't exist in MySQL 5.6 or in MariaDB.
|
||||
if self.connection.mysql_version < (8, 0, 0) or self.connection.mysql_is_mariadb:
|
||||
if lookup_type == 'regex':
|
||||
return '%s REGEXP BINARY %s'
|
||||
return '%s REGEXP %s'
|
||||
|
||||
match_option = 'c' if lookup_type == 'regex' else 'i'
|
||||
return "REGEXP_LIKE(%%s, %%s, '%s')" % match_option
|
||||
|
||||
def insert_statement(self, ignore_conflicts=False):
|
||||
return 'INSERT IGNORE INTO' if ignore_conflicts else super().insert_statement(ignore_conflicts)
|
||||
@@ -0,0 +1,103 @@
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.models import NOT_PROVIDED
|
||||
|
||||
|
||||
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||
|
||||
sql_rename_table = "RENAME TABLE %(old_table)s TO %(new_table)s"
|
||||
|
||||
sql_alter_column_null = "MODIFY %(column)s %(type)s NULL"
|
||||
sql_alter_column_not_null = "MODIFY %(column)s %(type)s NOT NULL"
|
||||
sql_alter_column_type = "MODIFY %(column)s %(type)s"
|
||||
|
||||
# No 'CASCADE' which works as a no-op in MySQL but is undocumented
|
||||
sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
|
||||
|
||||
sql_rename_column = "ALTER TABLE %(table)s CHANGE %(old_column)s %(new_column)s %(type)s"
|
||||
|
||||
sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s"
|
||||
|
||||
sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
|
||||
|
||||
sql_delete_index = "DROP INDEX %(name)s ON %(table)s"
|
||||
|
||||
sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s PRIMARY KEY (%(columns)s)"
|
||||
sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY"
|
||||
|
||||
sql_create_index = 'CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s'
|
||||
|
||||
def quote_value(self, value):
|
||||
self.connection.ensure_connection()
|
||||
quoted = self.connection.connection.escape(value, self.connection.connection.encoders)
|
||||
if isinstance(value, str):
|
||||
quoted = quoted.decode()
|
||||
return quoted
|
||||
|
||||
def _is_limited_data_type(self, field):
|
||||
db_type = field.db_type(self.connection)
|
||||
return db_type is not None and db_type.lower() in self.connection._limited_data_types
|
||||
|
||||
def skip_default(self, field):
|
||||
return self._is_limited_data_type(field)
|
||||
|
||||
def add_field(self, model, field):
|
||||
super().add_field(model, field)
|
||||
|
||||
# Simulate the effect of a one-off default.
|
||||
# field.default may be unhashable, so a set isn't used for "in" check.
|
||||
if self.skip_default(field) and field.default not in (None, NOT_PROVIDED):
|
||||
effective_default = self.effective_default(field)
|
||||
self.execute('UPDATE %(table)s SET %(column)s = %%s' % {
|
||||
'table': self.quote_name(model._meta.db_table),
|
||||
'column': self.quote_name(field.column),
|
||||
}, [effective_default])
|
||||
|
||||
def _field_should_be_indexed(self, model, field):
|
||||
create_index = super()._field_should_be_indexed(model, field)
|
||||
storage = self.connection.introspection.get_storage_engine(
|
||||
self.connection.cursor(), model._meta.db_table
|
||||
)
|
||||
# No need to create an index for ForeignKey fields except if
|
||||
# db_constraint=False because the index from that constraint won't be
|
||||
# created.
|
||||
if (storage == "InnoDB" and
|
||||
create_index and
|
||||
field.get_internal_type() == 'ForeignKey' and
|
||||
field.db_constraint):
|
||||
return False
|
||||
return not self._is_limited_data_type(field) and create_index
|
||||
|
||||
def _delete_composed_index(self, model, fields, *args):
|
||||
"""
|
||||
MySQL can remove an implicit FK index on a field when that field is
|
||||
covered by another index like a unique_together. "covered" here means
|
||||
that the more complex index starts like the simpler one.
|
||||
http://bugs.mysql.com/bug.php?id=37910 / Django ticket #24757
|
||||
We check here before removing the [unique|index]_together if we have to
|
||||
recreate a FK index.
|
||||
"""
|
||||
first_field = model._meta.get_field(fields[0])
|
||||
if first_field.get_internal_type() == 'ForeignKey':
|
||||
constraint_names = self._constraint_names(model, [first_field.column], index=True)
|
||||
if not constraint_names:
|
||||
self.execute(self._create_index_sql(model, [first_field], suffix=""))
|
||||
return super()._delete_composed_index(model, fields, *args)
|
||||
|
||||
def _set_field_new_type_null_status(self, field, new_type):
|
||||
"""
|
||||
Keep the null property of the old field. If it has changed, it will be
|
||||
handled separately.
|
||||
"""
|
||||
if field.null:
|
||||
new_type += " NULL"
|
||||
else:
|
||||
new_type += " NOT NULL"
|
||||
return new_type
|
||||
|
||||
def _alter_column_type_sql(self, model, old_field, new_field, new_type):
|
||||
new_type = self._set_field_new_type_null_status(old_field, new_type)
|
||||
return super()._alter_column_type_sql(model, old_field, new_field, new_type)
|
||||
|
||||
def _rename_field_sql(self, table, old_field, new_field, new_type):
|
||||
new_type = self._set_field_new_type_null_status(old_field, new_type)
|
||||
return super()._rename_field_sql(table, old_field, new_field, new_type)
|
||||
@@ -0,0 +1,60 @@
|
||||
from django.core import checks
|
||||
from django.db.backends.base.validation import BaseDatabaseValidation
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
class DatabaseValidation(BaseDatabaseValidation):
|
||||
def check(self, **kwargs):
|
||||
issues = super().check(**kwargs)
|
||||
issues.extend(self._check_sql_mode(**kwargs))
|
||||
return issues
|
||||
|
||||
def _check_sql_mode(self, **kwargs):
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute("SELECT @@sql_mode")
|
||||
sql_mode = cursor.fetchone()
|
||||
modes = set(sql_mode[0].split(',') if sql_mode else ())
|
||||
if not (modes & {'STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES'}):
|
||||
return [checks.Warning(
|
||||
"MySQL Strict Mode is not set for database connection '%s'" % self.connection.alias,
|
||||
hint="MySQL's Strict Mode fixes many data integrity problems in MySQL, "
|
||||
"such as data truncation upon insertion, by escalating warnings into "
|
||||
"errors. It is strongly recommended you activate it. See: "
|
||||
"https://docs.djangoproject.com/en/%s/ref/databases/#mysql-sql-mode"
|
||||
% (get_docs_version(),),
|
||||
id='mysql.W002',
|
||||
)]
|
||||
return []
|
||||
|
||||
def check_field_type(self, field, field_type):
|
||||
"""
|
||||
MySQL has the following field length restriction:
|
||||
No character (varchar) fields can have a length exceeding 255
|
||||
characters if they have a unique index on them.
|
||||
MySQL doesn't support a database index on some data types.
|
||||
"""
|
||||
errors = []
|
||||
if (field_type.startswith('varchar') and field.unique and
|
||||
(field.max_length is None or int(field.max_length) > 255)):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'MySQL does not allow unique CharFields to have a max_length > 255.',
|
||||
obj=field,
|
||||
id='mysql.E001',
|
||||
)
|
||||
)
|
||||
|
||||
if field.db_index and field_type.lower() in self.connection._limited_data_types:
|
||||
errors.append(
|
||||
checks.Warning(
|
||||
'MySQL does not support a database index on %s columns.'
|
||||
% field_type,
|
||||
hint=(
|
||||
"An index won't be created. Silence this warning if "
|
||||
"you don't care about it."
|
||||
),
|
||||
obj=field,
|
||||
id='fields.W162',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
Reference in New Issue
Block a user