diff --git a/frappe/database/database.py b/frappe/database/database.py index ca4b5a5310..3ff9534d7f 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1019,21 +1019,17 @@ class Database(object): return self.get_value(dt, dn, ignore=True, cache=cache) - def count(self, dt, filters=None, debug=False, cache=False): + def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True): """Returns `COUNT(*)` for given DocType and filters.""" if cache and not filters: cache_count = frappe.cache().get_value("doctype:count:{}".format(dt)) if cache_count is not None: return cache_count - query = self.query.get_sql(table=dt, filters=filters, fields=Count("*")) - if filters: - count = self.sql(query, debug=debug)[0][0] - return count - else: - count = self.sql(query, debug=debug)[0][0] - if cache: - frappe.cache().set_value("doctype:count:{}".format(dt), count, expires_in_sec=86400) - return count + query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"), distinct=distinct) + count = query.run(debug=debug)[0][0] + if not filters and cache: + frappe.cache().set_value("doctype:count:{}".format(dt), count, expires_in_sec=86400) + return count @staticmethod def format_date(date): diff --git a/frappe/database/query.py b/frappe/database/query.py index 136f5c86b6..f7393bfa54 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -4,10 +4,10 @@ from typing import Any, Dict, List, Tuple, Union import frappe from frappe import _ -from frappe.query_builder import Criterion, Field, Order +from frappe.query_builder import Criterion, Field, Order, Table -def like(key: str, value: str) -> frappe.qb: +def like(key: Field, value: str) -> frappe.qb: """Wrapper method for `LIKE` Args: @@ -17,10 +17,10 @@ def like(key: str, value: str) -> frappe.qb: Returns: frappe.qb: `frappe.qb object with `LIKE` """ - return Field(key).like(value) + return key.like(value) -def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb: +def func_in(key: Field, value: Union[List, Tuple]) -> frappe.qb: """Wrapper method for `IN` Args: @@ -30,10 +30,10 @@ def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb: Returns: frappe.qb: `frappe.qb object with `IN` """ - return Field(key).isin(value) + return key.isin(value) -def not_like(key: str, value: str) -> frappe.qb: +def not_like(key: Field, value: str) -> frappe.qb: """Wrapper method for `NOT LIKE` Args: @@ -43,10 +43,10 @@ def not_like(key: str, value: str) -> frappe.qb: Returns: frappe.qb: `frappe.qb object with `NOT LIKE` """ - return Field(key).not_like(value) + return key.not_like(value) -def func_not_in(key: str, value: Union[List, Tuple]): +def func_not_in(key: Field, value: Union[List, Tuple]): """Wrapper method for `NOT IN` Args: @@ -56,10 +56,10 @@ def func_not_in(key: str, value: Union[List, Tuple]): Returns: frappe.qb: `frappe.qb object with `NOT IN` """ - return Field(key).notin(value) + return key.notin(value) -def func_regex(key: str, value: str) -> frappe.qb: +def func_regex(key: Field, value: str) -> frappe.qb: """Wrapper method for `REGEX` Args: @@ -69,10 +69,10 @@ def func_regex(key: str, value: str) -> frappe.qb: Returns: frappe.qb: `frappe.qb object with `REGEX` """ - return Field(key).regex(value) + return key.regex(value) -def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb: +def func_between(key: Field, value: Union[List, Tuple]) -> frappe.qb: """Wrapper method for `BETWEEN` Args: @@ -82,7 +82,7 @@ def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb: Returns: frappe.qb: `frappe.qb object with `BETWEEN` """ - return Field(key)[slice(*value)] + return key[slice(*value)] def make_function(key: Any, value: Union[int, str]): @@ -139,7 +139,9 @@ OPERATOR_MAP = { class Query: - def get_condition(self, table: str, **kwargs) -> frappe.qb: + tables: dict = {} + + def get_condition(self, table: Union[str, Table], **kwargs) -> frappe.qb: """Get initial table object Args: @@ -148,11 +150,20 @@ class Query: Returns: frappe.qb: DocType with initial condition """ + table_object = self.get_table(table) if kwargs.get("update"): - return frappe.qb.update(table) + return frappe.qb.update(table_object) if kwargs.get("into"): - return frappe.qb.into(table) - return frappe.qb.from_(table) + return frappe.qb.into(table_object) + return frappe.qb.from_(table_object) + + def get_table(self, table_name: Union[str, Table]) -> Table: + if isinstance(table_name, Table): + return table_name + table_name = table_name.strip('"').strip("'") + if table_name not in self.tables: + self.tables[table_name] = frappe.qb.DocType(table_name) + return self.tables[table_name] def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb: """Generate filters from Criterion objects @@ -217,8 +228,13 @@ class Query: conditions = conditions.where(_operator(Field(filters[0]), filters[2])) break else: - _operator = OPERATOR_MAP[f[1]] - conditions = conditions.where(_operator(Field(f[0]), f[2])) + _operator = OPERATOR_MAP[f[-2]] + if len(f) == 4: + table_object = self.get_table(f[0]) + _field = table_object[f[1]] + else: + _field = Field(f[0]) + conditions = conditions.where(_operator(_field, f[-1])) return self.add_conditions(conditions, **kwargs) @@ -249,7 +265,7 @@ class Query: if isinstance(value, (list, tuple)): if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]: _operator = OPERATOR_MAP[value[0]] - conditions = conditions.where(_operator(key, value[1])) + conditions = conditions.where(_operator(Field(key), value[1])) else: _operator = OPERATOR_MAP[value[0]] conditions = conditions.where(_operator(Field(key), value[1])) @@ -293,10 +309,19 @@ class Query: self, table: str, fields: Union[List, Tuple], - filters: Union[Dict[str, Union[str, int]], str, int] = None, + filters: Union[Dict[str, Union[str, int]], str, int, List[Union[List, str, int]]] = None, **kwargs, ): + # Clean up state before each query + self.tables = {} criterion = self.build_conditions(table, filters, **kwargs) + + if len(self.tables) > 1: + primary_table = self.tables[table] + del self.tables[table] + for table_object in self.tables.values(): + criterion = criterion.left_join(table_object).on(table_object.parent == primary_table.name) + if isinstance(fields, (list, tuple)): query = criterion.select(*kwargs.get("field_objects", fields)) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index b45f80f6ff..2813061347 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -48,15 +48,12 @@ def get_list(): @frappe.read_only() def get_count(): args = get_form_params() - if is_virtual_doctype(args.doctype): controller = get_controller(args.doctype) data = controller(args.doctype).get_count(args) else: - distinct = "distinct " if args.distinct == "true" else "" - args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"] - data = execute(**args)[0].get("total_count") - + distinct = args["distinct"] == "true" + data = frappe.db.count(args["doctype"], args["filters"], distinct=distinct) return data diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 6cba55c425..4d105ef28e 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -482,6 +482,33 @@ class TestDB(unittest.TestCase): frappe.db.delete("ToDo", {"description": test_body}) + def test_count(self): + frappe.db.delete("Note") + + frappe.get_doc(doctype="Note", title="note1", content="something").insert() + frappe.get_doc(doctype="Note", title="note2", content="someting else").insert() + + # Count with no filtes + self.assertEquals((frappe.db.count("Note")), 2) + + # simple filters + self.assertEquals((frappe.db.count("Note", ["title", "=", "note1"])), 1) + + frappe.get_doc(doctype="Note", title="note3", content="something other").insert() + + # List of list filters with tables + self.assertEquals( + ( + frappe.db.count( + "Note", + [["Note", "title", "like", "note%"], ["Note", "content", "like", "some%"]], + ) + ), + 3, + ) + + frappe.db.rollback() + @run_only_if(db_type_is.MARIADB) class TestDDLCommandsMaria(unittest.TestCase): diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py new file mode 100644 index 0000000000..949c3e9433 --- /dev/null +++ b/frappe/tests/test_query.py @@ -0,0 +1,20 @@ +import unittest + +import frappe +from frappe.tests.test_query_builder import db_type_is, run_only_if + + +@run_only_if(db_type_is.MARIADB) +class TestQuery(unittest.TestCase): + def test_multiple_tables_in_filters(self): + self.assertEqual( + frappe.db.query.get_sql( + "DocType", + ["*"], + [ + ["BOM Update Log", "name", "like", "f%"], + ["DocType", "parent", "=", "something"], + ], + ).get_sql(), + "SELECT * FROM `tabDocType` LEFT JOIN `tabBOM Update Log` ON `tabBOM Update Log`.`parent`=`tabDocType`.`name` WHERE `tabBOM Update Log`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'", + )