Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Oracle schema backend, passes most tests and is pretty complete.

  • Loading branch information...
commit 157604a87fa7e1331c25fcbed558f0799aa5b8df 1 parent 44f907d
Andrew Godwin authored August 13, 2013
1  django/db/backends/oracle/base.py
@@ -92,6 +92,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
92 92
     supports_tablespaces = True
93 93
     supports_sequence_reset = False
94 94
     supports_combined_alters = False
  95
+    max_index_name_length = 30
95 96
 
96 97
 
97 98
 class DatabaseOperations(BaseDatabaseOperations):
139  django/db/backends/oracle/introspection.py
@@ -134,3 +134,142 @@ def get_indexes(self, cursor, table_name):
134 134
             indexes[row[0]] = {'primary_key': bool(row[1]),
135 135
                                'unique': bool(row[2])}
136 136
         return indexes
  137
+
  138
+    def get_constraints(self, cursor, table_name):
  139
+        """
  140
+        Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns.
  141
+        """
  142
+        constraints = {}
  143
+        # Loop over the constraints, getting PKs and uniques
  144
+        cursor.execute("""
  145
+            SELECT
  146
+                user_constraints.constraint_name,
  147
+                LOWER(cols.column_name) AS column_name,
  148
+                CASE user_constraints.constraint_type
  149
+                    WHEN 'P' THEN 1
  150
+                    ELSE 0
  151
+                END AS is_primary_key,
  152
+                CASE user_indexes.uniqueness
  153
+                    WHEN 'UNIQUE' THEN 1
  154
+                    ELSE 0
  155
+                END AS is_unique,
  156
+                CASE user_constraints.constraint_type
  157
+                    WHEN 'C' THEN 1
  158
+                    ELSE 0
  159
+                END AS is_check_constraint
  160
+            FROM
  161
+                user_constraints
  162
+            INNER JOIN
  163
+                user_indexes ON user_indexes.index_name = user_constraints.index_name
  164
+            LEFT OUTER JOIN
  165
+                user_cons_columns cols ON user_constraints.constraint_name = cols.constraint_name
  166
+            WHERE
  167
+                (
  168
+                    user_constraints.constraint_type = 'P' OR
  169
+                    user_constraints.constraint_type = 'U'
  170
+                )
  171
+                AND user_constraints.table_name = UPPER(%s)
  172
+            ORDER BY cols.position
  173
+        """, [table_name])
  174
+        for constraint, column, pk, unique, check in cursor.fetchall():
  175
+            # If we're the first column, make the record
  176
+            if constraint not in constraints:
  177
+                constraints[constraint] = {
  178
+                    "columns": [],
  179
+                    "primary_key": pk,
  180
+                    "unique": unique,
  181
+                    "foreign_key": None,
  182
+                    "check": check,
  183
+                    "index": True,
  184
+                }
  185
+            # Record the details
  186
+            constraints[constraint]['columns'].append(column)
  187
+        # Check constraints
  188
+        cursor.execute("""
  189
+            SELECT
  190
+                cons.constraint_name,
  191
+                LOWER(cols.column_name) AS column_name
  192
+            FROM
  193
+                user_constraints cons
  194
+            LEFT OUTER JOIN
  195
+                user_cons_columns cols ON cons.constraint_name = cols.constraint_name
  196
+            WHERE
  197
+                cons.constraint_type = 'C' AND
  198
+                cons.table_name = UPPER(%s)
  199
+            ORDER BY cols.position
  200
+        """, [table_name])
  201
+        for constraint, column in cursor.fetchall():
  202
+            # If we're the first column, make the record
  203
+            if constraint not in constraints:
  204
+                constraints[constraint] = {
  205
+                    "columns": [],
  206
+                    "primary_key": False,
  207
+                    "unique": False,
  208
+                    "foreign_key": None,
  209
+                    "check": True,
  210
+                    "index": False,
  211
+                }
  212
+            # Record the details
  213
+            constraints[constraint]['columns'].append(column)
  214
+        # Foreign key constraints
  215
