Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added SQLite backend which passes all current tests

  • Loading branch information...
commit d683263f97aedd67f17f4b78ac65d915f4e70d36 1 parent 7e81213
Andrew Godwin authored
3  django/db/backends/__init__.py
@@ -432,6 +432,9 @@ class BaseDatabaseFeatures(object):
432 432
     # What's the maximum length for index names?
433 433
     max_index_name_length = 63
434 434
 
  435
+    # Does it support foreign keys?
  436
+    supports_foreign_keys = True
  437
+
435 438
     def __init__(self, connection):
436 439
         self.connection = connection
437 440
 
4  django/db/backends/schema.py
@@ -187,7 +187,7 @@ def create_model(self, model):
187 187
                     }
188 188
                 )
189 189
             # FK
190  
-            if field.rel:
  190
+            if field.rel and self.connection.features.supports_foreign_keys:
191 191
                 to_table = field.rel.to._meta.db_table
192 192
                 to_column = field.rel.to._meta.get_field(field.rel.field_name).column
193 193
                 self.deferred_sql.append(
@@ -311,7 +311,7 @@ def create_field(self, model, field, keep_default=False):
311 311
                 }
312 312
             }
313 313
         # Add any FK constraints later
314  
-        if field.rel:
  314
+        if field.rel and self.connection.features.supports_foreign_keys:
315 315
             to_table = field.rel.to._meta.db_table
316 316
             to_column = field.rel.to._meta.get_field(field.rel.field_name).column
317 317
             self.deferred_sql.append(
1  django/db/backends/sqlite3/base.py
@@ -96,6 +96,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
96 96
     supports_mixed_date_datetime_comparisons = False
97 97
     has_bulk_insert = True
98 98
     can_combine_inserts_with_and_without_auto_increment_pk = False
  99
+    supports_foreign_keys = False
99 100
 
100 101
     @cached_property
101 102
     def supports_stddev(self):
36  django/db/backends/sqlite3/introspection.py
@@ -154,7 +154,7 @@ def get_indexes(self, cursor, table_name):
154 154
             if len(info) != 1:
155 155
                 continue
156 156
             name = info[0][2] # seqno, cid, name
157  
-            indexes[name] = {'primary_key': False,
  157
+            indexes[name] = {'primary_key': indexes.get(name, {}).get("primary_key", False),
158 158
                              'unique': unique}
159 159
         return indexes
160 160
 
@@ -182,3 +182,37 @@ def _table_info(self, cursor, name):
182 182
                  'null_ok': not field[3],
183 183
                  'pk': field[5]     # undocumented
184 184
                  } for field in cursor.fetchall()]
  185
+
  186
+    def get_constraints(self, cursor, table_name):
  187
+        """
  188
+        Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns.
  189
+        """
  190
+        constraints = {}
  191
+        # Get the index info
  192
+        cursor.execute("PRAGMA index_list(%s)" % self.connection.ops.quote_name(table_name))
  193
+        for number, index, unique in cursor.fetchall():
  194
+            # Get the index info for that index
  195
+            cursor.execute('PRAGMA index_info(%s)' % self.connection.ops.quote_name(index))
  196
+            for index_rank, column_rank, column in cursor.fetchall():
  197
+                if index not in constraints:
  198
+                    constraints[index] = {
  199
+                        "columns": set(),
  200
+                        "primary_key": False,
  201
+                        "unique": bool(unique),
  202
+                        "foreign_key": False,
  203
+                        "check": False,
  204
+                        "index": True,
  205
+                    }
  206
+                constraints[index]['columns'].add(column)
  207
+        # Get the PK
  208
+        pk_column = self.get_primary_key_column(cursor, table_name)
  209
+        if pk_column:
  210
+            constraints["__primary__"] = {
  211
+                "columns": set([pk_column]),
  212
+                "primary_key": True,
  213
+                "unique": False,  # It's not actually a unique constraint
  214
+                "foreign_key": False,
  215
+                "check": False,
  216
+                "index": False,
  217
+            }
  218
+        return constraints
110  django/db/backends/sqlite3/schema.py
... ...
@@ -1,6 +1,116 @@
1 1
 from django.db.backends.schema import BaseDatabaseSchemaEditor
  2
+from django.db.models.loading import cache
  3
+from django.db.models.fields.related import ManyToManyField
2 4
 
3 5
 
4 6
 class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
5 7
 
6 8
     sql_delete_table = "DROP TABLE %(table)s"
  9
+
  10
+    def _remake_table(self, model, create_fields=[], delete_fields=[], alter_fields=[], rename_fields=[], override_uniques=None):
  11
+        "Shortcut to transform a model from old_model into new_model"
  12
+        # Work out the new fields dict / mapping
  13
+        body = dict((f.name, f) for f in model._meta.local_fields)
  14
+        mapping = dict((f.column, f.column) for f in model._meta.local_fields)
  15
+        # If any of the new or altered fields is introducing a new PK,
  16
+        # remove the old one
  17
+        restore_pk_field = None
  18
