Repository: apache/airflow

Issue:
PostgresToGCSOperator with server_side_cursor use unique_name that breaks postgres identifier limit
### Apache Airflow Provider(s)

google

### Versions of Apache Airflow Providers

<= 10.15.0

### Apache Airflow version

2.8.1

### Operating System

Debian GNU/Linux 12

### Deployment

Official Apache Airflow Helm Chart

### Deployment details

_No response_

### What happened

when total characters of dag_id and task_id greater than 24. (64 - 36 - 4, postgres identifier length - uuid.uuid4 length - underscore length)
PostgresToGCSOperator with argument `use_server_side_cursor = True` run into psycopg2 error
> psycopg2.errors.SyntaxError: unterminated quoted identifier

### What you think should happen instead

possible expectations

- Add a configuration options for the limitation of postgres identifier length. (default = 64)
- Detect postgres identifier length before execution. Error out as earier as possible
- Shorten the `_unique_name` generation for the cursor, currently
  ```
  def _unique_name(self):
    return f"{self.dag_id}__{self.task_id}__{uuid.uuid4()}" if self.use_server_side_cursor else None
  ```

### How to reproduce

```python
from airflow.providers.google.cloud.transfers.gcs_to_bigquery import GCSToBigQueryOperator

task = PostgresToGCSOperator(
  task_id="t" * 25,
  use_server_side_cursor=True,
  doc_md=f"""postgres identifier overflow""",
)
```

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template! If you are willing to raise PR to address this issue please do so, no need to wait for approval.

I guess it might be use something like hash instead, md5, sha1 or similar.

E.g. n Amazon Provider exists simple helper for generate deterministic identifiers: 
https://github.com/apache/airflow/blob/main/airflow/providers/amazon/aws/utils/identifiers.py

https://github.com/apache/airflow/blob/390490fdd4e72960a7cdac2dd09ebd503a8e30df/airflow/providers/amazon/aws/operators/ecs.py#L528-L531

If it shouldn't be a deterministic it could use [`slugify`](https://github.com/un33k/python-slugify) to truncate and fit to a limits

Pretty much different solutions for someone who might want to fix it.

BTW, postgres identifiers limited by 63 bytes, not 64 characters: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
Hello,
Can I try to fix this?
yes. please. I've heaivy workloads currently. But please share the fix with me. I'd love to learn more about the development process. @dybolo 

Resolving patch:
diff --git a/airflow/providers/google/cloud/transfers/postgres_to_gcs.py b/airflow/providers/google/cloud/transfers/postgres_to_gcs.py
--- a/airflow/providers/google/cloud/transfers/postgres_to_gcs.py
+++ b/airflow/providers/google/cloud/transfers/postgres_to_gcs.py
@@ -26,6 +26,7 @@
 from decimal import Decimal
 
 import pendulum
+from slugify import slugify
 
 from airflow.providers.google.cloud.transfers.sql_to_gcs import BaseSQLToGCSOperator
 from airflow.providers.postgres.hooks.postgres import PostgresHook
@@ -113,7 +114,23 @@ def __init__(
         self.cursor_itersize = cursor_itersize
 
     def _unique_name(self):
-        return f"{self.dag_id}__{self.task_id}__{uuid.uuid4()}" if self.use_server_side_cursor else None
+        """
+        Generate a non-deterministic UUID for the cursor name using the task_id and dag_id.
+
+        Ensures the resulting name fits within the maximum length allowed for an identifier in Postgres.
+        """
+        if self.use_server_side_cursor:
+            separator = "__"
+            random_sufix = str(uuid.uuid4())
+            available_length = 63 - len(random_sufix) - (len(separator) * 2)
+            return separator.join(
+                (
+                    slugify(self.dag_id, allow_unicode=False, max_length=available_length // 2),
+                    slugify(self.task_id, allow_unicode=False, max_length=available_length // 2),
+                    random_sufix,
+                )
+            )
+        return None
 
     def query(self):
         """Query Postgres and returns a cursor to the results."""


Test patch:
diff --git a/tests/providers/google/cloud/transfers/test_postgres_to_gcs.py b/tests/providers/google/cloud/transfers/test_postgres_to_gcs.py
--- a/tests/providers/google/cloud/transfers/test_postgres_to_gcs.py
+++ b/tests/providers/google/cloud/transfers/test_postgres_to_gcs.py
@@ -28,6 +28,7 @@
 TABLES = {"postgres_to_gcs_operator", "postgres_to_gcs_operator_empty"}
 
 TASK_ID = "test-postgres-to-gcs"
+LONG_TASK_ID = "t" * 100
 POSTGRES_CONN_ID = "postgres_default"
 SQL = "SELECT * FROM postgres_to_gcs_operator"
 BUCKET = "gs://test"
@@ -149,6 +150,25 @@ def test_exec_success_server_side_cursor(self, gcs_hook_mock_class):
         gcs_hook_mock.upload.side_effect = self._assert_uploaded_file_content
         op.execute(None)
 
+    @patch("airflow.providers.google.cloud.transfers.sql_to_gcs.GCSHook")
+    def test_exec_success_server_side_cursor_unique_name(self, gcs_hook_mock_class):
+        """
+        Test that the server side cursor unique name generator is successful
+        with a task id that surpasses postgres identifier limit.
+        """
+        op = PostgresToGCSOperator(
+            task_id=LONG_TASK_ID,
+            postgres_conn_id=POSTGRES_CONN_ID,
+            sql=SQL,
+            bucket=BUCKET,
+            filename=FILENAME,
+            use_server_side_cursor=True,
+            cursor_itersize=100,
+        )
+        gcs_hook_mock = gcs_hook_mock_class.return_value
+        gcs_hook_mock.upload.side_effect = self._assert_uploaded_file_content
+        op.execute(None)
+
     @patch("airflow.providers.google.cloud.transfers.sql_to_gcs.GCSHook")
     def test_file_splitting(self, gcs_hook_mock_class):
         """Test that ndjson is split by approx_max_file_size_bytes param."""