Development of an internal social media platform with personalised dashboards for students
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.

schema.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import contextlib
  2. import copy
  3. from decimal import Decimal
  4. from django.apps.registry import Apps
  5. from django.db.backends.base.schema import BaseDatabaseSchemaEditor
  6. from django.db.backends.ddl_references import Statement
  7. from django.db.transaction import atomic
  8. from django.db.utils import NotSupportedError
  9. class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
  10. sql_delete_table = "DROP TABLE %(table)s"
  11. sql_create_fk = None
  12. sql_create_inline_fk = "REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED"
  13. sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
  14. sql_delete_unique = "DROP INDEX %(name)s"
  15. def __enter__(self):
  16. # Some SQLite schema alterations need foreign key constraints to be
  17. # disabled. Enforce it here for the duration of the transaction.
  18. self.connection.disable_constraint_checking()
  19. return super().__enter__()
  20. def __exit__(self, exc_type, exc_value, traceback):
  21. super().__exit__(exc_type, exc_value, traceback)
  22. self.connection.enable_constraint_checking()
  23. def quote_value(self, value):
  24. # The backend "mostly works" without this function and there are use
  25. # cases for compiling Python without the sqlite3 libraries (e.g.
  26. # security hardening).
  27. try:
  28. import sqlite3
  29. value = sqlite3.adapt(value)
  30. except ImportError:
  31. pass
  32. except sqlite3.ProgrammingError:
  33. pass
  34. # Manual emulation of SQLite parameter quoting
  35. if isinstance(value, bool):
  36. return str(int(value))
  37. elif isinstance(value, (Decimal, float, int)):
  38. return str(value)
  39. elif isinstance(value, str):
  40. return "'%s'" % value.replace("\'", "\'\'")
  41. elif value is None:
  42. return "NULL"
  43. elif isinstance(value, (bytes, bytearray, memoryview)):
  44. # Bytes are only allowed for BLOB fields, encoded as string
  45. # literals containing hexadecimal data and preceded by a single "X"
  46. # character.
  47. return "X'%s'" % value.hex()
  48. else:
  49. raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value)))
  50. def _is_referenced_by_fk_constraint(self, table_name, column_name=None, ignore_self=False):
  51. """
  52. Return whether or not the provided table name is referenced by another
  53. one. If `column_name` is specified, only references pointing to that
  54. column are considered. If `ignore_self` is True, self-referential
  55. constraints are ignored.
  56. """
  57. with self.connection.cursor() as cursor:
  58. for other_table in self.connection.introspection.get_table_list(cursor):
  59. if ignore_self and other_table.name == table_name:
  60. continue
  61. constraints = self.connection.introspection._get_foreign_key_constraints(cursor, other_table.name)
  62. for constraint in constraints.values():
  63. constraint_table, constraint_column = constraint['foreign_key']
  64. if (constraint_table == table_name and
  65. (column_name is None or constraint_column == column_name)):
  66. return True
  67. return False
  68. def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True):
  69. if disable_constraints and self._is_referenced_by_fk_constraint(old_db_table):
  70. if self.connection.in_atomic_block:
  71. raise NotSupportedError((
  72. 'Renaming the %r table while in a transaction is not '
  73. 'supported on SQLite because it would break referential '
  74. 'integrity. Try adding `atomic = False` to the Migration class.'
  75. ) % old_db_table)
  76. self.connection.enable_constraint_checking()
  77. super().alter_db_table(model, old_db_table, new_db_table)
  78. self.connection.disable_constraint_checking()
  79. else:
  80. super().alter_db_table(model, old_db_table, new_db_table)
  81. def alter_field(self, model, old_field, new_field, strict=False):
  82. old_field_name = old_field.name
  83. table_name = model._meta.db_table
  84. _, old_column_name = old_field.get_attname_column()
  85. if (new_field.name != old_field_name and
  86. self._is_referenced_by_fk_constraint(table_name, old_column_name, ignore_self=True)):
  87. if self.connection.in_atomic_block:
  88. raise NotSupportedError((
  89. 'Renaming the %r.%r column while in a transaction is not '
  90. 'supported on SQLite because it would break referential '
  91. 'integrity. Try adding `atomic = False` to the Migration class.'
  92. ) % (model._meta.db_table, old_field_name))
  93. with atomic(self.connection.alias):
  94. super().alter_field(model, old_field, new_field, strict=strict)
  95. # Follow SQLite's documented procedure for performing changes
  96. # that don't affect the on-disk content.
  97. # https://sqlite.org/lang_altertable.html#otheralter
  98. with self.connection.cursor() as cursor:
  99. schema_version = cursor.execute('PRAGMA schema_version').fetchone()[0]
  100. cursor.execute('PRAGMA writable_schema = 1')
  101. references_template = ' REFERENCES "%s" ("%%s") ' % table_name
  102. new_column_name = new_field.get_attname_column()[1]
  103. search = references_template % old_column_name
  104. replacement = references_template % new_column_name
  105. cursor.execute('UPDATE sqlite_master SET sql = replace(sql, %s, %s)', (search, replacement))
  106. cursor.execute('PRAGMA schema_version = %d' % (schema_version + 1))
  107. cursor.execute('PRAGMA writable_schema = 0')
  108. # The integrity check will raise an exception and rollback
  109. # the transaction if the sqlite_master updates corrupt the
  110. # database.
  111. cursor.execute('PRAGMA integrity_check')
  112. # Perform a VACUUM to refresh the database representation from
  113. # the sqlite_master table.
  114. with self.connection.cursor() as cursor:
  115. cursor.execute('VACUUM')
  116. else:
  117. super().alter_field(model, old_field, new_field, strict=strict)
  118. def _remake_table(self, model, create_field=None, delete_field=None, alter_field=None):
  119. """
  120. Shortcut to transform a model from old_model into new_model
  121. The essential steps are:
  122. 1. rename the model's existing table, e.g. "app_model" to "app_model__old"
  123. 2. create a table with the updated definition called "app_model"
  124. 3. copy the data from the old renamed table to the new table
  125. 4. delete the "app_model__old" table
  126. """
  127. # Self-referential fields must be recreated rather than copied from
  128. # the old model to ensure their remote_field.field_name doesn't refer
  129. # to an altered field.
  130. def is_self_referential(f):
  131. return f.is_relation and f.remote_field.model is model
  132. # Work out the new fields dict / mapping
  133. body = {
  134. f.name: f.clone() if is_self_referential(f) else f
  135. for f in model._meta.local_concrete_fields
  136. }
  137. # Since mapping might mix column names and default values,
  138. # its values must be already quoted.
  139. mapping = {f.column: self.quote_name(f.column) for f in model._meta.local_concrete_fields}
  140. # This maps field names (not columns) for things like unique_together
  141. rename_mapping = {}
  142. # If any of the new or altered fields is introducing a new PK,
  143. # remove the old one
  144. restore_pk_field = None
  145. if getattr(create_field, 'primary_key', False) or (
  146. alter_field and getattr(alter_field[1], 'primary_key', False)):
  147. for name, field in list(body.items()):
  148. if field.primary_key:
  149. field.primary_key = False
  150. restore_pk_field = field
  151. if field.auto_created:
  152. del body[name]
  153. del mapping[field.column]
  154. # Add in any created fields
  155. if create_field:
  156. body[create_field.name] = create_field
  157. # Choose a default and insert it into the copy map
  158. if not create_field.many_to_many and create_field.concrete:
  159. mapping[create_field.column] = self.quote_value(
  160. self.effective_default(create_field)
  161. )
  162. # Add in any altered fields
  163. if alter_field:
  164. old_field, new_field = alter_field
  165. body.pop(old_field.name, None)
  166. mapping.pop(old_field.column, None)
  167. body[new_field.name] = new_field
  168. if old_field.null and not new_field.null:
  169. case_sql = "coalesce(%(col)s, %(default)s)" % {
  170. 'col': self.quote_name(old_field.column),
  171. 'default': self.quote_value(self.effective_default(new_field))
  172. }
  173. mapping[new_field.column] = case_sql
  174. else:
  175. mapping[new_field.column] = self.quote_name(old_field.column)
  176. rename_mapping[old_field.name] = new_field.name
  177. # Remove any deleted fields
  178. if delete_field:
  179. del body[delete_field.name]
  180. del mapping[delete_field.column]
  181. # Remove any implicit M2M tables
  182. if delete_field.many_to_many and delete_field.remote_field.through._meta.auto_created:
  183. return self.delete_model(delete_field.remote_field.through)
  184. # Work inside a new app registry
  185. apps = Apps()
  186. # Provide isolated instances of the fields to the new model body so
  187. # that the existing model's internals aren't interfered with when
  188. # the dummy model is constructed.
  189. body = copy.deepcopy(body)
  190. # Work out the new value of unique_together, taking renames into
  191. # account
  192. unique_together = [
  193. [rename_mapping.get(n, n) for n in unique]
  194. for unique in model._meta.unique_together
  195. ]
  196. # Work out the new value for index_together, taking renames into
  197. # account
  198. index_together = [
  199. [rename_mapping.get(n, n) for n in index]
  200. for index in model._meta.index_together
  201. ]
  202. indexes = model._meta.indexes
  203. if delete_field:
  204. indexes = [
  205. index for index in indexes
  206. if delete_field.name not in index.fields
  207. ]
  208. # Construct a new model for the new state
  209. meta_contents = {
  210. 'app_label': model._meta.app_label,
  211. 'db_table': model._meta.db_table,
  212. 'unique_together': unique_together,
  213. 'index_together': index_together,
  214. 'indexes': indexes,
  215. 'apps': apps,
  216. }
  217. meta = type("Meta", (), meta_contents)
  218. body['Meta'] = meta
  219. body['__module__'] = model.__module__
  220. temp_model = type(model._meta.object_name, model.__bases__, body)
  221. # We need to modify model._meta.db_table, but everything explodes
  222. # if the change isn't reversed before the end of this method. This
  223. # context manager helps us avoid that situation.
  224. @contextlib.contextmanager
  225. def altered_table_name(model, temporary_table_name):
  226. original_table_name = model._meta.db_table
  227. model._meta.db_table = temporary_table_name
  228. yield
  229. model._meta.db_table = original_table_name
  230. with altered_table_name(model, model._meta.db_table + "__old"):
  231. # Rename the old table to make way for the new
  232. self.alter_db_table(
  233. model, temp_model._meta.db_table, model._meta.db_table,
  234. disable_constraints=False,
  235. )
  236. # Create a new table with the updated schema.
  237. self.create_model(temp_model)
  238. # Copy data from the old table into the new table
  239. field_maps = list(mapping.items())
  240. self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
  241. self.quote_name(temp_model._meta.db_table),
  242. ', '.join(self.quote_name(x) for x, y in field_maps),
  243. ', '.join(y for x, y in field_maps),
  244. self.quote_name(model._meta.db_table),
  245. ))
  246. # Delete the old table
  247. self.delete_model(model, handle_autom2m=False)
  248. # Run deferred SQL on correct table
  249. for sql in self.deferred_sql:
  250. self.execute(sql)
  251. self.deferred_sql = []
  252. # Fix any PK-removed field
  253. if restore_pk_field:
  254. restore_pk_field.primary_key = True
  255. def delete_model(self, model, handle_autom2m=True):
  256. if handle_autom2m:
  257. super().delete_model(model)
  258. else:
  259. # Delete the table (and only that)
  260. self.execute(self.sql_delete_table % {
  261. "table": self.quote_name(model._meta.db_table),
  262. })
  263. # Remove all deferred statements referencing the deleted table.
  264. for sql in list(self.deferred_sql):
  265. if isinstance(sql, Statement) and sql.references_table(model._meta.db_table):
  266. self.deferred_sql.remove(sql)
  267. def add_field(self, model, field):
  268. """
  269. Create a field on a model. Usually involves adding a column, but may
  270. involve adding a table instead (for M2M fields).
  271. """
  272. # Special-case implicit M2M tables
  273. if field.many_to_many and field.remote_field.through._meta.auto_created:
  274. return self.create_model(field.remote_field.through)
  275. self._remake_table(model, create_field=field)
  276. def remove_field(self, model, field):
  277. """
  278. Remove a field from a model. Usually involves deleting a column,
  279. but for M2Ms may involve deleting a table.
  280. """
  281. # M2M fields are a special case
  282. if field.many_to_many:
  283. # For implicit M2M tables, delete the auto-created table
  284. if field.remote_field.through._meta.auto_created:
  285. self.delete_model(field.remote_field.through)
  286. # For explicit "through" M2M fields, do nothing
  287. # For everything else, remake.
  288. else:
  289. # It might not actually have a column behind it
  290. if field.db_parameters(connection=self.connection)['type'] is None:
  291. return
  292. self._remake_table(model, delete_field=field)
  293. def _alter_field(self, model, old_field, new_field, old_type, new_type,
  294. old_db_params, new_db_params, strict=False):
  295. """Perform a "physical" (non-ManyToMany) field update."""
  296. # Alter by remaking table
  297. self._remake_table(model, alter_field=(old_field, new_field))
  298. # Rebuild tables with FKs pointing to this field if the PK type changed.
  299. if old_field.primary_key and new_field.primary_key and old_type != new_type:
  300. for rel in new_field.model._meta.related_objects:
  301. if not rel.many_to_many:
  302. self._remake_table(rel.related_model)
  303. def _alter_many_to_many(self, model, old_field, new_field, strict):
  304. """Alter M2Ms to repoint their to= endpoints."""
  305. if old_field.remote_field.through._meta.db_table == new_field.remote_field.through._meta.db_table:
  306. # The field name didn't change, but some options did; we have to propagate this altering.
  307. self._remake_table(
  308. old_field.remote_field.through,
  309. alter_field=(
  310. # We need the field that points to the target model, so we can tell alter_field to change it -
  311. # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
  312. old_field.remote_field.through._meta.get_field(old_field.m2m_reverse_field_name()),
  313. new_field.remote_field.through._meta.get_field(new_field.m2m_reverse_field_name()),
  314. ),
  315. )
  316. return
  317. # Make a new through table
  318. self.create_model(new_field.remote_field.through)
  319. # Copy the data across
  320. self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
  321. self.quote_name(new_field.remote_field.through._meta.db_table),
  322. ', '.join([
  323. "id",
  324. new_field.m2m_column_name(),
  325. new_field.m2m_reverse_name(),
  326. ]),
  327. ', '.join([
  328. "id",
  329. old_field.m2m_column_name(),
  330. old_field.m2m_reverse_name(),
  331. ]),
  332. self.quote_name(old_field.remote_field.through._meta.db_table),
  333. ))
  334. # Delete the old through table
  335. self.delete_model(old_field.remote_field.through)