+        if any(f.primary_key for f in create_fields) or any(n.primary_key for o, n in alter_fields):
  19
+            for name, field in list(body.items()):
  20
+                if field.primary_key:
  21
+                    field.primary_key = False
  22
+                    restore_pk_field = field
  23
+                    if field.auto_created:
  24
+                        del body[name]
  25
+                        del mapping[field.column]
  26
+        # Add in any created fields
  27
+        for field in create_fields:
  28
+            body[field.name] = field
  29
+        # Add in any altered fields
  30
+        for (old_field, new_field) in alter_fields:
  31
+            del body[old_field.name]
  32
+            del mapping[old_field.column]
  33
+            body[new_field.name] = new_field
  34
+            mapping[new_field.column] = old_field.column
  35
+        # Remove any deleted fields
  36
+        for field in delete_fields:
  37
+            del body[field.name]
  38
+            del mapping[field.column]
  39
+        # Construct a new model for the new state
  40
+        meta_contents = {
  41
+            'app_label': model._meta.app_label,
  42
+            'db_table': model._meta.db_table + "__new",
  43
+            'unique_together': model._meta.unique_together if override_uniques is None else override_uniques,
  44
+        }
  45
+        meta = type("Meta", tuple(), meta_contents)
  46
+        body['Meta'] = meta
  47
+        body['__module__'] = "__fake__"
  48
+        with cache.temporary_state():
  49
+            del cache.app_models[model._meta.app_label][model._meta.object_name.lower()]
  50
+            temp_model = type(model._meta.object_name, model.__bases__, body)
  51
+        # Create a new table with that format
  52
+        self.create_model(temp_model)
  53
+        # Copy data from the old table
  54
+        field_maps = list(mapping.items())
  55
+        self.execute("INSERT INTO %s (%s) SELECT %s FROM %s;" % (
  56
+            self.quote_name(temp_model._meta.db_table),
  57
+            ', '.join([x for x, y in field_maps]),
  58
+            ', '.join([y for x, y in field_maps]),
  59
+            self.quote_name(model._meta.db_table),
  60
+        ))
  61
+        # Delete the old table
  62
+        self.delete_model(model)
  63
+        # Rename the new to the old
  64
+        self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table)
  65
+        # Run deferred SQL on correct table
  66
+        for sql in self.deferred_sql:
  67
+            self.execute(sql.replace(temp_model._meta.db_table, model._meta.db_table))
  68
+        self.deferred_sql = []
  69
+        # Fix any PK-removed field
  70
+        if restore_pk_field:
  71
+            restore_pk_field.primary_key = True
  72
+
  73
+    def create_field(self, model, field):
  74
+        """
  75
+        Creates a field on a model.
  76
+        Usually involves adding a column, but may involve adding a
  77
+        table instead (for M2M fields)
  78
+        """
  79
+        # Special-case implicit M2M tables
  80
+        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
  81
+            return self.create_model(field.rel.through)
  82
+        # Detect bad field combinations
  83
+        if (not field.null and
  84
+           (not field.has_default() or field.get_default() is None) and
  85
+           not field.empty_strings_allowed):
  86
+            raise ValueError("You cannot add a null=False column without a default value on SQLite.")
  87
+        self._remake_table(model, create_fields=[field])
  88
+
  89
+    def delete_field(self, model, field):
  90
+        """
  91
+        Removes a field from a model. Usually involves deleting a column,
  92
+        but for M2Ms may involve deleting a table.
  93
+        """
  94
+        # Special-case implicit M2M tables
  95
+        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
  96
+            return self.delete_model(field.rel.through)
  97
+        # For everything else, remake.
  98
+        self._remake_table(model, delete_fields=[field])
  99
+
  100
+    def alter_field(self, model, old_field, new_field, strict=False):
  101
+        # Ensure this field is even column-based
  102
+        old_type = old_field.db_type(connection=self.connection)
  103
+        new_type = self._type_for_alter(new_field)
  104
+        if old_type is None and new_type is None:
  105
+            # TODO: Handle M2M fields being repointed
  106
+            return
  107
+        elif old_type is None or new_type is None:
  108
+            raise ValueError("Cannot alter field %s into %s - they are not compatible types" % (
  109
+                    old_field,
  110
+                    new_field,
  111
+                ))
  112
+        # Alter by remaking table
  113
+        self._remake_table(model, alter_fields=[(old_field, new_field)])
  114
+
  115
+    def alter_unique_together(self, model, old_unique_together, new_unique_together):
  116
+        self._remake_table(model, override_uniques=new_unique_together)
21  django/db/models/loading.py
@@ -265,6 +265,10 @@ def restore_state(self, state):
265 265
         self.app_models = state['app_models']
266 266
         self.app_errors = state['app_errors']
267 267
 
  268
+    def temporary_state(self):
  269
+        "Returns a context manager that restores the state on exit"
  270
+        return StateContextManager(self)
  271
+
268 272
     def unregister_all(self):