+        cursor.execute("""
  216
+            SELECT
  217
+                cons.constraint_name,
  218
+                LOWER(cols.column_name) AS column_name,
  219
+                LOWER(rcons.table_name),
  220
+                LOWER(rcols.column_name)
  221
+            FROM
  222
+                user_constraints cons
  223
+            INNER JOIN
  224
+                user_constraints rcons ON cons.r_constraint_name = rcons.constraint_name
  225
+            INNER JOIN
  226
+                user_cons_columns rcols ON rcols.constraint_name = rcons.constraint_name
  227
+            LEFT OUTER JOIN
  228
+                user_cons_columns cols ON cons.constraint_name = cols.constraint_name
  229
+            WHERE
  230
+                cons.constraint_type = 'R' AND
  231
+                cons.table_name = UPPER(%s)
  232
+            ORDER BY cols.position
  233
+        """, [table_name])
  234
+        for constraint, column, other_table, other_column in cursor.fetchall():
  235
+            # If we're the first column, make the record
  236
+            if constraint not in constraints:
  237
+                constraints[constraint] = {
  238
+                    "columns": [],
  239
+                    "primary_key": False,
  240
+                    "unique": False,
  241
+                    "foreign_key": (other_table, other_column),
  242
+                    "check": False,
  243
+                    "index": False,
  244
+                }
  245
+            # Record the details
  246
+            constraints[constraint]['columns'].append(column)
  247
+        # Now get indexes
  248
+        cursor.execute("""
  249
+            SELECT
  250
+                index_name,
  251
+                LOWER(column_name)
  252
+            FROM
  253
+                user_ind_columns cols
  254
+            WHERE
  255
+                table_name = UPPER(%s) AND
  256
+                NOT EXISTS (
  257
+                    SELECT 1
  258
+                    FROM user_constraints cons
  259
+                    WHERE cols.index_name = cons.index_name
  260
+                )
  261
+        """, [table_name])
  262
+        for constraint, column in cursor.fetchall():
  263
+            # If we're the first column, make the record
  264
+            if constraint not in constraints:
  265
+                constraints[constraint] = {
  266
+                    "columns": [],
  267
+                    "primary_key": False,
  268
+                    "unique": False,
  269
+                    "foreign_key": None,
  270
+                    "check": False,
  271
+                    "index": True,
  272
+                }
  273
+            # Record the details
  274
+            constraints[constraint]['columns'].append(column)
  275
+        return constraints
77  django/db/backends/oracle/schema.py
... ...
@@ -1,4 +1,6 @@
  1
+import copy
1 2
 from django.db.backends.schema import BaseDatabaseSchemaEditor
  3
+from django.db.utils import DatabaseError
2 4
 
3 5
 
4 6
 class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
@@ -12,3 +14,78 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
12 14
     sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
13 15
     sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
14 16
     
  17
+    def delete_model(self, model):
  18
+        # Run superclass action
  19
+        super(DatabaseSchemaEditor, self).delete_model(model)
  20
+        # Clean up any autoincrement trigger
  21
+        self.execute("""
  22
+            DECLARE
  23
+                i INTEGER;
  24
+            BEGIN
  25
+                SELECT COUNT(*) INTO i FROM USER_CATALOG
  26
+                    WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE';
  27
+                IF i = 1 THEN
  28
+                    EXECUTE IMMEDIATE 'DROP SEQUENCE "%(sq_name)s"';
  29
+                END IF;
  30
+            END;
  31
+        /""" % {'sq_name': self.connection.ops._get_sequence_name(model._meta.db_table)})
  32
+
  33
+    def alter_field(self, model, old_field, new_field, strict=False):
  34
+        try:
  35
+            # Run superclass action
  36
+            super(DatabaseSchemaEditor, self).alter_field(model, old_field, new_field, strict)
  37
+        except DatabaseError as e:
  38
+            description = str(e)
  39
+            # If we're changing to/from LOB fields, we need to do a
  40
+            # SQLite-ish workaround
  41
