From afe0513f436f36f39504486c295f978db246289c Mon Sep 17 00:00:00 2001
From: peter_baserow <peter@baserow.io>
Date: Fri, 14 Feb 2025 17:02:29 +0000
Subject: [PATCH 1/4] First pass at adding distribution support in
 integrations/builder.

---
 backend/src/baserow/contrib/database/apps.py  |  2 +
 .../database/fields/field_aggregations.py     | 12 ++++
 .../contrib/database/fields/registries.py     | 46 ++++++++++++++-
 .../local_baserow/service_types.py            | 56 +++++++++++++------
 .../dataSource/DataSourceDropdown.vue         |  2 +-
 .../components/CollectionElementHeader.vue    |  2 +-
 .../general/RecordSelectorElementForm.vue     | 14 +++--
 .../modules/builder/dataProviderTypes.js      |  2 +-
 .../modules/builder/elementTypeMixins.js      | 17 +++++-
 .../builder/mixins/collectionElementForm.js   | 12 +++-
 .../modules/builder/store/elementContent.js   |  7 ++-
 web-frontend/modules/core/serviceTypes.js     |  6 +-
 .../modules/integrations/serviceTypes.js      | 30 +++++++---
 13 files changed, 166 insertions(+), 42 deletions(-)

diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py
index 150a7a422..43c95f01e 100755
--- a/backend/src/baserow/contrib/database/apps.py
+++ b/backend/src/baserow/contrib/database/apps.py
@@ -243,6 +243,7 @@ class DatabaseConfig(AppConfig):
             CheckedFieldAggregationType,
             CheckedPercentageFieldAggregationType,
             CountFieldAggregationType,
+            DistributionFieldAggregationType,
             EarliestDateFieldAggregationType,
             EmptyCountFieldAggregationType,
             EmptyPercentageFieldAggregationType,
@@ -279,6 +280,7 @@ class DatabaseConfig(AppConfig):
         field_aggregation_registry.register(StdDevFieldAggregationType())
         field_aggregation_registry.register(VarianceFieldAggregationType())
         field_aggregation_registry.register(MedianFieldAggregationType())
