You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

308 lines
14 KiB

  1. import codecs
  2. import contextlib
  3. import copy
  4. from decimal import Decimal
  5. from django.apps.registry import Apps
  6. from django.db.backends.base.schema import BaseDatabaseSchemaEditor
  7. from django.utils import six
  8. class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
  9. sql_delete_table = "DROP TABLE %(table)s"
  10. sql_create_inline_fk = "REFERENCES %(to_table)s (%(to_column)s)"
  11. sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
  12. sql_delete_unique = "DROP INDEX %(name)s"
  13. def __enter__(self):
  14. with self.connection.cursor() as c:
  15. # Some SQLite schema alterations need foreign key constraints to be
  16. # disabled. This is the default in SQLite but can be changed with a
  17. # build flag and might change in future, so can't be relied upon.
  18. # We enforce it here for the duration of the transaction.
  19. c.execute('PRAGMA foreign_keys')
  20. self._initial_pragma_fk = c.fetchone()[0]
  21. c.execute('PRAGMA foreign_keys = 0')
  22. return super(DatabaseSchemaEditor, self).__enter__()
  23. def __exit__(self, exc_type, exc_value, traceback):
  24. super(DatabaseSchemaEditor, self).__exit__(exc_type, exc_value, traceback)
  25. with self.connection.cursor() as c:
  26. # Restore initial FK setting - PRAGMA values can't be parametrized
  27. c.execute('PRAGMA foreign_keys = %s' % int(self._initial_pragma_fk))
  28. def quote_value(self, value):
  29. # The backend "mostly works" without this function and there are use
  30. # cases for compiling Python without the sqlite3 libraries (e.g.
  31. # security hardening).
  32. import sqlite3
  33. try:
  34. value = sqlite3.adapt(value)
  35. except sqlite3.ProgrammingError:
  36. pass
  37. # Manual emulation of SQLite parameter quoting
  38. if isinstance(value, type(True)):
  39. return str(int(value))
  40. elif isinstance(value, (Decimal, float)):
  41. return str(value)
  42. elif isinstance(value, six.integer_types):
  43. return str(value)
  44. elif isinstance(value, six.string_types):
  45. return "'%s'" % six.text_type(value).replace("\'", "\'\'")
  46. elif value is None:
  47. return "NULL"
  48. elif isinstance(value, (bytes, bytearray, six.memoryview)):
  49. # Bytes are only allowed for BLOB fields, encoded as string
  50. # literals containing hexadecimal data and preceded by a single "X"
  51. # character:
  52. # value = b'\x01\x02' => value_hex = b'0102' => return X'0102'
  53. value = bytes(value)
  54. hex_encoder = codecs.getencoder('hex_codec')
  55. value_hex, _length = hex_encoder(value)
  56. # Use 'ascii' encoding for b'01' => '01', no need to use force_text here.
  57. return "X'%s'" % value_hex.decode('ascii')
  58. else:
  59. raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value)))
  60. def _remake_table(self, model, create_fields=[], delete_fields=[], alter_fields=[], override_uniques=None,
  61. override_indexes=None):
  62. """
  63. Shortcut to transform a model from old_model into new_model
  64. The essential steps are:
  65. 1. rename the model's existing table, e.g. "app_model" to "app_model__old"
  66. 2. create a table with the updated definition called "app_model"
  67. 3. copy the data from the old renamed table to the new table
  68. 4. delete the "app_model__old" table
  69. """
  70. # Self-referential fields must be recreated rather than copied from
  71. # the old model to ensure their remote_field.field_name doesn't refer
  72. # to an altered field.
  73. def is_self_referential(f):
  74. return f.is_relation and f.remote_field.model is model
  75. # Work out the new fields dict / mapping
  76. body = {
  77. f.name: f.clone() if is_self_referential(f) else f
  78. for f in model._meta.local_concrete_fields
  79. }
  80. # Since mapping might mix column names and default values,
  81. # its values must be already quoted.
  82. mapping = {f.column: self.quote_name(f.column) for f in model._meta.local_concrete_fields}
  83. # This maps field names (not columns) for things like unique_together
  84. rename_mapping = {}
  85. # If any of the new or altered fields is introducing a new PK,
  86. # remove the old one
  87. restore_pk_field = None
  88. if any(f.primary_key for f in create_fields) or any(n.primary_key for o, n in alter_fields):
  89. for name, field in list(body.items()):
  90. if field.primary_key:
  91. field.primary_key = False
  92. restore_pk_field = field
  93. if field.auto_created:
  94. del body[name]
  95. del mapping[field.column]
  96. # Add in any created fields
  97. for field in create_fields:
  98. body[field.name] = field
  99. # Choose a default and insert it into the copy map
  100. if not field.many_to_many and field.concrete:
  101. mapping[field.column] = self.quote_value(
  102. self.effective_default(field)
  103. )
  104. # Add in any altered fields
  105. for (old_field, new_field) in alter_fields:
  106. body.pop(old_field.name, None)
  107. mapping.pop(old_field.column, None)
  108. body[new_field.name] = new_field
  109. if old_field.null and not new_field.null:
  110. case_sql = "coalesce(%(col)s, %(default)s)" % {
  111. 'col': self.quote_name(old_field.column),
  112. 'default': self.quote_value(self.effective_default(new_field))
  113. }
  114. mapping[new_field.column] = case_sql
  115. else:
  116. mapping[new_field.column] = self.quote_name(old_field.column)
  117. rename_mapping[old_field.name] = new_field.name
  118. # Remove any deleted fields
  119. for field in delete_fields:
  120. del body[field.name]
  121. del mapping[field.column]
  122. # Remove any implicit M2M tables
  123. if field.many_to_many and field.remote_field.through._meta.auto_created:
  124. return self.delete_model(field.remote_field.through)
  125. # Work inside a new app registry
  126. apps = Apps()
  127. # Provide isolated instances of the fields to the new model body so
  128. # that the existing model's internals aren't interfered with when
  129. # the dummy model is constructed.
  130. body = copy.deepcopy(body)
  131. # Work out the new value of unique_together, taking renames into
  132. # account
  133. if override_uniques is None:
  134. override_uniques = [
  135. [rename_mapping.get(n, n) for n in unique]
  136. for unique in model._meta.unique_together
  137. ]
  138. # Work out the new value for index_together, taking renames into
  139. # account
  140. if override_indexes is None:
  141. override_indexes = [
  142. [rename_mapping.get(n, n) for n in index]
  143. for index in model._meta.index_together
  144. ]
  145. # Construct a new model for the new state
  146. meta_contents = {
  147. 'app_label': model._meta.app_label,
  148. 'db_table': model._meta.db_table,
  149. 'unique_together': override_uniques,
  150. 'index_together': override_indexes,
  151. 'apps': apps,
  152. }
  153. meta = type("Meta", tuple(), meta_contents)
  154. body['Meta'] = meta
  155. body['__module__'] = model.__module__
  156. temp_model = type(model._meta.object_name, model.__bases__, body)
  157. # We need to modify model._meta.db_table, but everything explodes
  158. # if the change isn't reversed before the end of this method. This
  159. # context manager helps us avoid that situation.
  160. @contextlib.contextmanager
  161. def altered_table_name(model, temporary_table_name):
  162. original_table_name = model._meta.db_table
  163. model._meta.db_table = temporary_table_name
  164. yield
  165. model._meta.db_table = original_table_name
  166. with altered_table_name(model, model._meta.db_table + "__old"):
  167. # Rename the old table to make way for the new
  168. self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table)
  169. # Create a new table with the updated schema. We remove things
  170. # from the deferred SQL that match our table name, too
  171. self.deferred_sql = [x for x in self.deferred_sql if temp_model._meta.db_table not in x]
  172. self.create_model(temp_model)
  173. # Copy data from the old table into the new table
  174. field_maps = list(mapping.items())
  175. self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
  176. self.quote_name(temp_model._meta.db_table),
  177. ', '.join(self.quote_name(x) for x, y in field_maps),
  178. ', '.join(y for x, y in field_maps),
  179. self.quote_name(model._meta.db_table),
  180. ))
  181. # Delete the old table
  182. self.delete_model(model, handle_autom2m=False)
  183. # Run deferred SQL on correct table
  184. for sql in self.deferred_sql:
  185. self.execute(sql)
  186. self.deferred_sql = []
  187. # Fix any PK-removed field
  188. if restore_pk_field:
  189. restore_pk_field.primary_key = True
  190. def delete_model(self, model, handle_autom2m=True):
  191. if handle_autom2m:
  192. super(DatabaseSchemaEditor, self).delete_model(model)
  193. else:
  194. # Delete the table (and only that)
  195. self.execute(self.sql_delete_table % {
  196. "table": self.quote_name(model._meta.db_table),
  197. })
  198. def add_field(self, model, field):
  199. """
  200. Creates a field on a model.
  201. Usually involves adding a column, but may involve adding a
  202. table instead (for M2M fields)
  203. """
  204. # Special-case implicit M2M tables
  205. if field.many_to_many and field.remote_field.through._meta.auto_created:
  206. return self.create_model(field.remote_field.through)
  207. self._remake_table(model, create_fields=[field])
  208. def remove_field(self, model, field):
  209. """
  210. Removes a field from a model. Usually involves deleting a column,
  211. but for M2Ms may involve deleting a table.
  212. """
  213. # M2M fields are a special case
  214. if field.many_to_many:
  215. # For implicit M2M tables, delete the auto-created table
  216. if field.remote_field.through._meta.auto_created:
  217. self.delete_model(field.remote_field.through)
  218. # For explicit "through" M2M fields, do nothing
  219. # For everything else, remake.
  220. else:
  221. # It might not actually have a column behind it
  222. if field.db_parameters(connection=self.connection)['type'] is None:
  223. return
  224. self._remake_table(model, delete_fields=[field])
  225. def _alter_field(self, model, old_field, new_field, old_type, new_type,
  226. old_db_params, new_db_params, strict=False):
  227. """Actually perform a "physical" (non-ManyToMany) field update."""
  228. # Alter by remaking table
  229. self._remake_table(model, alter_fields=[(old_field, new_field)])
  230. def alter_index_together(self, model, old_index_together, new_index_together):
  231. """
  232. Deals with a model changing its index_together.
  233. Note: The input index_togethers must be doubly-nested, not the single-
  234. nested ["foo", "bar"] format.
  235. """
  236. self._remake_table(model, override_indexes=new_index_together)
  237. def alter_unique_together(self, model, old_unique_together, new_unique_together):
  238. """
  239. Deals with a model changing its unique_together.
  240. Note: The input unique_togethers must be doubly-nested, not the single-
  241. nested ["foo", "bar"] format.
  242. """
  243. self._remake_table(model, override_uniques=new_unique_together)
  244. def _alter_many_to_many(self, model, old_field, new_field, strict):
  245. """
  246. Alters M2Ms to repoint their to= endpoints.
  247. """
  248. if old_field.remote_field.through._meta.db_table == new_field.remote_field.through._meta.db_table:
  249. # The field name didn't change, but some options did; we have to propagate this altering.
  250. self._remake_table(
  251. old_field.remote_field.through,
  252. alter_fields=[(
  253. # We need the field that points to the target model, so we can tell alter_field to change it -
  254. # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
  255. old_field.remote_field.through._meta.get_field(old_field.m2m_reverse_field_name()),
  256. new_field.remote_field.through._meta.get_field(new_field.m2m_reverse_field_name()),
  257. )],
  258. override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()),
  259. )
  260. return
  261. # Make a new through table
  262. self.create_model(new_field.remote_field.through)
  263. # Copy the data across
  264. self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
  265. self.quote_name(new_field.remote_field.through._meta.db_table),
  266. ', '.join([
  267. "id",
  268. new_field.m2m_column_name(),
  269. new_field.m2m_reverse_name(),
  270. ]),
  271. ', '.join([
  272. "id",
  273. old_field.m2m_column_name(),
  274. old_field.m2m_reverse_name(),
  275. ]),
  276. self.quote_name(old_field.remote_field.through._meta.db_table),
  277. ))
  278. # Delete the old through table
  279. self.delete_model(old_field.remote_field.through)