|
11 | 11 | ModelState, |
12 | 12 | ProjectState, |
13 | 13 | ) |
14 | | -from django.db.models import BaseConstraint, Index, Q, UniqueConstraint |
| 14 | +from django.db.models import BaseConstraint, F, Index, Q, UniqueConstraint, functions |
15 | 15 | from django.test import override_settings, utils |
16 | 16 |
|
17 | 17 | from django_pg_migration_tools import operations |
18 | 18 | from tests.example_app.models import ( |
19 | | - AnotherCharModel, |
20 | 19 | CharIDModel, |
21 | 20 | CharModel, |
22 | 21 | IntModel, |
|
26 | 25 | NotNullIntFieldModel, |
27 | 26 | NullFKFieldModel, |
28 | 27 | NullIntFieldModel, |
| 28 | + UniqueConditionCharModel, |
| 29 | + UniqueExpressionCharModel, |
29 | 30 | get_check_constraint, |
30 | 31 | ) |
31 | 32 |
|
@@ -1306,6 +1307,117 @@ def test_when_condition_on_constraint_only_creates_index(self): |
1306 | 1307 | ) |
1307 | 1308 | assert not cursor.fetchone() |
1308 | 1309 |
|
| 1310 | + @pytest.mark.django_db(transaction=True) |
| 1311 | + def test_when_expression_on_constraint_only_creates_index(self): |
| 1312 | + constraint_name = "unique_expression_int_field" |
| 1313 | + |
| 1314 | + # Prove that: |
| 1315 | + # - An invalid index doesn't exist. |
| 1316 | + # - The constraint/index doesn't exist yet. |
| 1317 | + with connection.cursor() as cursor: |
| 1318 | + cursor.execute( |
| 1319 | + _CHECK_VALID_INDEX_EXISTS_QUERY, |
| 1320 | + {"index_name": constraint_name}, |
| 1321 | + ) |
| 1322 | + assert not cursor.fetchone() |
| 1323 | + # Also, set the lock_timeout to check it has been returned to |
| 1324 | + # its original value once the unique index creation is completed. |
| 1325 | + cursor.execute(_SET_LOCK_TIMEOUT) |
| 1326 | + |
| 1327 | + project_state = ProjectState() |
| 1328 | + project_state.add_model(ModelState.from_model(IntModel)) |
| 1329 | + new_state = project_state.clone() |
| 1330 | + |
| 1331 | + operation = operations.SaferAddUniqueConstraint( |
| 1332 | + model_name="intmodel", |
| 1333 | + constraint=UniqueConstraint( |
| 1334 | + functions.Greatest(F("int_field"), 2), |
| 1335 | + name=constraint_name, |
| 1336 | + ), |
| 1337 | + ) |
| 1338 | + operation.state_forwards(self.app_label, new_state) |
| 1339 | + # Proceed to add the unique index followed by the constraint: |
| 1340 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1341 | + with utils.CaptureQueriesContext(connection) as queries: |
| 1342 | + operation.database_forwards( |
| 1343 | + self.app_label, editor, from_state=project_state, to_state=new_state |
| 1344 | + ) |
| 1345 | + |
| 1346 | + # Confirm that exists as index |
| 1347 | + with connection.cursor() as cursor: |
| 1348 | + cursor.execute( |
| 1349 | + _CHECK_INDEX_EXISTS_QUERY, |
| 1350 | + { |
| 1351 | + "table_name": "example_app_intmodel", |
| 1352 | + "index_name": constraint_name, |
| 1353 | + }, |
| 1354 | + ) |
| 1355 | + assert cursor.fetchone() |
| 1356 | + |
| 1357 | + # Assert on the sequence of expected SQL queries: |
| 1358 | + # |
| 1359 | + # 1. Check the original lock_timeout value to be able to restore it |
| 1360 | + # later. |
| 1361 | + assert queries[0]["sql"] == "SHOW lock_timeout;" |
| 1362 | + # 2. Remove the timeout. |
| 1363 | + assert queries[1]["sql"] == "SET lock_timeout = '0';" |
| 1364 | + # 3. Verify if the index is invalid. |
| 1365 | + assert queries[2]["sql"] == dedent( |
| 1366 | + f""" |
| 1367 | + SELECT relname |
| 1368 | + FROM pg_class, pg_index |
| 1369 | + WHERE ( |
| 1370 | + pg_index.indisvalid = false |
| 1371 | + AND pg_index.indexrelid = pg_class.oid |
| 1372 | + AND relname = '{constraint_name}' |
| 1373 | + ); |
| 1374 | + """ |
| 1375 | + ) |
| 1376 | + # 4. Finally create the index concurrently. |
| 1377 | + assert ( |
| 1378 | + queries[3]["sql"] |
| 1379 | + == f'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "{constraint_name}" ON "example_app_intmodel" ((GREATEST("int_field", 2)))' |
| 1380 | + ) |
| 1381 | + # 6. Set the timeout back to what it was originally. |
| 1382 | + assert queries[4]["sql"] == "SET lock_timeout = '1s';" |
| 1383 | + |
| 1384 | + # There are no additional queries |
| 1385 | + assert len(queries) == 5 |
| 1386 | + |
| 1387 | + # Reverse the migration to drop the index and constraint, and verify |
| 1388 | + # that the lock_timeout queries are correct. |
| 1389 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1390 | + with utils.CaptureQueriesContext(connection) as reverse_queries: |
| 1391 | + operation.database_backwards( |
| 1392 | + self.app_label, editor, from_state=new_state, to_state=project_state |
| 1393 | + ) |
| 1394 | + |
| 1395 | + # 2. perform the ALTER TABLE. |
| 1396 | + assert reverse_queries[0]["sql"] == "SHOW lock_timeout;" |
| 1397 | + |
| 1398 | + # 3. Remove the timeout. |
| 1399 | + assert reverse_queries[1]["sql"] == "SET lock_timeout = '0';" |
| 1400 | + # 4. Verify if the index is invalid. |
| 1401 | + assert ( |
| 1402 | + reverse_queries[2]["sql"] |
| 1403 | + == f'DROP INDEX CONCURRENTLY IF EXISTS "{constraint_name}"' |
| 1404 | + ) |
| 1405 | + |
| 1406 | + assert reverse_queries[3]["sql"] == "SET lock_timeout = '1s';" |
| 1407 | + |
| 1408 | + assert len(reverse_queries) == 4 |
| 1409 | + |
| 1410 | + # Verify the index representing the constraint doesn't exist any more. |
| 1411 | + with connection.cursor() as cursor: |
| 1412 | + cursor.execute( |
| 1413 | + _CHECK_INDEX_EXISTS_QUERY, |
| 1414 | + { |
| 1415 | + "table_name": "example_app_intmodel", |
| 1416 | + "index_name": constraint_name, |
| 1417 | + }, |
| 1418 | + ) |
| 1419 | + assert not cursor.fetchone() |
| 1420 | + |
1309 | 1421 |
|
1310 | 1422 | class TestBuildPostgresIdentifier: |
1311 | 1423 | def test_happy_path(self): |
@@ -1491,11 +1603,116 @@ def test_operation_where_condition_on_unique_constraint(self): |
1491 | 1603 | assert cursor.fetchone() |
1492 | 1604 |
|
1493 | 1605 | project_state = ProjectState() |
1494 | | - project_state.add_model(ModelState.from_model(AnotherCharModel)) |
| 1606 | + project_state.add_model(ModelState.from_model(UniqueConditionCharModel)) |
| 1607 | + new_state = project_state.clone() |
| 1608 | + |
| 1609 | + operation = operations.SaferRemoveUniqueConstraint( |
| 1610 | + model_name="uniqueconditioncharmodel", |
| 1611 | + name=constraint_name, |
| 1612 | + ) |
| 1613 | + |
| 1614 | + operation.state_forwards(self.app_label, new_state) |
| 1615 | + # Proceed to remove the constraint. |
| 1616 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1617 | + with utils.CaptureQueriesContext(connection) as queries: |
| 1618 | + operation.database_forwards( |
| 1619 | + self.app_label, editor, from_state=project_state, to_state=new_state |
| 1620 | + ) |
| 1621 | + |
| 1622 | + # Prove the index is not there any longer. |
| 1623 | + with connection.cursor() as cursor: |
| 1624 | + cursor.execute( |
| 1625 | + _CHECK_VALID_INDEX_EXISTS_QUERY, |
| 1626 | + {"index_name": constraint_name}, |
| 1627 | + ) |
| 1628 | + assert not cursor.fetchone() |
| 1629 | + |
| 1630 | + # Assert on the sequence of expected SQL queries: |
| 1631 | + # |
| 1632 | + # 1. Check the original lock_timeout value to be able to restore it |
| 1633 | + # later. |
| 1634 | + assert queries[0]["sql"] == "SHOW lock_timeout;" |
| 1635 | + # 2. Remove the timeout. |
| 1636 | + assert queries[1]["sql"] == "SET lock_timeout = '0';" |
| 1637 | + |
| 1638 | + # 3. Drop the index concurrently. |
| 1639 | + assert ( |
| 1640 | + queries[2]["sql"] |
| 1641 | + == f'DROP INDEX CONCURRENTLY IF EXISTS "{constraint_name}"' |
| 1642 | + ) |
| 1643 | + # 4. Set the timeout back to what it was originally. |
| 1644 | + assert queries[3]["sql"] == "SET lock_timeout = '1s';" |
| 1645 | + |
| 1646 | + assert len(queries) == 4 |
| 1647 | + |
| 1648 | + # Before reversing, set the lock_timeout value so we can observe it |
| 1649 | + # being re-set. |
| 1650 | + with connection.cursor() as cursor: |
| 1651 | + cursor.execute(_SET_LOCK_TIMEOUT) |
| 1652 | + |
| 1653 | + # Reverse the migration to recreate the constraint. |
| 1654 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1655 | + with utils.CaptureQueriesContext(connection) as reverse_queries: |
| 1656 | + operation.database_backwards( |
| 1657 | + self.app_label, editor, from_state=new_state, to_state=project_state |
| 1658 | + ) |
| 1659 | + |
| 1660 | + # These will be the same as when creating a constraint safely. I.e., |
| 1661 | + # adding the index concurrently without timeouts, and using this index |
| 1662 | + # to create the constraint. |
| 1663 | + # |
| 1664 | + |
| 1665 | + # 1. Check the original lock_timeout value to be able to restore it |
| 1666 | + # later. |
| 1667 | + assert reverse_queries[0]["sql"] == "SHOW lock_timeout;" |
| 1668 | + # 2. Remove the timeout. |
| 1669 | + assert reverse_queries[1]["sql"] == "SET lock_timeout = '0';" |
| 1670 | + # 3. Verify if the index is invalid. |
| 1671 | + assert reverse_queries[2]["sql"] == dedent( |
| 1672 | + f""" |
| 1673 | + SELECT relname |
| 1674 | + FROM pg_class, pg_index |
| 1675 | + WHERE ( |
| 1676 | + pg_index.indisvalid = false |
| 1677 | + AND pg_index.indexrelid = pg_class.oid |
| 1678 | + AND relname = '{constraint_name}' |
| 1679 | + ); |
| 1680 | + """ |
| 1681 | + ) |
| 1682 | + # 4. Finally create the index concurrently. |
| 1683 | + assert ( |
| 1684 | + reverse_queries[3]["sql"] |
| 1685 | + == f'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "{constraint_name}" ON "example_app_uniqueconditioncharmodel" ("char_field") WHERE "char_field" IN (\'c\', \'something\')' |
| 1686 | + ) |
| 1687 | + # 5. Set the timeout back to what it was originally. |
| 1688 | + assert reverse_queries[4]["sql"] == "SET lock_timeout = '1s';" |
| 1689 | + |
| 1690 | + # Nothing else. |
| 1691 | + assert len(reverse_queries) == 5 |
| 1692 | + |
| 1693 | + @pytest.mark.django_db(transaction=True) |
| 1694 | + def test_operation_where_expression_on_unique_constraint(self): |
| 1695 | + constraint_name = "unique_char_field_with_expression" |
| 1696 | + |
| 1697 | + with connection.cursor() as cursor: |
| 1698 | + # Set the lock_timeout to check it has been returned to |
| 1699 | + # its original value once the index creation is completed. |
| 1700 | + cursor.execute(_SET_LOCK_TIMEOUT) |
| 1701 | + |
| 1702 | + # Prove that the constraint/index exists before the operation removes it. |
| 1703 | + with connection.cursor() as cursor: |
| 1704 | + cursor.execute( |
| 1705 | + _CHECK_VALID_INDEX_EXISTS_QUERY, |
| 1706 | + {"index_name": constraint_name}, |
| 1707 | + ) |
| 1708 | + assert cursor.fetchone() |
| 1709 | + |
| 1710 | + project_state = ProjectState() |
| 1711 | + project_state.add_model(ModelState.from_model(UniqueExpressionCharModel)) |
1495 | 1712 | new_state = project_state.clone() |
1496 | 1713 |
|
1497 | 1714 | operation = operations.SaferRemoveUniqueConstraint( |
1498 | | - model_name="anothercharmodel", |
| 1715 | + model_name="uniqueexpressioncharmodel", |
1499 | 1716 | name=constraint_name, |
1500 | 1717 | ) |
1501 | 1718 |
|
@@ -1570,7 +1787,7 @@ def test_operation_where_condition_on_unique_constraint(self): |
1570 | 1787 | # 4. Finally create the index concurrently. |
1571 | 1788 | assert ( |
1572 | 1789 | reverse_queries[3]["sql"] |
1573 | | - == f'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "{constraint_name}" ON "example_app_anothercharmodel" ("char_field") WHERE "char_field" IN (\'c\', \'something\')' |
| 1790 | + == f'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "{constraint_name}" ON "example_app_uniqueexpressioncharmodel" ((LOWER("char_field")))' |
1574 | 1791 | ) |
1575 | 1792 | # 5. Set the timeout back to what it was originally. |
1576 | 1793 | assert reverse_queries[4]["sql"] == "SET lock_timeout = '1s';" |
|
0 commit comments