221 lines
8.8 KiB
Python
221 lines
8.8 KiB
Python
import inspect
|
|
import re
|
|
|
|
from django.db.models import ForeignKey
|
|
|
|
from south.db import generic
|
|
from django.core.management.commands import inspectdb
|
|
|
|
class DatabaseOperations(generic.DatabaseOperations):
|
|
|
|
"""
|
|
SQLite3 implementation of database operations.
|
|
"""
|
|
|
|
backend_name = "sqlite3"
|
|
|
|
# SQLite ignores several constraints. I wish I could.
|
|
supports_foreign_keys = False
|
|
has_check_constraints = False
|
|
|
|
def add_column(self, table_name, name, field, *args, **kwds):
|
|
"""
|
|
Adds a column.
|
|
"""
|
|
# If it's not nullable, and has no default, raise an error (SQLite is picky)
|
|
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.")
|
|
# Initialise the field.
|
|
field.set_attributes_from_name(name)
|
|
# We add columns by remaking the table; even though SQLite supports
|
|
# adding columns, it doesn't support adding PRIMARY KEY or UNIQUE cols.
|
|
self._remake_table(table_name, added={
|
|
field.column: self._column_sql_for_create(table_name, name, field, False),
|
|
})
|
|
|
|
def _remake_table(self, table_name, added={}, renames={}, deleted=[], altered={},
|
|
primary_key_override=None, uniques_deleted=[]):
|
|
"""
|
|
Given a table and three sets of changes (renames, deletes, alters),
|
|
recreates it with the modified schema.
|
|
"""
|
|
# Dry runs get skipped completely
|
|
if self.dry_run:
|
|
return
|
|
# Temporary table's name
|
|
temp_name = "_south_new_" + table_name
|
|
# Work out the (possibly new) definitions of each column
|
|
definitions = {}
|
|
cursor = self._get_connection().cursor()
|
|
# Get the index descriptions
|
|
indexes = self._get_connection().introspection.get_indexes(cursor, table_name)
|
|
multi_indexes = self._get_multi_indexes(table_name)
|
|
# Work out new column defs.
|
|
for column_info in self._get_connection().introspection.get_table_description(cursor, table_name):
|
|
name = column_info[0]
|
|
if name in deleted:
|
|
continue
|
|
# Get the type, ignoring PRIMARY KEY (we need to be consistent)
|
|
type = column_info[1].replace("PRIMARY KEY", "")
|
|
# Add on unique or primary key if needed.
|
|
if indexes[name]['unique'] and name not in uniques_deleted:
|
|
type += " UNIQUE"
|
|
if (primary_key_override and primary_key_override == name) or \
|
|
(not primary_key_override and indexes[name]['primary_key']):
|
|
type += " PRIMARY KEY"
|
|
# Deal with a rename
|
|
if name in renames:
|
|
name = renames[name]
|
|
# Add to the defs
|
|
definitions[name] = type
|
|
# Add on altered columns
|
|
definitions.update(altered)
|
|
# Add on the new columns
|
|
definitions.update(added)
|
|
# Alright, Make the table
|
|
self.execute("CREATE TABLE %s (%s)" % (
|
|
self.quote_name(temp_name),
|
|
", ".join(["%s %s" % (self.quote_name(cname), ctype) for cname, ctype in definitions.items()]),
|
|
))
|
|
# Copy over the data
|
|
self._copy_data(table_name, temp_name, renames)
|
|
# Delete the old table, move our new one over it
|
|
self.delete_table(table_name)
|
|
self.rename_table(temp_name, table_name)
|
|
# Recreate multi-valued indexes
|
|
# We can't do that before since it's impossible to rename indexes
|
|
# and index name scope is global
|
|
self._make_multi_indexes(table_name, multi_indexes, renames=renames, deleted=deleted, uniques_deleted=uniques_deleted)
|
|
|
|
|
|
def _copy_data(self, src, dst, field_renames={}):
|
|
"Used to copy data into a new table"
|
|
# Make a list of all the fields to select
|
|
cursor = self._get_connection().cursor()
|
|
src_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, src)]
|
|
dst_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, dst)]
|
|
src_fields_new = []
|
|
dst_fields_new = []
|
|
for field in src_fields:
|
|
if field in field_renames:
|
|
dst_fields_new.append(self.quote_name(field_renames[field]))
|
|
elif field in dst_fields:
|
|
dst_fields_new.append(self.quote_name(field))
|
|
else:
|
|
continue
|
|
src_fields_new.append(self.quote_name(field))
|
|
# Copy over the data
|
|
self.execute("INSERT INTO %s (%s) SELECT %s FROM %s;" % (
|
|
self.quote_name(dst),
|
|
', '.join(dst_fields_new),
|
|
', '.join(src_fields_new),
|
|
self.quote_name(src),
|
|
))
|
|
|
|
def _create_unique(self, table_name, columns):
|
|
self.execute("CREATE UNIQUE INDEX %s ON %s(%s);" % (
|
|
self.quote_name('%s_%s' % (table_name, '__'.join(columns))),
|
|
self.quote_name(table_name),
|
|
', '.join(self.quote_name(c) for c in columns),
|
|
))
|
|
|
|
def _get_multi_indexes(self, table_name):
|
|
indexes = []
|
|
cursor = self._get_connection().cursor()
|
|
cursor.execute('PRAGMA index_list(%s)' % self.quote_name(table_name))
|
|
# seq, name, unique
|
|
for index, unique in [(field[1], field[2]) for field in cursor.fetchall()]:
|
|
if not unique:
|
|
continue
|
|
cursor.execute('PRAGMA index_info(%s)' % self.quote_name(index))
|
|
info = cursor.fetchall()
|
|
if len(info) == 1:
|
|
continue
|
|
columns = []
|
|
for field in info:
|
|
columns.append(field[2])
|
|
indexes.append(columns)
|
|
return indexes
|
|
|
|
def _make_multi_indexes(self, table_name, indexes, deleted=[], renames={}, uniques_deleted=[]):
|
|
for index in indexes:
|
|
columns = []
|
|
|
|
for name in index:
|
|
# Handle deletion
|
|
if name in deleted:
|
|
columns = []
|
|
break
|
|
|
|
# Handle renames
|
|
if name in renames:
|
|
name = renames[name]
|
|
columns.append(name)
|
|
|
|
if columns and columns != uniques_deleted:
|
|
self._create_unique(table_name, columns)
|
|
|
|
def _column_sql_for_create(self, table_name, name, field, explicit_name=True):
|
|
"Given a field and its name, returns the full type for the CREATE TABLE."
|
|
field.set_attributes_from_name(name)
|
|
if not explicit_name:
|
|
name = field.db_column
|
|
else:
|
|
field.column = name
|
|
sql = self.column_sql(table_name, name, field, with_name=False, field_prepared=True)
|
|
#if field.primary_key:
|
|
# sql += " PRIMARY KEY"
|
|
#if field.unique:
|
|
# sql += " UNIQUE"
|
|
return sql
|
|
|
|
def alter_column(self, table_name, name, field, explicit_name=True):
|
|
"""
|
|
Changes a column's SQL definition
|
|
"""
|
|
# Remake the table correctly
|
|
self._remake_table(table_name, altered={
|
|
name: self._column_sql_for_create(table_name, name, field, explicit_name),
|
|
})
|
|
|
|
def delete_column(self, table_name, column_name):
|
|
"""
|
|
Deletes a column.
|
|
"""
|
|
self._remake_table(table_name, deleted=[column_name])
|
|
|
|
def rename_column(self, table_name, old, new):
|
|
"""
|
|
Renames a column from one name to another.
|
|
"""
|
|
self._remake_table(table_name, renames={old: new})
|
|
|
|
def create_unique(self, table_name, columns):
|
|
"""
|
|
Create an unique index on columns
|
|
"""
|
|
self._create_unique(table_name, columns)
|
|
|
|
def delete_unique(self, table_name, columns):
|
|
"""
|
|
Delete an unique index
|
|
"""
|
|
self._remake_table(table_name, uniques_deleted=columns)
|
|
|
|
def create_primary_key(self, table_name, columns):
|
|
if not isinstance(columns, (list, tuple)):
|
|
columns = [columns]
|
|
assert len(columns) == 1, "SQLite backend does not support multi-column primary keys"
|
|
self._remake_table(table_name, primary_key_override=columns[0])
|
|
|
|
# Not implemented this yet.
|
|
def delete_primary_key(self, table_name):
|
|
# By passing True in, we make sure we wipe all existing PKs.
|
|
self._remake_table(table_name, primary_key_override=True)
|
|
|
|
# No cascades on deletes
|
|
def delete_table(self, table_name, cascade=True):
|
|
generic.DatabaseOperations.delete_table(self, table_name, False)
|