+            if 'ORA-22858' in description or 'ORA-22859' in description:
  42
+                self._alter_field_lob_workaround(model, old_field, new_field)
  43
+            else:
  44
+                raise
  45
+
  46
+    def _alter_field_lob_workaround(self, model, old_field, new_field):
  47
+        """
  48
+        Oracle refuses to change a column type from/to LOB to/from a regular
  49
+        column. In Django, this shows up when the field is changed from/to
  50
+        a TextField.
  51
+        What we need to do instead is:
  52
+        - Add the desired field with a temporary name
  53
+        - Update the table to transfer values from old to new
  54
+        - Drop old column
  55
+        - Rename the new column
  56
+        """
  57
+        # Make a new field that's like the new one but with a temporary
  58
+        # column name.
  59
+        new_temp_field = copy.deepcopy(new_field)
  60
+        new_temp_field.column = self._generate_temp_name(new_field.column)
  61
+        # Add it
  62
+        self.add_field(model, new_temp_field)
  63
+        # Transfer values across
  64
+        self.execute("UPDATE %s set %s=%s" % (
  65
+            self.quote_name(model._meta.db_table),
  66
+            self.quote_name(new_temp_field.column),
  67
+            self.quote_name(old_field.column),
  68
+        ))
  69
+        # Drop the old field
  70
+        self.remove_field(model, old_field)
  71
+        # Rename the new field
  72
+        self.alter_field(model, new_temp_field, new_field)
  73
+        # Close the connection to force cx_Oracle to get column types right
  74
+        # on a new cursor
  75
+        self.connection.close()
  76
+
  77
+    def normalize_name(self, name):
  78
+        """
  79
+        Get the properly shortened and uppercased identifier as returned by quote_name(), but without the actual quotes.
  80
+        """
  81
+        nn = self.quote_name(name)
  82
+        if nn[0] == '"' and nn[-1] == '"':
  83
+            nn = nn[1:-1]
  84
+        return nn
  85
+
  86
+    def _generate_temp_name(self, for_name):
  87
+        """
  88
+        Generates temporary names for workarounds that need temp columns
  89
+        """
  90
+        suffix = hex(hash(for_name)).upper()[1:]
  91
+        return self.normalize_name(for_name + "_" + suffix)
35  django/db/backends/schema.py
@@ -113,6 +113,11 @@ def column_sql(self, model, field, include_default=False):
113 113
             sql += " %s" % self.connection.ops.tablespace_sql(tablespace, inline=True)
114 114
         # Work out nullability
115 115
         null = field.null
  116
+        # If we were told to include a default value, do so
  117
+        default_value = self.effective_default(field)
  118
+        if include_default and default_value is not None:
  119
+            sql += " DEFAULT %s"
  120
+            params += [default_value]
116 121
         # Oracle treats the empty string ('') as null, so coerce the null
117 122
         # option whenever '' is a possible value.
118 123
         if (field.empty_strings_allowed and not field.primary_key and
@@ -127,11 +132,6 @@ def column_sql(self, model, field, include_default=False):
127 132
             sql += " PRIMARY KEY"
128 133
         elif field.unique:
129 134
             sql += " UNIQUE"
130  
-        # If we were told to include a default value, do so
131  
-        default_value = self.effective_default(field)
132  
-        if include_default and default_value is not None:
133  
-            sql += " DEFAULT %s"
134  
-            params += [default_value]
135 135
         # Return the sql
136 136
         return sql, params
137 137
 
@@ -176,7 +176,7 @@ def create_model(self, model):
176 176
             ))
177 177
             params.extend(extra_params)
178 178
             # Indexes
179  
-            if field.db_index:
  179
+            if field.db_index and not field.unique:
180 180
                 self.deferred_sql.append(
181 181
                     self.sql_create_index % {
182 182
                         "name": self._create_index_name(model, [field.column], suffix=""),
@@ -198,6 +198,11 @@ def create_model(self, model):
198 198
                         "to_column": self.quote_name(to_column),
199 199
                     }
200 200
                 )
  201
