diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 8bf62c6c9f..cc105f813a 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -236,7 +236,7 @@ def get_import_status(data_import_name: str): import_status = {"status": data_import.status} logs = frappe.get_all( "Data Import Log", - fields=["count(*) as count", "success"], + fields=[{"COUNT": "*", "as": "count"}, "success"], filters={"data_import": data_import_name}, group_by="success", ) diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index b4d903d7ca..17828b84f4 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -20,7 +20,7 @@ def get_things_todo(as_list=False): """Return a count of incomplete ToDos.""" data = frappe.get_list( "ToDo", - fields=["name", "description"] if as_list else "count(*)", + fields=["name", "description"] if as_list else [{"COUNT": "*"}], filters=[["ToDo", "status", "=", "Open"]], or_filters=[ ["ToDo", "allocated_to", "=", frappe.session.user], diff --git a/frappe/database/query.py b/frappe/database/query.py index 555746f185..a34474f532 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -52,6 +52,7 @@ FUNCTION_MAPPING = { "IFNULL": functions.IfNull, "CONCAT": functions.Concat, "NOW": functions.Now, + "NULLIF": functions.NullIf, } # Mapping from operator names to pypika Arithmetic enum values @@ -1579,6 +1580,12 @@ class SQLFunctionParser: if not arg: frappe.throw(_("Empty string arguments are not allowed"), frappe.ValidationError) + # Special case: allow '*' for COUNT(*) and similar aggregate functions + if arg == "*": + # Return as-is for SQL star expansion (COUNT(*), etc.) + # pypika will handle this correctly when used with aggregate functions + return Column("*") + # Check for string literals (quoted strings) if self._is_string_literal(arg): return self._validate_string_literal(arg) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index b91b338ddb..44355b943f 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -201,7 +201,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): data = frappe.get_list( doctype, - fields=[datefield, f"SUM({value_field})", "COUNT(*)"], + fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}], filters=filters, group_by=datefield, order_by=datefield, @@ -244,7 +244,7 @@ def get_heatmap_chart_config(chart, filters, heatmap_year): doctype, fields=[ timestamp_field, - f"{aggregate_function}({value_field})", + {aggregate_function: value_field}, ], filters=filters, group_by=f"date({datefield})", @@ -270,7 +270,7 @@ def get_group_by_chart_config(chart, filters) -> dict | None: doctype, fields=[ f"{group_by_field} as name", - f"{aggregate_function}({value_field}) as count", + {aggregate_function: value_field, "as": "count"}, ], filters=filters, parent_doctype=chart.parent_document_type, diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py index ff64636b71..52e885e583 100644 --- a/frappe/desk/doctype/route_history/route_history.py +++ b/frappe/desk/doctype/route_history/route_history.py @@ -46,7 +46,7 @@ def deferred_insert(routes): def frequently_visited_links(): return frappe.get_all( "Route History", - fields=["route", "count(name) as count"], + fields=["route", {"COUNT": "name", "as": "count"}], filters={"user": frappe.session.user}, group_by="route", order_by="count desc", diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index 94af0a06aa..8de906648f 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -69,7 +69,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d doctype, filters=current_filters, group_by=f"`tab{doctype}`.{field}", - fields=["count(*) as count", f"`{field}` as name"], + fields=[{"COUNT": "*", "as": "count"}, f"`{field}` as name"], order_by="count desc", limit=1000, ) diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 5aa9f0a40e..11b2b979ae 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -65,7 +65,7 @@ def get_notifications_for_doctypes(config, notification_count): try: if isinstance(condition, dict): result = frappe.get_list( - d, fields=["count(*) as count"], filters=condition, ignore_ifnull=True + d, fields=[{"COUNT": "*", "as": "count"}], filters=condition, ignore_ifnull=True )[0].count else: result = frappe.get_attr(condition)() diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 97fa71b271..757a502128 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -686,7 +686,7 @@ def get_stats(stats, doctype, filters=None): try: tag_count = frappe.get_list( doctype, - fields=[column, "count(*)"], + fields=[column, {"COUNT": "*"}], filters=[*filters, [column, "!=", ""]], group_by=column, as_list=True, @@ -697,7 +697,7 @@ def get_stats(stats, doctype, filters=None): results[column] = scrub_user_tags(tag_count) no_tag_count = frappe.get_list( doctype, - fields=[column, "count(*)"], + fields=[column, {"COUNT": "1"}], filters=[*filters, [column, "in", ("", ",")]], as_list=True, group_by=column, @@ -736,7 +736,7 @@ def get_filter_dashboard_data(stats, doctype, filters=None): if tag["type"] not in ["Date", "Datetime"]: tagcount = frappe.get_list( doctype, - fields=[tag["name"], "count(*)"], + fields=[tag["name"], {"COUNT": "*"}], filters=[*filters, "ifnull(`{}`,'')!=''".format(tag["name"])], group_by=tag["name"], as_list=True, @@ -758,7 +758,7 @@ def get_filter_dashboard_data(stats, doctype, filters=None): "No Data", frappe.get_list( doctype, - fields=[tag["name"], "count(*)"], + fields=[tag["name"], {"COUNT": "*"}], filters=[*filters, "({0} = '' or {0} is null)".format(tag["name"])], as_list=True, )[0][1], diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 3bafdcb2bd..1282a9b97a 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -176,8 +176,8 @@ def search_widget( if not meta.translated_doctype: _txt = frappe.db.escape((txt or "").replace("%", "").replace("@", "")) # locate returns 0 if string is not found, convert 0 to null and then sort null to end in order by - _relevance = f"(1 / nullif(locate({_txt}, `tab{doctype}`.`name`), 0))" - formatted_fields.append(f"""{_relevance} as `_relevance`""") + _relevance = {"DIV": [1, {"NULLIF": [{"LOCATE": [_txt, "name"]}, 0]}], "as": "_relevance"} + formatted_fields.append(f"{_relevance} as _relevance") # Since we are sorting by alias postgres needs to know number of column we are sorting if frappe.db.db_type == "mariadb": order_by = f"ifnull(_relevance, -9999) desc, {order_by}" diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c41f1b8454..67559fe8d6 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -950,7 +950,7 @@ def get_max_email_uid(email_account): "sent_or_received": "Received", "email_account": email_account, }, - fields=["max(uid) as uid"], + fields=[{"MAX": "uid", "as": "uid"}], ): return cint(result[0].get("uid", 0)) + 1 return 1 diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 286c492140..9481ca3fa3 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -403,7 +403,7 @@ class TestDB(IntegrationTestCase): random_field, ) self.assertEqual( - next(iter(frappe.get_all("ToDo", fields=[f"count(`{random_field}`)"], limit=1)[0])), + next(iter(frappe.get_all("ToDo", fields=[{"COUNT": f"`{random_field}`"}], limit=1)[0])), "count" if frappe.conf.db_type == "postgres" else f"count(`{random_field}`)", ) diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 03ef9bfb65..df7f99e0b4 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -483,15 +483,13 @@ class TestDBQuery(IntegrationTestCase): self.assertTrue("count" in data[0]) data = DatabaseQuery("DocType").execute( - fields=["name", "issingle", "locate('', name) as _relevance"], - limit_start=0, - limit_page_length=1, + fields=["name", "issingle", "locate('','name') as _relevance"], limit_start=0, limit_page_length=1 ) self.assertTrue("_relevance" in data[0]) # Test that fields with keywords in strings are allowed data = DatabaseQuery("DocType").execute( - fields=["name", "locate('select', name)"], + fields=["name", "locate('select', 'name')"], limit_start=0, limit_page_length=1, ) @@ -818,7 +816,7 @@ class TestDBQuery(IntegrationTestCase): frappe.db.get_list( "Web Form", filters=[["Web Form Field", "reqd", "=", 1]], - fields=["count(*) as count"], + fields=[{"COUNT": "*", "as": "count"}], order_by="count desc", limit=50, ) @@ -846,7 +844,7 @@ class TestDBQuery(IntegrationTestCase): "DocType", filters={"docstatus": 0, "document_type": ("!=", "")}, group_by="document_type", - fields=["document_type", "sum(is_submittable) as is_submittable"], + fields=["document_type", {"SUM": "is_submittable", "as": "is_submittable"}], limit=1, as_list=True, ) @@ -1222,7 +1220,7 @@ class TestDBQuery(IntegrationTestCase): self.assertEqual(len(data["values"]), 1) def test_select_star_expansion(self): - count = frappe.get_list("Language", ["SUM(1)", "COUNT(*)"], as_list=1, order_by=None)[0] + count = frappe.get_list("Language", [{"SUM": 1}, {"COUNT": "*"}], as_list=1, order_by=None)[0] self.assertEqual(count[0], frappe.db.count("Language")) self.assertEqual(count[1], frappe.db.count("Language")) diff --git a/frappe/tests/test_frappe_client.py b/frappe/tests/test_frappe_client.py index 61a1754319..5f8c491f10 100644 --- a/frappe/tests/test_frappe_client.py +++ b/frappe/tests/test_frappe_client.py @@ -70,13 +70,13 @@ class TestFrappeClient(IntegrationTestCase): getlist_users = server.get_list( "User", - fields=["count(name) as user_count"], + fields=[{"COUNT": "name", "as": "user_count"}], filters={"user_type": "System User"}, group_by="user_type", ) getall_users = frappe.db.get_all( "User", - fields=["count(name) as system_user_count"], + fields=[{"COUNT": "name", "as": "system_user_count"}], filters={"user_type": "System User"}, group_by="user_type", ) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 04efba4177..951bb5f075 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -127,7 +127,7 @@ def get_workflow_state_count(doctype, workflow_state_field, states): if workflow_state_field in frappe.get_meta(doctype).get_valid_columns(): result = frappe.get_all( doctype, - fields=[workflow_state_field, "count(*) as count"], + fields=[workflow_state_field, {"COUNT": "*", "as": "count"}], filters={workflow_state_field: ["not in", states]}, group_by=workflow_state_field, )