test_models.py 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321
  1. import unittest
  2. from django.core.checks import Error, Warning
  3. from django.core.checks.model_checks import _check_lazy_references
  4. from django.db import connection, connections, models
  5. from django.db.models.functions import Abs, Lower, Round
  6. from django.db.models.signals import post_init
  7. from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
  8. from django.test.utils import isolate_apps, override_settings, register_lookup
  9. class EmptyRouter:
  10. pass
  11. def get_max_column_name_length():
  12. allowed_len = None
  13. db_alias = None
  14. for db in ('default', 'other'):
  15. connection = connections[db]
  16. max_name_length = connection.ops.max_name_length()
  17. if max_name_length is not None and not connection.features.truncates_names:
  18. if allowed_len is None or max_name_length < allowed_len:
  19. allowed_len = max_name_length
  20. db_alias = db
  21. return (allowed_len, db_alias)
  22. @isolate_apps('invalid_models_tests')
  23. class IndexTogetherTests(SimpleTestCase):
  24. def test_non_iterable(self):
  25. class Model(models.Model):
  26. class Meta:
  27. index_together = 42
  28. self.assertEqual(Model.check(), [
  29. Error(
  30. "'index_together' must be a list or tuple.",
  31. obj=Model,
  32. id='models.E008',
  33. ),
  34. ])
  35. def test_non_list(self):
  36. class Model(models.Model):
  37. class Meta:
  38. index_together = 'not-a-list'
  39. self.assertEqual(Model.check(), [
  40. Error(
  41. "'index_together' must be a list or tuple.",
  42. obj=Model,
  43. id='models.E008',
  44. ),
  45. ])
  46. def test_list_containing_non_iterable(self):
  47. class Model(models.Model):
  48. class Meta:
  49. index_together = [('a', 'b'), 42]
  50. self.assertEqual(Model.check(), [
  51. Error(
  52. "All 'index_together' elements must be lists or tuples.",
  53. obj=Model,
  54. id='models.E009',
  55. ),
  56. ])
  57. def test_pointing_to_missing_field(self):
  58. class Model(models.Model):
  59. class Meta:
  60. index_together = [['missing_field']]
  61. self.assertEqual(Model.check(), [
  62. Error(
  63. "'index_together' refers to the nonexistent field 'missing_field'.",
  64. obj=Model,
  65. id='models.E012',
  66. ),
  67. ])
  68. def test_pointing_to_non_local_field(self):
  69. class Foo(models.Model):
  70. field1 = models.IntegerField()
  71. class Bar(Foo):
  72. field2 = models.IntegerField()
  73. class Meta:
  74. index_together = [['field2', 'field1']]
  75. self.assertEqual(Bar.check(), [
  76. Error(
  77. "'index_together' refers to field 'field1' which is not "
  78. "local to model 'Bar'.",
  79. hint='This issue may be caused by multi-table inheritance.',
  80. obj=Bar,
  81. id='models.E016',
  82. ),
  83. ])
  84. def test_pointing_to_m2m_field(self):
  85. class Model(models.Model):
  86. m2m = models.ManyToManyField('self')
  87. class Meta:
  88. index_together = [['m2m']]
  89. self.assertEqual(Model.check(), [
  90. Error(
  91. "'index_together' refers to a ManyToManyField 'm2m', but "
  92. "ManyToManyFields are not permitted in 'index_together'.",
  93. obj=Model,
  94. id='models.E013',
  95. ),
  96. ])
  97. def test_pointing_to_fk(self):
  98. class Foo(models.Model):
  99. pass
  100. class Bar(models.Model):
  101. foo_1 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_1')
  102. foo_2 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_2')
  103. class Meta:
  104. index_together = [['foo_1_id', 'foo_2']]
  105. self.assertEqual(Bar.check(), [])
  106. # unique_together tests are very similar to index_together tests.
  107. @isolate_apps('invalid_models_tests')
  108. class UniqueTogetherTests(SimpleTestCase):
  109. def test_non_iterable(self):
  110. class Model(models.Model):
  111. class Meta:
  112. unique_together = 42
  113. self.assertEqual(Model.check(), [
  114. Error(
  115. "'unique_together' must be a list or tuple.",
  116. obj=Model,
  117. id='models.E010',
  118. ),
  119. ])
  120. def test_list_containing_non_iterable(self):
  121. class Model(models.Model):
  122. one = models.IntegerField()
  123. two = models.IntegerField()
  124. class Meta:
  125. unique_together = [('a', 'b'), 42]
  126. self.assertEqual(Model.check(), [
  127. Error(
  128. "All 'unique_together' elements must be lists or tuples.",
  129. obj=Model,
  130. id='models.E011',
  131. ),
  132. ])
  133. def test_non_list(self):
  134. class Model(models.Model):
  135. class Meta:
  136. unique_together = 'not-a-list'
  137. self.assertEqual(Model.check(), [
  138. Error(
  139. "'unique_together' must be a list or tuple.",
  140. obj=Model,
  141. id='models.E010',
  142. ),
  143. ])
  144. def test_valid_model(self):
  145. class Model(models.Model):
  146. one = models.IntegerField()
  147. two = models.IntegerField()
  148. class Meta:
  149. # unique_together can be a simple tuple
  150. unique_together = ('one', 'two')
  151. self.assertEqual(Model.check(), [])
  152. def test_pointing_to_missing_field(self):
  153. class Model(models.Model):
  154. class Meta:
  155. unique_together = [['missing_field']]
  156. self.assertEqual(Model.check(), [
  157. Error(
  158. "'unique_together' refers to the nonexistent field 'missing_field'.",
  159. obj=Model,
  160. id='models.E012',
  161. ),
  162. ])
  163. def test_pointing_to_m2m(self):
  164. class Model(models.Model):
  165. m2m = models.ManyToManyField('self')
  166. class Meta:
  167. unique_together = [['m2m']]
  168. self.assertEqual(Model.check(), [
  169. Error(
  170. "'unique_together' refers to a ManyToManyField 'm2m', but "
  171. "ManyToManyFields are not permitted in 'unique_together'.",
  172. obj=Model,
  173. id='models.E013',
  174. ),
  175. ])
  176. def test_pointing_to_fk(self):
  177. class Foo(models.Model):
  178. pass
  179. class Bar(models.Model):
  180. foo_1 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_1')
  181. foo_2 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_2')
  182. class Meta:
  183. unique_together = [['foo_1_id', 'foo_2']]
  184. self.assertEqual(Bar.check(), [])
  185. @isolate_apps('invalid_models_tests')
  186. class IndexesTests(TestCase):
  187. def test_pointing_to_missing_field(self):
  188. class Model(models.Model):
  189. class Meta:
  190. indexes = [models.Index(fields=['missing_field'], name='name')]
  191. self.assertEqual(Model.check(), [
  192. Error(
  193. "'indexes' refers to the nonexistent field 'missing_field'.",
  194. obj=Model,
  195. id='models.E012',
  196. ),
  197. ])
  198. def test_pointing_to_m2m_field(self):
  199. class Model(models.Model):
  200. m2m = models.ManyToManyField('self')
  201. class Meta:
  202. indexes = [models.Index(fields=['m2m'], name='name')]
  203. self.assertEqual(Model.check(), [
  204. Error(
  205. "'indexes' refers to a ManyToManyField 'm2m', but "
  206. "ManyToManyFields are not permitted in 'indexes'.",
  207. obj=Model,
  208. id='models.E013',
  209. ),
  210. ])
  211. def test_pointing_to_non_local_field(self):
  212. class Foo(models.Model):
  213. field1 = models.IntegerField()
  214. class Bar(Foo):
  215. field2 = models.IntegerField()
  216. class Meta:
  217. indexes = [models.Index(fields=['field2', 'field1'], name='name')]
  218. self.assertEqual(Bar.check(), [
  219. Error(
  220. "'indexes' refers to field 'field1' which is not local to "
  221. "model 'Bar'.",
  222. hint='This issue may be caused by multi-table inheritance.',
  223. obj=Bar,
  224. id='models.E016',
  225. ),
  226. ])
  227. def test_pointing_to_fk(self):
  228. class Foo(models.Model):
  229. pass
  230. class Bar(models.Model):
  231. foo_1 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_1')
  232. foo_2 = models.ForeignKey(Foo, on_delete=models.CASCADE, related_name='bar_2')
  233. class Meta:
  234. indexes = [models.Index(fields=['foo_1_id', 'foo_2'], name='index_name')]
  235. self.assertEqual(Bar.check(), [])
  236. def test_name_constraints(self):
  237. class Model(models.Model):
  238. class Meta:
  239. indexes = [
  240. models.Index(fields=['id'], name='_index_name'),
  241. models.Index(fields=['id'], name='5index_name'),
  242. ]
  243. self.assertEqual(Model.check(), [
  244. Error(
  245. "The index name '%sindex_name' cannot start with an "
  246. "underscore or a number." % prefix,
  247. obj=Model,
  248. id='models.E033',
  249. ) for prefix in ('_', '5')
  250. ])
  251. def test_max_name_length(self):
  252. index_name = 'x' * 31
  253. class Model(models.Model):
  254. class Meta:
  255. indexes = [models.Index(fields=['id'], name=index_name)]
  256. self.assertEqual(Model.check(), [
  257. Error(
  258. "The index name '%s' cannot be longer than 30 characters."
  259. % index_name,
  260. obj=Model,
  261. id='models.E034',
  262. ),
  263. ])
  264. def test_index_with_condition(self):
  265. class Model(models.Model):
  266. age = models.IntegerField()
  267. class Meta:
  268. indexes = [
  269. models.Index(
  270. fields=['age'],
  271. name='index_age_gte_10',
  272. condition=models.Q(age__gte=10),
  273. ),
  274. ]
  275. errors = Model.check(databases=self.databases)
  276. expected = [] if connection.features.supports_partial_indexes else [
  277. Warning(
  278. '%s does not support indexes with conditions.'
  279. % connection.display_name,
  280. hint=(
  281. "Conditions will be ignored. Silence this warning if you "
  282. "don't care about it."
  283. ),
  284. obj=Model,
  285. id='models.W037',
  286. )
  287. ]
  288. self.assertEqual(errors, expected)
  289. def test_index_with_condition_required_db_features(self):
  290. class Model(models.Model):
  291. age = models.IntegerField()
  292. class Meta:
  293. required_db_features = {'supports_partial_indexes'}
  294. indexes = [
  295. models.Index(
  296. fields=['age'],
  297. name='index_age_gte_10',
  298. condition=models.Q(age__gte=10),
  299. ),
  300. ]
  301. self.assertEqual(Model.check(databases=self.databases), [])
  302. def test_index_with_include(self):
  303. class Model(models.Model):
  304. age = models.IntegerField()
  305. class Meta:
  306. indexes = [
  307. models.Index(
  308. fields=['age'],
  309. name='index_age_include_id',
  310. include=['id'],
  311. ),
  312. ]
  313. errors = Model.check(databases=self.databases)
  314. expected = [] if connection.features.supports_covering_indexes else [
  315. Warning(
  316. '%s does not support indexes with non-key columns.'
  317. % connection.display_name,
  318. hint=(
  319. "Non-key columns will be ignored. Silence this warning if "
  320. "you don't care about it."
  321. ),
  322. obj=Model,
  323. id='models.W040',
  324. )
  325. ]
  326. self.assertEqual(errors, expected)
  327. def test_index_with_include_required_db_features(self):
  328. class Model(models.Model):
  329. age = models.IntegerField()
  330. class Meta:
  331. required_db_features = {'supports_covering_indexes'}
  332. indexes = [
  333. models.Index(
  334. fields=['age'],
  335. name='index_age_include_id',
  336. include=['id'],
  337. ),
  338. ]
  339. self.assertEqual(Model.check(databases=self.databases), [])
  340. @skipUnlessDBFeature('supports_covering_indexes')
  341. def test_index_include_pointing_to_missing_field(self):
  342. class Model(models.Model):
  343. class Meta:
  344. indexes = [
  345. models.Index(fields=['id'], include=['missing_field'], name='name'),
  346. ]
  347. self.assertEqual(Model.check(databases=self.databases), [
  348. Error(
  349. "'indexes' refers to the nonexistent field 'missing_field'.",
  350. obj=Model,
  351. id='models.E012',
  352. ),
  353. ])
  354. @skipUnlessDBFeature('supports_covering_indexes')
  355. def test_index_include_pointing_to_m2m_field(self):
  356. class Model(models.Model):
  357. m2m = models.ManyToManyField('self')
  358. class Meta:
  359. indexes = [models.Index(fields=['id'], include=['m2m'], name='name')]
  360. self.assertEqual(Model.check(databases=self.databases), [
  361. Error(
  362. "'indexes' refers to a ManyToManyField 'm2m', but "
  363. "ManyToManyFields are not permitted in 'indexes'.",
  364. obj=Model,
  365. id='models.E013',
  366. ),
  367. ])
  368. @skipUnlessDBFeature('supports_covering_indexes')
  369. def test_index_include_pointing_to_non_local_field(self):
  370. class Parent(models.Model):
  371. field1 = models.IntegerField()
  372. class Child(Parent):
  373. field2 = models.IntegerField()
  374. class Meta:
  375. indexes = [
  376. models.Index(fields=['field2'], include=['field1'], name='name'),
  377. ]
  378. self.assertEqual(Child.check(databases=self.databases), [
  379. Error(
  380. "'indexes' refers to field 'field1' which is not local to "
  381. "model 'Child'.",
  382. hint='This issue may be caused by multi-table inheritance.',
  383. obj=Child,
  384. id='models.E016',
  385. ),
  386. ])
  387. @skipUnlessDBFeature('supports_covering_indexes')
  388. def test_index_include_pointing_to_fk(self):
  389. class Target(models.Model):
  390. pass
  391. class Model(models.Model):
  392. fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
  393. fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
  394. class Meta:
  395. constraints = [
  396. models.Index(
  397. fields=['id'],
  398. include=['fk_1_id', 'fk_2'],
  399. name='name',
  400. ),
  401. ]
  402. self.assertEqual(Model.check(databases=self.databases), [])
  403. def test_func_index(self):
  404. class Model(models.Model):
  405. name = models.CharField(max_length=10)
  406. class Meta:
  407. indexes = [models.Index(Lower('name'), name='index_lower_name')]
  408. warn = Warning(
  409. '%s does not support indexes on expressions.'
  410. % connection.display_name,
  411. hint=(
  412. "An index won't be created. Silence this warning if you don't "
  413. "care about it."
  414. ),
  415. obj=Model,
  416. id='models.W043',
  417. )
  418. expected = [] if connection.features.supports_expression_indexes else [warn]
  419. self.assertEqual(Model.check(databases=self.databases), expected)
  420. def test_func_index_required_db_features(self):
  421. class Model(models.Model):
  422. name = models.CharField(max_length=10)
  423. class Meta:
  424. indexes = [models.Index(Lower('name'), name='index_lower_name')]
  425. required_db_features = {'supports_expression_indexes'}
  426. self.assertEqual(Model.check(databases=self.databases), [])
  427. def test_func_index_complex_expression_custom_lookup(self):
  428. class Model(models.Model):
  429. height = models.IntegerField()
  430. weight = models.IntegerField()
  431. class Meta:
  432. indexes = [
  433. models.Index(
  434. models.F('height') / (models.F('weight__abs') + models.Value(5)),
  435. name='name',
  436. ),
  437. ]
  438. with register_lookup(models.IntegerField, Abs):
  439. self.assertEqual(Model.check(), [])
  440. def test_func_index_pointing_to_missing_field(self):
  441. class Model(models.Model):
  442. class Meta:
  443. indexes = [models.Index(Lower('missing_field').desc(), name='name')]
  444. self.assertEqual(Model.check(), [
  445. Error(
  446. "'indexes' refers to the nonexistent field 'missing_field'.",
  447. obj=Model,
  448. id='models.E012',
  449. ),
  450. ])
  451. def test_func_index_pointing_to_missing_field_nested(self):
  452. class Model(models.Model):
  453. class Meta:
  454. indexes = [
  455. models.Index(Abs(Round('missing_field')), name='name'),
  456. ]
  457. self.assertEqual(Model.check(), [
  458. Error(
  459. "'indexes' refers to the nonexistent field 'missing_field'.",
  460. obj=Model,
  461. id='models.E012',
  462. ),
  463. ])
  464. def test_func_index_pointing_to_m2m_field(self):
  465. class Model(models.Model):
  466. m2m = models.ManyToManyField('self')
  467. class Meta:
  468. indexes = [models.Index(Lower('m2m'), name='name')]
  469. self.assertEqual(Model.check(), [
  470. Error(
  471. "'indexes' refers to a ManyToManyField 'm2m', but "
  472. "ManyToManyFields are not permitted in 'indexes'.",
  473. obj=Model,
  474. id='models.E013',
  475. ),
  476. ])
  477. def test_func_index_pointing_to_non_local_field(self):
  478. class Foo(models.Model):
  479. field1 = models.CharField(max_length=15)
  480. class Bar(Foo):
  481. class Meta:
  482. indexes = [models.Index(Lower('field1'), name='name')]
  483. self.assertEqual(Bar.check(), [
  484. Error(
  485. "'indexes' refers to field 'field1' which is not local to "
  486. "model 'Bar'.",
  487. hint='This issue may be caused by multi-table inheritance.',
  488. obj=Bar,
  489. id='models.E016',
  490. ),
  491. ])
  492. def test_func_index_pointing_to_fk(self):
  493. class Foo(models.Model):
  494. pass
  495. class Bar(models.Model):
  496. foo_1 = models.ForeignKey(Foo, models.CASCADE, related_name='bar_1')
  497. foo_2 = models.ForeignKey(Foo, models.CASCADE, related_name='bar_2')
  498. class Meta:
  499. indexes = [
  500. models.Index(Lower('foo_1_id'), Lower('foo_2'), name='index_name'),
  501. ]
  502. self.assertEqual(Bar.check(), [])
  503. @isolate_apps('invalid_models_tests')
  504. class FieldNamesTests(TestCase):
  505. databases = {'default', 'other'}
  506. def test_ending_with_underscore(self):
  507. class Model(models.Model):
  508. field_ = models.CharField(max_length=10)
  509. m2m_ = models.ManyToManyField('self')
  510. self.assertEqual(Model.check(), [
  511. Error(
  512. 'Field names must not end with an underscore.',
  513. obj=Model._meta.get_field('field_'),
  514. id='fields.E001',
  515. ),
  516. Error(
  517. 'Field names must not end with an underscore.',
  518. obj=Model._meta.get_field('m2m_'),
  519. id='fields.E001',
  520. ),
  521. ])
  522. max_column_name_length, column_limit_db_alias = get_max_column_name_length()
  523. @unittest.skipIf(max_column_name_length is None, "The database doesn't have a column name length limit.")
  524. def test_M2M_long_column_name(self):
  525. """
  526. #13711 -- Model check for long M2M column names when database has
  527. column name length limits.
  528. """
  529. # A model with very long name which will be used to set relations to.
  530. class VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(models.Model):
  531. title = models.CharField(max_length=11)
  532. # Main model for which checks will be performed.
  533. class ModelWithLongField(models.Model):
  534. m2m_field = models.ManyToManyField(
  535. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  536. related_name='rn1',
  537. )
  538. m2m_field2 = models.ManyToManyField(
  539. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  540. related_name='rn2', through='m2msimple',
  541. )
  542. m2m_field3 = models.ManyToManyField(
  543. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  544. related_name='rn3',
  545. through='m2mcomplex',
  546. )
  547. fk = models.ForeignKey(
  548. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  549. models.CASCADE,
  550. related_name='rn4',
  551. )
  552. # Models used for setting `through` in M2M field.
  553. class m2msimple(models.Model):
  554. id2 = models.ForeignKey(ModelWithLongField, models.CASCADE)
  555. class m2mcomplex(models.Model):
  556. id2 = models.ForeignKey(ModelWithLongField, models.CASCADE)
  557. long_field_name = 'a' * (self.max_column_name_length + 1)
  558. models.ForeignKey(
  559. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  560. models.CASCADE,
  561. ).contribute_to_class(m2msimple, long_field_name)
  562. models.ForeignKey(
  563. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  564. models.CASCADE,
  565. db_column=long_field_name
  566. ).contribute_to_class(m2mcomplex, long_field_name)
  567. errors = ModelWithLongField.check(databases=('default', 'other'))
  568. # First error because of M2M field set on the model with long name.
  569. m2m_long_name = "verylongmodelnamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_id"
  570. if self.max_column_name_length > len(m2m_long_name):
  571. # Some databases support names longer than the test name.
  572. expected = []
  573. else:
  574. expected = [
  575. Error(
  576. 'Autogenerated column name too long for M2M field "%s". '
  577. 'Maximum length is "%s" for database "%s".'
  578. % (m2m_long_name, self.max_column_name_length, self.column_limit_db_alias),
  579. hint="Use 'through' to create a separate model for "
  580. "M2M and then set column_name using 'db_column'.",
  581. obj=ModelWithLongField,
  582. id='models.E019',
  583. )
  584. ]
  585. # Second error because the FK specified in the `through` model
  586. # `m2msimple` has auto-generated name longer than allowed.
  587. # There will be no check errors in the other M2M because it
  588. # specifies db_column for the FK in `through` model even if the actual
  589. # name is longer than the limits of the database.
  590. expected.append(
  591. Error(
  592. 'Autogenerated column name too long for M2M field "%s_id". '
  593. 'Maximum length is "%s" for database "%s".'
  594. % (long_field_name, self.max_column_name_length, self.column_limit_db_alias),
  595. hint="Use 'through' to create a separate model for "
  596. "M2M and then set column_name using 'db_column'.",
  597. obj=ModelWithLongField,
  598. id='models.E019',
  599. )
  600. )
  601. self.assertEqual(errors, expected)
  602. # Check for long column names is called only for specified database
  603. # aliases.
  604. self.assertEqual(ModelWithLongField.check(databases=None), [])
  605. @unittest.skipIf(max_column_name_length is None, "The database doesn't have a column name length limit.")
  606. def test_local_field_long_column_name(self):
  607. """
  608. #13711 -- Model check for long column names
  609. when database does not support long names.
  610. """
  611. class ModelWithLongField(models.Model):
  612. title = models.CharField(max_length=11)
  613. long_field_name = 'a' * (self.max_column_name_length + 1)
  614. long_field_name2 = 'b' * (self.max_column_name_length + 1)
  615. models.CharField(max_length=11).contribute_to_class(ModelWithLongField, long_field_name)
  616. models.CharField(max_length=11, db_column='vlmn').contribute_to_class(ModelWithLongField, long_field_name2)
  617. self.assertEqual(ModelWithLongField.check(databases=('default', 'other')), [
  618. Error(
  619. 'Autogenerated column name too long for field "%s". '
  620. 'Maximum length is "%s" for database "%s".'
  621. % (long_field_name, self.max_column_name_length, self.column_limit_db_alias),
  622. hint="Set the column name manually using 'db_column'.",
  623. obj=ModelWithLongField,
  624. id='models.E018',
  625. )
  626. ])
  627. # Check for long column names is called only for specified database
  628. # aliases.
  629. self.assertEqual(ModelWithLongField.check(databases=None), [])
  630. def test_including_separator(self):
  631. class Model(models.Model):
  632. some__field = models.IntegerField()
  633. self.assertEqual(Model.check(), [
  634. Error(
  635. 'Field names must not contain "__".',
  636. obj=Model._meta.get_field('some__field'),
  637. id='fields.E002',
  638. )
  639. ])
  640. def test_pk(self):
  641. class Model(models.Model):
  642. pk = models.IntegerField()
  643. self.assertEqual(Model.check(), [
  644. Error(
  645. "'pk' is a reserved word that cannot be used as a field name.",
  646. obj=Model._meta.get_field('pk'),
  647. id='fields.E003',
  648. )
  649. ])
  650. def test_db_column_clash(self):
  651. class Model(models.Model):
  652. foo = models.IntegerField()
  653. bar = models.IntegerField(db_column='foo')
  654. self.assertEqual(Model.check(), [
  655. Error(
  656. "Field 'bar' has column name 'foo' that is used by "
  657. "another field.",
  658. hint="Specify a 'db_column' for the field.",
  659. obj=Model,
  660. id='models.E007',
  661. )
  662. ])
  663. @isolate_apps('invalid_models_tests')
  664. class ShadowingFieldsTests(SimpleTestCase):
  665. def test_field_name_clash_with_child_accessor(self):
  666. class Parent(models.Model):
  667. pass
  668. class Child(Parent):
  669. child = models.CharField(max_length=100)
  670. self.assertEqual(Child.check(), [
  671. Error(
  672. "The field 'child' clashes with the field "
  673. "'child' from model 'invalid_models_tests.parent'.",
  674. obj=Child._meta.get_field('child'),
  675. id='models.E006',
  676. )
  677. ])
  678. def test_multiinheritance_clash(self):
  679. class Mother(models.Model):
  680. clash = models.IntegerField()
  681. class Father(models.Model):
  682. clash = models.IntegerField()
  683. class Child(Mother, Father):
  684. # Here we have two clashed: id (automatic field) and clash, because
  685. # both parents define these fields.
  686. pass
  687. self.assertEqual(Child.check(), [
  688. Error(
  689. "The field 'id' from parent model "
  690. "'invalid_models_tests.mother' clashes with the field 'id' "
  691. "from parent model 'invalid_models_tests.father'.",
  692. obj=Child,
  693. id='models.E005',
  694. ),
  695. Error(
  696. "The field 'clash' from parent model "
  697. "'invalid_models_tests.mother' clashes with the field 'clash' "
  698. "from parent model 'invalid_models_tests.father'.",
  699. obj=Child,
  700. id='models.E005',
  701. )
  702. ])
  703. def test_inheritance_clash(self):
  704. class Parent(models.Model):
  705. f_id = models.IntegerField()
  706. class Target(models.Model):
  707. # This field doesn't result in a clash.
  708. f_id = models.IntegerField()
  709. class Child(Parent):
  710. # This field clashes with parent "f_id" field.
  711. f = models.ForeignKey(Target, models.CASCADE)
  712. self.assertEqual(Child.check(), [
  713. Error(
  714. "The field 'f' clashes with the field 'f_id' "
  715. "from model 'invalid_models_tests.parent'.",
  716. obj=Child._meta.get_field('f'),
  717. id='models.E006',
  718. )
  719. ])
  720. def test_multigeneration_inheritance(self):
  721. class GrandParent(models.Model):
  722. clash = models.IntegerField()
  723. class Parent(GrandParent):
  724. pass
  725. class Child(Parent):
  726. pass
  727. class GrandChild(Child):
  728. clash = models.IntegerField()
  729. self.assertEqual(GrandChild.check(), [
  730. Error(
  731. "The field 'clash' clashes with the field 'clash' "
  732. "from model 'invalid_models_tests.grandparent'.",
  733. obj=GrandChild._meta.get_field('clash'),
  734. id='models.E006',
  735. )
  736. ])
  737. def test_id_clash(self):
  738. class Target(models.Model):
  739. pass
  740. class Model(models.Model):
  741. fk = models.ForeignKey(Target, models.CASCADE)
  742. fk_id = models.IntegerField()
  743. self.assertEqual(Model.check(), [
  744. Error(
  745. "The field 'fk_id' clashes with the field 'fk' from model "
  746. "'invalid_models_tests.model'.",
  747. obj=Model._meta.get_field('fk_id'),
  748. id='models.E006',
  749. )
  750. ])
  751. @isolate_apps('invalid_models_tests')
  752. class OtherModelTests(SimpleTestCase):
  753. def test_unique_primary_key(self):
  754. invalid_id = models.IntegerField(primary_key=False)
  755. class Model(models.Model):
  756. id = invalid_id
  757. self.assertEqual(Model.check(), [
  758. Error(
  759. "'id' can only be used as a field name if the field also sets "
  760. "'primary_key=True'.",
  761. obj=Model,
  762. id='models.E004',
  763. ),
  764. ])
  765. def test_ordering_non_iterable(self):
  766. class Model(models.Model):
  767. class Meta:
  768. ordering = 'missing_field'
  769. self.assertEqual(Model.check(), [
  770. Error(
  771. "'ordering' must be a tuple or list "
  772. "(even if you want to order by only one field).",
  773. obj=Model,
  774. id='models.E014',
  775. ),
  776. ])
  777. def test_just_ordering_no_errors(self):
  778. class Model(models.Model):
  779. order = models.PositiveIntegerField()
  780. class Meta:
  781. ordering = ['order']
  782. self.assertEqual(Model.check(), [])
  783. def test_just_order_with_respect_to_no_errors(self):
  784. class Question(models.Model):
  785. pass
  786. class Answer(models.Model):
  787. question = models.ForeignKey(Question, models.CASCADE)
  788. class Meta:
  789. order_with_respect_to = 'question'
  790. self.assertEqual(Answer.check(), [])
  791. def test_ordering_with_order_with_respect_to(self):
  792. class Question(models.Model):
  793. pass
  794. class Answer(models.Model):
  795. question = models.ForeignKey(Question, models.CASCADE)
  796. order = models.IntegerField()
  797. class Meta:
  798. order_with_respect_to = 'question'
  799. ordering = ['order']
  800. self.assertEqual(Answer.check(), [
  801. Error(
  802. "'ordering' and 'order_with_respect_to' cannot be used together.",
  803. obj=Answer,
  804. id='models.E021',
  805. ),
  806. ])
  807. def test_non_valid(self):
  808. class RelationModel(models.Model):
  809. pass
  810. class Model(models.Model):
  811. relation = models.ManyToManyField(RelationModel)
  812. class Meta:
  813. ordering = ['relation']
  814. self.assertEqual(Model.check(), [
  815. Error(
  816. "'ordering' refers to the nonexistent field, related field, "
  817. "or lookup 'relation'.",
  818. obj=Model,
  819. id='models.E015',
  820. ),
  821. ])
  822. def test_ordering_pointing_to_missing_field(self):
  823. class Model(models.Model):
  824. class Meta:
  825. ordering = ('missing_field',)
  826. self.assertEqual(Model.check(), [
  827. Error(
  828. "'ordering' refers to the nonexistent field, related field, "
  829. "or lookup 'missing_field'.",
  830. obj=Model,
  831. id='models.E015',
  832. )
  833. ])
  834. def test_ordering_pointing_to_missing_foreignkey_field(self):
  835. class Model(models.Model):
  836. missing_fk_field = models.IntegerField()
  837. class Meta:
  838. ordering = ('missing_fk_field_id',)
  839. self.assertEqual(Model.check(), [
  840. Error(
  841. "'ordering' refers to the nonexistent field, related field, "
  842. "or lookup 'missing_fk_field_id'.",
  843. obj=Model,
  844. id='models.E015',
  845. )
  846. ])
  847. def test_ordering_pointing_to_missing_related_field(self):
  848. class Model(models.Model):
  849. test = models.IntegerField()
  850. class Meta:
  851. ordering = ('missing_related__id',)
  852. self.assertEqual(Model.check(), [
  853. Error(
  854. "'ordering' refers to the nonexistent field, related field, "
  855. "or lookup 'missing_related__id'.",
  856. obj=Model,
  857. id='models.E015',
  858. )
  859. ])
  860. def test_ordering_pointing_to_missing_related_model_field(self):
  861. class Parent(models.Model):
  862. pass
  863. class Child(models.Model):
  864. parent = models.ForeignKey(Parent, models.CASCADE)
  865. class Meta:
  866. ordering = ('parent__missing_field',)
  867. self.assertEqual(Child.check(), [
  868. Error(
  869. "'ordering' refers to the nonexistent field, related field, "
  870. "or lookup 'parent__missing_field'.",
  871. obj=Child,
  872. id='models.E015',
  873. )
  874. ])
  875. def test_ordering_pointing_to_non_related_field(self):
  876. class Child(models.Model):
  877. parent = models.IntegerField()
  878. class Meta:
  879. ordering = ('parent__missing_field',)
  880. self.assertEqual(Child.check(), [
  881. Error(
  882. "'ordering' refers to the nonexistent field, related field, "
  883. "or lookup 'parent__missing_field'.",
  884. obj=Child,
  885. id='models.E015',
  886. )
  887. ])
  888. def test_ordering_pointing_to_two_related_model_field(self):
  889. class Parent2(models.Model):
  890. pass
  891. class Parent1(models.Model):
  892. parent2 = models.ForeignKey(Parent2, models.CASCADE)
  893. class Child(models.Model):
  894. parent1 = models.ForeignKey(Parent1, models.CASCADE)
  895. class Meta:
  896. ordering = ('parent1__parent2__missing_field',)
  897. self.assertEqual(Child.check(), [
  898. Error(
  899. "'ordering' refers to the nonexistent field, related field, "
  900. "or lookup 'parent1__parent2__missing_field'.",
  901. obj=Child,
  902. id='models.E015',
  903. )
  904. ])
  905. def test_ordering_pointing_multiple_times_to_model_fields(self):
  906. class Parent(models.Model):
  907. field1 = models.CharField(max_length=100)
  908. field2 = models.CharField(max_length=100)
  909. class Child(models.Model):
  910. parent = models.ForeignKey(Parent, models.CASCADE)
  911. class Meta:
  912. ordering = ('parent__field1__field2',)
  913. self.assertEqual(Child.check(), [
  914. Error(
  915. "'ordering' refers to the nonexistent field, related field, "
  916. "or lookup 'parent__field1__field2'.",
  917. obj=Child,
  918. id='models.E015',
  919. )
  920. ])
  921. def test_ordering_allows_registered_lookups(self):
  922. class Model(models.Model):
  923. test = models.CharField(max_length=100)
  924. class Meta:
  925. ordering = ('test__lower',)
  926. with register_lookup(models.CharField, Lower):
  927. self.assertEqual(Model.check(), [])
  928. def test_ordering_pointing_to_lookup_not_transform(self):
  929. class Model(models.Model):
  930. test = models.CharField(max_length=100)
  931. class Meta:
  932. ordering = ('test__isnull',)
  933. self.assertEqual(Model.check(), [])
  934. def test_ordering_pointing_to_related_model_pk(self):
  935. class Parent(models.Model):
  936. pass
  937. class Child(models.Model):
  938. parent = models.ForeignKey(Parent, models.CASCADE)
  939. class Meta:
  940. ordering = ('parent__pk',)
  941. self.assertEqual(Child.check(), [])
  942. def test_ordering_pointing_to_foreignkey_field(self):
  943. class Parent(models.Model):
  944. pass
  945. class Child(models.Model):
  946. parent = models.ForeignKey(Parent, models.CASCADE)
  947. class Meta:
  948. ordering = ('parent_id',)
  949. self.assertFalse(Child.check())
  950. def test_name_beginning_with_underscore(self):
  951. class _Model(models.Model):
  952. pass
  953. self.assertEqual(_Model.check(), [
  954. Error(
  955. "The model name '_Model' cannot start or end with an underscore "
  956. "as it collides with the query lookup syntax.",
  957. obj=_Model,
  958. id='models.E023',
  959. )
  960. ])
  961. def test_name_ending_with_underscore(self):
  962. class Model_(models.Model):
  963. pass
  964. self.assertEqual(Model_.check(), [
  965. Error(
  966. "The model name 'Model_' cannot start or end with an underscore "
  967. "as it collides with the query lookup syntax.",
  968. obj=Model_,
  969. id='models.E023',
  970. )
  971. ])
  972. def test_name_contains_double_underscores(self):
  973. class Test__Model(models.Model):
  974. pass
  975. self.assertEqual(Test__Model.check(), [
  976. Error(
  977. "The model name 'Test__Model' cannot contain double underscores "
  978. "as it collides with the query lookup syntax.",
  979. obj=Test__Model,
  980. id='models.E024',
  981. )
  982. ])
  983. def test_property_and_related_field_accessor_clash(self):
  984. class Model(models.Model):
  985. fk = models.ForeignKey('self', models.CASCADE)
  986. @property
  987. def fk_id(self):
  988. pass
  989. self.assertEqual(Model.check(), [
  990. Error(
  991. "The property 'fk_id' clashes with a related field accessor.",
  992. obj=Model,
  993. id='models.E025',
  994. )
  995. ])
  996. def test_single_primary_key(self):
  997. class Model(models.Model):
  998. foo = models.IntegerField(primary_key=True)
  999. bar = models.IntegerField(primary_key=True)
  1000. self.assertEqual(Model.check(), [
  1001. Error(
  1002. "The model cannot have more than one field with 'primary_key=True'.",
  1003. obj=Model,
  1004. id='models.E026',
  1005. )
  1006. ])
  1007. @override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model')
  1008. def test_swappable_missing_app_name(self):
  1009. class Model(models.Model):
  1010. class Meta:
  1011. swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE'
  1012. self.assertEqual(Model.check(), [
  1013. Error(
  1014. "'TEST_SWAPPED_MODEL_BAD_VALUE' is not of the form 'app_label.app_name'.",
  1015. id='models.E001',
  1016. ),
  1017. ])
  1018. @override_settings(TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target')
  1019. def test_swappable_missing_app(self):
  1020. class Model(models.Model):
  1021. class Meta:
  1022. swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL'
  1023. self.assertEqual(Model.check(), [
  1024. Error(
  1025. "'TEST_SWAPPED_MODEL_BAD_MODEL' references 'not_an_app.Target', "
  1026. 'which has not been installed, or is abstract.',
  1027. id='models.E002',
  1028. ),
  1029. ])
  1030. def test_two_m2m_through_same_relationship(self):
  1031. class Person(models.Model):
  1032. pass
  1033. class Group(models.Model):
  1034. primary = models.ManyToManyField(Person, through='Membership', related_name='primary')
  1035. secondary = models.ManyToManyField(Person, through='Membership', related_name='secondary')
  1036. class Membership(models.Model):
  1037. person = models.ForeignKey(Person, models.CASCADE)
  1038. group = models.ForeignKey(Group, models.CASCADE)
  1039. self.assertEqual(Group.check(), [
  1040. Error(
  1041. "The model has two identical many-to-many relations through "
  1042. "the intermediate model 'invalid_models_tests.Membership'.",
  1043. obj=Group,
  1044. id='models.E003',
  1045. )
  1046. ])
  1047. def test_two_m2m_through_same_model_with_different_through_fields(self):
  1048. class Country(models.Model):
  1049. pass
  1050. class ShippingMethod(models.Model):
  1051. to_countries = models.ManyToManyField(
  1052. Country, through='ShippingMethodPrice',
  1053. through_fields=('method', 'to_country'),
  1054. )
  1055. from_countries = models.ManyToManyField(
  1056. Country, through='ShippingMethodPrice',
  1057. through_fields=('method', 'from_country'),
  1058. related_name='+',
  1059. )
  1060. class ShippingMethodPrice(models.Model):
  1061. method = models.ForeignKey(ShippingMethod, models.CASCADE)
  1062. to_country = models.ForeignKey(Country, models.CASCADE)
  1063. from_country = models.ForeignKey(Country, models.CASCADE)
  1064. self.assertEqual(ShippingMethod.check(), [])
  1065. def test_onetoone_with_parent_model(self):
  1066. class Place(models.Model):
  1067. pass
  1068. class ParkingLot(Place):
  1069. other_place = models.OneToOneField(Place, models.CASCADE, related_name='other_parking')
  1070. self.assertEqual(ParkingLot.check(), [])
  1071. def test_onetoone_with_explicit_parent_link_parent_model(self):
  1072. class Place(models.Model):
  1073. pass
  1074. class ParkingLot(Place):
  1075. place = models.OneToOneField(Place, models.CASCADE, parent_link=True, primary_key=True)
  1076. other_place = models.OneToOneField(Place, models.CASCADE, related_name='other_parking')
  1077. self.assertEqual(ParkingLot.check(), [])
  1078. def test_m2m_table_name_clash(self):
  1079. class Foo(models.Model):
  1080. bar = models.ManyToManyField('Bar', db_table='myapp_bar')
  1081. class Meta:
  1082. db_table = 'myapp_foo'
  1083. class Bar(models.Model):
  1084. class Meta:
  1085. db_table = 'myapp_bar'
  1086. self.assertEqual(Foo.check(), [
  1087. Error(
  1088. "The field's intermediary table 'myapp_bar' clashes with the "
  1089. "table name of 'invalid_models_tests.Bar'.",
  1090. obj=Foo._meta.get_field('bar'),
  1091. id='fields.E340',
  1092. )
  1093. ])
  1094. @override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
  1095. def test_m2m_table_name_clash_database_routers_installed(self):
  1096. class Foo(models.Model):
  1097. bar = models.ManyToManyField('Bar', db_table='myapp_bar')
  1098. class Meta:
  1099. db_table = 'myapp_foo'
  1100. class Bar(models.Model):
  1101. class Meta:
  1102. db_table = 'myapp_bar'
  1103. self.assertEqual(Foo.check(), [
  1104. Warning(
  1105. "The field's intermediary table 'myapp_bar' clashes with the "
  1106. "table name of 'invalid_models_tests.Bar'.",
  1107. obj=Foo._meta.get_field('bar'),
  1108. hint=(
  1109. "You have configured settings.DATABASE_ROUTERS. Verify "
  1110. "that the table of 'invalid_models_tests.Bar' is "
  1111. "correctly routed to a separate database."
  1112. ),
  1113. id='fields.W344',
  1114. ),
  1115. ])
  1116. def test_m2m_field_table_name_clash(self):
  1117. class Foo(models.Model):
  1118. pass
  1119. class Bar(models.Model):
  1120. foos = models.ManyToManyField(Foo, db_table='clash')
  1121. class Baz(models.Model):
  1122. foos = models.ManyToManyField(Foo, db_table='clash')
  1123. self.assertEqual(Bar.check() + Baz.check(), [
  1124. Error(
  1125. "The field's intermediary table 'clash' clashes with the "
  1126. "table name of 'invalid_models_tests.Baz.foos'.",
  1127. obj=Bar._meta.get_field('foos'),
  1128. id='fields.E340',
  1129. ),
  1130. Error(
  1131. "The field's intermediary table 'clash' clashes with the "
  1132. "table name of 'invalid_models_tests.Bar.foos'.",
  1133. obj=Baz._meta.get_field('foos'),
  1134. id='fields.E340',
  1135. )
  1136. ])
  1137. @override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
  1138. def test_m2m_field_table_name_clash_database_routers_installed(self):
  1139. class Foo(models.Model):
  1140. pass
  1141. class Bar(models.Model):
  1142. foos = models.ManyToManyField(Foo, db_table='clash')
  1143. class Baz(models.Model):
  1144. foos = models.ManyToManyField(Foo, db_table='clash')
  1145. self.assertEqual(Bar.check() + Baz.check(), [
  1146. Warning(
  1147. "The field's intermediary table 'clash' clashes with the "
  1148. "table name of 'invalid_models_tests.%s.foos'."
  1149. % clashing_model,
  1150. obj=model_cls._meta.get_field('foos'),
  1151. hint=(
  1152. "You have configured settings.DATABASE_ROUTERS. Verify "
  1153. "that the table of 'invalid_models_tests.%s.foos' is "
  1154. "correctly routed to a separate database." % clashing_model
  1155. ),
  1156. id='fields.W344',
  1157. ) for model_cls, clashing_model in [(Bar, 'Baz'), (Baz, 'Bar')]
  1158. ])
  1159. def test_m2m_autogenerated_table_name_clash(self):
  1160. class Foo(models.Model):
  1161. class Meta:
  1162. db_table = 'bar_foos'
  1163. class Bar(models.Model):
  1164. # The autogenerated `db_table` will be bar_foos.
  1165. foos = models.ManyToManyField(Foo)
  1166. class Meta:
  1167. db_table = 'bar'
  1168. self.assertEqual(Bar.check(), [
  1169. Error(
  1170. "The field's intermediary table 'bar_foos' clashes with the "
  1171. "table name of 'invalid_models_tests.Foo'.",
  1172. obj=Bar._meta.get_field('foos'),
  1173. id='fields.E340',
  1174. )
  1175. ])
  1176. @override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
  1177. def test_m2m_autogenerated_table_name_clash_database_routers_installed(self):
  1178. class Foo(models.Model):
  1179. class Meta:
  1180. db_table = 'bar_foos'
  1181. class Bar(models.Model):
  1182. # The autogenerated db_table is bar_foos.
  1183. foos = models.ManyToManyField(Foo)
  1184. class Meta:
  1185. db_table = 'bar'
  1186. self.assertEqual(Bar.check(), [
  1187. Warning(
  1188. "The field's intermediary table 'bar_foos' clashes with the "
  1189. "table name of 'invalid_models_tests.Foo'.",
  1190. obj=Bar._meta.get_field('foos'),
  1191. hint=(
  1192. "You have configured settings.DATABASE_ROUTERS. Verify "
  1193. "that the table of 'invalid_models_tests.Foo' is "
  1194. "correctly routed to a separate database."
  1195. ),
  1196. id='fields.W344',
  1197. ),
  1198. ])
  1199. def test_m2m_unmanaged_shadow_models_not_checked(self):
  1200. class A1(models.Model):
  1201. pass
  1202. class C1(models.Model):
  1203. mm_a = models.ManyToManyField(A1, db_table='d1')
  1204. # Unmanaged models that shadow the above models. Reused table names
  1205. # shouldn't be flagged by any checks.
  1206. class A2(models.Model):
  1207. class Meta:
  1208. managed = False
  1209. class C2(models.Model):
  1210. mm_a = models.ManyToManyField(A2, through='Intermediate')
  1211. class Meta:
  1212. managed = False
  1213. class Intermediate(models.Model):
  1214. a2 = models.ForeignKey(A2, models.CASCADE, db_column='a1_id')
  1215. c2 = models.ForeignKey(C2, models.CASCADE, db_column='c1_id')
  1216. class Meta:
  1217. db_table = 'd1'
  1218. managed = False
  1219. self.assertEqual(C1.check(), [])
  1220. self.assertEqual(C2.check(), [])
  1221. def test_m2m_to_concrete_and_proxy_allowed(self):
  1222. class A(models.Model):
  1223. pass
  1224. class Through(models.Model):
  1225. a = models.ForeignKey('A', models.CASCADE)
  1226. c = models.ForeignKey('C', models.CASCADE)
  1227. class ThroughProxy(Through):
  1228. class Meta:
  1229. proxy = True
  1230. class C(models.Model):
  1231. mm_a = models.ManyToManyField(A, through=Through)
  1232. mm_aproxy = models.ManyToManyField(A, through=ThroughProxy, related_name='proxied_m2m')
  1233. self.assertEqual(C.check(), [])
  1234. @isolate_apps('django.contrib.auth', kwarg_name='apps')
  1235. def test_lazy_reference_checks(self, apps):
  1236. class DummyModel(models.Model):
  1237. author = models.ForeignKey('Author', models.CASCADE)
  1238. class Meta:
  1239. app_label = 'invalid_models_tests'
  1240. class DummyClass:
  1241. def __call__(self, **kwargs):
  1242. pass
  1243. def dummy_method(self):
  1244. pass
  1245. def dummy_function(*args, **kwargs):
  1246. pass
  1247. apps.lazy_model_operation(dummy_function, ('auth', 'imaginarymodel'))
  1248. apps.lazy_model_operation(dummy_function, ('fanciful_app', 'imaginarymodel'))
  1249. post_init.connect(dummy_function, sender='missing-app.Model', apps=apps)
  1250. post_init.connect(DummyClass(), sender='missing-app.Model', apps=apps)
  1251. post_init.connect(DummyClass().dummy_method, sender='missing-app.Model', apps=apps)
  1252. self.assertEqual(_check_lazy_references(apps), [
  1253. Error(
  1254. "%r contains a lazy reference to auth.imaginarymodel, "
  1255. "but app 'auth' doesn't provide model 'imaginarymodel'." % dummy_function,
  1256. obj=dummy_function,
  1257. id='models.E022',
  1258. ),
  1259. Error(
  1260. "%r contains a lazy reference to fanciful_app.imaginarymodel, "
  1261. "but app 'fanciful_app' isn't installed." % dummy_function,
  1262. obj=dummy_function,
  1263. id='models.E022',
  1264. ),
  1265. Error(
  1266. "An instance of class 'DummyClass' was connected to "
  1267. "the 'post_init' signal with a lazy reference to the sender "
  1268. "'missing-app.model', but app 'missing-app' isn't installed.",
  1269. hint=None,
  1270. obj='invalid_models_tests.test_models',
  1271. id='signals.E001',
  1272. ),
  1273. Error(
  1274. "Bound method 'DummyClass.dummy_method' was connected to the "
  1275. "'post_init' signal with a lazy reference to the sender "
  1276. "'missing-app.model', but app 'missing-app' isn't installed.",
  1277. hint=None,
  1278. obj='invalid_models_tests.test_models',
  1279. id='signals.E001',
  1280. ),
  1281. Error(
  1282. "The field invalid_models_tests.DummyModel.author was declared "
  1283. "with a lazy reference to 'invalid_models_tests.author', but app "
  1284. "'invalid_models_tests' isn't installed.",
  1285. hint=None,
  1286. obj=DummyModel.author.field,
  1287. id='fields.E307',
  1288. ),
  1289. Error(
  1290. "The function 'dummy_function' was connected to the 'post_init' "
  1291. "signal with a lazy reference to the sender "
  1292. "'missing-app.model', but app 'missing-app' isn't installed.",
  1293. hint=None,
  1294. obj='invalid_models_tests.test_models',
  1295. id='signals.E001',
  1296. ),
  1297. ])
  1298. @isolate_apps('invalid_models_tests')
  1299. class JSONFieldTests(TestCase):
  1300. @skipUnlessDBFeature('supports_json_field')
  1301. def test_ordering_pointing_to_json_field_value(self):
  1302. class Model(models.Model):
  1303. field = models.JSONField()
  1304. class Meta:
  1305. ordering = ['field__value']
  1306. self.assertEqual(Model.check(databases=self.databases), [])
  1307. def test_check_jsonfield(self):
  1308. class Model(models.Model):
  1309. field = models.JSONField()
  1310. error = Error(
  1311. '%s does not support JSONFields.' % connection.display_name,
  1312. obj=Model,
  1313. id='fields.E180',
  1314. )
  1315. expected = [] if connection.features.supports_json_field else [error]
  1316. self.assertEqual(Model.check(databases=self.databases), expected)
  1317. def test_check_jsonfield_required_db_features(self):
  1318. class Model(models.Model):
  1319. field = models.JSONField()
  1320. class Meta:
  1321. required_db_features = {'supports_json_field'}
  1322. self.assertEqual(Model.check(databases=self.databases), [])
  1323. @isolate_apps('invalid_models_tests')
  1324. class ConstraintsTests(TestCase):
  1325. def test_check_constraints(self):
  1326. class Model(models.Model):
  1327. age = models.IntegerField()
  1328. class Meta:
  1329. constraints = [models.CheckConstraint(check=models.Q(age__gte=18), name='is_adult')]
  1330. errors = Model.check(databases=self.databases)
  1331. warn = Warning(
  1332. '%s does not support check constraints.' % connection.display_name,
  1333. hint=(
  1334. "A constraint won't be created. Silence this warning if you "
  1335. "don't care about it."
  1336. ),
  1337. obj=Model,
  1338. id='models.W027',
  1339. )
  1340. expected = [] if connection.features.supports_table_check_constraints else [warn]
  1341. self.assertCountEqual(errors, expected)
  1342. def test_check_constraints_required_db_features(self):
  1343. class Model(models.Model):
  1344. age = models.IntegerField()
  1345. class Meta:
  1346. required_db_features = {'supports_table_check_constraints'}
  1347. constraints = [models.CheckConstraint(check=models.Q(age__gte=18), name='is_adult')]
  1348. self.assertEqual(Model.check(databases=self.databases), [])
  1349. def test_check_constraint_pointing_to_missing_field(self):
  1350. class Model(models.Model):
  1351. class Meta:
  1352. required_db_features = {'supports_table_check_constraints'}
  1353. constraints = [
  1354. models.CheckConstraint(
  1355. name='name', check=models.Q(missing_field=2),
  1356. ),
  1357. ]
  1358. self.assertEqual(Model.check(databases=self.databases), [
  1359. Error(
  1360. "'constraints' refers to the nonexistent field "
  1361. "'missing_field'.",
  1362. obj=Model,
  1363. id='models.E012',
  1364. ),
  1365. ] if connection.features.supports_table_check_constraints else [])
  1366. @skipUnlessDBFeature('supports_table_check_constraints')
  1367. def test_check_constraint_pointing_to_reverse_fk(self):
  1368. class Model(models.Model):
  1369. parent = models.ForeignKey('self', models.CASCADE, related_name='parents')
  1370. class Meta:
  1371. constraints = [
  1372. models.CheckConstraint(name='name', check=models.Q(parents=3)),
  1373. ]
  1374. self.assertEqual(Model.check(databases=self.databases), [
  1375. Error(
  1376. "'constraints' refers to the nonexistent field 'parents'.",
  1377. obj=Model,
  1378. id='models.E012',
  1379. ),
  1380. ])
  1381. @skipUnlessDBFeature('supports_table_check_constraints')
  1382. def test_check_constraint_pointing_to_m2m_field(self):
  1383. class Model(models.Model):
  1384. m2m = models.ManyToManyField('self')
  1385. class Meta:
  1386. constraints = [
  1387. models.CheckConstraint(name='name', check=models.Q(m2m=2)),
  1388. ]
  1389. self.assertEqual(Model.check(databases=self.databases), [
  1390. Error(
  1391. "'constraints' refers to a ManyToManyField 'm2m', but "
  1392. "ManyToManyFields are not permitted in 'constraints'.",
  1393. obj=Model,
  1394. id='models.E013',
  1395. ),
  1396. ])
  1397. @skipUnlessDBFeature('supports_table_check_constraints')
  1398. def test_check_constraint_pointing_to_fk(self):
  1399. class Target(models.Model):
  1400. pass
  1401. class Model(models.Model):
  1402. fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
  1403. fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
  1404. class Meta:
  1405. constraints = [
  1406. models.CheckConstraint(
  1407. name='name',
  1408. check=models.Q(fk_1_id=2) | models.Q(fk_2=2),
  1409. ),
  1410. ]
  1411. self.assertEqual(Model.check(databases=self.databases), [])
  1412. @skipUnlessDBFeature('supports_table_check_constraints')
  1413. def test_check_constraint_pointing_to_pk(self):
  1414. class Model(models.Model):
  1415. age = models.SmallIntegerField()
  1416. class Meta:
  1417. constraints = [
  1418. models.CheckConstraint(
  1419. name='name',
  1420. check=models.Q(pk__gt=5) & models.Q(age__gt=models.F('pk')),
  1421. ),
  1422. ]
  1423. self.assertEqual(Model.check(databases=self.databases), [])
  1424. @skipUnlessDBFeature('supports_table_check_constraints')
  1425. def test_check_constraint_pointing_to_non_local_field(self):
  1426. class Parent(models.Model):
  1427. field1 = models.IntegerField()
  1428. class Child(Parent):
  1429. pass
  1430. class Meta:
  1431. constraints = [
  1432. models.CheckConstraint(name='name', check=models.Q(field1=1)),
  1433. ]
  1434. self.assertEqual(Child.check(databases=self.databases), [
  1435. Error(
  1436. "'constraints' refers to field 'field1' which is not local to "
  1437. "model 'Child'.",
  1438. hint='This issue may be caused by multi-table inheritance.',
  1439. obj=Child,
  1440. id='models.E016',
  1441. ),
  1442. ])
  1443. @skipUnlessDBFeature('supports_table_check_constraints')
  1444. def test_check_constraint_pointing_to_joined_fields(self):
  1445. class Model(models.Model):
  1446. name = models.CharField(max_length=10)
  1447. field1 = models.PositiveSmallIntegerField()
  1448. field2 = models.PositiveSmallIntegerField()
  1449. field3 = models.PositiveSmallIntegerField()
  1450. parent = models.ForeignKey('self', models.CASCADE)
  1451. class Meta:
  1452. constraints = [
  1453. models.CheckConstraint(
  1454. name='name1', check=models.Q(
  1455. field1__lt=models.F('parent__field1') + models.F('parent__field2')
  1456. )
  1457. ),
  1458. models.CheckConstraint(
  1459. name='name2', check=models.Q(name=Lower('parent__name'))
  1460. ),
  1461. models.CheckConstraint(
  1462. name='name3', check=models.Q(parent__field3=models.F('field1'))
  1463. ),
  1464. ]
  1465. joined_fields = ['parent__field1', 'parent__field2', 'parent__field3', 'parent__name']
  1466. errors = Model.check(databases=self.databases)
  1467. expected_errors = [
  1468. Error(
  1469. "'constraints' refers to the joined field '%s'." % field_name,
  1470. obj=Model,
  1471. id='models.E041',
  1472. ) for field_name in joined_fields
  1473. ]
  1474. self.assertCountEqual(errors, expected_errors)
  1475. @skipUnlessDBFeature('supports_table_check_constraints')
  1476. def test_check_constraint_pointing_to_joined_fields_complex_check(self):
  1477. class Model(models.Model):
  1478. name = models.PositiveSmallIntegerField()
  1479. field1 = models.PositiveSmallIntegerField()
  1480. field2 = models.PositiveSmallIntegerField()
  1481. parent = models.ForeignKey('self', models.CASCADE)
  1482. class Meta:
  1483. constraints = [
  1484. models.CheckConstraint(
  1485. name='name',
  1486. check=models.Q(
  1487. (
  1488. models.Q(name='test') &
  1489. models.Q(field1__lt=models.F('parent__field1'))
  1490. ) |
  1491. (
  1492. models.Q(name__startswith=Lower('parent__name')) &
  1493. models.Q(field1__gte=(
  1494. models.F('parent__field1') + models.F('parent__field2')
  1495. ))
  1496. )
  1497. ) | (models.Q(name='test1'))
  1498. ),
  1499. ]
  1500. joined_fields = ['parent__field1', 'parent__field2', 'parent__name']
  1501. errors = Model.check(databases=self.databases)
  1502. expected_errors = [
  1503. Error(
  1504. "'constraints' refers to the joined field '%s'." % field_name,
  1505. obj=Model,
  1506. id='models.E041',
  1507. ) for field_name in joined_fields
  1508. ]
  1509. self.assertCountEqual(errors, expected_errors)
  1510. def test_unique_constraint_with_condition(self):
  1511. class Model(models.Model):
  1512. age = models.IntegerField()
  1513. class Meta:
  1514. constraints = [
  1515. models.UniqueConstraint(
  1516. fields=['age'],
  1517. name='unique_age_gte_100',
  1518. condition=models.Q(age__gte=100),
  1519. ),
  1520. ]
  1521. errors = Model.check(databases=self.databases)
  1522. expected = [] if connection.features.supports_partial_indexes else [
  1523. Warning(
  1524. '%s does not support unique constraints with conditions.'
  1525. % connection.display_name,
  1526. hint=(
  1527. "A constraint won't be created. Silence this warning if "
  1528. "you don't care about it."
  1529. ),
  1530. obj=Model,
  1531. id='models.W036',
  1532. ),
  1533. ]
  1534. self.assertEqual(errors, expected)
  1535. def test_unique_constraint_with_condition_required_db_features(self):
  1536. class Model(models.Model):
  1537. age = models.IntegerField()
  1538. class Meta:
  1539. required_db_features = {'supports_partial_indexes'}
  1540. constraints = [
  1541. models.UniqueConstraint(
  1542. fields=['age'],
  1543. name='unique_age_gte_100',
  1544. condition=models.Q(age__gte=100),
  1545. ),
  1546. ]
  1547. self.assertEqual(Model.check(databases=self.databases), [])
  1548. def test_unique_constraint_condition_pointing_to_missing_field(self):
  1549. class Model(models.Model):
  1550. age = models.SmallIntegerField()
  1551. class Meta:
  1552. required_db_features = {'supports_partial_indexes'}
  1553. constraints = [
  1554. models.UniqueConstraint(
  1555. name='name',
  1556. fields=['age'],
  1557. condition=models.Q(missing_field=2),
  1558. ),
  1559. ]
  1560. self.assertEqual(Model.check(databases=self.databases), [
  1561. Error(
  1562. "'constraints' refers to the nonexistent field "
  1563. "'missing_field'.",
  1564. obj=Model,
  1565. id='models.E012',
  1566. ),
  1567. ] if connection.features.supports_partial_indexes else [])
  1568. def test_unique_constraint_condition_pointing_to_joined_fields(self):
  1569. class Model(models.Model):
  1570. age = models.SmallIntegerField()
  1571. parent = models.ForeignKey('self', models.CASCADE)
  1572. class Meta:
  1573. required_db_features = {'supports_partial_indexes'}
  1574. constraints = [
  1575. models.UniqueConstraint(
  1576. name='name',
  1577. fields=['age'],
  1578. condition=models.Q(parent__age__lt=2),
  1579. ),
  1580. ]
  1581. self.assertEqual(Model.check(databases=self.databases), [
  1582. Error(
  1583. "'constraints' refers to the joined field 'parent__age__lt'.",
  1584. obj=Model,
  1585. id='models.E041',
  1586. )
  1587. ] if connection.features.supports_partial_indexes else [])
  1588. def test_deferrable_unique_constraint(self):
  1589. class Model(models.Model):
  1590. age = models.IntegerField()
  1591. class Meta:
  1592. constraints = [
  1593. models.UniqueConstraint(
  1594. fields=['age'],
  1595. name='unique_age_deferrable',
  1596. deferrable=models.Deferrable.DEFERRED,
  1597. ),
  1598. ]
  1599. errors = Model.check(databases=self.databases)
  1600. expected = [] if connection.features.supports_deferrable_unique_constraints else [
  1601. Warning(
  1602. '%s does not support deferrable unique constraints.'
  1603. % connection.display_name,
  1604. hint=(
  1605. "A constraint won't be created. Silence this warning if "
  1606. "you don't care about it."
  1607. ),
  1608. obj=Model,
  1609. id='models.W038',
  1610. ),
  1611. ]
  1612. self.assertEqual(errors, expected)
  1613. def test_deferrable_unique_constraint_required_db_features(self):
  1614. class Model(models.Model):
  1615. age = models.IntegerField()
  1616. class Meta:
  1617. required_db_features = {'supports_deferrable_unique_constraints'}
  1618. constraints = [
  1619. models.UniqueConstraint(
  1620. fields=['age'],
  1621. name='unique_age_deferrable',
  1622. deferrable=models.Deferrable.IMMEDIATE,
  1623. ),
  1624. ]
  1625. self.assertEqual(Model.check(databases=self.databases), [])
  1626. def test_unique_constraint_pointing_to_missing_field(self):
  1627. class Model(models.Model):
  1628. class Meta:
  1629. constraints = [models.UniqueConstraint(fields=['missing_field'], name='name')]
  1630. self.assertEqual(Model.check(databases=self.databases), [
  1631. Error(
  1632. "'constraints' refers to the nonexistent field "
  1633. "'missing_field'.",
  1634. obj=Model,
  1635. id='models.E012',
  1636. ),
  1637. ])
  1638. def test_unique_constraint_pointing_to_m2m_field(self):
  1639. class Model(models.Model):
  1640. m2m = models.ManyToManyField('self')
  1641. class Meta:
  1642. constraints = [models.UniqueConstraint(fields=['m2m'], name='name')]
  1643. self.assertEqual(Model.check(databases=self.databases), [
  1644. Error(
  1645. "'constraints' refers to a ManyToManyField 'm2m', but "
  1646. "ManyToManyFields are not permitted in 'constraints'.",
  1647. obj=Model,
  1648. id='models.E013',
  1649. ),
  1650. ])
  1651. def test_unique_constraint_pointing_to_non_local_field(self):
  1652. class Parent(models.Model):
  1653. field1 = models.IntegerField()
  1654. class Child(Parent):
  1655. field2 = models.IntegerField()
  1656. class Meta:
  1657. constraints = [
  1658. models.UniqueConstraint(fields=['field2', 'field1'], name='name'),
  1659. ]
  1660. self.assertEqual(Child.check(databases=self.databases), [
  1661. Error(
  1662. "'constraints' refers to field 'field1' which is not local to "
  1663. "model 'Child'.",
  1664. hint='This issue may be caused by multi-table inheritance.',
  1665. obj=Child,
  1666. id='models.E016',
  1667. ),
  1668. ])
  1669. def test_unique_constraint_pointing_to_fk(self):
  1670. class Target(models.Model):
  1671. pass
  1672. class Model(models.Model):
  1673. fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
  1674. fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
  1675. class Meta:
  1676. constraints = [
  1677. models.UniqueConstraint(fields=['fk_1_id', 'fk_2'], name='name'),
  1678. ]
  1679. self.assertEqual(Model.check(databases=self.databases), [])
  1680. def test_unique_constraint_with_include(self):
  1681. class Model(models.Model):
  1682. age = models.IntegerField()
  1683. class Meta:
  1684. constraints = [
  1685. models.UniqueConstraint(
  1686. fields=['age'],
  1687. name='unique_age_include_id',
  1688. include=['id'],
  1689. ),
  1690. ]
  1691. errors = Model.check(databases=self.databases)
  1692. expected = [] if connection.features.supports_covering_indexes else [
  1693. Warning(
  1694. '%s does not support unique constraints with non-key columns.'
  1695. % connection.display_name,
  1696. hint=(
  1697. "A constraint won't be created. Silence this warning if "
  1698. "you don't care about it."
  1699. ),
  1700. obj=Model,
  1701. id='models.W039',
  1702. ),
  1703. ]
  1704. self.assertEqual(errors, expected)
  1705. def test_unique_constraint_with_include_required_db_features(self):
  1706. class Model(models.Model):
  1707. age = models.IntegerField()
  1708. class Meta:
  1709. required_db_features = {'supports_covering_indexes'}
  1710. constraints = [
  1711. models.UniqueConstraint(
  1712. fields=['age'],
  1713. name='unique_age_include_id',
  1714. include=['id'],
  1715. ),
  1716. ]
  1717. self.assertEqual(Model.check(databases=self.databases), [])
  1718. @skipUnlessDBFeature('supports_covering_indexes')
  1719. def test_unique_constraint_include_pointing_to_missing_field(self):
  1720. class Model(models.Model):
  1721. class Meta:
  1722. constraints = [
  1723. models.UniqueConstraint(
  1724. fields=['id'],
  1725. include=['missing_field'],
  1726. name='name',
  1727. ),
  1728. ]
  1729. self.assertEqual(Model.check(databases=self.databases), [
  1730. Error(
  1731. "'constraints' refers to the nonexistent field "
  1732. "'missing_field'.",
  1733. obj=Model,
  1734. id='models.E012',
  1735. ),
  1736. ])
  1737. @skipUnlessDBFeature('supports_covering_indexes')
  1738. def test_unique_constraint_include_pointing_to_m2m_field(self):
  1739. class Model(models.Model):
  1740. m2m = models.ManyToManyField('self')
  1741. class Meta:
  1742. constraints = [
  1743. models.UniqueConstraint(
  1744. fields=['id'],
  1745. include=['m2m'],
  1746. name='name',
  1747. ),
  1748. ]
  1749. self.assertEqual(Model.check(databases=self.databases), [
  1750. Error(
  1751. "'constraints' refers to a ManyToManyField 'm2m', but "
  1752. "ManyToManyFields are not permitted in 'constraints'.",
  1753. obj=Model,
  1754. id='models.E013',
  1755. ),
  1756. ])
  1757. @skipUnlessDBFeature('supports_covering_indexes')
  1758. def test_unique_constraint_include_pointing_to_non_local_field(self):
  1759. class Parent(models.Model):
  1760. field1 = models.IntegerField()
  1761. class Child(Parent):
  1762. field2 = models.IntegerField()
  1763. class Meta:
  1764. constraints = [
  1765. models.UniqueConstraint(
  1766. fields=['field2'],
  1767. include=['field1'],
  1768. name='name',
  1769. ),
  1770. ]
  1771. self.assertEqual(Child.check(databases=self.databases), [
  1772. Error(
  1773. "'constraints' refers to field 'field1' which is not local to "
  1774. "model 'Child'.",
  1775. hint='This issue may be caused by multi-table inheritance.',
  1776. obj=Child,
  1777. id='models.E016',
  1778. ),
  1779. ])
  1780. @skipUnlessDBFeature('supports_covering_indexes')
  1781. def test_unique_constraint_include_pointing_to_fk(self):
  1782. class Target(models.Model):
  1783. pass
  1784. class Model(models.Model):
  1785. fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
  1786. fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
  1787. class Meta:
  1788. constraints = [
  1789. models.UniqueConstraint(
  1790. fields=['id'],
  1791. include=['fk_1_id', 'fk_2'],
  1792. name='name',
  1793. ),
  1794. ]
  1795. self.assertEqual(Model.check(databases=self.databases), [])
  1796. def test_func_unique_constraint(self):
  1797. class Model(models.Model):
  1798. name = models.CharField(max_length=10)
  1799. class Meta:
  1800. constraints = [
  1801. models.UniqueConstraint(Lower('name'), name='lower_name_uq'),
  1802. ]
  1803. warn = Warning(
  1804. '%s does not support unique constraints on expressions.'
  1805. % connection.display_name,
  1806. hint=(
  1807. "A constraint won't be created. Silence this warning if you "
  1808. "don't care about it."
  1809. ),
  1810. obj=Model,
  1811. id='models.W044',
  1812. )
  1813. expected = [] if connection.features.supports_expression_indexes else [warn]
  1814. self.assertEqual(Model.check(databases=self.databases), expected)
  1815. def test_func_unique_constraint_required_db_features(self):
  1816. class Model(models.Model):
  1817. name = models.CharField(max_length=10)
  1818. class Meta:
  1819. constraints = [
  1820. models.UniqueConstraint(Lower('name'), name='lower_name_unq'),
  1821. ]
  1822. required_db_features = {'supports_expression_indexes'}
  1823. self.assertEqual(Model.check(databases=self.databases), [])
  1824. @skipUnlessDBFeature('supports_expression_indexes')
  1825. def test_func_unique_constraint_expression_custom_lookup(self):
  1826. class Model(models.Model):
  1827. height = models.IntegerField()
  1828. weight = models.IntegerField()
  1829. class Meta:
  1830. constraints = [
  1831. models.UniqueConstraint(
  1832. models.F('height') / (models.F('weight__abs') + models.Value(5)),
  1833. name='name',
  1834. ),
  1835. ]
  1836. with register_lookup(models.IntegerField, Abs):
  1837. self.assertEqual(Model.check(databases=self.databases), [])
  1838. @skipUnlessDBFeature('supports_expression_indexes')
  1839. def test_func_unique_constraint_pointing_to_missing_field(self):
  1840. class Model(models.Model):
  1841. class Meta:
  1842. constraints = [
  1843. models.UniqueConstraint(Lower('missing_field').desc(), name='name'),
  1844. ]
  1845. self.assertEqual(Model.check(databases=self.databases), [
  1846. Error(
  1847. "'constraints' refers to the nonexistent field "
  1848. "'missing_field'.",
  1849. obj=Model,
  1850. id='models.E012',
  1851. ),
  1852. ])
  1853. @skipUnlessDBFeature('supports_expression_indexes')
  1854. def test_func_unique_constraint_pointing_to_missing_field_nested(self):
  1855. class Model(models.Model):
  1856. class Meta:
  1857. constraints = [
  1858. models.UniqueConstraint(Abs(Round('missing_field')), name='name'),
  1859. ]
  1860. self.assertEqual(Model.check(databases=self.databases), [
  1861. Error(
  1862. "'constraints' refers to the nonexistent field "
  1863. "'missing_field'.",
  1864. obj=Model,
  1865. id='models.E012',
  1866. ),
  1867. ])
  1868. @skipUnlessDBFeature('supports_expression_indexes')
  1869. def test_func_unique_constraint_pointing_to_m2m_field(self):
  1870. class Model(models.Model):
  1871. m2m = models.ManyToManyField('self')
  1872. class Meta:
  1873. constraints = [models.UniqueConstraint(Lower('m2m'), name='name')]
  1874. self.assertEqual(Model.check(databases=self.databases), [
  1875. Error(
  1876. "'constraints' refers to a ManyToManyField 'm2m', but "
  1877. "ManyToManyFields are not permitted in 'constraints'.",
  1878. obj=Model,
  1879. id='models.E013',
  1880. ),
  1881. ])
  1882. @skipUnlessDBFeature('supports_expression_indexes')
  1883. def test_func_unique_constraint_pointing_to_non_local_field(self):
  1884. class Foo(models.Model):
  1885. field1 = models.CharField(max_length=15)
  1886. class Bar(Foo):
  1887. class Meta:
  1888. constraints = [models.UniqueConstraint(Lower('field1'), name='name')]
  1889. self.assertEqual(Bar.check(databases=self.databases), [
  1890. Error(
  1891. "'constraints' refers to field 'field1' which is not local to "
  1892. "model 'Bar'.",
  1893. hint='This issue may be caused by multi-table inheritance.',
  1894. obj=Bar,
  1895. id='models.E016',
  1896. ),
  1897. ])
  1898. @skipUnlessDBFeature('supports_expression_indexes')
  1899. def test_func_unique_constraint_pointing_to_fk(self):
  1900. class Foo(models.Model):
  1901. id = models.CharField(primary_key=True, max_length=255)
  1902. class Bar(models.Model):
  1903. foo_1 = models.ForeignKey(Foo, models.CASCADE, related_name='bar_1')
  1904. foo_2 = models.ForeignKey(Foo, models.CASCADE, related_name='bar_2')
  1905. class Meta:
  1906. constraints = [
  1907. models.UniqueConstraint(
  1908. Lower('foo_1_id'),
  1909. Lower('foo_2'),
  1910. name='name',
  1911. ),
  1912. ]
  1913. self.assertEqual(Bar.check(databases=self.databases), [])