+            # Autoincrement SQL
  202
+            if field.get_internal_type() == "AutoField":
  203
+                autoinc_sql = self.connection.ops.autoinc_sql(model._meta.db_table, field.column)
  204
+                if autoinc_sql:
  205
+                    self.deferred_sql.extend(autoinc_sql)
201 206
         # Add any unique_togethers
202 207
         for fields in model._meta.unique_together:
203 208
             columns = [model._meta.get_field_by_name(field)[0].column for field in fields]
@@ -353,6 +358,16 @@ def add_field(self, model, field):
353 358
                 }
354 359
             }
355 360
             self.execute(sql)
  361
+        # Add an index, if required
  362
+        if field.db_index and not field.unique:
  363
+            self.deferred_sql.append(
  364
+                self.sql_create_index % {
  365
+                    "name": self._create_index_name(model, [field.column], suffix=""),
  366
+                    "table": self.quote_name(model._meta.db_table),
  367
+                    "columns": self.quote_name(field.column),
  368
+                    "extra": "",
  369
+                }
  370
+            )
356 371
         # Add any FK constraints later
357 372
         if field.rel and self.connection.features.supports_foreign_keys:
358 373
             to_table = field.rel.to._meta.db_table
@@ -412,7 +427,7 @@ def alter_field(self, model, old_field, new_field, strict=False):
412 427
                 new_field,
413 428
             ))
414 429
         # Has unique been removed?
415  
-        if old_field.unique and not new_field.unique:
  430
+        if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)):
416 431
             # Find the unique constraint for this field
417 432
             constraint_names = self._constraint_names(model, [old_field.column], unique=True)
418 433
             if strict and len(constraint_names) != 1:
@@ -647,9 +662,15 @@ def _create_index_name(self, model, column_names, suffix=""):
647 662
         if len(index_name) > self.connection.features.max_index_name_length:
648 663
             part = ('_%s%s%s' % (column_names[0], index_unique_name, suffix))
649 664
             index_name = '%s%s' % (table_name[:(self.connection.features.max_index_name_length - len(part))], part)
  665
+        # It shouldn't start with an underscore (Oracle hates this)
  666
+        if index_name[0] == "_":
  667
+            index_name = index_name[1:]
650 668
         # If it's STILL too long, just hash it down
651 669
         if len(index_name) > self.connection.features.max_index_name_length:
652 670
             index_name = hashlib.md5(index_name).hexdigest()[:self.connection.features.max_index_name_length]
  671
+        # It can't start with a number on Oracle, so prepend D if we need to
  672
+        if index_name[0].isdigit():
  673
+            index_name = "D%s" % index_name[:-1]
653 674
         return index_name
654 675
 
655 676
     def _constraint_names(self, model, column_names=None, unique=None, primary_key=None, index=None, foreign_key=None, check=None):
4  tests/schema/tests.py
@@ -167,7 +167,7 @@ def test_alter(self):
167 167
         # Ensure the field is right to begin with
168 168
         columns = self.column_classes(Author)
169 169
         self.assertEqual(columns['name'][0], "CharField")
170  
-        self.assertEqual(columns['name'][1][6], False)
  170
+        self.assertEqual(bool(columns['name'][1][6]), bool(connection.features.interprets_empty_strings_as_nulls))
171 171
         # Alter the name field to a TextField
172 172
         new_field = TextField(null=True)
173 173
         new_field.set_attributes_from_name("name")
@@ -195,7 +195,7 @@ def test_alter(self):
195 195
         # Ensure the field is right afterwards
196 196
         columns = self.column_classes(Author)
197 197
         self.assertEqual(columns['name'][0], "TextField")
198  
-        self.assertEqual(columns['name'][1][6], False)
  198
+        self.assertEqual(columns['name'][1][6], bool(connection.features.interprets_empty_strings_as_nulls))
199 199
 
200 200
     def test_rename(self):
201 201
         """

0 notes on commit 157604a

Please sign in to comment.
Something went wrong with that request. Please try again.