|
1 | 1 | from textwrap import dedent |
2 | 2 | from typing import Any |
| 3 | +from unittest.mock import patch |
3 | 4 |
|
4 | 5 | import pytest |
5 | 6 | from django.db import ( |
@@ -1436,6 +1437,144 @@ def test_when_expression_on_constraint_only_creates_index(self): |
1436 | 1437 | ) |
1437 | 1438 | assert not cursor.fetchone() |
1438 | 1439 |
|
| 1440 | + # Disable the overall test transaction because a unique concurrent index |
| 1441 | + # cannot be triggered/tested inside of a transaction. |
| 1442 | + @pytest.mark.django_db(transaction=True) |
| 1443 | + def test_when_nulls_not_distinct(self): |
| 1444 | + # Prove that: |
| 1445 | + # - An invalid index doesn't exist. |
| 1446 | + # - The constraint doesn't exist yet. |
| 1447 | + with connection.cursor() as cursor: |
| 1448 | + cursor.execute( |
| 1449 | + psycopg_sql.SQL(operations.IndexQueries.CHECK_INVALID_INDEX) |
| 1450 | + .format(index_name=psycopg_sql.Literal("unique_null_int_field")) |
| 1451 | + .as_string(cursor.connection) |
| 1452 | + ) |
| 1453 | + assert not cursor.fetchone() |
| 1454 | + cursor.execute( |
| 1455 | + psycopg_sql.SQL(operations.ConstraintQueries.CHECK_EXISTING_CONSTRAINT) |
| 1456 | + .format(constraint_name=psycopg_sql.Literal("unique_null_int_field")) |
| 1457 | + .as_string(cursor.connection) |
| 1458 | + ) |
| 1459 | + assert not cursor.fetchone() |
| 1460 | + # Also, set the lock_timeout to check it has been returned to |
| 1461 | + # its original value once the unique index creation is completed. |
| 1462 | + cursor.execute(_SET_LOCK_TIMEOUT) |
| 1463 | + |
| 1464 | + project_state = ProjectState() |
| 1465 | + project_state.add_model(ModelState.from_model(NullIntFieldModel)) |
| 1466 | + new_state = project_state.clone() |
| 1467 | + |
| 1468 | + operation = operations.SaferAddUniqueConstraint( |
| 1469 | + model_name="nullintfieldmodel", |
| 1470 | + constraint=UniqueConstraint( |
| 1471 | + fields=("int_field",), |
| 1472 | + name="unique_null_int_field", |
| 1473 | + nulls_distinct=False, |
| 1474 | + ), |
| 1475 | + ) |
| 1476 | + operation.state_forwards(self.app_label, new_state) |
| 1477 | + # Proceed to add the unique index followed by the constraint: |
| 1478 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1479 | + if not connection.features.supports_nulls_distinct_unique_constraints: |
| 1480 | + # Postgres versions <= 14 do not support NULLS NOT DISTINCT |
| 1481 | + with pytest.raises(operations.ConstraintNotSupported): |
| 1482 | + operation.database_forwards( |
| 1483 | + self.app_label, |
| 1484 | + editor, |
| 1485 | + from_state=project_state, |
| 1486 | + to_state=new_state, |
| 1487 | + ) |
| 1488 | + return |
| 1489 | + |
| 1490 | + with utils.CaptureQueriesContext(connection) as queries: |
| 1491 | + operation.database_forwards( |
| 1492 | + self.app_label, editor, from_state=project_state, to_state=new_state |
| 1493 | + ) |
| 1494 | + |
| 1495 | + with connection.cursor() as cursor: |
| 1496 | + cursor.execute( |
| 1497 | + _CHECK_INDEX_EXISTS_QUERY, |
| 1498 | + { |
| 1499 | + "table_name": "example_app_nullintfieldmodel", |
| 1500 | + "index_name": "unique_null_int_field", |
| 1501 | + }, |
| 1502 | + ) |
| 1503 | + assert cursor.fetchone() |
| 1504 | + cursor.execute( |
| 1505 | + _CHECK_CONSTRAINT_EXISTS_QUERY, |
| 1506 | + { |
| 1507 | + "table_name": "example_app_nullintfieldmodel", |
| 1508 | + "constraint_name": "unique_null_int_field", |
| 1509 | + }, |
| 1510 | + ) |
| 1511 | + assert cursor.fetchone() |
| 1512 | + |
| 1513 | + # Assert on the sequence of expected SQL queries: |
| 1514 | + # |
| 1515 | + # 1. Check if the constraint already exists. |
| 1516 | + assert queries[0]["sql"] == dedent(""" |
| 1517 | + SELECT con.conname |
| 1518 | + FROM pg_catalog.pg_constraint con |
| 1519 | + INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = con.connamespace |
| 1520 | + WHERE con.conname = 'unique_null_int_field' |
| 1521 | + AND nsp.nspname = current_schema(); |
| 1522 | + """) |
| 1523 | + # 2. Check the original lock_timeout value to be able to restore it |
| 1524 | + # later. |
| 1525 | + assert queries[1]["sql"] == "SHOW lock_timeout;" |
| 1526 | + # 3. Remove the timeout. |
| 1527 | + assert queries[2]["sql"] == "SET lock_timeout = '0';" |
| 1528 | + # 4. Verify if the index is invalid. |
| 1529 | + assert queries[3]["sql"] == dedent(""" |
| 1530 | + SELECT relname |
| 1531 | + FROM pg_class, pg_index |
| 1532 | + WHERE ( |
| 1533 | + pg_index.indisvalid = false |
| 1534 | + AND pg_index.indexrelid = pg_class.oid |
| 1535 | + AND relname = 'unique_null_int_field' |
| 1536 | + ); |
| 1537 | + """) |
| 1538 | + # 5. Finally create the index concurrently. |
| 1539 | + assert ( |
| 1540 | + queries[4]["sql"] |
| 1541 | + == 'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "unique_null_int_field" ON "example_app_nullintfieldmodel" ("int_field") NULLS NOT DISTINCT' |
| 1542 | + ) |
| 1543 | + # 6. Set the timeout back to what it was originally. |
| 1544 | + assert queries[5]["sql"] == "SET lock_timeout = '1s';" |
| 1545 | + |
| 1546 | + # 7. Add the table constraint. |
| 1547 | + assert ( |
| 1548 | + queries[6]["sql"] |
| 1549 | + == 'ALTER TABLE "example_app_nullintfieldmodel" ADD CONSTRAINT "unique_null_int_field" UNIQUE USING INDEX "unique_null_int_field"' |
| 1550 | + ) |
| 1551 | + |
| 1552 | + def test_when_nulls_not_distinct_and_not_supported(self): |
| 1553 | + project_state = ProjectState() |
| 1554 | + project_state.add_model(ModelState.from_model(NullIntFieldModel)) |
| 1555 | + new_state = project_state.clone() |
| 1556 | + |
| 1557 | + operation = operations.SaferAddUniqueConstraint( |
| 1558 | + model_name="nullintfieldmodel", |
| 1559 | + constraint=UniqueConstraint( |
| 1560 | + fields=("int_field",), |
| 1561 | + name="unique_null_int_field", |
| 1562 | + nulls_distinct=False, |
| 1563 | + ), |
| 1564 | + ) |
| 1565 | + with connection.schema_editor(atomic=False, collect_sql=False) as editor: |
| 1566 | + with patch.object( |
| 1567 | + connection, "features", supports_nulls_distinct_unique_constraints=False |
| 1568 | + ): |
| 1569 | + # Postgres versions <= 14 do not support NULLS NOT DISTINCT |
| 1570 | + with pytest.raises(operations.ConstraintNotSupported): |
| 1571 | + operation.database_forwards( |
| 1572 | + self.app_label, |
| 1573 | + editor, |
| 1574 | + from_state=project_state, |
| 1575 | + to_state=new_state, |
| 1576 | + ) |
| 1577 | + |
1439 | 1578 |
|
1440 | 1579 | class TestBuildPostgresIdentifier: |
1441 | 1580 | def test_happy_path(self): |
|
0 commit comments