diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index dc242bcff6..02f790fa9c 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -505,7 +505,10 @@ def get_permission_query_conditions_for_communication(user): return None else: accounts = frappe.get_all( - "User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx" + "User Email", + filters={"parent": user}, + fields=["email_account"], + distinct=True, ) if not accounts: diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index e2464854e4..175ccc5a5c 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -78,7 +78,10 @@ def get_active_domains(): def _get_active_domains(): domains = frappe.get_all( - "Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True + "Has Domain", + filters={"parent": "Domain Settings"}, + fields=["domain"], + distinct=True, ) active_domains = [row.get("domain") for row in domains] diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 72bd7e6988..7360ca94d2 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -158,10 +158,7 @@ class TestServerScript(IntegrationTestCase): def test_permission_query(self): sql = frappe.db.get_list("ToDo", run=False) - if frappe.conf.db_type != "postgres": - self.assertTrue("where (1 = 1)" in sql.lower()) - else: - self.assertTrue("where (1 = '1')" in sql.lower()) + self.assertTrue("where (1 = 1)" in sql.lower()) self.assertTrue(isinstance(frappe.db.get_list("ToDo"), list)) def test_attribute_error(self): diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 210737e4d5..bacce751ac 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1018,7 +1018,10 @@ def ask_pass_update(): from frappe.utils import set_default password_list = frappe.get_all( - "User Email", filters={"awaiting_password": 1, "used_oauth": 0}, pluck="parent", distinct=True + "User Email", + filters={"awaiting_password": 1, "used_oauth": 0}, + pluck="parent", + distinct=True, ) set_default("email_user_password", ",".join(password_list)) diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py index ffdee044b6..529d91bbac 100644 --- a/frappe/database/operator_map.py +++ b/frappe/database/operator_map.py @@ -24,6 +24,17 @@ def like(key: Field, value: str) -> frappe.qb: return key.like(value) +def ilike(key: Field, value: str) -> frappe.qb: + """Wrapper method for `ILIKE` + Args: + key (str): field + value (str): criterion + Return: + frappe.qb: `frappe.qb` object with `ILIKE` + """ + return key.ilike(value) + + def func_in(key: Field, value: list | tuple) -> frappe.qb: """Wrapper method for `IN`. @@ -136,6 +147,7 @@ OPERATOR_MAP: dict[str, Callable] = { "in": func_in, "not in": func_not_in, "like": like, + "ilike": ilike, "not like": not_like, "regex": func_regex, "between": func_between, diff --git a/frappe/database/query.py b/frappe/database/query.py index 4657754522..9b71d307a3 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1,5 +1,6 @@ import datetime import re +import warnings from functools import lru_cache from typing import TYPE_CHECKING, Any @@ -227,6 +228,7 @@ class Engine: if self.apply_permissions: self.check_read_permission() + is_select = False if update: self.query = qb.update(self.table, immutable=False) elif into: @@ -236,6 +238,7 @@ class Engine: else: self.query = qb.from_(self.table, immutable=False) self.apply_fields(fields) + is_select = True self.apply_filters(filters) self.apply_or_filters(or_filters) @@ -260,7 +263,19 @@ class Engine: self.apply_group_by(group_by) if order_by: - self.apply_order_by(order_by) + if not ( + self.is_postgres and is_select and (distinct or group_by) + ): # ignore in Postgres since order by fields need to appear in select distinct + self.apply_order_by(order_by) + else: + warnings.warn( + ( + "ORDER BY fields have been ignored because PostgreSQL requires them to " + "appear in the SELECT list when using DISTINCT or GROUP BY." + ), + UserWarning, + stacklevel=2, + ) if self.apply_permissions: self.add_permission_conditions() @@ -512,7 +527,12 @@ class Engine: ) return operator_fn(_field, nodes or ("",)) - operator_fn = OPERATOR_MAP[_operator.casefold()] + if ( + self.is_postgres and _operator.casefold() == "like" + ): # use `ILIKE` to support case insensitive search in postgres + operator_fn = OPERATOR_MAP["ilike"] + else: + operator_fn = OPERATOR_MAP[_operator.casefold()] if _value is None and isinstance(_field, Field): if operator_fn == builtin_operator.ne: filter_field_name = ( @@ -1425,7 +1445,7 @@ class Engine: if fieldtype == "Time": return "'00:00:00'" - if fieldtype in ("Float", "Int", "Currency", "Percent"): + if fieldtype in ("Float", "Int", "Currency", "Percent", "Check"): return "0" try: diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index fe2113302e..0716e49961 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -305,7 +305,12 @@ def get_references_across_doctypes_by_dynamic_link_field( for doctype, fieldname, doctype_fieldname in links: try: filters = [[doctype_fieldname, "in", to_doctypes]] if to_doctypes else [] - for linked_to in frappe.get_all(doctype, pluck=doctype_fieldname, filters=filters, distinct=1): + for linked_to in frappe.get_all( + doctype, + pluck=doctype_fieldname, + filters=filters, + distinct=1, + ): if linked_to: links_by_doctype[linked_to].append( {"doctype": doctype, "fieldname": fieldname, "doctype_fieldname": doctype_fieldname} diff --git a/frappe/desk/search.py b/frappe/desk/search.py index dc23bf5778..4c9a92e698 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -195,15 +195,9 @@ def search_widget( _relevance_expr = {"DIV": [1, {"NULLIF": [{"LOCATE": [_txt, "name"]}, 0]}]} # For MariaDB, wrap in IFNULL for sorting to push nulls to end - if frappe.db.db_type in ("mariadb", "sqlite"): - _relevance = {"IFNULL": [_relevance_expr, -9999], "as": "_relevance"} - formatted_fields.append(_relevance) - order_by = f"_relevance desc, {order_by}" - elif frappe.db.db_type == "postgres": - _relevance = {**_relevance_expr, "as": "_relevance"} - formatted_fields.append(_relevance) - # Since we are sorting by alias postgres needs to know number of column we are sorting - order_by = f"{len(formatted_fields)} desc nulls last, {order_by}" + _relevance = {"IFNULL": [_relevance_expr, -9999], "as": "_relevance"} + formatted_fields.append(_relevance) + order_by = f"_relevance desc, {order_by}" values = frappe.get_list( doctype, diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 238557a4c9..50fe5b4174 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -57,8 +57,8 @@ def _get_children(doctype, parent="", ignore_permissions=False, include_disabled ) if frappe.db.has_column(doctype, "disabled") and not include_disabled: - qb = qb.where(Field("disabled").eq(False)) - + # used 0 instead of `false` since type of check in postgres is smallint + qb = qb.where(Field("disabled").eq(0)) # Order by name and execute return qb.orderby("name").run(as_dict=True) diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 01c55372e1..d0b9507e01 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -18,7 +18,8 @@ class TestSMTP(IntegrationTestCase): def test_get_email_account(self): existing_email_accounts = frappe.get_all( - "Email Account", fields=["name", "enable_outgoing", "default_outgoing", "append_to", "use_imap"] + "Email Account", + fields=["name", "enable_outgoing", "default_outgoing", "append_to", "use_imap"], ) unset_details = {"enable_outgoing": 0, "default_outgoing": 0, "append_to": None, "use_imap": 0} for email_account in existing_email_accounts: diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 0f9ae4d5c4..14ece934d9 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -403,8 +403,8 @@ class TestDB(IntegrationTestCase): random_field, ) self.assertEqual( - next(iter(frappe.get_all("ToDo", fields=[{"COUNT": random_field}], limit=1)[0])), - "COUNT" if frappe.conf.db_type == "postgres" else f"COUNT(`{random_field}`)", + next(iter(frappe.get_all("ToDo", fields=[{"COUNT": random_field}], limit=1, order_by=None)[0])), + "count" if frappe.conf.db_type == "postgres" else f"COUNT(`{random_field}`)", ) # Testing update diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index c9cfa96fbc..d6d444eb34 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -848,11 +848,7 @@ class TestDBQuery(IntegrationTestCase): limit=1, as_list=True, ) - if frappe.conf.db_type == "mariadb": - self.assertTrue(len(doctypes[0]) == 2) - else: - self.assertTrue(len(doctypes[0]) == 3) - self.assertTrue(isinstance(doctypes[0][2], datetime.datetime)) + self.assertTrue(len(doctypes[0]) == 2) # same for pg as well since we order_by None def test_field_comparison(self): """Test DatabaseQuery.execute to test field comparison""" diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 1db2491e56..96e78990f3 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -68,6 +68,8 @@ class TestQuery(IntegrationTestCase): setup_for_tests() def test_multiple_tables_in_filters(self): + query = "SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' AND `tabDocField`.`parentfield`='fields' WHERE `tabDocField`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'" + query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE") self.assertQueryEqual( frappe.qb.get_query( "DocType", @@ -77,7 +79,7 @@ class TestQuery(IntegrationTestCase): ["DocType", "parent", "=", "something"], ], ).get_sql(), - "SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' AND `tabDocField`.`parentfield`='fields' WHERE `tabDocField`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'", + query, ) def test_string_fields(self): @@ -360,13 +362,15 @@ class TestQuery(IntegrationTestCase): "SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name`='frappe'", ) + query = "SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name` LIKE 'frap%'" + query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE") self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], filters={"module.app_name": ("like", "frap%")}, ).get_sql(), - "SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name` LIKE 'frap%'", + query, ) self.assertQueryEqual( @@ -422,141 +426,125 @@ class TestQuery(IntegrationTestCase): def test_or_filters(self): """Test OR filter conditions.""" # Test 1: Basic dict or_filters - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters={"name": "User", "module": "Core"}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'", ) # Test 2: List format or_filters - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters=[["name", "=", "User"], ["module", "=", "Core"]], ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'", ) # Test 3: OR filters with operators - self.assertEqual( + query = "SELECT `name` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `module` IN ('Core','Custom')" + query = query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE") + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters={"name": ("like", "User%"), "module": ("in", ["Core", "Custom"])}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `module` IN ('Core','Custom')".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + query, ) # Test 4: Combining filters (AND) with or_filters (OR) - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], filters={"issingle": 0}, or_filters={"name": "User", "module": "Core"}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND (`name`='User' OR `module`='Core')".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND (`name`='User' OR `module`='Core')", ) # Test 5: Multiple AND filters with OR filters - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], filters={"issingle": 0, "custom": 0}, or_filters={"name": "User", "module": "Core"}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND `custom`=0 AND (`name`='User' OR `module`='Core')".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND `custom`=0 AND (`name`='User' OR `module`='Core')", ) # Test 6: OR filters with simple list (name IN) - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", or_filters=["User", "Role", "Note"], ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name` IN ('User','Role','Note')".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name` IN ('User','Role','Note')", ) # Test 7: OR filters with greater than and less than - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters={"idx": (">", 5), "issingle": ("=", 1)}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `idx`>5 OR `issingle`=1".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `idx`>5 OR `issingle`=1", ) # Test 8: OR filters with list including doctype - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters=[["DocType", "name", "=", "User"], ["DocType", "name", "=", "Role"]], ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `name`='Role'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `name`='Role'", ) # Test 9: OR filters with != operator - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters={"name": ("!=", "User"), "module": ("!=", "Core")}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name`<>'User' OR `module`<>'Core'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name`<>'User' OR `module`<>'Core'", ) # Test 10: Empty or_filters should return query without OR conditions - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], filters={"custom": 0}, or_filters={}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `custom`=0".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `custom`=0", ) # Test 11: OR filters with not in operator - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name"], or_filters={"name": ("not in", ["User", "Role"]), "module": ("=", "Core")}, ).get_sql(), - "SELECT `name` FROM `tabDocType` WHERE `name` NOT IN ('User','Role') OR `module`='Core'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `name` FROM `tabDocType` WHERE `name` NOT IN ('User','Role') OR `module`='Core'", ) # Test 12: OR filters with mixed field types - self.assertEqual( + query = ( + "SELECT `name`,`module` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `issingle`=1 OR `custom`=0" + ) + query = query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE") + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name", "module"], @@ -566,9 +554,7 @@ class TestQuery(IntegrationTestCase): ["custom", "=", 0], ], ).get_sql(), - "SELECT `name`,`module` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `issingle`=1 OR `custom`=0".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + query, ) def test_nested_filters(self): @@ -665,7 +651,11 @@ class TestQuery(IntegrationTestCase): .where( (User.creation > "2023-01-01") & ( - (User.email.like("%@example.com")) + ( + User.email.ilike("%@example.com") + if frappe.db.db_type == "postgres" + else User.email.like("%@example.com") + ) | ((User.first_name.isin(["Admin", "Guest"])) & (User.enabled != 1)) ) ) @@ -710,49 +700,41 @@ class TestQuery(IntegrationTestCase): def test_implicit_join_query(self): self.maxDiff = None - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "Note", filters={"name": "Test Note Title"}, fields=["name", "`tabNote Seen By`.`user` as seen_by"], ).get_sql(), - "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'", ) # output doesn't contain parentfield condition because it can't be inferred - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "Note", filters={"name": "Test Note Title"}, fields=["name", "`tabNote Seen By`.`user` as seen_by", "`tabNote Seen By`.`idx` as idx"], ).get_sql(), - "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'", ) # output contains parentfield condition because it can be inferred by "seen_by.user" - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "Note", filters={"name": "Test Note Title"}, fields=["name", "seen_by.user as seen_by", "`tabNote Seen By`.`idx` as idx"], ).get_sql(), - "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' AND `tabNote Seen By`.`parentfield`='seen_by' WHERE `tabNote`.`name`='Test Note Title'".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' AND `tabNote Seen By`.`parentfield`='seen_by' WHERE `tabNote`.`name`='Test Note Title'", ) - self.assertEqual( + self.assertQueryEqual( frappe.qb.get_query( "DocType", fields=["name", "module.app_name as app_name"], ).get_sql(), - "SELECT `tabDocType`.`name`,`tabModule Def`.`app_name` `app_name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module`".replace( - "`", '"' if frappe.db.db_type == "postgres" else "`" - ), + "SELECT `tabDocType`.`name`,`tabModule Def`.`app_name` `app_name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module`", ) # fields now has strict validation, so this test is not valid anymore @@ -878,7 +860,9 @@ class TestQuery(IntegrationTestCase): if frappe.db.db_type == "mariadb": self.assertIn("IFNULL(`name`,'')='' OR `name` IN ('_Test Blog Post 1','_Test Blog Post')", query) elif frappe.db.db_type == "postgres": - self.assertIn("\"name\" IS NULL OR \"name\" IN ('_Test Blog Post 1','_Test Blog Post')", query) + self.assertIn( + "IFNULL(\"name\",'')='' OR \"name\" IN ('_Test Blog Post 1','_Test Blog Post')", query + ) # works in pg due to `coalesce` sub during sql execution frappe.set_user("Administrator") clear_user_permissions_for_doctype("Test Blog Post", "test2@example.com") @@ -1762,39 +1746,39 @@ class TestQuery(IntegrationTestCase): # Test simple addition query = frappe.qb.get_query("User", fields=[{"ADD": [1, 2], "as": "sum_result"}]) sql = query.get_sql() - self.assertIn("1+2 `sum_result`", sql) + self.assertIn(self.normalize_sql("1+2 `sum_result`"), sql) # Test simple subtraction query = frappe.qb.get_query("User", fields=[{"SUB": [10, 5], "as": "diff_result"}]) sql = query.get_sql() - self.assertIn("10-5 `diff_result`", sql) + self.assertIn(self.normalize_sql("10-5 `diff_result`"), sql) # Test simple multiplication query = frappe.qb.get_query("User", fields=[{"MUL": [3, 4], "as": "prod_result"}]) sql = query.get_sql() - self.assertIn("3*4 `prod_result`", sql) + self.assertIn(self.normalize_sql("3*4 `prod_result`"), sql) # Test simple division query = frappe.qb.get_query("User", fields=[{"DIV": [10, 2], "as": "div_result"}]) sql = query.get_sql() - self.assertIn("10/2 `div_result`", sql) + self.assertIn(self.normalize_sql("10/2 `div_result`"), sql) # Test operator with field names query = frappe.qb.get_query("User", fields=[{"ADD": ["enabled", "login_after"], "as": "field_sum"}]) sql = query.get_sql() - self.assertIn("`enabled`+`login_after` `field_sum`", sql) + self.assertIn(self.normalize_sql("`enabled`+`login_after` `field_sum`"), sql) # Test nested operators query = frappe.qb.get_query("User", fields=[{"ADD": [{"MUL": [2, 3]}, 4], "as": "nested_result"}]) sql = query.get_sql() - self.assertIn("2*3+4 `nested_result`", sql) + self.assertIn(self.normalize_sql("2*3+4 `nested_result`"), sql) # Test operator with function - NULLIF query = frappe.qb.get_query( "User", fields=[{"DIV": [1, {"NULLIF": ["enabled", 0]}], "as": "safe_div"}] ) sql = query.get_sql() - self.assertIn("1/NULLIF(`enabled`,0) `safe_div`", sql) + self.assertIn(self.normalize_sql("1/NULLIF(`enabled`,0) `safe_div`"), self.normalize_sql(sql)) # Test complex nested expression: (1 / NULLIF(value, 0)) query = frappe.qb.get_query( @@ -1805,8 +1789,8 @@ class TestQuery(IntegrationTestCase): ], ) sql = query.get_sql() - self.assertIn("`name`", sql) - self.assertIn("1/NULLIF(`enabled`,0) `inverse`", sql) + self.assertIn(self.normalize_sql("`name`"), sql) + self.assertIn(self.normalize_sql("1/NULLIF(`enabled`,0) `inverse`"), self.normalize_sql(sql)) # Test operator with LOCATE function (search relevance pattern) query = frappe.qb.get_query( @@ -1817,7 +1801,10 @@ class TestQuery(IntegrationTestCase): ], ) sql = query.get_sql() - self.assertIn("1/NULLIF(LOCATE('test',`name`),0) `relevance`", sql) + self.assertIn( + self.normalize_sql("1/NULLIF(LOCATE('test',`name`),0) `relevance`"), + self.normalize_sql(sql), + ) # Test multiple operators in fields query = frappe.qb.get_query( @@ -1829,9 +1816,9 @@ class TestQuery(IntegrationTestCase): ], ) sql = query.get_sql() - self.assertIn("`name`", sql) - self.assertIn("`enabled`+1 `enabled_plus_one`", sql) - self.assertIn("`enabled`*2 `enabled_times_two`", sql) + self.assertIn(self.normalize_sql("`name`"), sql) + self.assertIn(self.normalize_sql("`enabled`+1 `enabled_plus_one`"), sql) + self.assertIn(self.normalize_sql("`enabled`*2 `enabled_times_two`"), sql) # Test operator without alias query = frappe.qb.get_query("User", fields=[{"ADD": [1, 1]}]) @@ -1898,9 +1885,12 @@ class TestQuery(IntegrationTestCase): ) sql = query.get_sql() - self.assertIn("GROUP BY `created_date`", sql) - self.assertIn("ORDER BY `created_date`", sql) - self.assertIn("`creation` `created_date`", sql) + self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql)) + if ( + frappe.db.db_type != "postgres" + ): # since Postgres requires fields in Order by to be grouped or aggregated, order by is dropped + self.assertIn(self.normalize_sql("ORDER BY `created_date`"), self.normalize_sql(sql)) + self.assertIn(self.normalize_sql("`creation` `created_date`"), self.normalize_sql(sql)) def test_field_alias_permission_check(self): query = frappe.qb.get_query( @@ -1910,7 +1900,7 @@ class TestQuery(IntegrationTestCase): ) sql = query.get_sql() # If we get here without PermissionError, the test passes - self.assertIn("GROUP BY `created_date`", sql) + self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql)) # This function is used as a permission query condition hook diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 700ed6efda..a3cd923a7e 100644 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -176,7 +176,10 @@ class UserPermissions: self.can_read += self.can_write self.shared = frappe.get_all( - "DocShare", {"user": self.name, "read": 1}, distinct=True, pluck="share_doctype" + "DocShare", + {"user": self.name, "read": 1}, + distinct=True, + pluck="share_doctype", ) self.can_read = list(set(self.can_read + self.shared)) self.all_read += self.can_read