269 273
         """
270 274
         Wipes the AppCache clean of all registered models.
@@ -275,6 +279,23 @@ def unregister_all(self):
275 279
         self.app_models = SortedDict()
276 280
         self.app_errors = {}
277 281
 
  282
+
  283
+class StateContextManager(object):
  284
+    """
  285
+    Context manager for locking cache state.
  286
+    Useful for making temporary models you don't want to stay in the cache.
  287
+    """
  288
+
  289
+    def __init__(self, cache):
  290
+        self.cache = cache
  291
+
  292
+    def __enter__(self):
  293
+        self.state = self.cache.save_state()
  294
+
  295
+    def __exit__(self, type, value, traceback):
  296
+        self.cache.restore_state(self.state)
  297
+
  298
+
278 299
 cache = AppCache()
279 300
 
280 301
 # These methods were always module level, so are kept that way for backwards
11  tests/modeltests/schema/models.py
@@ -29,6 +29,17 @@ class Meta:
29 29
         managed = False
30 30
 
31 31
 
  32
+class BookWithSlug(models.Model):
  33
+    author = models.ForeignKey(Author)
  34
+    title = models.CharField(max_length=100, db_index=True)
  35
+    pub_date = models.DateTimeField()
  36
+    slug = models.CharField(max_length=20, unique=True)
  37
+
  38
+    class Meta:
  39
+        managed = False
  40
+        db_table = "schema_book"
  41
+
  42
+
32 43
 class Tag(models.Model):
33 44
     title = models.CharField(max_length=255)
34 45
     slug = models.SlugField(unique=True)
24  tests/modeltests/schema/tests.py
@@ -2,11 +2,12 @@
2 2
 import copy
3 3
 import datetime
4 4
 from django.test import TestCase
  5
+from django.utils.unittest import skipUnless
5 6
 from django.db import connection, DatabaseError, IntegrityError
6 7
 from django.db.models.fields import IntegerField, TextField, CharField, SlugField
7 8
 from django.db.models.fields.related import ManyToManyField
8 9
 from django.db.models.loading import cache
9  
-from .models import Author, Book, AuthorWithM2M, Tag, TagUniqueRename, UniqueTest
  10
+from .models import Author, Book, BookWithSlug, AuthorWithM2M, Tag, TagUniqueRename, UniqueTest
10 11
 
11 12
 
12 13
 class SchemaTests(TestCase):
@@ -18,7 +19,7 @@ class SchemaTests(TestCase):
18 19
     as the code it is testing.
19 20
     """
20 21
 
21  
-    models = [Author, Book, AuthorWithM2M, Tag, UniqueTest]
  22
+    models = [Author, Book, BookWithSlug, AuthorWithM2M, Tag, TagUniqueRename, UniqueTest]
22 23
 
23 24
     # Utility functions
24 25
 
@@ -70,13 +71,21 @@ def tearDown(self):
70 71
 
71 72
     def column_classes(self, model):
72 73
         cursor = connection.cursor()
73  
-        return dict(
  74
+        columns = dict(
74 75
             (d[0], (connection.introspection.get_field_type(d[1], d), d))
75 76
             for d in connection.introspection.get_table_description(
76 77
                 cursor,
77 78
                 model._meta.db_table,
78 79
             )
79 80
         )
  81
+        # SQLite has a different format for field_type
  82
+        for name, (type, desc) in columns.items():
  83
+            if isinstance(type, tuple):
  84
+                columns[name] = (type[0], desc)
  85
+        # SQLite also doesn't error properly
  86
+        if not columns:
  87
+            raise DatabaseError("Table does not exist (empty pragma)")
  88
+        return columns
80 89
 
81 90
     # Tests
82 91
 
@@ -104,6 +113,7 @@ def test_creation_deletion(self):
104 113
             lambda: list(Author.objects.all()),
105 114
         )
106 115
 
  116
+    @skipUnless(connection.features.supports_foreign_keys, "No FK support")
107 117
     def test_creation_fk(self):
108 118
         "Tests that creating tables out of FK order works"
109 119
         # Create the table
@@ -449,13 +459,11 @@ def test_indexes(self):
449 459
             connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
450 460
         )
451 461
         # Add a unique column, verify that creates an implicit index
452  
-        new_field = CharField(max_length=20, unique=True)
453  
-        new_field.set_attributes_from_name("slug")
454 462
         editor = connection.schema_editor()
455 463
         editor.start()
456 464
         editor.create_field(
457 465
             Book,
458  
-            new_field,
  466
+            BookWithSlug._meta.get_field_by_name("slug")[0],
459 467
         )
460 468
         editor.commit()
461 469
         self.assertIn(
@@ -468,8 +476,8 @@ def test_indexes(self):
468 476
         editor = connection.schema_editor()
469 477
         editor.start()
470 478
         editor.alter_field(
471  
-            Book,
472  
-            new_field,
  479
+            BookWithSlug,
  480
+            BookWithSlug._meta.get_field_by_name("slug")[0],
473 481
             new_field2,
474 482
             strict = True,
475 483
         )

0 notes on commit d683263

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