Commit d683263f authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Added SQLite backend which passes all current tests

parent 7e81213b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -432,6 +432,9 @@ class BaseDatabaseFeatures(object):
    # What's the maximum length for index names?
    max_index_name_length = 63

    # Does it support foreign keys?
    supports_foreign_keys = True

    def __init__(self, connection):
        self.connection = connection

+2 −2
Original line number Diff line number Diff line
@@ -187,7 +187,7 @@ class BaseDatabaseSchemaEditor(object):
                    }
                )
            # FK
            if field.rel:
            if field.rel and self.connection.features.supports_foreign_keys:
                to_table = field.rel.to._meta.db_table
                to_column = field.rel.to._meta.get_field(field.rel.field_name).column
                self.deferred_sql.append(
@@ -311,7 +311,7 @@ class BaseDatabaseSchemaEditor(object):
                }
            }
        # Add any FK constraints later
        if field.rel:
        if field.rel and self.connection.features.supports_foreign_keys:
            to_table = field.rel.to._meta.db_table
            to_column = field.rel.to._meta.get_field(field.rel.field_name).column
            self.deferred_sql.append(
+1 −0
Original line number Diff line number Diff line
@@ -96,6 +96,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
    supports_mixed_date_datetime_comparisons = False
    has_bulk_insert = True
    can_combine_inserts_with_and_without_auto_increment_pk = False
    supports_foreign_keys = False

    @cached_property
    def supports_stddev(self):
+35 −1
Original line number Diff line number Diff line
@@ -154,7 +154,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
            if len(info) != 1:
                continue
            name = info[0][2] # seqno, cid, name
            indexes[name] = {'primary_key': False,
            indexes[name] = {'primary_key': indexes.get(name, {}).get("primary_key", False),
                             'unique': unique}
        return indexes

@@ -182,3 +182,37 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
                 'null_ok': not field[3],
                 'pk': field[5]     # undocumented
                 } for field in cursor.fetchall()]

    def get_constraints(self, cursor, table_name):
        """
        Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns.
        """
        constraints = {}
        # Get the index info
        cursor.execute("PRAGMA index_list(%s)" % self.connection.ops.quote_name(table_name))
        for number, index, unique in cursor.fetchall():
            # Get the index info for that index
            cursor.execute('PRAGMA index_info(%s)' % self.connection.ops.quote_name(index))
            for index_rank, column_rank, column in cursor.fetchall():
                if index not in constraints:
                    constraints[index] = {
                        "columns": set(),
                        "primary_key": False,
                        "unique": bool(unique),
                        "foreign_key": False,
                        "check": False,
                        "index": True,
                    }
                constraints[index]['columns'].add(column)
        # Get the PK
        pk_column = self.get_primary_key_column(cursor, table_name)
        if pk_column:
            constraints["__primary__"] = {
                "columns": set([pk_column]),
                "primary_key": True,
                "unique": False,  # It's not actually a unique constraint
                "foreign_key": False,
                "check": False,
                "index": False,
            }
        return constraints
+110 −0
Original line number Diff line number Diff line
from django.db.backends.schema import BaseDatabaseSchemaEditor
from django.db.models.loading import cache
from django.db.models.fields.related import ManyToManyField


class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):

    sql_delete_table = "DROP TABLE %(table)s"

    def _remake_table(self, model, create_fields=[], delete_fields=[], alter_fields=[], rename_fields=[], override_uniques=None):
        "Shortcut to transform a model from old_model into new_model"
        # Work out the new fields dict / mapping
        body = dict((f.name, f) for f in model._meta.local_fields)
        mapping = dict((f.column, f.column) for f in model._meta.local_fields)
        # If any of the new or altered fields is introducing a new PK,
        # remove the old one
        restore_pk_field = None
        if any(f.primary_key for f in create_fields) or any(n.primary_key for o, n in alter_fields):
            for name, field in list(body.items()):
                if field.primary_key:
                    field.primary_key = False
                    restore_pk_field = field
                    if field.auto_created:
                        del body[name]
                        del mapping[field.column]
        # Add in any created fields
        for field in create_fields:
            body[field.name] = field
        # Add in any altered fields
        for (old_field, new_field) in alter_fields:
            del body[old_field.name]
            del mapping[old_field.column]
            body[new_field.name] = new_field
            mapping[new_field.column] = old_field.column
        # Remove any deleted fields
        for field in delete_fields:
            del body[field.name]
            del mapping[field.column]
        # Construct a new model for the new state
        meta_contents = {
            'app_label': model._meta.app_label,
            'db_table': model._meta.db_table + "__new",
            'unique_together': model._meta.unique_together if override_uniques is None else override_uniques,
        }
        meta = type("Meta", tuple(), meta_contents)
        body['Meta'] = meta
        body['__module__'] = "__fake__"
        with cache.temporary_state():
            del cache.app_models[model._meta.app_label][model._meta.object_name.lower()]
            temp_model = type(model._meta.object_name, model.__bases__, body)
        # Create a new table with that format
        self.create_model(temp_model)
        # Copy data from the old table
        field_maps = list(mapping.items())
        self.execute("INSERT INTO %s (%s) SELECT %s FROM %s;" % (
            self.quote_name(temp_model._meta.db_table),
            ', '.join([x for x, y in field_maps]),
            ', '.join([y for x, y in field_maps]),
            self.quote_name(model._meta.db_table),
        ))
        # Delete the old table
        self.delete_model(model)
        # Rename the new to the old
        self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table)
        # Run deferred SQL on correct table
        for sql in self.deferred_sql:
            self.execute(sql.replace(temp_model._meta.db_table, model._meta.db_table))
        self.deferred_sql = []
        # Fix any PK-removed field
        if restore_pk_field:
            restore_pk_field.primary_key = True

    def create_field(self, model, field):
        """
        Creates a field on a model.
        Usually involves adding a column, but may involve adding a
        table instead (for M2M fields)
        """
        # Special-case implicit M2M tables
        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
            return self.create_model(field.rel.through)
        # Detect bad field combinations
        if (not field.null and
           (not field.has_default() or field.get_default() is None) and
           not field.empty_strings_allowed):
            raise ValueError("You cannot add a null=False column without a default value on SQLite.")
        self._remake_table(model, create_fields=[field])

    def delete_field(self, model, field):
        """
        Removes a field from a model. Usually involves deleting a column,
        but for M2Ms may involve deleting a table.
        """
        # Special-case implicit M2M tables
        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
            return self.delete_model(field.rel.through)
        # For everything else, remake.
        self._remake_table(model, delete_fields=[field])

    def alter_field(self, model, old_field, new_field, strict=False):
        # Ensure this field is even column-based
        old_type = old_field.db_type(connection=self.connection)
        new_type = self._type_for_alter(new_field)
        if old_type is None and new_type is None:
            # TODO: Handle M2M fields being repointed
            return
        elif old_type is None or new_type is None:
            raise ValueError("Cannot alter field %s into %s - they are not compatible types" % (
                    old_field,
                    new_field,
                ))
        # Alter by remaking table
        self._remake_table(model, alter_fields=[(old_field, new_field)])

    def alter_unique_together(self, model, old_unique_together, new_unique_together):
        self._remake_table(model, override_uniques=new_unique_together)
Loading