Repository: django/django
Issue category: Bug Report

Changing output_field for GeneratedField leads to ProgrammingError with Postgres 16.5+
Description
	 
Consider the following model and assume the initial migration has been applied:
class Order(models.Model): 
	order_no = models.IntegerField() 
	item_no = models.CharField(max_length=25) 
	qty = models.IntegerField() 
	cost = models.DecimalField(max_digits=10, decimal_places=2) 
	total_cost = models.GeneratedField( 
		expression=F("cost") * F("qty"), 
		output_field=models.BigIntegerField(), 
		db_persist=True, 
	)
During a code review we determined the output field should be a Decimal field and the field was modified as follows:
total_cost = models.GeneratedField( 
	expression=F("cost") * F("qty"), 
	output_field=models.DecimalField(decimal_places=2, max_digits=16), 
	db_persist=True, 
)
And a new migration was generated:
migrations.AlterField( 
	model_name='order', 
	name='total_cost', 
	field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('cost'), '*', models.F('qty')), output_field=models.DecimalField(decimal_places=2, max_digits=16)), 
),
In Postgres 16.4 and earlier, this migration is applied without error. (I'm aware that the value of the total_cost field is not recomputed for existing records when this migration is applied.).
Starting with Postgres 16.5 and up, this migration fails with the following error:
psycopg2.errors.InvalidColumnDefinition: cannot specify USING when altering type of generated column
DETAIL: Column "total_cost" is a generated column.
...
django.db.utils.ProgrammingError: cannot specify USING when altering type of generated column
This appears to be a result of the following change in Postgres 16.5 (​release notes): 
Disallow a USING clause when altering the type of a generated column (Peter Eisentraut) [§](​https://postgr.es/c/5867ee005)
A generated column already has an expression specifying the column contents, so including USING doesn't make sense.
The Django ​documentation for GeneratedField makes it clear that 
There are many database-specific restrictions on generated fields that Django doesn’t validate and the database may raise an error
For this reason, I almost didn't open a ticket. However, there is logic in db/backends/base/schema.py that checks if the expression of a GeneratedField changed. Consider this migration (which changes the expression from multiplication to addition):
migrations.AlterField( 
	model_name='order', 
	name='total_cost', 
	field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('cost'), '+', models.F('qty')), output_field=models.BigIntegerField()), 
),
Attempting to apply this migration will raise the following error (even in Postgres 16.4):
ValueError: Modifying GeneratedFields is not supported - the field sales.Order.total_cost must be removed and re-added with the new definition.
This error is more helpful. It explains the problem better and even suggests a workaround.
Should we throw a similar error if the output_field of a GeneratedField is changed? Or add a tip to the documentation?
The above was tested with:
Django version 5.1.3
psycopg2 version 2.9.10
Postgres versions: 16.4, 16.5, 16.6, 17.0, and 17.2


Functions to edit:
django/db/backends/postgresql/schema.py:DatabaseSchemaEditor._using_sql

Patch:
diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py
index 75bf33147233..964009988cbb 100644
--- a/django/db/backends/postgresql/schema.py
+++ b/django/db/backends/postgresql/schema.py
@@ -120,6 +120,8 @@ def _create_like_index_sql(self, model, field):
         return None
 
     def _using_sql(self, new_field, old_field):
+        if new_field.generated:
+            return ""
         using_sql = " USING %(column)s::%(type)s"
         new_internal_type = new_field.get_internal_type()
         old_internal_type = old_field.get_internal_type()

Test patch: diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 6312a7d4a2e1..5557426e274f 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -6210,6 +6210,37 @@ def _test_add_generated_field(self, db_persist):
             operation.database_backwards(app_label, editor, new_state, project_state)
         self.assertColumnNotExists(f"{app_label}_pony", "modified_pink")
 
+    @skipUnlessDBFeature("supports_stored_generated_columns")
+    def test_generated_field_changes_output_field(self):
+        app_label = "test_gfcof"
+        operation = migrations.AddField(
+            "Pony",
+            "modified_pink",
+            models.GeneratedField(
+                expression=F("pink") + F("pink"),
+                output_field=models.IntegerField(),
+                db_persist=True,
+            ),
+        )
+        from_state, to_state = self.make_test_state(app_label, operation)
+        # Add generated column.
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, from_state, to_state)
+        # Update output_field used in the generated field.
+        operation = migrations.AlterField(
+            "Pony",
+            "modified_pink",
+            models.GeneratedField(
+                expression=F("pink") + F("pink"),
+                output_field=models.DecimalField(decimal_places=2, max_digits=16),
+                db_persist=True,
+            ),
+        )
+        from_state = to_state.clone()
+        to_state = self.apply_operations(app_label, from_state, [operation])
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, from_state, to_state)
+
     @skipUnlessDBFeature("supports_stored_generated_columns")
     def test_add_generated_field_stored(self):
         self._test_add_generated_field(db_persist=True)