diff --git a/django/contrib/gis/db/backends/oracle/schema.py b/django/contrib/gis/db/backends/oracle/schema.py index 973a56e766..78470da07d 100644 --- a/django/contrib/gis/db/backends/oracle/schema.py +++ b/django/contrib/gis/db/backends/oracle/schema.py @@ -1,6 +1,6 @@ from django.contrib.gis.db.models.fields import GeometryField from django.db.backends.oracle.schema import DatabaseSchemaEditor -from django.db.backends.utils import truncate_name +from django.db.backends.utils import strip_quotes, truncate_name class OracleGISSchemaEditor(DatabaseSchemaEditor): @@ -91,4 +91,4 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor): def _create_spatial_index_name(self, model, field): # Oracle doesn't allow object names > 30 characters. Use this scheme # instead of self._create_index_name() for backwards compatibility. - return truncate_name('%s_%s_id' % (model._meta.db_table, field.column), 30) + return truncate_name('%s_%s_id' % (strip_quotes(model._meta.db_table), field.column), 30) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 3db3c76b7b..b00853b74d 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -2,6 +2,7 @@ import hashlib import logging from datetime import datetime +from django.db.backends.utils import strip_quotes from django.db.transaction import TransactionManagementError, atomic from django.utils import six, timezone from django.utils.encoding import force_bytes @@ -841,7 +842,7 @@ class BaseDatabaseSchemaEditor(object): The name is divided into 3 parts: the table name, the column names, and a unique digest and suffix. """ - table_name = model._meta.db_table + table_name = strip_quotes(model._meta.db_table) hash_data = [table_name] + list(column_names) hash_suffix_part = '%s%s' % (self._digest(*hash_data), suffix) max_length = self.connection.ops.max_name_length() or 200 diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index d55c003383..e283c574f2 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -6,7 +6,7 @@ import uuid from django.conf import settings from django.db.backends.base.operations import BaseDatabaseOperations -from django.db.backends.utils import truncate_name +from django.db.backends.utils import strip_quotes, truncate_name from django.utils import six, timezone from django.utils.encoding import force_bytes, force_text @@ -450,11 +450,13 @@ WHEN (new.%(col_name)s IS NULL) def _get_sequence_name(self, table): name_length = self.max_name_length() - 3 - return '%s_SQ' % truncate_name(table, name_length).upper() + sequence_name = '%s_SQ' % strip_quotes(table) + return truncate_name(sequence_name, name_length).upper() def _get_trigger_name(self, table): name_length = self.max_name_length() - 3 - return '%s_TR' % truncate_name(table, name_length).upper() + trigger_name = '%s_TR' % strip_quotes(table) + return truncate_name(trigger_name, name_length).upper() def bulk_insert_sql(self, fields, placeholder_rows): return " UNION ALL ".join( diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py index c01bcc2598..73f1dbb690 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -209,3 +209,13 @@ def format_number(value, max_digits, decimal_places): if decimal_places is not None: return "%.*f" % (decimal_places, value) return "{:f}".format(value) + + +def strip_quotes(table_name): + """ + Strip quotes off of quoted table names to make them safe for use in index + names, sequence names, etc. For example '"USER"."TABLE"' (an Oracle naming + scheme) becomes 'USER"."TABLE'. + """ + has_quotes = table_name.startswith('"') and table_name.endswith('"') + return table_name[1:-1] if has_quotes else table_name diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 10ba7696ca..e551ed1dbe 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1565,7 +1565,8 @@ class ManyToManyField(RelatedField): elif self.db_table: return self.db_table else: - return utils.truncate_name('%s_%s' % (opts.db_table, self.name), connection.ops.max_name_length()) + m2m_table_name = '%s_%s' % (utils.strip_quotes(opts.db_table), self.name) + return utils.truncate_name(m2m_table_name, connection.ops.max_name_length()) def _get_m2m_attr(self, related, attr): """ diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 06a80c4df6..73c1371cfe 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -2266,3 +2266,34 @@ class SchemaTests(TransactionTestCase): editor, Author, tob_auto_now_add, 'tob_auto_now_add', now.time(), cast_function=lambda x: x.time(), ) + + @unittest.skipUnless(connection.vendor == 'oracle', 'Oracle specific db_table syntax') + def test_creation_with_db_table_double_quotes(self): + oracle_user = connection.creation._test_database_user() + + class Student(Model): + name = CharField(max_length=30) + + class Meta: + app_label = 'schema' + apps = new_apps + db_table = '"%s"."DJANGO_STUDENT_TABLE"' % oracle_user + + class Document(Model): + name = CharField(max_length=30) + students = ManyToManyField(Student) + + class Meta: + app_label = 'schema' + apps = new_apps + db_table = '"%s"."DJANGO_DOCUMENT_TABLE"' % oracle_user + + self.local_models = [Student, Document] + + with connection.schema_editor() as editor: + editor.create_model(Student) + editor.create_model(Document) + + doc = Document.objects.create(name='Test Name') + student = Student.objects.create(name='Some man') + doc.students.add(student)