From cb68c2df32a45e87960a81c1eae2aafce7d83dc5 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 14 Jan 2026 12:14:25 +0530 Subject: [PATCH 01/14] fix(query): aggregate order_field when used with select group_by --- frappe/database/query.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index ba675c077a..db160ef1da 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -266,6 +266,8 @@ class Engine: self.field_aliases = set() self.db_query_compat = db_query_compat self.permitted_fields_cache = {} # Cache for get_permitted_fields results + self.is_aggregate_query = False + self._grouped_sqls = set() if isinstance(table, Table): self.table = table @@ -314,11 +316,12 @@ class Engine: self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait) if group_by: + self.is_aggregate_query = True # for postgres (group by used with order by) self.apply_group_by(group_by) if order_by: if not ( - self.is_postgres and is_select and (distinct or group_by) + self.is_postgres and is_select and distinct ): # ignore in Postgres since order by fields need to appear in select distinct self.apply_order_by(order_by) else: @@ -351,6 +354,10 @@ class Engine: for field in self.fields: if isinstance(field, Field | DynamicTableField) and field.alias: self.field_aliases.add(field.alias) + elif self.is_postgres and getattr( + field, "alias", None + ): # captures aggregate functions (for pg order by fix) + self.field_aliases.add(field.alias) if self.apply_permissions: self.fields = self.apply_field_permissions() @@ -1120,8 +1127,25 @@ class Engine: # Note: Comma handling is done in parse_fields before this method is called return self.parse_string_field(field) + def _normalize_postgres_order_field(self, field): + """In PostgreSQL order_by fields need to either be in group_by or be aggregated + when used with select and group_by""" + if self.is_postgres and self.is_aggregate_query: + current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) + if current_sql in self._grouped_sqls: + return field + clean_name = current_sql.strip('"') + if clean_name in self.field_aliases: + return field + if not isinstance(field, functions.AggregateFunction): + return functions.Max(field) + return field + def apply_group_by(self, group_by: str | None = None): parsed_group_by_fields = self._validate_group_by(group_by) + self._grouped_sqls = { + f.get_sql() if hasattr(f, "get_sql") else str(f) for f in parsed_group_by_fields + } self.query = self.query.groupby(*parsed_group_by_fields) def apply_order_by(self, order_by: str | None): @@ -1131,7 +1155,9 @@ class Engine: parsed_order_fields = self._validate_order_by(order_by) for order_field, order_direction in parsed_order_fields: - self.query = self.query.orderby(order_field, order=order_direction) + self.query = self.query.orderby( + self._normalize_postgres_order_field(order_field), order=order_direction + ) def _apply_default_order_by(self): """Apply default ordering based on configured DocType metadata""" @@ -1150,14 +1176,18 @@ class Engine: order_direction = Order.desc if spec_order == "desc" else Order.asc else: order_direction = Order.asc if spec_order == "asc" else Order.desc - self.query = self.query.orderby(field, order=order_direction) + self.query = self.query.orderby( + self._normalize_postgres_order_field(field), order=order_direction + ) else: field = self.table[sort_field] if self.db_query_compat: order_direction = Order.desc if sort_order.lower() == "desc" else Order.asc else: order_direction = Order.asc if sort_order.lower() == "asc" else Order.desc - self.query = self.query.orderby(field, order=order_direction) + self.query = self.query.orderby( + self._normalize_postgres_order_field(field), order=order_direction + ) def _parse_backtick_field_notation(self, field_name: str) -> tuple[str, str] | None: """ From 157a657c1f1dd03fd0e0596b5d05f0b630eaefea Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 14 Jan 2026 18:14:21 +0530 Subject: [PATCH 02/14] test: add tests for order_by group_by behavior in postgres --- frappe/tests/test_query.py | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 86c627e490..c18e607491 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -2289,6 +2289,71 @@ class TestQuery(IntegrationTestCase): # the filter should still apply and return no results self.assertEqual(len(result), 0, "Filter should not be bypassed by shared doc OR condition") + @run_only_if(db_type_is.POSTGRES) + def test_order_by_group_by_postgres(self): + """PostgreSQL specific test that tests if order_by fields are correctly handled when used with group_by""" + # test order by fields already in group by (no aggregate needed) + query = frappe.qb.get_query( + "User", + fields=["creation as created_date", {"COUNT": "*"}], + group_by="created_date", + order_by="created_date", + ).get_sql() + + self.assertQueryEqual( + query, + 'SELECT "creation" "created_date",COUNT(*) FROM "tabUser" GROUP BY "created_date" ORDER BY "created_date" DESC', + ) + + # test order by fields not in group by (aggregate needed) + query = frappe.qb.get_query( + "User", + fields=["creation as created_date", {"COUNT": "*"}], + group_by="created_date", + order_by="name", + ).get_sql() + + self.assertQueryEqual( + query, + 'SELECT "creation" "created_date",COUNT(*) FROM "tabUser" GROUP BY "created_date" ORDER BY MAX("name") DESC', + ) + + query = frappe.qb.get_query( + "User", + fields=["user_type as type", "enabled as status", {"COUNT": "*"}], + group_by="type, status", + order_by="status asc", + ).get_sql() + + self.assertQueryEqual( + query, + 'SELECT "user_type" "type","enabled" "status",COUNT(*) FROM "tabUser" GROUP BY "type","status" ORDER BY "status" ASC', + ) + + # test no double aggregation rule + query = frappe.qb.get_query( + "User", + fields=["creation", {"COUNT": "*", "as": "total"}], + group_by="creation", + order_by="total desc", + ).get_sql() + + self.assertQueryEqual( + query, + 'SELECT "creation",COUNT(*) "total" FROM "tabUser" GROUP BY "creation" ORDER BY "total" DESC', + ) + + # test multiple order_by fields not in group_by + query = frappe.qb.get_query( + "User", + fields=["user_type", {"COUNT": "*"}], + group_by="user_type", + order_by="creation desc, modified asc", + ).get_sql() + + self.assertIn('MAX("creation") DESC', query) + self.assertIn('MAX("modified") ASC', query) + # This function is used as a permission query condition hook def test_permission_hook_condition(user): From 50e675f009c3f543cd85aa54ccbd95bbb08d3dae Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 14 Jan 2026 18:33:40 +0530 Subject: [PATCH 03/14] refactor: update warning to apply only to select distinct queries --- frappe/database/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index db160ef1da..18ca931bda 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -328,7 +328,7 @@ class Engine: warnings.warn( ( "ORDER BY fields have been ignored because PostgreSQL requires them to " - "appear in the SELECT list when using DISTINCT or GROUP BY." + "appear in the SELECT list when using with DISTINCT" ), UserWarning, stacklevel=2, From fd5da930f396b33c678f5486ad80a1352b86fc9c Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 14 Jan 2026 19:21:24 +0530 Subject: [PATCH 04/14] fix(query): ensure aggregate queries without group_by trigger postgres sort normalization --- frappe/database/query.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/database/query.py b/frappe/database/query.py index 18ca931bda..cefa779609 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -315,6 +315,10 @@ class Engine: if for_update: self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait) + if any(isinstance(f, functions.AggregateFunction) for f in getattr(self, "fields", [])): + # check if any field in select is aggregated (done to prevent breaking queries in postgres due to order by rule) + self.is_aggregate_query = True + if group_by: self.is_aggregate_query = True # for postgres (group by used with order by) self.apply_group_by(group_by) From 96520edf5a90d70c9039145edaf96f23f6621038 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 14 Jan 2026 20:07:31 +0530 Subject: [PATCH 05/14] test: add coverage for aggregate fields selected but not grouped --- frappe/tests/test_query.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index c18e607491..2962918316 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -2354,6 +2354,13 @@ class TestQuery(IntegrationTestCase): self.assertIn('MAX("creation") DESC', query) self.assertIn('MAX("modified") ASC', query) + # for queries that have aggregate fields selected but not grouped (these queries are redundant but exist in some parts of codebase) + query = frappe.qb.get_query( + "User", fields=[{"COUNT": "*", "as": "result"}], order_by="creation desc" + ).get_sql() + + self.assertQueryEqual(query, 'SELECT COUNT(*) "result" FROM "tabUser" ORDER BY MAX("creation") DESC') + # This function is used as a permission query condition hook def test_permission_hook_condition(user): From 92e0a215b062e47f3b74e1aa95dad7e77b61c129 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 12:59:49 +0530 Subject: [PATCH 06/14] refactor: better naming for tracking grouped fields --- frappe/database/query.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index cefa779609..d5d2dd1aec 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -267,7 +267,7 @@ class Engine: self.db_query_compat = db_query_compat self.permitted_fields_cache = {} # Cache for get_permitted_fields results self.is_aggregate_query = False - self._grouped_sqls = set() + self._grouped_queries = set() if isinstance(table, Table): self.table = table @@ -1136,7 +1136,7 @@ class Engine: when used with select and group_by""" if self.is_postgres and self.is_aggregate_query: current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) - if current_sql in self._grouped_sqls: + if current_sql in self._grouped_queries: return field clean_name = current_sql.strip('"') if clean_name in self.field_aliases: @@ -1147,7 +1147,7 @@ class Engine: def apply_group_by(self, group_by: str | None = None): parsed_group_by_fields = self._validate_group_by(group_by) - self._grouped_sqls = { + self._grouped_queries = { f.get_sql() if hasattr(f, "get_sql") else str(f) for f in parsed_group_by_fields } self.query = self.query.groupby(*parsed_group_by_fields) From 66c870d7308f75601024eee5937b0ff0643dc1c9 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 18:11:32 +0530 Subject: [PATCH 07/14] feat(dx): add validation to check if selected fields are grouped or aggregated for a better dev experience --- frappe/database/query.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frappe/database/query.py b/frappe/database/query.py index d5d2dd1aec..8118326c20 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1135,6 +1135,7 @@ class Engine: """In PostgreSQL order_by fields need to either be in group_by or be aggregated when used with select and group_by""" if self.is_postgres and self.is_aggregate_query: + self._validate_select_field_grouping_postgres() # DX: validate query current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) if current_sql in self._grouped_queries: return field @@ -1741,6 +1742,24 @@ class Engine: return True + def _validate_select_field_grouping_postgres(self): + """DX: In PostgreSQL, selected fields used with group by need to either be aggregated or be grouped, + the Query Builder validates this rule if user is unaware""" + for field in self.fields: + if isinstance(field, AggregateFunction): + continue + alias = getattr(field, "alias", None) + field_val = alias if alias is not None else field + field_val = str(field_val).replace('"', "") + if field_val not in self._grouped_queries: + frappe.throw( + _( + "PostgreSQL grouping error: The field '{0}' is selected but neither grouped nor aggregated. " + "Add it to 'group_by' or aggregate it." + ).format(field_val), + frappe.ValidationError, + ) + class DynamicTableField: def __init__( From 4530014ee5014cdba2c7d5446c3536efcb53cf88 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 18:14:26 +0530 Subject: [PATCH 08/14] test: add test for postgres query validation feat --- frappe/tests/test_query.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 2962918316..5a73d7a310 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -2361,6 +2361,21 @@ class TestQuery(IntegrationTestCase): self.assertQueryEqual(query, 'SELECT COUNT(*) "result" FROM "tabUser" ORDER BY MAX("creation") DESC') + @run_only_if(db_type_is.POSTGRES) + def test_query_validation_postgres(self): + """PostgreSQL specific test that tests if query that is built is valid in PostgreSQL, as part of a better DX""" + with self.assertRaises(frappe.ValidationError) as pgerr: + frappe.qb.get_query( + "User", + fields=["user_type as type", "enabled as status", {"COUNT": "*"}], + group_by="type", + order_by="type", + ).run() + self.assertEqual( + str(pgerr.exception), + "PostgreSQL grouping error: The field 'status' is selected but neither grouped nor aggregated. Add it to 'group_by' or aggregate it.", + ) + # This function is used as a permission query condition hook def test_permission_hook_condition(user): From 151fc37fbd49d8a171c401b28ca7b229dc0b636f Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 18:50:28 +0530 Subject: [PATCH 09/14] fix(validation): maintain compatibility with different way of writing queries --- frappe/database/query.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 8118326c20..0628286432 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1745,18 +1745,23 @@ class Engine: def _validate_select_field_grouping_postgres(self): """DX: In PostgreSQL, selected fields used with group by need to either be aggregated or be grouped, the Query Builder validates this rule if user is unaware""" + # string processing needed since this may break in certain queries e.g."tabDocType"."module" + clean_groups = { + str(g).replace('"', "").replace("`", "").split(".")[-1] for g in self._grouped_queries + } for field in self.fields: if isinstance(field, AggregateFunction): continue alias = getattr(field, "alias", None) - field_val = alias if alias is not None else field - field_val = str(field_val).replace('"', "") - if field_val not in self._grouped_queries: + field_alias = alias.replace('"', "").replace("`", "") if alias else None + field_source = str(field).replace('"', "").replace("`", "").split(".")[-1] + is_grouped = (field_source in clean_groups) or (field_alias in clean_groups) + if not is_grouped: frappe.throw( _( "PostgreSQL grouping error: The field '{0}' is selected but neither grouped nor aggregated. " "Add it to 'group_by' or aggregate it." - ).format(field_val), + ).format(field_alias or field_source), frappe.ValidationError, ) From c35f27114408c2579f4d05d1e9dae659c7c5d68e Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 21:29:08 +0530 Subject: [PATCH 10/14] refactor: perform validation only once --- frappe/database/query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 0628286432..837915f34c 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -345,6 +345,8 @@ class Engine: self.query._fields_list = getattr(self, "fields", []) self.query.immutable = True + if self.is_postgres and self.is_aggregate_query: + self._validate_select_field_grouping_postgres() # DX: validate query return self.query def validate_doctype(self): @@ -1135,7 +1137,6 @@ class Engine: """In PostgreSQL order_by fields need to either be in group_by or be aggregated when used with select and group_by""" if self.is_postgres and self.is_aggregate_query: - self._validate_select_field_grouping_postgres() # DX: validate query current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) if current_sql in self._grouped_queries: return field From 4530996223ec0be1e8f772c2afdbe01f41963692 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 22:32:18 +0530 Subject: [PATCH 11/14] revert(validation): revert validation due to breakage in old queries --- frappe/database/query.py | 25 ------------------------- frappe/tests/test_query.py | 15 --------------- 2 files changed, 40 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 837915f34c..d5d2dd1aec 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -345,8 +345,6 @@ class Engine: self.query._fields_list = getattr(self, "fields", []) self.query.immutable = True - if self.is_postgres and self.is_aggregate_query: - self._validate_select_field_grouping_postgres() # DX: validate query return self.query def validate_doctype(self): @@ -1743,29 +1741,6 @@ class Engine: return True - def _validate_select_field_grouping_postgres(self): - """DX: In PostgreSQL, selected fields used with group by need to either be aggregated or be grouped, - the Query Builder validates this rule if user is unaware""" - # string processing needed since this may break in certain queries e.g."tabDocType"."module" - clean_groups = { - str(g).replace('"', "").replace("`", "").split(".")[-1] for g in self._grouped_queries - } - for field in self.fields: - if isinstance(field, AggregateFunction): - continue - alias = getattr(field, "alias", None) - field_alias = alias.replace('"', "").replace("`", "") if alias else None - field_source = str(field).replace('"', "").replace("`", "").split(".")[-1] - is_grouped = (field_source in clean_groups) or (field_alias in clean_groups) - if not is_grouped: - frappe.throw( - _( - "PostgreSQL grouping error: The field '{0}' is selected but neither grouped nor aggregated. " - "Add it to 'group_by' or aggregate it." - ).format(field_alias or field_source), - frappe.ValidationError, - ) - class DynamicTableField: def __init__( diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 5a73d7a310..2962918316 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -2361,21 +2361,6 @@ class TestQuery(IntegrationTestCase): self.assertQueryEqual(query, 'SELECT COUNT(*) "result" FROM "tabUser" ORDER BY MAX("creation") DESC') - @run_only_if(db_type_is.POSTGRES) - def test_query_validation_postgres(self): - """PostgreSQL specific test that tests if query that is built is valid in PostgreSQL, as part of a better DX""" - with self.assertRaises(frappe.ValidationError) as pgerr: - frappe.qb.get_query( - "User", - fields=["user_type as type", "enabled as status", {"COUNT": "*"}], - group_by="type", - order_by="type", - ).run() - self.assertEqual( - str(pgerr.exception), - "PostgreSQL grouping error: The field 'status' is selected but neither grouped nor aggregated. Add it to 'group_by' or aggregate it.", - ) - # This function is used as a permission query condition hook def test_permission_hook_condition(user): From ab2a9f8134beb57137d4bf68858813074f5035ff Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 15 Jan 2026 22:55:52 +0530 Subject: [PATCH 12/14] refactor: reduce unnecessary noise in code --- frappe/database/query.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index d5d2dd1aec..99e336e668 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -356,11 +356,7 @@ class Engine: # Track field aliases for use in group_by/order_by for field in self.fields: - if isinstance(field, Field | DynamicTableField) and field.alias: - self.field_aliases.add(field.alias) - elif self.is_postgres and getattr( - field, "alias", None - ): # captures aggregate functions (for pg order by fix) + if isinstance(field, Field | DynamicTableField | AggregateFunction) and field.alias: self.field_aliases.add(field.alias) if self.apply_permissions: From 620d5def7b44ee9f1883a0cbb5ed2073df3b7386 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Fri, 16 Jan 2026 13:11:57 +0530 Subject: [PATCH 13/14] test: add coverage for variations in query_building --- frappe/tests/test_query.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 2962918316..92a04c91f3 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -2361,6 +2361,28 @@ class TestQuery(IntegrationTestCase): self.assertQueryEqual(query, 'SELECT COUNT(*) "result" FROM "tabUser" ORDER BY MAX("creation") DESC') + # test in case user uses `original_col` name instead of alias + query = frappe.qb.get_query( + "User", fields=["name as user_name"], group_by="user_name", order_by="user_name" + ) + a = query.run() + + query = frappe.qb.get_query("User", fields=["name as user_name"], group_by="name", order_by="name") + b = query.run() + + query = frappe.qb.get_query( + "User", fields=["name as user_name"], group_by="name", order_by="user_name" + ) + c = query.run() + + query = frappe.qb.get_query( + "User", fields=["name as user_name"], group_by="user_name", order_by="name" + ) + d = query.run() + + for val in [b, c, d]: + self.assertEqual(a, val, "Query result mismatch detected.") + # This function is used as a permission query condition hook def test_permission_hook_condition(user): From 7d31c299ebc641d747b4d080aac4bae718e994aa Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Fri, 6 Feb 2026 22:10:23 +0530 Subject: [PATCH 14/14] refactor: minor refactors --- frappe/database/query.py | 44 ++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 0acaf80a04..0ac638d0ea 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1130,15 +1130,14 @@ class Engine: def _normalize_postgres_order_field(self, field): """In PostgreSQL order_by fields need to either be in group_by or be aggregated when used with select and group_by""" - if self.is_postgres and self.is_aggregate_query: - current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) - if current_sql in self._grouped_queries: - return field - clean_name = current_sql.strip('"') - if clean_name in self.field_aliases: - return field - if not isinstance(field, functions.AggregateFunction): - return functions.Max(field) + current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field) + if current_sql in self._grouped_queries: + return field + clean_name = current_sql.strip('"') + if clean_name in self.field_aliases: + return field + if not isinstance(field, functions.AggregateFunction): + return functions.Max(field) return field def apply_group_by(self, group_by: str | None = None): @@ -1155,9 +1154,12 @@ class Engine: parsed_order_fields = self._validate_order_by(order_by) for order_field, order_direction in parsed_order_fields: - self.query = self.query.orderby( - self._normalize_postgres_order_field(order_field), order=order_direction - ) + if self.is_postgres and self.is_aggregate_query: + self.query = self.query.orderby( + self._normalize_postgres_order_field(order_field), order=order_direction + ) + else: + self.query = self.query.orderby(order_field, order=order_direction) def _apply_default_order_by(self): """Apply default ordering based on configured DocType metadata""" @@ -1176,18 +1178,24 @@ class Engine: order_direction = Order.desc if spec_order == "desc" else Order.asc else: order_direction = Order.asc if spec_order == "asc" else Order.desc - self.query = self.query.orderby( - self._normalize_postgres_order_field(field), order=order_direction - ) + if self.is_postgres and self.is_aggregate_query: + self.query = self.query.orderby( + self._normalize_postgres_order_field(field), order=order_direction + ) + else: + self.query = self.query.orderby(field, order=order_direction) else: field = self.table[sort_field] if self.db_query_compat: order_direction = Order.desc if sort_order.lower() == "desc" else Order.asc else: order_direction = Order.asc if sort_order.lower() == "asc" else Order.desc - self.query = self.query.orderby( - self._normalize_postgres_order_field(field), order=order_direction - ) + if self.is_postgres and self.is_aggregate_query: + self.query = self.query.orderby( + self._normalize_postgres_order_field(field), order=order_direction + ) + else: + self.query = self.query.orderby(field, order=order_direction) def _parse_backtick_field_notation(self, field_name: str) -> tuple[str, str] | None: """