+        field_aggregation_registry.register(DistributionFieldAggregationType())
 
         from .fields.field_converters import (
             AutonumberFieldConverter,
diff --git a/backend/src/baserow/contrib/database/fields/field_aggregations.py b/backend/src/baserow/contrib/database/fields/field_aggregations.py
index 551a7198b..cf1c9ab0a 100644
--- a/backend/src/baserow/contrib/database/fields/field_aggregations.py
+++ b/backend/src/baserow/contrib/database/fields/field_aggregations.py
@@ -35,6 +35,7 @@ from baserow.contrib.database.formula.types.formula_types import (
 from baserow.contrib.database.views.view_aggregations import (
     AverageViewAggregationType,
     CountViewAggregationType,
+    DistributionViewAggregationType,
     EmptyCountViewAggregationType,
     MaxViewAggregationType,
     MedianViewAggregationType,
@@ -341,3 +342,14 @@ class MedianFieldAggregationType(FieldAggregationType):
     type = "median"
     raw_type = MedianViewAggregationType
     compatible_field_types = raw_type.compatible_field_types
+
+
+class DistributionFieldAggregationType(FieldAggregationType):
+    """
+    Compute the distribution of values
+    """
+
+    type = "distribution"
+    result_type = "array"
+    raw_type = DistributionViewAggregationType
+    compatible_field_types = raw_type.compatible_field_types
diff --git a/backend/src/baserow/contrib/database/fields/registries.py b/backend/src/baserow/contrib/database/fields/registries.py
index b1e2cb7fd..8c43c0b7f 100644
--- a/backend/src/baserow/contrib/database/fields/registries.py
+++ b/backend/src/baserow/contrib/database/fields/registries.py
@@ -52,7 +52,10 @@ from baserow.contrib.database.views.exceptions import (
     AggregationTypeAlreadyRegistered,
     AggregationTypeDoesNotExist,
 )
-from baserow.contrib.database.views.utils import AnnotatedAggregation
+from baserow.contrib.database.views.utils import (
+    AnnotatedAggregation,
+    DistributionAggregation,
+)
 from baserow.core.registries import ImportExportConfig
 from baserow.core.registry import (
     APIUrlsInstanceMixin,
@@ -2235,6 +2238,7 @@ class FieldAggregationType(Instance):
             raise IncompatibleField()
 
         aggregation_dict = self._get_aggregation_dict(queryset, model_field, field)
+        distribution_dict = self._get_distribution_dict(queryset, model_field, field)
 
         # Check if the returned aggregations contain a `AnnotatedAggregation`,
         # and if so, apply the annotations and only keep the actual aggregation in
@@ -2246,8 +2250,10 @@ class FieldAggregationType(Instance):
                 aggregation_dict[key] = value.aggregation
 
         results = queryset.aggregate(**aggregation_dict)
+        results.update(distribution_dict)
         return self._compute_final_aggregation(
-            results[f"{field.db_column}_raw"], results.get("total", None)
+            results.get(f"{field.db_column}_raw", results[field.db_column]),
+            results.get("total", None),
         )
 
     def field_is_compatible(self, field: "Field") -> bool:
@@ -2280,6 +2286,36 @@ class FieldAggregationType(Instance):
 
         return self.raw_type().get_aggregation(field.db_column, model_field, field)
 
+    def _get_distribution_dict(
+        self, queryset: QuerySet, model_field: DjangoField, field: Field
+    ) -> dict[str, any]:
+        """
+        Returns a dictionary defining the distributions for the queryset.aggregate
+        call.
+
+        :param queryset: The queryset to select only the rows that should
+            be aggregated.
+        :param model_field: The Django model field of the field that
+            the aggregation is for.
+        :param field: The field that the aggregation is for.
+        :return:
+        """
+
+        aggregation = self._get_raw_aggregation(model_field, field.specific)
+        if not isinstance(aggregation, DistributionAggregation):
+            return {}
+
+        formatted = []
+        raw_calculation = aggregation.calculate(queryset.all())
+        for result in raw_calculation:
+            formatted.append(
+                {
+                    "value": result[0],
+                    "count": result[1],
+                }
+            )
+        return {field.db_column: formatted}
+
     def _get_aggregation_dict(
         self,
         queryset: QuerySet,
@@ -2288,7 +2324,7 @@ class FieldAggregationType(Instance):
         include_agg_type=False,
     ) -> dict:
         """
-        Returns a dictinary defining the aggregation for the queryset.aggregate
+        Returns a dictionary defining the aggregation for the queryset.aggregate
         call.
 
         :param queryset: The queryset to select only the rows that should
@@ -2301,6 +2337,10 @@ class FieldAggregationType(Instance):
         aggregation = self._get_raw_aggregation(model_field, field.specific)
         key = f"{field.db_column}_{self.type}" if include_agg_type else field.db_column
         aggregation_dict = {f"{key}_raw": aggregation}
+        if isinstance(aggregation, DistributionAggregation):
+            return {}
+
+        aggregation_dict = {f"{field.db_column}_raw": aggregation}
         # Check if the returned aggregations contain a `AnnotatedAggregation`,
         # and if so, apply the annotations and only keep the actual aggregation in
         # the dict. This is needed because some aggregations require annotated values
diff --git a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
index 3571cb5fc..e04280d02 100644
--- a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
+++ b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
@@ -1277,24 +1277,43 @@ class LocalBaserowAggregateRowsUserServiceType(
         if not service.field or not service.aggregation_type:
             return None
 
+        result_key = "results" if self.returns_list(service) else "result"
+
         # The `result` must be an allowed field, otherwise we have no schema.
-        if allowed_fields is not None and "result" not in allowed_fields:
+        if allowed_fields is not None and result_key not in allowed_fields:
             return {}
 
         # Pluck out the aggregation type which this service uses. We'll use its
         # `result_type` to inform the schema what the expected `result` format is.
         aggregation_type = field_aggregation_registry.get(service.aggregation_type)
 
-        return {
-            "title": self.get_schema_name(service),
-            "type": "object",
-            "properties": {
-                "result": {
+        schema = {"title": self.get_schema_name(service)}
+
+        if aggregation_type.result_type == "array":
+            schema["type"] = "array"
+            schema["items"] = {
+                "type": "object",
+                "properties": {
+                    "value": {
+                        "title": f"{service.field.name} value",
+                        "type": "string",
+                    },
+                    "count": {
+                        "title": f"{service.field.name} distribution",
+                        "type": "number",
+                    },
+                },
+            }
+        else:
+            schema["type"] = "object"
+            schema["properties"] = {
+                f"{result_key}": {
                     "title": f"{service.field.name} result",
                     "type": aggregation_type.result_type,
                 }
-            },
-        }
+            }
+
+        return schema
 
     def get_context_data(
         self,
@@ -1363,6 +1382,10 @@ class LocalBaserowAggregateRowsUserServiceType(
         field_id: int
         aggregation_type: str
 
+    def returns_list(self, service: LocalBaserowAggregateRows) -> bool:
+        aggregation_type = field_aggregation_registry.get(service.aggregation_type)
+        return aggregation_type.result_type == "array"
+
     def prepare_values(
         self,
         values: Dict[str, Any],
@@ -1549,9 +1572,12 @@ class LocalBaserowAggregateRowsUserServiceType(
         :return: Aggregations.
         """
 
-        only_field_names = self.get_used_field_names(service, dispatch_context)
-        if only_field_names and "result" not in only_field_names:
-            return {"data": {"result": None}}
+        result_key = "results" if self.returns_list(service) else "result"
+
+        # TODO: resolve
+        # only_field_names = self.get_used_field_names(service, dispatch_context)
+        # if only_field_names and result_key not in only_field_names:
+        #     return {"data": {result_key: []}}
 
         try:
             table = resolved_values["table"]
@@ -1569,7 +1595,7 @@ class LocalBaserowAggregateRowsUserServiceType(
             result = agg_type.aggregate(queryset, model_field, field)
 
             return {
-                "data": {"result": result},
+                "data": {result_key: result},
                 "baserow_table_model": model,
             }
         except DjangoFieldDoesNotExist as ex:
@@ -1601,10 +1627,8 @@ class LocalBaserowAggregateRowsUserServiceType(
         Returns the usual properties for this service type.
         """
 
-        if path[0] == "result":
-            return ["result"]
-
-        return []
+        # TODO: confirm this is right
+        return ["result", "value", "count"]
 
 
 class LocalBaserowGetRowUserServiceType(
diff --git a/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue b/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
index 05fd06423..6f863a8ca 100644
--- a/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
+++ b/web-frontend/modules/builder/components/dataSource/DataSourceDropdown.vue
@@ -86,7 +86,7 @@ export default {
         return dataSource.name
       }
       const service = this.$registry.get('service', dataSource.type)
-      const suffix = service.returnsList
+      const suffix = service.returnsList({ service: dataSource })
         ? this.$t('integrationsCommon.multipleRows')
         : this.$t('integrationsCommon.singleRow')
       return `${dataSource.name} (${suffix})`
diff --git a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
index 596ce6f4e..884a0b8a9 100644
--- a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
+++ b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
@@ -1,7 +1,7 @@
 <template>
   <component
     :is="serviceType.adhocHeaderComponent"
-    v-if="dataSource"
+    v-if="dataSource && elementType.adhocFilteringSupported(dataSource)"
     class="collection-element__header margin-bottom-1"
     :sortable-properties="
       elementType.adhocSortableProperties(element, dataSource)
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
index f338d31dd..e1c1cd141 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/RecordSelectorElementForm.vue
@@ -190,15 +190,17 @@ export default {
       if (this.localDataSources === null) {
         return null
       }
-      return this.localDataSources.filter(
-        (dataSource) =>
-          this.$registry.get('service', dataSource.type).returnsList
+      return this.localDataSources.filter((dataSource) =>
+        this.$registry
+          .get('service', dataSource.type)
+          .returnsList({ service: dataSource })
       )
     },
     listSharedDataSources() {
-      return this.sharedDataSources.filter(
-        (dataSource) =>
-          this.$registry.get('service', dataSource.type).returnsList
+      return this.sharedDataSources.filter((dataSource) =>
+        this.$registry
+          .get('service', dataSource.type)
+          .returnsList({ service: dataSource })
       )
     },
   },
diff --git a/web-frontend/modules/builder/dataProviderTypes.js b/web-frontend/modules/builder/dataProviderTypes.js
index 7cea464d4..07fad7184 100644
--- a/web-frontend/modules/builder/dataProviderTypes.js
+++ b/web-frontend/modules/builder/dataProviderTypes.js
@@ -117,7 +117,7 @@ export class DataSourceDataProviderType extends DataProviderType {
 
     const serviceType = this.app.$registry.get('service', dataSource.type)
 
-    if (serviceType.returnsList) {
+    if (serviceType.returnsList({ service: dataSource })) {
       return dataSourceContents[dataSource.id]?.results
     } else {
       return dataSourceContents[dataSource.id]
diff --git a/web-frontend/modules/builder/elementTypeMixins.js b/web-frontend/modules/builder/elementTypeMixins.js
index 6ecbcf608..39e48bd6d 100644
--- a/web-frontend/modules/builder/elementTypeMixins.js
+++ b/web-frontend/modules/builder/elementTypeMixins.js
@@ -1,4 +1,5 @@
 import { ELEMENT_EVENTS, SHARE_TYPES } from '@baserow/modules/builder/enums'
+import { LocalBaserowAggregateRowsServiceType } from '@baserow/modules/integrations/serviceTypes'
 
 export const ContainerElementTypeMixin = (Base) =>
   class extends Base {
@@ -54,6 +55,17 @@ export const CollectionElementTypeMixin = (Base) =>
   class extends Base {
     isCollectionElement = true
 
+    /**
+     * Response for returning whether this collection element, using this dataSource,
+     * supports adhoc filtering (e.g. filtering, sorting, searching). Normally they
+     * will, but if the collection element is used by an aggregation returning a list
+     * of records, then it will not.
+     * @param dataSource
+     */
+    adhocFilteringSupported(dataSource) {
+      return dataSource.type !== LocalBaserowAggregateRowsServiceType.getType()
+    }
+
     /**
      * A helper function responsible for returning this collection element's
      * schema properties.
@@ -326,7 +338,10 @@ export const CollectionElementTypeMixin = (Base) =>
       const serviceType = this.app.$registry.get('service', dataSource.type)
 
       // If the data source type doesn't return a list, we should have a schema_property
-      if (!serviceType.returnsList && !parentWithDataSource.schema_property) {
+      if (
+        !serviceType.returnsList({ service: dataSource }) &&
+        !parentWithDataSource.schema_property
+      ) {
         return true
       }
 
diff --git a/web-frontend/modules/builder/mixins/collectionElementForm.js b/web-frontend/modules/builder/mixins/collectionElementForm.js
index c5db6cd9d..6bf0adc55 100644
--- a/web-frontend/modules/builder/mixins/collectionElementForm.js
+++ b/web-frontend/modules/builder/mixins/collectionElementForm.js
@@ -49,7 +49,13 @@ export default {
      * @returns {boolean} - Whether the property options are available.
      */
     propertyOptionsAvailable() {
-      return this.selectedDataSource && this.selectedDataSourceReturnsList
+      const { element } = this.applicationContext
+      const elementType = this.$registry.get('element', element.type)
+      return (
+        this.selectedDataSource &&
+        this.selectedDataSourceReturnsList &&
+        elementType.adhocFilteringSupported(this.selectedDataSource)
+      )
     },
     /**
      * In collection element forms, the ability to view paging options
@@ -150,7 +156,9 @@ export default {
       return this.$registry.get('service', this.selectedDataSource.type)
     },
     selectedDataSourceReturnsList() {
-      return this.selectedDataSourceType?.returnsList
+      return this.selectedDataSourceType?.returnsList({
+        service: this.selectedDataSource,
+      })
     },
     maxItemPerPage() {
       if (!this.selectedDataSourceType) {
diff --git a/web-frontend/modules/builder/store/elementContent.js b/web-frontend/modules/builder/store/elementContent.js
index baf1f3066..ccfee3138 100644
--- a/web-frontend/modules/builder/store/elementContent.js
+++ b/web-frontend/modules/builder/store/elementContent.js
@@ -179,7 +179,10 @@ const actions = {
 
     // We have a data source, but if it doesn't return a list,
     // it needs to have a `schema_property` to work correctly.
-    if (!serviceType.returnsList && element.schema_property === null) {
+    if (
+      !serviceType.returnsList({ service: dataSource }) &&
+      element.schema_property === null
+    ) {
       // If we previously had a list data source, we might have content,
       // so rather than leave the content *until a schema property is set*,
       // clear it.
@@ -246,7 +249,7 @@ const actions = {
           })
         }
 
-        if (serviceType.returnsList) {
+        if (serviceType.returnsList({ service: dataSource })) {
           // The service type returns a list of results, we'll set the content
           // using the results key and set the range for future paging.
           commit('SET_CONTENT', {
diff --git a/web-frontend/modules/core/serviceTypes.js b/web-frontend/modules/core/serviceTypes.js
index 6b65ccca2..9053b1bce 100644
--- a/web-frontend/modules/core/serviceTypes.js
+++ b/web-frontend/modules/core/serviceTypes.js
@@ -29,9 +29,11 @@ export class ServiceType extends Registerable {
   }
 
   /**
-   * Whether the service returns a collection of records.
+   * Whether the service returns a collection of records. Most of the time this
+   * will simply return true or false, but a service can be provided to determine
+   * if a specific service type returns a list of records.
    */
-  get returnsList() {
+  returnsList({ service }) {
     return false
   }
 
diff --git a/web-frontend/modules/integrations/serviceTypes.js b/web-frontend/modules/integrations/serviceTypes.js
index f144edac5..0ff7b2174 100644
--- a/web-frontend/modules/integrations/serviceTypes.js
+++ b/web-frontend/modules/integrations/serviceTypes.js
@@ -131,7 +131,7 @@ export class LocalBaserowListRowsServiceType extends LocalBaserowTableServiceTyp
     return LocalBaserowAdhocHeader
   }
 
-  get returnsList() {
+  returnsList({ service }) {
     return true
   }
 
@@ -232,6 +232,15 @@ export class LocalBaserowAggregateRowsServiceType extends LocalBaserowTableServi
     return 'local_baserow_aggregate_rows'
   }
 
+  returnsList({ service }) {
+    // TODO: store this in the registry
+    return service.aggregation_type === 'distribution'
+  }
+
+  get maxResultLimit() {
+    return 100
+  }
+
   get name() {
     return this.app.i18n.t('serviceType.localBaserowAggregateRows')
   }
@@ -240,12 +249,19 @@ export class LocalBaserowAggregateRowsServiceType extends LocalBaserowTableServi
     return LocalBaserowAggregateRowsForm
   }
 
-  /**
-   * Local Baserow aggregate rows does not currently support the distribution
-   * aggregation type, this will be resolved in a future release.
-   */
-  get unsupportedAggregationTypes() {
-    return [DistributionViewAggregationType.getType()]
+  getDefaultCollectionFields(service) {
+    return Object.keys(service.schema.items.properties)
+      .filter((field) => field !== 'id')
+      .map((field) => {
+        const outputType = 'text'
+        const valueFormula = `get('current_record.${field}')`
+        return {
+          name: service.schema.items.properties[field].title,
+          type: outputType,
+          value: valueFormula,
+          id: uuid(),
+        }
+      })
   }
 
   getResult(service, data) {

From dc3b8afac83c2b6a2138db8a48c6c153fa695703 Mon Sep 17 00:00:00 2001
From: peter_baserow <peter@baserow.io>
Date: Fri, 21 Mar 2025 13:07:04 +0000
Subject: [PATCH 2/4] Removing the temporary code which disabled the
 distribution agg type.

---
 .../local_baserow/service_types.py            | 14 ----------
 .../test_aggregate_rows_service_type.py       | 26 -------------------
 .../AggregateRowsDataSourceForm.vue           |  7 -----
 .../LocalBaserowAggregateRowsForm.vue         |  7 -----
 4 files changed, 54 deletions(-)

diff --git a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
index e04280d02..76df74c27 100644
--- a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
+++ b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
@@ -63,9 +63,6 @@ from baserow.contrib.database.views.exceptions import (
 )
 from baserow.contrib.database.views.models import DEFAULT_SORT_TYPE_KEY
 from baserow.contrib.database.views.service import ViewService
-from baserow.contrib.database.views.view_aggregations import (
-    DistributionViewAggregationType,
-)
 from baserow.contrib.integrations.local_baserow.api.serializers import (
     LocalBaserowTableServiceFieldMappingSerializer,
 )
@@ -1241,10 +1238,6 @@ class LocalBaserowAggregateRowsUserServiceType(
     dispatch_type = DispatchTypes.DISPATCH_DATA_SOURCE
     serializer_mixins = LocalBaserowTableServiceFilterableMixin.mixin_serializer_mixins
 
-    # Local Baserow aggregate rows does not currently support the distribution
-    # aggregation type, this will be resolved in a future release.
-    unsupported_aggregation_types = [DistributionViewAggregationType.type]
-
     def get_schema_name(self, service: LocalBaserowAggregateRows) -> str:
         """
         The Local Baserow aggregation schema name added to the `title` in
@@ -1412,13 +1405,6 @@ class LocalBaserowAggregateRowsUserServiceType(
             "aggregation_type", getattr(instance, "aggregation_type", "")
         )
 
-        if aggregation_type in self.unsupported_aggregation_types:
-            raise DRFValidationError(
-                detail=f"The {aggregation_type} aggregation type "
-                "is not currently supported.",
-                code="unsupported_aggregation_type",
-            )
-
         if "table" in values:
             # Reset the field if the table has changed
             if (
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py
index e9298ac1a..ea6e7e251 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py
@@ -587,29 +587,3 @@ def test_local_baserow_aggregate_rows_dispatch_data_field_type_not_compatible_an
         exc.value.args[0] == f"The field with ID {field.id} is not compatible "
         f"with the aggregation type {service.aggregation_type}"
     )
-
-
-@pytest.mark.django_db
-def test_create_local_baserow_aggregate_rows_service_with_unsupported_aggregation_type(
-    data_fixture,
-):
-    user = data_fixture.create_user()
-    page = data_fixture.create_builder_page(user=user)
-    dashboard = page.builder
-    table = data_fixture.create_database_table(user=user)
-    field = data_fixture.create_number_field(table=table)
-    view = data_fixture.create_grid_view(user=user, table=table)
-    integration = data_fixture.create_local_baserow_integration(
-        application=dashboard, user=user
-    )
-    service = data_fixture.create_local_baserow_aggregate_rows_service(
-        table=table, field=field, integration=integration
-    )
-    service_type = service.get_type()
-    unsupported_agg_type = service_type.unsupported_aggregation_types[0]
-
-    with pytest.raises(
-        ValidationError,
-        match=f"The {unsupported_agg_type} aggregation type is not currently supported.",
-    ):
-        service_type.prepare_values({"aggregation_type": unsupported_agg_type}, user)
diff --git a/web-frontend/modules/dashboard/components/data_source/AggregateRowsDataSourceForm.vue b/web-frontend/modules/dashboard/components/data_source/AggregateRowsDataSourceForm.vue
index 45345cbc0..daacffbfa 100644
--- a/web-frontend/modules/dashboard/components/data_source/AggregateRowsDataSourceForm.vue
+++ b/web-frontend/modules/dashboard/components/data_source/AggregateRowsDataSourceForm.vue
@@ -112,9 +112,6 @@
             :key="viewAggregation.getType()"
             :name="viewAggregation.getName()"
             :value="viewAggregation.getType()"
-            :disabled="
-              unsupportedAggregationTypes.includes(viewAggregation.getType())
-            "
           >
           </DropdownItem>
         </Dropdown>
@@ -227,10 +224,6 @@ export default {
     aggregationTypeNames() {
       return this.viewAggregationTypes.map((aggType) => aggType.getType())
     },
-    unsupportedAggregationTypes() {
-      return this.$registry.get('service', 'local_baserow_aggregate_rows')
-        .unsupportedAggregationTypes
-    },
   },
   watch: {
     dataSource: {
diff --git a/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowAggregateRowsForm.vue b/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowAggregateRowsForm.vue
index 8d705e66e..87d178aab 100644
--- a/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowAggregateRowsForm.vue
+++ b/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowAggregateRowsForm.vue
@@ -45,9 +45,6 @@
               :key="viewAggregation.getType()"
               :name="viewAggregation.getName()"
               :value="viewAggregation.getType()"
-              :disabled="
-                unsupportedAggregationTypes.includes(viewAggregation.getType())
-              "
             >
             </DropdownItem>
           </Dropdown>
@@ -130,10 +127,6 @@ export default {
     }
   },
   computed: {
-    unsupportedAggregationTypes() {
-      return this.$registry.get('service', 'local_baserow_aggregate_rows')
-        .unsupportedAggregationTypes
-    },
     viewAggregationTypes() {
       const selectedField = this.tableFields.find(
         (field) => field.id === this.values.field_id

From 03f73322b0f25b25ec518b0ac14628a30e0526cc Mon Sep 17 00:00:00 2001
From: peter_baserow <peter@baserow.io>
Date: Fri, 21 Mar 2025 13:14:48 +0000
Subject: [PATCH 3/4] We still need to check, per collection element, if the
 datasource's service type supports adhoc refinements. It can if it's the list
 rows service type.

---
 .../elements/components/CollectionElementHeader.vue        | 2 +-
 web-frontend/modules/builder/elementTypeMixins.js          | 5 +++--
 .../modules/builder/mixins/collectionElementForm.js        | 2 +-
 web-frontend/modules/integrations/serviceTypes.js          | 7 +++++++
 4 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
index 884a0b8a9..131b10242 100644
--- a/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
+++ b/web-frontend/modules/builder/components/elements/components/CollectionElementHeader.vue
@@ -1,7 +1,7 @@
 <template>
   <component
     :is="serviceType.adhocHeaderComponent"
-    v-if="dataSource && elementType.adhocFilteringSupported(dataSource)"
+    v-if="dataSource && elementType.adhocRefinementsSupported(dataSource)"
     class="collection-element__header margin-bottom-1"
     :sortable-properties="
       elementType.adhocSortableProperties(element, dataSource)
diff --git a/web-frontend/modules/builder/elementTypeMixins.js b/web-frontend/modules/builder/elementTypeMixins.js
index 39e48bd6d..b5c094098 100644
--- a/web-frontend/modules/builder/elementTypeMixins.js
+++ b/web-frontend/modules/builder/elementTypeMixins.js
@@ -62,8 +62,9 @@ export const CollectionElementTypeMixin = (Base) =>
      * of records, then it will not.
      * @param dataSource
      */
-    adhocFilteringSupported(dataSource) {
-      return dataSource.type !== LocalBaserowAggregateRowsServiceType.getType()
+    adhocRefinementsSupported(dataSource) {
+      const serviceType = this.app.$registry.get('service', dataSource.type)
+      return serviceType.adhocRefinementsSupported
     }
 
     /**
diff --git a/web-frontend/modules/builder/mixins/collectionElementForm.js b/web-frontend/modules/builder/mixins/collectionElementForm.js
index 6bf0adc55..2e510613e 100644
--- a/web-frontend/modules/builder/mixins/collectionElementForm.js
+++ b/web-frontend/modules/builder/mixins/collectionElementForm.js
@@ -54,7 +54,7 @@ export default {
       return (
         this.selectedDataSource &&
         this.selectedDataSourceReturnsList &&
-        elementType.adhocFilteringSupported(this.selectedDataSource)
+        elementType.adhocRefinementsSupported(this.selectedDataSource)
       )
     },
     /**
diff --git a/web-frontend/modules/integrations/serviceTypes.js b/web-frontend/modules/integrations/serviceTypes.js
index 0ff7b2174..72395ce47 100644
--- a/web-frontend/modules/integrations/serviceTypes.js
+++ b/web-frontend/modules/integrations/serviceTypes.js
@@ -8,6 +8,11 @@ import LocalBaserowAdhocHeader from '@baserow/modules/integrations/localBaserow/
 import { DistributionViewAggregationType } from '@baserow/modules/database/viewAggregationTypes'
 
 export class LocalBaserowTableServiceType extends ServiceType {
+  // Determines whether collection elements with data sources using this
+  // service are allowed to perform adhoc refinements (filtering, sorting, searching).
+  // By default, they cannot, only the list rows service type can.
+  adhocRefinementsSupported = false
+
   get integrationType() {
     return this.app.$registry.get(
       'integration',
@@ -112,6 +117,8 @@ export class LocalBaserowGetRowServiceType extends LocalBaserowTableServiceType
 }
 
 export class LocalBaserowListRowsServiceType extends LocalBaserowTableServiceType {
+  adhocRefinementsSupported = true
+
   static getType() {
     return 'local_baserow_list_rows'
   }

From c2b931980373fff81ab31b1d0000e28164382dc2 Mon Sep 17 00:00:00 2001
From: peter_baserow <peter@baserow.io>
Date: Fri, 21 Mar 2025 13:48:19 +0000
Subject: [PATCH 4/4] Ensure that aggregation is working in both dashboard
 summaries, and builder summaries.

---
 .../baserow/contrib/database/fields/registries.py | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/backend/src/baserow/contrib/database/fields/registries.py b/backend/src/baserow/contrib/database/fields/registries.py
index 8c43c0b7f..dc840a0dd 100644
--- a/backend/src/baserow/contrib/database/fields/registries.py
+++ b/backend/src/baserow/contrib/database/fields/registries.py
@@ -2174,7 +2174,7 @@ class FieldConverter(Instance):
         """
 
         raise NotImplementedError(
-            "Each field converter must have an alter_field " "method."
+            "Each field converter must have an alter_field method."
         )
 
 
@@ -2251,8 +2251,12 @@ class FieldAggregationType(Instance):
 
         results = queryset.aggregate(**aggregation_dict)
         results.update(distribution_dict)
+
+        raw_aggregation_result = results.get(
+            f"{field.db_column}_raw", results.get(field.db_column)
+        )
         return self._compute_final_aggregation(
-            results.get(f"{field.db_column}_raw", results[field.db_column]),
+            raw_aggregation_result,
             results.get("total", None),
         )
 
@@ -2265,8 +2269,6 @@ class FieldAggregationType(Instance):
         :return: True if the field is compatible, False otherwise.
         """
 
-        from baserow.contrib.database.fields.registries import field_type_registry
-
         field_type = field_type_registry.get_by_model(field.specific_class)
 
         return any(
@@ -2340,7 +2342,6 @@ class FieldAggregationType(Instance):
         if isinstance(aggregation, DistributionAggregation):
             return {}
 
-        aggregation_dict = {f"{field.db_column}_raw": aggregation}
         # Check if the returned aggregations contain a `AnnotatedAggregation`,
         # and if so, apply the annotations and only keep the actual aggregation in
         # the dict. This is needed because some aggregations require annotated values
@@ -2354,7 +2355,9 @@ class FieldAggregationType(Instance):
 
         return aggregation_dict
 
-    def _compute_final_aggregation(self, raw_aggregation_result, total_count: int):
+    def _compute_final_aggregation(
+        self, raw_aggregation_result, total_count: Optional[int] = None
+    ):
         """
         For field aggregation types that require 'with_total' the number of all
         rows to compute the final number this method will be called to compute