sqldiff.py 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382
  1. # -*- coding: utf-8 -*-
  2. """
  3. sqldiff.py - Prints the (approximated) difference between models and database
  4. TODO:
  5. - better support for relations
  6. - better support for constraints (mainly postgresql?)
  7. - support for table spaces with postgresql
  8. - when a table is not managed (meta.managed==False) then only do a one-way
  9. sqldiff ? show differences from db->table but not the other way around since
  10. it's not managed.
  11. KNOWN ISSUES:
  12. - MySQL has by far the most problems with introspection. Please be
  13. carefull when using MySQL with sqldiff.
  14. - Booleans are reported back as Integers, so there's no way to know if
  15. there was a real change.
  16. - Varchar sizes are reported back without unicode support so their size
  17. may change in comparison to the real length of the varchar.
  18. - Some of the 'fixes' to counter these problems might create false
  19. positives or false negatives.
  20. """
  21. import importlib
  22. import sys
  23. import six
  24. import argparse
  25. from typing import Dict, Union, Callable, Optional # NOQA
  26. from django.apps import apps
  27. from django.core.management import BaseCommand, CommandError
  28. from django.core.management.base import OutputWrapper
  29. from django.core.management.color import no_style
  30. from django.db import connection, transaction, models
  31. from django.db.models.fields import AutoField, IntegerField
  32. from django.db.models.options import normalize_together
  33. from django_extensions.management.utils import signalcommand
  34. ORDERING_FIELD = IntegerField('_order', null=True)
  35. def flatten(lst, ltypes=(list, tuple)):
  36. ltype = type(lst)
  37. lst = list(lst)
  38. i = 0
  39. while i < len(lst):
  40. while isinstance(lst[i], ltypes):
  41. if not lst[i]:
  42. lst.pop(i)
  43. i -= 1
  44. break
  45. else:
  46. lst[i:i + 1] = lst[i]
  47. i += 1
  48. return ltype(lst)
  49. def all_local_fields(meta):
  50. all_fields = []
  51. if meta.proxy:
  52. for parent in meta.parents:
  53. all_fields.extend(all_local_fields(parent._meta))
  54. else:
  55. for f in meta.local_fields:
  56. col_type = f.db_type(connection=connection)
  57. if col_type is None:
  58. continue
  59. all_fields.append(f)
  60. return all_fields
  61. class SQLDiff:
  62. DATA_TYPES_REVERSE_OVERRIDE = {} # type: Dict[int, Union[str, Callable]]
  63. IGNORE_MISSING_TABLES = [
  64. "django_migrations",
  65. ]
  66. DIFF_TYPES = [
  67. 'error',
  68. 'comment',
  69. 'table-missing-in-db',
  70. 'table-missing-in-model',
  71. 'field-missing-in-db',
  72. 'field-missing-in-model',
  73. 'fkey-missing-in-db',
  74. 'fkey-missing-in-model',
  75. 'index-missing-in-db',
  76. 'index-missing-in-model',
  77. 'unique-missing-in-db',
  78. 'unique-missing-in-model',
  79. 'field-type-differ',
  80. 'field-parameter-differ',
  81. 'notnull-differ',
  82. ]
  83. DIFF_TEXTS = {
  84. 'error': 'error: %(0)s',
  85. 'comment': 'comment: %(0)s',
  86. 'table-missing-in-db': "table '%(0)s' missing in database",
  87. 'table-missing-in-model': "table '%(0)s' missing in models",
  88. 'field-missing-in-db': "field '%(1)s' defined in model but missing in database",
  89. 'field-missing-in-model': "field '%(1)s' defined in database but missing in model",
  90. 'fkey-missing-in-db': "field '%(1)s' FOREIGN KEY defined in model but missing in database",
  91. 'fkey-missing-in-model': "field '%(1)s' FOREIGN KEY defined in database but missing in model",
  92. 'index-missing-in-db': "field '%(1)s' INDEX named '%(2)s' defined in model but missing in database",
  93. 'index-missing-in-model': "field '%(1)s' INDEX defined in database schema but missing in model",
  94. 'unique-missing-in-db': "field '%(1)s' UNIQUE named '%(2)s' defined in model but missing in database",
  95. 'unique-missing-in-model': "field '%(1)s' UNIQUE defined in database schema but missing in model",
  96. 'field-type-differ': "field '%(1)s' not of same type: db='%(3)s', model='%(2)s'",
  97. 'field-parameter-differ': "field '%(1)s' parameters differ: db='%(3)s', model='%(2)s'",
  98. 'notnull-differ': "field '%(1)s' null constraint should be '%(2)s' in the database",
  99. }
  100. SQL_FIELD_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % (
  101. style.SQL_KEYWORD('ALTER TABLE'),
  102. style.SQL_TABLE(qn(args[0])),
  103. style.SQL_KEYWORD('ADD COLUMN'),
  104. style.SQL_FIELD(qn(args[1])),
  105. ' '.join(style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) for i, a in enumerate(args[2:]))
  106. )
  107. SQL_FIELD_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s\n\t%s %s;" % (
  108. style.SQL_KEYWORD('ALTER TABLE'),
  109. style.SQL_TABLE(qn(args[0])),
  110. style.SQL_KEYWORD('DROP COLUMN'),
  111. style.SQL_FIELD(qn(args[1]))
  112. )
  113. SQL_FKEY_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s %s (%s)%s;" % (
  114. style.SQL_KEYWORD('ALTER TABLE'),
  115. style.SQL_TABLE(qn(args[0])),
  116. style.SQL_KEYWORD('ADD COLUMN'),
  117. style.SQL_FIELD(qn(args[1])),
  118. ' '.join(style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) for i, a in enumerate(args[4:])),
  119. style.SQL_KEYWORD('REFERENCES'),
  120. style.SQL_TABLE(qn(args[2])),
  121. style.SQL_FIELD(qn(args[3])),
  122. connection.ops.deferrable_sql()
  123. )
  124. SQL_INDEX_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s (%s%s);" % (
  125. style.SQL_KEYWORD('CREATE INDEX'),
  126. style.SQL_TABLE(qn(args[2])),
  127. # style.SQL_TABLE(qn("%s" % '_'.join('_'.join(a) if isinstance(a, (list, tuple)) else a for a in args[0:3] if a))),
  128. style.SQL_KEYWORD('ON'), style.SQL_TABLE(qn(args[0])),
  129. style.SQL_FIELD(', '.join(qn(e) for e in args[1])),
  130. style.SQL_KEYWORD(args[3])
  131. )
  132. SQL_INDEX_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s;" % (
  133. style.SQL_KEYWORD('DROP INDEX'),
  134. style.SQL_TABLE(qn(args[1]))
  135. )
  136. SQL_UNIQUE_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s (%s);" % (
  137. style.SQL_KEYWORD('ALTER TABLE'),
  138. style.SQL_TABLE(qn(args[0])),
  139. style.SQL_KEYWORD('ADD CONSTRAINT'),
  140. style.SQL_TABLE(qn(args[2])),
  141. style.SQL_KEYWORD('UNIQUE'),
  142. style.SQL_FIELD(', '.join(qn(e) for e in args[1]))
  143. )
  144. SQL_UNIQUE_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % (
  145. style.SQL_KEYWORD('ALTER TABLE'),
  146. style.SQL_TABLE(qn(args[0])),
  147. style.SQL_KEYWORD('DROP'),
  148. style.SQL_KEYWORD('CONSTRAINT'),
  149. style.SQL_TABLE(qn(args[1]))
  150. )
  151. SQL_FIELD_TYPE_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % (
  152. style.SQL_KEYWORD('ALTER TABLE'),
  153. style.SQL_TABLE(qn(args[0])),
  154. style.SQL_KEYWORD("MODIFY"),
  155. style.SQL_FIELD(qn(args[1])),
  156. style.SQL_COLTYPE(args[2])
  157. )
  158. SQL_FIELD_PARAMETER_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % (
  159. style.SQL_KEYWORD('ALTER TABLE'),
  160. style.SQL_TABLE(qn(args[0])),
  161. style.SQL_KEYWORD("MODIFY"),
  162. style.SQL_FIELD(qn(args[1])),
  163. style.SQL_COLTYPE(args[2])
  164. )
  165. SQL_NOTNULL_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (
  166. style.SQL_KEYWORD('ALTER TABLE'),
  167. style.SQL_TABLE(qn(args[0])),
  168. style.SQL_KEYWORD('MODIFY'),
  169. style.SQL_FIELD(qn(args[1])),
  170. style.SQL_KEYWORD(args[2]),
  171. style.SQL_KEYWORD('NOT NULL')
  172. )
  173. SQL_ERROR = lambda self, style, qn, args: style.NOTICE('-- Error: %s' % style.ERROR(args[0]))
  174. SQL_COMMENT = lambda self, style, qn, args: style.NOTICE('-- Comment: %s' % style.SQL_TABLE(args[0]))
  175. SQL_TABLE_MISSING_IN_DB = lambda self, style, qn, args: style.NOTICE('-- Table missing: %s' % args[0])
  176. SQL_TABLE_MISSING_IN_MODEL = lambda self, style, qn, args: style.NOTICE('-- Model missing for table: %s' % args[0])
  177. can_detect_notnull_differ = False
  178. can_detect_unsigned_differ = False
  179. unsigned_suffix = None # type: Optional[str]
  180. def __init__(self, app_models, options, stdout, stderr):
  181. self.has_differences = None
  182. self.app_models = app_models
  183. self.options = options
  184. self.dense = options['dense_output']
  185. self.stdout = stdout
  186. self.stderr = stderr
  187. self.introspection = connection.introspection
  188. self.differences = []
  189. self.unknown_db_fields = {}
  190. self.new_db_fields = set()
  191. self.null = {}
  192. self.unsigned = set()
  193. self.DIFF_SQL = {
  194. 'error': self.SQL_ERROR,
  195. 'comment': self.SQL_COMMENT,
  196. 'table-missing-in-db': self.SQL_TABLE_MISSING_IN_DB,
  197. 'table-missing-in-model': self.SQL_TABLE_MISSING_IN_MODEL,
  198. 'field-missing-in-db': self.SQL_FIELD_MISSING_IN_DB,
  199. 'field-missing-in-model': self.SQL_FIELD_MISSING_IN_MODEL,
  200. 'fkey-missing-in-db': self.SQL_FKEY_MISSING_IN_DB,
  201. 'fkey-missing-in-model': self.SQL_FIELD_MISSING_IN_MODEL,
  202. 'index-missing-in-db': self.SQL_INDEX_MISSING_IN_DB,
  203. 'index-missing-in-model': self.SQL_INDEX_MISSING_IN_MODEL,
  204. 'unique-missing-in-db': self.SQL_UNIQUE_MISSING_IN_DB,
  205. 'unique-missing-in-model': self.SQL_UNIQUE_MISSING_IN_MODEL,
  206. 'field-type-differ': self.SQL_FIELD_TYPE_DIFFER,
  207. 'field-parameter-differ': self.SQL_FIELD_PARAMETER_DIFFER,
  208. 'notnull-differ': self.SQL_NOTNULL_DIFFER,
  209. }
  210. def load(self):
  211. self.cursor = connection.cursor()
  212. self.django_tables = self.introspection.django_table_names(only_existing=self.options['only_existing'])
  213. # TODO: We are losing information about tables which are views here
  214. self.db_tables = [table_info.name for table_info in self.introspection.get_table_list(self.cursor)]
  215. if self.can_detect_notnull_differ:
  216. self.load_null()
  217. if self.can_detect_unsigned_differ:
  218. self.load_unsigned()
  219. def load_null(self):
  220. raise NotImplementedError("load_null functions must be implemented if diff backend has 'can_detect_notnull_differ' set to True")
  221. def load_unsigned(self):
  222. raise NotImplementedError("load_unsigned function must be implemented if diff backend has 'can_detect_unsigned_differ' set to True")
  223. def add_app_model_marker(self, app_label, model_name):
  224. self.differences.append((app_label, model_name, []))
  225. def add_difference(self, diff_type, *args):
  226. assert diff_type in self.DIFF_TYPES, 'Unknown difference type'
  227. self.differences[-1][-1].append((diff_type, args))
  228. def get_data_types_reverse_override(self):
  229. # type: () -> Dict[int, Union[str, Callable]]
  230. return self.DATA_TYPES_REVERSE_OVERRIDE
  231. def format_field_names(self, field_names):
  232. return field_names
  233. def sql_to_dict(self, query, param):
  234. """
  235. Execute query and return a dict
  236. sql_to_dict(query, param) -> list of dicts
  237. code from snippet at http://www.djangosnippets.org/snippets/1383/
  238. """
  239. cursor = connection.cursor()
  240. cursor.execute(query, param)
  241. fieldnames = [name[0] for name in cursor.description]
  242. fieldnames = self.format_field_names(fieldnames)
  243. result = []
  244. for row in cursor.fetchall():
  245. rowset = []
  246. for field in zip(fieldnames, row):
  247. rowset.append(field)
  248. result.append(dict(rowset))
  249. return result
  250. def get_field_model_type(self, field):
  251. return field.db_type(connection=connection)
  252. def get_field_db_type_kwargs(self, current_kwargs, description, field=None, table_name=None, reverse_type=None):
  253. return {}
  254. def get_field_db_type(self, description, field=None, table_name=None):
  255. # DB-API cursor.description
  256. # (name, type_code, display_size, internal_size, precision, scale, null_ok) = description
  257. type_code = description[1]
  258. DATA_TYPES_REVERSE_OVERRIDE = self.get_data_types_reverse_override()
  259. if type_code in DATA_TYPES_REVERSE_OVERRIDE:
  260. reverse_type = DATA_TYPES_REVERSE_OVERRIDE[type_code]
  261. else:
  262. try:
  263. reverse_type = self.introspection.get_field_type(type_code, description)
  264. except KeyError:
  265. reverse_type = self.get_field_db_type_lookup(type_code)
  266. if not reverse_type:
  267. # type_code not found in data_types_reverse map
  268. key = (self.differences[-1][:2], description[:2])
  269. if key not in self.unknown_db_fields:
  270. self.unknown_db_fields[key] = 1
  271. self.add_difference('comment', "Unknown database type for field '%s' (%s)" % (description[0], type_code))
  272. return None
  273. if callable(reverse_type):
  274. reverse_type = reverse_type()
  275. kwargs = {}
  276. if isinstance(reverse_type, dict):
  277. kwargs.update(reverse_type['kwargs'])
  278. reverse_type = reverse_type['name']
  279. if type_code == 16946 and field and getattr(field, 'geom_type', None) == 'POINT':
  280. reverse_type = 'django.contrib.gis.db.models.fields.PointField'
  281. if isinstance(reverse_type, tuple):
  282. kwargs.update(reverse_type[1])
  283. reverse_type = reverse_type[0]
  284. if reverse_type == "CharField" and description[3]:
  285. kwargs['max_length'] = description[3]
  286. if reverse_type == "DecimalField":
  287. kwargs['max_digits'] = description[4]
  288. kwargs['decimal_places'] = description[5] and abs(description[5]) or description[5]
  289. if description[6]:
  290. kwargs['blank'] = True
  291. if reverse_type not in ('TextField', 'CharField'):
  292. kwargs['null'] = True
  293. if field and getattr(field, 'geography', False):
  294. kwargs['geography'] = True
  295. if reverse_type == 'GeometryField':
  296. geo_col = description[0]
  297. # Getting a more specific field type and any additional parameters
  298. # from the `get_geometry_type` routine for the spatial backend.
  299. reverse_type, geo_params = self.introspection.get_geometry_type(table_name, geo_col)
  300. if geo_params:
  301. kwargs.update(geo_params)
  302. reverse_type = 'django.contrib.gis.db.models.fields.%s' % reverse_type
  303. extra_kwargs = self.get_field_db_type_kwargs(kwargs, description, field, table_name, reverse_type)
  304. kwargs.update(extra_kwargs)
  305. field_class = self.get_field_class(reverse_type)
  306. field_db_type = field_class(**kwargs).db_type(connection=connection)
  307. tablespace = field.db_tablespace
  308. if not tablespace:
  309. tablespace = "public"
  310. if (tablespace, table_name, field.column) in self.unsigned and self.unsigned_suffix not in field_db_type:
  311. field_db_type = '%s %s' % (field_db_type, self.unsigned_suffix)
  312. return field_db_type
  313. def get_field_db_type_lookup(self, type_code):
  314. return None
  315. def get_field_class(self, class_path):
  316. if '.' in class_path:
  317. module_path, package_name = class_path.rsplit('.', 1)
  318. module = importlib.import_module(module_path)
  319. return getattr(module, package_name)
  320. return getattr(models, class_path)
  321. def get_field_db_nullable(self, field, table_name):
  322. tablespace = field.db_tablespace
  323. if tablespace == "":
  324. tablespace = "public"
  325. attname = field.db_column or field.attname
  326. return self.null.get((tablespace, table_name, attname), 'fixme')
  327. def strip_parameters(self, field_type):
  328. if field_type and field_type != 'double precision':
  329. return field_type.split(" ")[0].split("(")[0].lower()
  330. return field_type
  331. def expand_together(self, together, meta):
  332. new_together = []
  333. for fields in normalize_together(together):
  334. new_together.append(
  335. tuple(meta.get_field(field).attname for field in fields)
  336. )
  337. return new_together
  338. def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None):
  339. schema_editor = connection.SchemaEditorClass(connection)
  340. for field in all_local_fields(meta):
  341. if skip_list and field.attname in skip_list:
  342. continue
  343. if field.unique and meta.managed:
  344. attname = field.db_column or field.attname
  345. db_field_unique = table_indexes.get(attname, {}).get('unique')
  346. if not db_field_unique and table_constraints:
  347. db_field_unique = any(constraint['unique'] for contraint_name, constraint in six.iteritems(table_constraints) if [attname] == constraint['columns'])
  348. if attname in table_indexes and db_field_unique:
  349. continue
  350. index_name = schema_editor._create_index_name(table_name, [attname])
  351. self.add_difference('unique-missing-in-db', table_name, [attname], index_name + "_uniq")
  352. db_type = field.db_type(connection=connection)
  353. if db_type.startswith('varchar'):
  354. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops')
  355. if db_type.startswith('text'):
  356. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops')
  357. unique_together = self.expand_together(meta.unique_together, meta)
  358. db_unique_columns = normalize_together([v['columns'] for v in six.itervalues(table_constraints) if v['unique'] and not v['index']])
  359. for unique_columns in unique_together:
  360. if unique_columns in db_unique_columns:
  361. continue
  362. if skip_list and unique_columns in skip_list:
  363. continue
  364. index_name = schema_editor._create_index_name(table_name, unique_columns)
  365. self.add_difference('unique-missing-in-db', table_name, unique_columns, index_name + "_uniq")
  366. def find_unique_missing_in_model(self, meta, table_indexes, table_constraints, table_name):
  367. fields = dict([(field.column, field) for field in all_local_fields(meta)])
  368. unique_together = self.expand_together(meta.unique_together, meta)
  369. for constraint_name, constraint in six.iteritems(table_constraints):
  370. if not constraint['unique']:
  371. continue
  372. if constraint['index']:
  373. # unique indexes are handled by find_index_missing_in_model
  374. continue
  375. columns = constraint['columns']
  376. if len(columns) == 1:
  377. field = fields.get(columns[0])
  378. if field is None:
  379. pass
  380. elif field.unique:
  381. continue
  382. else:
  383. if tuple(columns) in unique_together:
  384. continue
  385. self.add_difference('unique-missing-in-model', table_name, constraint_name)
  386. def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table_name):
  387. schema_editor = connection.SchemaEditorClass(connection)
  388. for field in all_local_fields(meta):
  389. if field.db_index:
  390. attname = field.db_column or field.attname
  391. if attname not in table_indexes:
  392. index_name = schema_editor._create_index_name(table_name, [attname])
  393. self.add_difference('index-missing-in-db', table_name, [attname], index_name, '')
  394. db_type = field.db_type(connection=connection)
  395. if db_type.startswith('varchar'):
  396. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops')
  397. if db_type.startswith('text'):
  398. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops')
  399. index_together = self.expand_together(meta.index_together, meta)
  400. db_index_together = normalize_together([v['columns'] for v in six.itervalues(table_constraints) if v['index'] and not v['unique']])
  401. for columns in index_together:
  402. if columns in db_index_together:
  403. continue
  404. index_name = schema_editor._create_index_name(table_name, columns)
  405. self.add_difference('index-missing-in-db', table_name, columns, index_name + "_idx", '')
  406. for index in meta.indexes:
  407. if index.name not in table_constraints:
  408. self.add_difference('index-missing-in-db', table_name, index.fields, index.name, '')
  409. def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name):
  410. fields = dict([(field.column, field) for field in all_local_fields(meta)])
  411. meta_index_names = [idx.name for idx in meta.indexes]
  412. index_together = self.expand_together(meta.index_together, meta)
  413. for constraint_name, constraint in six.iteritems(table_constraints):
  414. if constraint_name in meta_index_names:
  415. continue
  416. if constraint['unique'] and not constraint['index']:
  417. # unique constraints are handled by find_unique_missing_in_model
  418. continue
  419. columns = constraint['columns']
  420. field = fields.get(columns[0])
  421. if (constraint['unique'] and constraint['index']) or field is None:
  422. # unique indexes do not exist in django ? only unique constraints
  423. pass
  424. elif len(columns) == 1:
  425. if constraint['primary_key'] and field.primary_key:
  426. continue
  427. if constraint['foreign_key'] and isinstance(field, models.ForeignKey) and field.db_constraint:
  428. continue
  429. if constraint['unique'] and field.unique:
  430. continue
  431. if constraint['index'] and constraint['type'] == 'idx' and constraint.get('orders') and field.unique:
  432. # django automatically creates a _like varchar_pattern_ops/text_pattern_ops index see https://code.djangoproject.com/ticket/12234
  433. # note: mysql does not have and/or introspect and fill the 'orders' attribute of constraint information
  434. continue
  435. if constraint['index'] and field.db_index:
  436. continue
  437. if constraint['check'] and field.db_check(connection=connection):
  438. continue
  439. if getattr(field, 'spatial_index', False):
  440. continue
  441. else:
  442. if constraint['index'] and tuple(columns) in index_together:
  443. continue
  444. self.add_difference('index-missing-in-model', table_name, constraint_name)
  445. def find_field_missing_in_model(self, fieldmap, table_description, table_name):
  446. for row in table_description:
  447. if row[0] not in fieldmap:
  448. self.add_difference('field-missing-in-model', table_name, row[0])
  449. def find_field_missing_in_db(self, fieldmap, table_description, table_name):
  450. db_fields = [row[0] for row in table_description]
  451. for field_name, field in six.iteritems(fieldmap):
  452. if field_name not in db_fields:
  453. field_output = []
  454. if field.remote_field:
  455. field_output.extend([field.remote_field.model._meta.db_table, field.remote_field.model._meta.get_field(field.remote_field.field_name).column])
  456. op = 'fkey-missing-in-db'
  457. else:
  458. op = 'field-missing-in-db'
  459. field_output.append(field.db_type(connection=connection))
  460. if self.options['include_defaults'] and field.has_default():
  461. field_output.append('DEFAULT %s' % field.get_prep_value(field.get_default()))
  462. if not field.null:
  463. field_output.append('NOT NULL')
  464. self.add_difference(op, table_name, field_name, *field_output)
  465. self.new_db_fields.add((table_name, field_name))
  466. def find_field_type_differ(self, meta, table_description, table_name, func=None):
  467. db_fields = dict([(row[0], row) for row in table_description])
  468. for field in all_local_fields(meta):
  469. if field.name not in db_fields:
  470. continue
  471. description = db_fields[field.name]
  472. model_type = self.get_field_model_type(field)
  473. db_type = self.get_field_db_type(description, field, table_name)
  474. # use callback function if defined
  475. if func:
  476. model_type, db_type = func(field, description, model_type, db_type)
  477. if not self.strip_parameters(db_type) == self.strip_parameters(model_type):
  478. self.add_difference('field-type-differ', table_name, field.name, model_type, db_type)
  479. def find_field_parameter_differ(self, meta, table_description, table_name, func=None):
  480. db_fields = dict([(row[0], row) for row in table_description])
  481. for field in all_local_fields(meta):
  482. if field.name not in db_fields:
  483. continue
  484. description = db_fields[field.name]
  485. model_type = self.get_field_model_type(field)
  486. db_type = self.get_field_db_type(description, field, table_name)
  487. if not self.strip_parameters(model_type) == self.strip_parameters(db_type):
  488. continue
  489. # use callback function if defined
  490. if func:
  491. model_type, db_type = func(field, description, model_type, db_type)
  492. model_check = field.db_parameters(connection=connection)['check']
  493. if ' CHECK' in db_type:
  494. db_type, db_check = db_type.split(" CHECK", 1)
  495. db_check = db_check.strip().lstrip("(").rstrip(")")
  496. else:
  497. db_check = None
  498. if not model_type == db_type or not model_check == db_check:
  499. self.add_difference('field-parameter-differ', table_name, field.name, model_type, db_type)
  500. def find_field_notnull_differ(self, meta, table_description, table_name):
  501. if not self.can_detect_notnull_differ:
  502. return
  503. for field in all_local_fields(meta):
  504. attname = field.db_column or field.attname
  505. if (table_name, attname) in self.new_db_fields:
  506. continue
  507. null = self.get_field_db_nullable(field, table_name)
  508. if field.null != null:
  509. action = field.null and 'DROP' or 'SET'
  510. self.add_difference('notnull-differ', table_name, attname, action)
  511. def get_constraints(self, cursor, table_name, introspection):
  512. return {}
  513. def find_differences(self):
  514. if self.options['all_applications']:
  515. self.add_app_model_marker(None, None)
  516. for table in self.db_tables:
  517. if table not in self.django_tables and table not in self.IGNORE_MISSING_TABLES:
  518. self.add_difference('table-missing-in-model', table)
  519. cur_app_label = None
  520. for app_model in self.app_models:
  521. meta = app_model._meta
  522. table_name = meta.db_table
  523. app_label = meta.app_label
  524. if not self.options['include_proxy_models'] and meta.proxy:
  525. continue
  526. if cur_app_label != app_label:
  527. # Marker indicating start of difference scan for this table_name
  528. self.add_app_model_marker(app_label, app_model.__name__)
  529. if table_name not in self.db_tables:
  530. # Table is missing from database
  531. self.add_difference('table-missing-in-db', table_name)
  532. continue
  533. if hasattr(self.introspection, 'get_constraints'):
  534. table_constraints = self.introspection.get_constraints(self.cursor, table_name)
  535. else:
  536. table_constraints = self.get_constraints(self.cursor, table_name, self.introspection)
  537. fieldmap = dict([(field.db_column or field.get_attname(), field) for field in all_local_fields(meta)])
  538. # add ordering field if model uses order_with_respect_to
  539. if meta.order_with_respect_to:
  540. fieldmap['_order'] = ORDERING_FIELD
  541. try:
  542. table_description = self.introspection.get_table_description(self.cursor, table_name)
  543. except Exception as e:
  544. self.add_difference('error', 'unable to introspect table: %s' % str(e).strip())
  545. transaction.rollback() # reset transaction
  546. continue
  547. # map table_contraints into table_indexes
  548. table_indexes = {}
  549. for contraint_name, dct in table_constraints.items():
  550. columns = dct['columns']
  551. if len(columns) == 1:
  552. table_indexes[columns[0]] = {
  553. 'primary_key': dct['primary_key'],
  554. 'unique': dct['unique'],
  555. 'type': dct.get('type'),
  556. 'contraint_name': contraint_name,
  557. }
  558. # Fields which are defined in database but not in model
  559. # 1) find: 'unique-missing-in-model'
  560. self.find_unique_missing_in_model(meta, table_indexes, table_constraints, table_name)
  561. # 2) find: 'index-missing-in-model'
  562. self.find_index_missing_in_model(meta, table_indexes, table_constraints, table_name)
  563. # 3) find: 'field-missing-in-model'
  564. self.find_field_missing_in_model(fieldmap, table_description, table_name)
  565. # Fields which are defined in models but not in database
  566. # 4) find: 'field-missing-in-db'
  567. self.find_field_missing_in_db(fieldmap, table_description, table_name)
  568. # 5) find: 'unique-missing-in-db'
  569. self.find_unique_missing_in_db(meta, table_indexes, table_constraints, table_name)
  570. # 6) find: 'index-missing-in-db'
  571. self.find_index_missing_in_db(meta, table_indexes, table_constraints, table_name)
  572. # Fields which have a different type or parameters
  573. # 7) find: 'type-differs'
  574. self.find_field_type_differ(meta, table_description, table_name)
  575. # 8) find: 'type-parameter-differs'
  576. self.find_field_parameter_differ(meta, table_description, table_name)
  577. # 9) find: 'field-notnull'
  578. self.find_field_notnull_differ(meta, table_description, table_name)
  579. self.has_differences = max([len(diffs) for _app_label, _model_name, diffs in self.differences])
  580. def print_diff(self, style=no_style()):
  581. """ Print differences to stdout """
  582. if self.options['sql']:
  583. self.print_diff_sql(style)
  584. else:
  585. self.print_diff_text(style)
  586. def print_diff_text(self, style):
  587. if not self.can_detect_notnull_differ:
  588. self.stdout.write(style.NOTICE("# Detecting notnull changes not implemented for this database backend"))
  589. self.stdout.write("")
  590. if not self.can_detect_unsigned_differ:
  591. self.stdout.write(style.NOTICE("# Detecting unsigned changes not implemented for this database backend"))
  592. self.stdout.write("")
  593. cur_app_label = None
  594. for app_label, model_name, diffs in self.differences:
  595. if not diffs:
  596. continue
  597. if not self.dense and app_label and cur_app_label != app_label:
  598. self.stdout.write("%s %s" % (style.NOTICE("+ Application:"), style.SQL_TABLE(app_label)))
  599. cur_app_label = app_label
  600. if not self.dense and model_name:
  601. self.stdout.write("%s %s" % (style.NOTICE("|-+ Differences for model:"), style.SQL_TABLE(model_name)))
  602. for diff in diffs:
  603. diff_type, diff_args = diff
  604. text = self.DIFF_TEXTS[diff_type] % dict(
  605. (str(i), style.SQL_TABLE(', '.join(e) if isinstance(e, (list, tuple)) else e))
  606. for i, e in enumerate(diff_args)
  607. )
  608. text = "'".join(i % 2 == 0 and style.ERROR(e) or e for i, e in enumerate(text.split("'")))
  609. if not self.dense:
  610. self.stdout.write("%s %s" % (style.NOTICE("|--+"), text))
  611. else:
  612. if app_label:
  613. self.stdout.write("%s %s %s %s %s" % (style.NOTICE("App"), style.SQL_TABLE(app_label), style.NOTICE('Model'), style.SQL_TABLE(model_name), text))
  614. else:
  615. self.stdout.write(text)
  616. def print_diff_sql(self, style):
  617. if not self.can_detect_notnull_differ:
  618. self.stdout.write(style.NOTICE("-- Detecting notnull changes not implemented for this database backend"))
  619. self.stdout.write("")
  620. cur_app_label = None
  621. qn = connection.ops.quote_name
  622. if not self.has_differences:
  623. if not self.dense:
  624. self.stdout.write(style.SQL_KEYWORD("-- No differences"))
  625. else:
  626. self.stdout.write(style.SQL_KEYWORD("BEGIN;"))
  627. for app_label, model_name, diffs in self.differences:
  628. if not diffs:
  629. continue
  630. if not self.dense and cur_app_label != app_label:
  631. self.stdout.write(style.NOTICE("-- Application: %s" % style.SQL_TABLE(app_label)))
  632. cur_app_label = app_label
  633. if not self.dense and model_name:
  634. self.stdout.write(style.NOTICE("-- Model: %s" % style.SQL_TABLE(model_name)))
  635. for diff in diffs:
  636. diff_type, diff_args = diff
  637. text = self.DIFF_SQL[diff_type](style, qn, diff_args)
  638. if self.dense:
  639. text = text.replace("\n\t", " ")
  640. self.stdout.write(text)
  641. self.stdout.write(style.SQL_KEYWORD("COMMIT;"))
  642. class GenericSQLDiff(SQLDiff):
  643. can_detect_notnull_differ = False
  644. can_detect_unsigned_differ = False
  645. def load_null(self):
  646. pass
  647. def load_unsigned(self):
  648. pass
  649. class MySQLDiff(SQLDiff):
  650. can_detect_notnull_differ = True
  651. can_detect_unsigned_differ = True
  652. unsigned_suffix = 'UNSIGNED'
  653. def load(self):
  654. super().load()
  655. self.auto_increment = set()
  656. self.load_auto_increment()
  657. def format_field_names(self, field_names):
  658. return [f.lower() for f in field_names]
  659. def load_null(self):
  660. tablespace = 'public'
  661. for table_name in self.db_tables:
  662. result = self.sql_to_dict("""
  663. SELECT column_name, is_nullable
  664. FROM information_schema.columns
  665. WHERE table_schema = DATABASE()
  666. AND table_name = %s""", [table_name])
  667. for table_info in result:
  668. key = (tablespace, table_name, table_info['column_name'])
  669. self.null[key] = table_info['is_nullable'] == 'YES'
  670. def load_unsigned(self):
  671. tablespace = 'public'
  672. for table_name in self.db_tables:
  673. result = self.sql_to_dict("""
  674. SELECT column_name
  675. FROM information_schema.columns
  676. WHERE table_schema = DATABASE()
  677. AND table_name = %s
  678. AND column_type LIKE '%%unsigned'""", [table_name])
  679. for table_info in result:
  680. key = (tablespace, table_name, table_info['column_name'])
  681. self.unsigned.add(key)
  682. def load_auto_increment(self):
  683. for table_name in self.db_tables:
  684. result = self.sql_to_dict("""
  685. SELECT column_name
  686. FROM information_schema.columns
  687. WHERE table_schema = DATABASE()
  688. AND table_name = %s
  689. AND extra = 'auto_increment'""", [table_name])
  690. for table_info in result:
  691. key = (table_name, table_info['column_name'])
  692. self.auto_increment.add(key)
  693. # All the MySQL hacks together create something of a problem
  694. # Fixing one bug in MySQL creates another issue. So just keep in mind
  695. # that this is way unreliable for MySQL atm.
  696. def get_field_db_type(self, description, field=None, table_name=None):
  697. db_type = super().get_field_db_type(description, field, table_name)
  698. if not db_type:
  699. return
  700. if field:
  701. # MySQL isn't really sure about char's and varchar's like sqlite
  702. field_type = self.get_field_model_type(field)
  703. # Fix char/varchar inconsistencies
  704. if self.strip_parameters(field_type) == 'char' and self.strip_parameters(db_type) == 'varchar':
  705. db_type = db_type.lstrip("var")
  706. # They like to call bools various integer types and introspection makes that a integer
  707. # just convert them all to bools
  708. if self.strip_parameters(field_type) == 'bool':
  709. if db_type == 'integer':
  710. db_type = 'bool'
  711. if (table_name, field.column) in self.auto_increment and 'AUTO_INCREMENT' not in db_type:
  712. db_type += ' AUTO_INCREMENT'
  713. return db_type
  714. def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name):
  715. fields = dict([(field.column, field) for field in all_local_fields(meta)])
  716. meta_index_names = [idx.name for idx in meta.indexes]
  717. index_together = self.expand_together(meta.index_together, meta)
  718. unique_together = self.expand_together(meta.unique_together, meta)
  719. for constraint_name, constraint in six.iteritems(table_constraints):
  720. if constraint_name in meta_index_names:
  721. continue
  722. if constraint['unique'] and not constraint['index']:
  723. # unique constraints are handled by find_unique_missing_in_model
  724. continue
  725. columns = constraint['columns']
  726. field = fields.get(columns[0])
  727. # extra check removed from superclass here, otherwise function is the same
  728. if len(columns) == 1:
  729. if constraint['primary_key'] and field.primary_key:
  730. continue
  731. if constraint['foreign_key'] and isinstance(field, models.ForeignKey) and field.db_constraint:
  732. continue
  733. if constraint['unique'] and field.unique:
  734. continue
  735. if constraint['index'] and constraint['type'] == 'idx' and constraint.get('orders') and field.unique:
  736. # django automatically creates a _like varchar_pattern_ops/text_pattern_ops index see https://code.djangoproject.com/ticket/12234
  737. # note: mysql does not have and/or introspect and fill the 'orders' attribute of constraint information
  738. continue
  739. if constraint['index'] and field.db_index:
  740. continue
  741. if constraint['check'] and field.db_check(connection=connection):
  742. continue
  743. if getattr(field, 'spatial_index', False):
  744. continue
  745. else:
  746. if constraint['index'] and tuple(columns) in index_together:
  747. continue
  748. if constraint['index'] and constraint['unique'] and tuple(columns) in unique_together:
  749. continue
  750. self.add_difference('index-missing-in-model', table_name, constraint_name)
  751. def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None):
  752. schema_editor = connection.SchemaEditorClass(connection)
  753. for field in all_local_fields(meta):
  754. if skip_list and field.attname in skip_list:
  755. continue
  756. if field.unique and meta.managed:
  757. attname = field.db_column or field.attname
  758. db_field_unique = table_indexes.get(attname, {}).get('unique')
  759. if not db_field_unique and table_constraints:
  760. db_field_unique = any(constraint['unique'] for contraint_name, constraint in six.iteritems(table_constraints) if [attname] == constraint['columns'])
  761. if attname in table_indexes and db_field_unique:
  762. continue
  763. index_name = schema_editor._create_index_name(table_name, [attname])
  764. self.add_difference('unique-missing-in-db', table_name, [attname], index_name + "_uniq")
  765. db_type = field.db_type(connection=connection)
  766. if db_type.startswith('varchar'):
  767. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops')
  768. if db_type.startswith('text'):
  769. self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops')
  770. unique_together = self.expand_together(meta.unique_together, meta)
  771. # This comparison changed from superclass - otherwise function is the same
  772. db_unique_columns = normalize_together([v['columns'] for v in six.itervalues(table_constraints) if v['unique']])
  773. for unique_columns in unique_together:
  774. if unique_columns in db_unique_columns:
  775. continue
  776. if skip_list and unique_columns in skip_list:
  777. continue
  778. index_name = schema_editor._create_index_name(table_name, unique_columns)
  779. self.add_difference('unique-missing-in-db', table_name, unique_columns, index_name + "_uniq")
  780. class SqliteSQLDiff(SQLDiff):
  781. can_detect_notnull_differ = True
  782. can_detect_unsigned_differ = False
  783. def load_null(self):
  784. for table_name in self.db_tables:
  785. # sqlite does not support tablespaces
  786. tablespace = "public"
  787. # index, column_name, column_type, nullable, default_value
  788. # see: http://www.sqlite.org/pragma.html#pragma_table_info
  789. for table_info in self.sql_to_dict("PRAGMA table_info('%s');" % table_name, []):
  790. key = (tablespace, table_name, table_info['name'])
  791. self.null[key] = not table_info['notnull']
  792. def load_unsigned(self):
  793. pass
  794. # Unique does not seem to be implied on Sqlite for Primary_key's
  795. # if this is more generic among databases this might be usefull
  796. # to add to the superclass's find_unique_missing_in_db method
  797. def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None):
  798. if skip_list is None:
  799. skip_list = []
  800. unique_columns = [field.db_column or field.attname for field in all_local_fields(meta) if field.unique]
  801. for constraint in six.itervalues(table_constraints):
  802. columns = constraint['columns']
  803. if len(columns) == 1:
  804. column = columns[0]
  805. if column in unique_columns and (constraint['unique'] or constraint['primary_key']):
  806. skip_list.append(column)
  807. unique_together = self.expand_together(meta.unique_together, meta)
  808. db_unique_columns = normalize_together([v['columns'] for v in six.itervalues(table_constraints) if v['unique']])
  809. for unique_columns in unique_together:
  810. if unique_columns in db_unique_columns:
  811. skip_list.append(unique_columns)
  812. super().find_unique_missing_in_db(meta, table_indexes, table_constraints, table_name, skip_list=skip_list)
  813. # Finding Indexes by using the get_indexes dictionary doesn't seem to work
  814. # for sqlite.
  815. def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table_name):
  816. pass
  817. def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name):
  818. pass
  819. def get_field_db_type(self, description, field=None, table_name=None):
  820. db_type = super().get_field_db_type(description, field, table_name)
  821. if not db_type:
  822. return None
  823. if field:
  824. field_type = self.get_field_model_type(field)
  825. # Fix char/varchar inconsistencies
  826. if self.strip_parameters(field_type) == 'char' and self.strip_parameters(db_type) == 'varchar':
  827. db_type = db_type.lstrip("var")
  828. return db_type
  829. class PostgresqlSQLDiff(SQLDiff):
  830. can_detect_notnull_differ = True
  831. can_detect_unsigned_differ = True
  832. DATA_TYPES_REVERSE_NAME = {
  833. 'hstore': 'django.contrib.postgres.fields.HStoreField',
  834. 'jsonb': 'django.contrib.postgres.fields.JSONField',
  835. }
  836. # Hopefully in the future we can add constraint checking and other more
  837. # advanced checks based on this database.
  838. SQL_LOAD_CONSTRAINTS = """
  839. SELECT nspname, relname, conname, attname, pg_get_constraintdef(pg_constraint.oid)
  840. FROM pg_constraint
  841. INNER JOIN pg_attribute ON pg_constraint.conrelid = pg_attribute.attrelid AND pg_attribute.attnum = any(pg_constraint.conkey)
  842. INNER JOIN pg_class ON conrelid=pg_class.oid
  843. INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace
  844. ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END,contype,nspname,relname,conname;
  845. """
  846. SQL_LOAD_NULL = """
  847. SELECT nspname, relname, attname, attnotnull
  848. FROM pg_attribute
  849. INNER JOIN pg_class ON attrelid=pg_class.oid
  850. INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace;
  851. """
  852. SQL_FIELD_TYPE_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD("TYPE"), style.SQL_COLTYPE(args[2]))
  853. SQL_FIELD_PARAMETER_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD("TYPE"), style.SQL_COLTYPE(args[2]))
  854. SQL_NOTNULL_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER COLUMN'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD(args[2]), style.SQL_KEYWORD('NOT NULL'))
  855. def load(self):
  856. super().load()
  857. self.check_constraints = {}
  858. self.load_constraints()
  859. def load_null(self):
  860. for dct in self.sql_to_dict(self.SQL_LOAD_NULL, []):
  861. key = (dct['nspname'], dct['relname'], dct['attname'])
  862. self.null[key] = not dct['attnotnull']
  863. def load_unsigned(self):
  864. # PostgreSQL does not support unsigned, so no columns are
  865. # unsigned. Nothing to do.
  866. pass
  867. def load_constraints(self):
  868. for dct in self.sql_to_dict(self.SQL_LOAD_CONSTRAINTS, []):
  869. key = (dct['nspname'], dct['relname'], dct['attname'])
  870. if 'CHECK' in dct['pg_get_constraintdef']:
  871. self.check_constraints[key] = dct
  872. def get_data_type_arrayfield(self, base_field):
  873. return {
  874. 'name': 'django.contrib.postgres.fields.ArrayField',
  875. 'kwargs': {
  876. 'base_field': self.get_field_class(base_field)(),
  877. },
  878. }
  879. def get_data_types_reverse_override(self):
  880. return {
  881. 1042: 'CharField',
  882. 1000: lambda: self.get_data_type_arrayfield(base_field='BooleanField'),
  883. 1001: lambda: self.get_data_type_arrayfield(base_field='BinaryField'),
  884. 1002: lambda: self.get_data_type_arrayfield(base_field='CharField'),
  885. 1005: lambda: self.get_data_type_arrayfield(base_field='IntegerField'),
  886. 1006: lambda: self.get_data_type_arrayfield(base_field='IntegerField'),
  887. 1007: lambda: self.get_data_type_arrayfield(base_field='IntegerField'),
  888. 1009: lambda: self.get_data_type_arrayfield(base_field='CharField'),
  889. 1014: lambda: self.get_data_type_arrayfield(base_field='CharField'),
  890. 1015: lambda: self.get_data_type_arrayfield(base_field='CharField'),
  891. 1016: lambda: self.get_data_type_arrayfield(base_field='BigIntegerField'),
  892. 1017: lambda: self.get_data_type_arrayfield(base_field='FloatField'),
  893. 1021: lambda: self.get_data_type_arrayfield(base_field='FloatField'),
  894. 1022: lambda: self.get_data_type_arrayfield(base_field='FloatField'),
  895. 1115: lambda: self.get_data_type_arrayfield(base_field='DateTimeField'),
  896. 1185: lambda: self.get_data_type_arrayfield(base_field='DateTimeField'),
  897. 1231: lambda: self.get_data_type_arrayfield(base_field='DecimalField'),
  898. # {'name': 'django.contrib.postgres.fields.ArrayField', 'kwargs': {'base_field': 'IntegerField'}},
  899. 1186: lambda: self.get_data_type_arrayfield(base_field='DurationField'),
  900. # 1186: 'django.db.models.fields.DurationField',
  901. 3614: 'django.contrib.postgres.search.SearchVectorField',
  902. 3802: 'django.contrib.postgres.fields.JSONField',
  903. }
  904. def get_constraints(self, cursor, table_name, introspection):
  905. """
  906. Find constraints for table
  907. Backport of django's introspection.get_constraints(...)
  908. """
  909. constraints = {}
  910. # Loop over the key table, collecting things as constraints
  911. # This will get PKs, FKs, and uniques, but not CHECK
  912. cursor.execute("""
  913. SELECT
  914. kc.constraint_name,
  915. kc.column_name,
  916. c.constraint_type,
  917. array(SELECT table_name::text || '.' || column_name::text FROM information_schema.constraint_column_usage WHERE constraint_name = kc.constraint_name)
  918. FROM information_schema.key_column_usage AS kc
  919. JOIN information_schema.table_constraints AS c ON
  920. kc.table_schema = c.table_schema AND
  921. kc.table_name = c.table_name AND
  922. kc.constraint_name = c.constraint_name
  923. WHERE
  924. kc.table_schema = %s AND
  925. kc.table_name = %s
  926. """, ["public", table_name])
  927. for constraint, column, kind, used_cols in cursor.fetchall():
  928. # If we're the first column, make the record
  929. if constraint not in constraints:
  930. constraints[constraint] = {
  931. "columns": [],
  932. "primary_key": kind.lower() == "primary key",
  933. "unique": kind.lower() in ["primary key", "unique"],
  934. "foreign_key": tuple(used_cols[0].split(".", 1)) if kind.lower() == "foreign key" else None,
  935. "check": False,
  936. "index": False,
  937. }
  938. # Record the details
  939. constraints[constraint]['columns'].append(column)
  940. # Now get CHECK constraint columns
  941. cursor.execute("""
  942. SELECT kc.constraint_name, kc.column_name
  943. FROM information_schema.constraint_column_usage AS kc
  944. JOIN information_schema.table_constraints AS c ON
  945. kc.table_schema = c.table_schema AND
  946. kc.table_name = c.table_name AND
  947. kc.constraint_name = c.constraint_name
  948. WHERE
  949. c.constraint_type = 'CHECK' AND
  950. kc.table_schema = %s AND
  951. kc.table_name = %s
  952. """, ["public", table_name])
  953. for constraint, column in cursor.fetchall():
  954. # If we're the first column, make the record
  955. if constraint not in constraints:
  956. constraints[constraint] = {
  957. "columns": [],
  958. "primary_key": False,
  959. "unique": False,
  960. "foreign_key": None,
  961. "check": True,
  962. "index": False,
  963. }
  964. # Record the details
  965. constraints[constraint]['columns'].append(column)
  966. # Now get indexes
  967. cursor.execute("""
  968. SELECT
  969. c2.relname,
  970. ARRAY(
  971. SELECT (SELECT attname FROM pg_catalog.pg_attribute WHERE attnum = i AND attrelid = c.oid)
  972. FROM unnest(idx.indkey) i
  973. ),
  974. idx.indisunique,
  975. idx.indisprimary
  976. FROM pg_catalog.pg_class c, pg_catalog.pg_class c2,
  977. pg_catalog.pg_index idx
  978. WHERE c.oid = idx.indrelid
  979. AND idx.indexrelid = c2.oid
  980. AND c.relname = %s
  981. """, [table_name])
  982. for index, columns, unique, primary in cursor.fetchall():
  983. if index not in constraints:
  984. constraints[index] = {
  985. "columns": list(columns),
  986. "primary_key": primary,
  987. "unique": unique,
  988. "foreign_key": None,
  989. "check": False,
  990. "index": True,
  991. }
  992. return constraints
  993. # def get_field_db_type_kwargs(self, current_kwargs, description, field=None, table_name=None, reverse_type=None):
  994. # kwargs = {}
  995. # if field and 'base_field' in current_kwargs:
  996. # # find
  997. # attname = field.db_column or field.attname
  998. # introspect_db_type = self.sql_to_dict(
  999. # """SELECT attname, format_type(atttypid, atttypmod) AS type
  1000. # FROM pg_attribute
  1001. # WHERE attrelid = %s::regclass
  1002. # AND attname = %s
  1003. # AND attnum > 0
  1004. # AND NOT attisdropped
  1005. # ORDER BY attnum;
  1006. # """,
  1007. # (table_name, attname)
  1008. # )[0]['type']
  1009. # # TODO: this gives the concrete type that the database uses, why not use this
  1010. # # much earlier in the process to compare to whatever django spits out as
  1011. # # the database type ?
  1012. # max_length = re.search("character varying\((\d+)\)\[\]", introspect_db_type)
  1013. # if max_length:
  1014. # kwargs['max_length'] = max_length[1]
  1015. # return kwargs
  1016. def get_field_db_type(self, description, field=None, table_name=None):
  1017. db_type = super().get_field_db_type(description, field, table_name)
  1018. if not db_type:
  1019. return
  1020. if field:
  1021. if db_type.endswith("[]"):
  1022. # TODO: This is a hack for array types. Ideally we either pass the correct
  1023. # constraints for the type in `get_data_type_arrayfield` which instantiates
  1024. # the array base_field or maybe even better restructure sqldiff entirely
  1025. # to be based around the concrete type yielded by the code below. That gives
  1026. # the complete type the database uses, why not use thie much earlier in the
  1027. # process to compare to whatever django spits out as the desired database type ?
  1028. attname = field.db_column or field.attname
  1029. introspect_db_type = self.sql_to_dict(
  1030. """SELECT attname, format_type(atttypid, atttypmod) AS type
  1031. FROM pg_attribute
  1032. WHERE attrelid = %s::regclass
  1033. AND attname = %s
  1034. AND attnum > 0
  1035. AND NOT attisdropped
  1036. ORDER BY attnum;
  1037. """,
  1038. (table_name, attname)
  1039. )[0]['type']
  1040. if introspect_db_type.startswith("character varying"):
  1041. introspect_db_type = introspect_db_type.replace("character varying", "varchar")
  1042. return introspect_db_type
  1043. if field.primary_key and isinstance(field, AutoField):
  1044. if db_type == 'integer':
  1045. db_type = 'serial'
  1046. elif db_type == 'bigint':
  1047. db_type = 'bigserial'
  1048. if table_name:
  1049. tablespace = field.db_tablespace
  1050. if tablespace == "":
  1051. tablespace = "public"
  1052. attname = field.db_column or field.attname
  1053. check_constraint = self.check_constraints.get((tablespace, table_name, attname), {}).get('pg_get_constraintdef', None)
  1054. if check_constraint:
  1055. check_constraint = check_constraint.replace("((", "(")
  1056. check_constraint = check_constraint.replace("))", ")")
  1057. check_constraint = '("'.join([')' in e and '" '.join(p.strip('"') for p in e.split(" ", 1)) or e for e in check_constraint.split("(")])
  1058. # TODO: might be more then one constraint in definition ?
  1059. db_type += ' ' + check_constraint
  1060. return db_type
  1061. def get_field_db_type_lookup(self, type_code):
  1062. try:
  1063. name = self.sql_to_dict("SELECT typname FROM pg_type WHERE typelem=%s;", [type_code])[0]['typname']
  1064. return self.DATA_TYPES_REVERSE_NAME.get(name.strip('_'))
  1065. except (IndexError, KeyError):
  1066. pass
  1067. """
  1068. def find_field_type_differ(self, meta, table_description, table_name):
  1069. def callback(field, description, model_type, db_type):
  1070. if field.primary_key and db_type=='integer':
  1071. db_type = 'serial'
  1072. return model_type, db_type
  1073. super().find_field_type_differ(meta, table_description, table_name, callback)
  1074. """
  1075. DATABASE_SQLDIFF_CLASSES = {
  1076. 'postgis': PostgresqlSQLDiff,
  1077. 'postgresql_psycopg2': PostgresqlSQLDiff,
  1078. 'postgresql': PostgresqlSQLDiff,
  1079. 'mysql': MySQLDiff,
  1080. 'sqlite3': SqliteSQLDiff,
  1081. 'oracle': GenericSQLDiff
  1082. }
  1083. class Command(BaseCommand):
  1084. help = """Prints the (approximated) difference between models and fields in the database for the given app name(s).
  1085. It indicates how columns in the database are different from the sql that would
  1086. be generated by Django. This command is not a database migration tool. (Though
  1087. it can certainly help) It's purpose is to show the current differences as a way
  1088. to check/debug ur models compared to the real database tables and columns."""
  1089. output_transaction = False
  1090. def add_arguments(self, parser):
  1091. super().add_arguments(parser)
  1092. parser.add_argument('app_label', nargs='*')
  1093. parser.add_argument(
  1094. '--all-applications', '-a', action='store_true',
  1095. default=False,
  1096. dest='all_applications',
  1097. help="Automaticly include all application from INSTALLED_APPS."
  1098. )
  1099. parser.add_argument(
  1100. '--not-only-existing', '-e', action='store_false',
  1101. default=True,
  1102. dest='only_existing',
  1103. help="Check all tables that exist in the database, not only tables that should exist based on models."
  1104. )
  1105. parser.add_argument(
  1106. '--dense-output', '-d', action='store_true', dest='dense_output',
  1107. default=False,
  1108. help="Shows the output in dense format, normally output is spreaded over multiple lines."
  1109. )
  1110. parser.add_argument(
  1111. '--output_text', '-t', action='store_false', dest='sql',
  1112. default=True,
  1113. help="Outputs the differences as descriptive text instead of SQL"
  1114. )
  1115. parser.add_argument(
  1116. '--include-proxy-models', action='store_true', dest='include_proxy_models',
  1117. default=False,
  1118. help="Include proxy models in the graph"
  1119. )
  1120. parser.add_argument(
  1121. '--include-defaults', action='store_true', dest='include_defaults',
  1122. default=False,
  1123. help="Include default values in SQL output (beta feature)"
  1124. )
  1125. parser.add_argument(
  1126. '--migrate-for-tests', action='store_true', dest='migrate_for_tests',
  1127. default=False,
  1128. help=argparse.SUPPRESS
  1129. )
  1130. def __init__(self, *args, **kwargs):
  1131. super().__init__(*args, **kwargs)
  1132. self.exit_code = 1
  1133. @signalcommand
  1134. def handle(self, *args, **options):
  1135. from django.conf import settings
  1136. app_labels = options['app_label']
  1137. engine = None
  1138. if hasattr(settings, 'DATABASES'):
  1139. engine = settings.DATABASES['default']['ENGINE']
  1140. else:
  1141. engine = settings.DATABASE_ENGINE
  1142. if engine == 'dummy':
  1143. # This must be the "dummy" database backend, which means the user
  1144. # hasn't set DATABASE_ENGINE.
  1145. raise CommandError("""Django doesn't know which syntax to use for your SQL statements,
  1146. because you haven't specified the DATABASE_ENGINE setting.
  1147. Edit your settings file and change DATABASE_ENGINE to something like 'postgresql' or 'mysql'.""")
  1148. if options['all_applications']:
  1149. app_models = apps.get_models(include_auto_created=True)
  1150. else:
  1151. if not app_labels:
  1152. raise CommandError('Enter at least one appname.')
  1153. if not isinstance(app_labels, (list, tuple, set)):
  1154. app_labels = [app_labels]
  1155. app_models = []
  1156. for app_label in app_labels:
  1157. app_config = apps.get_app_config(app_label)
  1158. app_models.extend(app_config.get_models(include_auto_created=True))
  1159. if not app_models:
  1160. raise CommandError('Unable to execute sqldiff no models founds.')
  1161. migrate_for_tests = options['migrate_for_tests']
  1162. if migrate_for_tests:
  1163. from django.core.management import call_command
  1164. call_command("migrate", *app_labels, no_input=True, run_syncdb=True)
  1165. if not engine:
  1166. engine = connection.__module__.split('.')[-2]
  1167. if '.' in engine:
  1168. engine = engine.split('.')[-1]
  1169. cls = DATABASE_SQLDIFF_CLASSES.get(engine, GenericSQLDiff)
  1170. sqldiff_instance = cls(app_models, options, stdout=self.stdout, stderr=self.stderr)
  1171. sqldiff_instance.load()
  1172. sqldiff_instance.find_differences()
  1173. if not sqldiff_instance.has_differences:
  1174. self.exit_code = 0
  1175. sqldiff_instance.print_diff(self.style)
  1176. def execute(self, *args, **options):
  1177. try:
  1178. super().execute(*args, **options)
  1179. except CommandError as e:
  1180. if options['traceback']:
  1181. raise
  1182. # self.stderr is not guaranteed to be set here
  1183. stderr = getattr(self, 'stderr', None)
  1184. if not stderr:
  1185. stderr = OutputWrapper(sys.stderr, self.style.ERROR)
  1186. stderr.write('%s: %s' % (e.__class__.__name__, e))
  1187. sys.exit(2)
  1188. def run_from_argv(self, argv):
  1189. super().run_from_argv(argv)
  1190. sys.exit(self.exit_code)