Repository: django/django

Issue:
# Fixed #36890 -- Supported StringAgg(distinct=True) on SQLite with the default delimiter.

#### Trac ticket number
<!-- Replace XXXXX with the corresponding Trac ticket number. -->
<!-- Or delete the line and write "N/A" if this is a trivial PR. -->

ticket-36890

#### Branch description
Currently, the generic StringAgg function raises an error on SQLite when distinct=True

This change enables support for distinct aggregation on SQLite by omitting the delimiter argument in the generated SQL when it matches the default comma.

#### AI Assistance Disclosure (REQUIRED)
<!-- Please select exactly ONE of the following: -->
- [ ] **No AI tools were used** in preparing this PR.
- [X] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

I used GitHub Copilot to assist with writing tests.

#### Checklist
- [X] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [X] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [X] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [X] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [X] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.


Implementation patch:
diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py
index 1cf82416cb73..d1139e8bcc3c 100644
--- a/django/db/models/aggregates.py
+++ b/django/db/models/aggregates.py
@@ -369,6 +369,24 @@ def as_mysql(self, compiler, connection, **extra_context):
         return sql, (*params, *delimiter_params)
 
     def as_sqlite(self, compiler, connection, **extra_context):
+        if (
+            self.distinct
+            and isinstance(self.delimiter.value, Value)
+            and self.delimiter.value.value == ","
+        ):
+            clone = self.copy()
+            source_expressions = clone.get_source_expressions()
+            clone.set_source_expressions(
+                source_expressions[:1] + source_expressions[2:]
+            )
+
+            return clone.as_sql(
+                compiler,
+                connection,
+                function="GROUP_CONCAT",
+                **extra_context,
+            )
+
         if connection.get_database_version() < (3, 44):
             return self.as_sql(
                 compiler,


Test patch:
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
index bf6bf2703112..0a975dcb529e 100644
--- a/tests/aggregation/tests.py
+++ b/tests/aggregation/tests.py
@@ -3,6 +3,7 @@
 import re
 from decimal import Decimal
 from itertools import chain
+from unittest import skipUnless
 
 from django.core.exceptions import FieldError
 from django.db import NotSupportedError, connection
@@ -579,6 +580,16 @@ def test_distinct_on_stringagg(self):
         )
         self.assertCountEqual(books["ratings"].split(","), ["3", "4", "4.5", "5"])
 
+    @skipUnless(connection.vendor == "sqlite", "Special default case for SQLite.")
+    def test_distinct_on_stringagg_sqlite_special_case(self):
+        """
+        Value(",") is the only delimiter usable on SQLite with distinct=True.
+        """
+        books = Book.objects.aggregate(
+            ratings=StringAgg(Cast(F("rating"), CharField()), Value(","), distinct=True)
+        )
+        self.assertCountEqual(books["ratings"].split(","), ["3.0", "4.0", "4.5", "5.0"])
+
     @skipIfDBFeature("supports_aggregate_distinct_multiple_argument")
     def test_raises_error_on_multiple_argument_distinct(self):
         message = (
@@ -589,7 +600,7 @@ def test_raises_error_on_multiple_argument_distinct(self):
             Book.objects.aggregate(
                 ratings=StringAgg(
                     Cast(F("rating"), CharField()),
-                    Value(","),
+                    Value(";"),
                     distinct=True,
                 )
             )