diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 3e9ff32d5e..38ffac1220 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -3,7 +3,7 @@ frappe.ui.form.on("User", { frm.set_query("default_workspace", () => { return { filters: { - for_user: ["in", [null, frappe.session.user]], + for_user: ["in", ["", frappe.session.user]], title: ["!=", "Welcome Workspace"], }, }; diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py index dc4c17c5d3..c8cb9aa099 100644 --- a/frappe/database/operator_map.py +++ b/frappe/database/operator_map.py @@ -48,6 +48,10 @@ def func_in(key: Field, value: list | tuple) -> frappe.qb: """ if isinstance(value, str): value = value.split(",") + + value = ["" if v is None else v for v in value] + if "" in value: + return Coalesce(key, "").isin(value) return key.isin(value) diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index 9375d17ef2..0635198fdc 100644 --- a/frappe/tests/test_query_builder.py +++ b/frappe/tests/test_query_builder.py @@ -4,6 +4,7 @@ from datetime import time import frappe from frappe.core.doctype.doctype.test_doctype import new_doctype +from frappe.database.operator_map import func_in from frappe.query_builder import Case from frappe.query_builder.builder import Function from frappe.query_builder.custom import ConstantColumn @@ -503,3 +504,67 @@ class TestMisc(IntegrationTestCase): roles = frappe.qb.from_(role).select(role.name) self.assertEqual(set(users.run() + roles.run()), set((users + roles).run())) + + +class TestOperatorIn(IntegrationTestCase): + def test_func_in_without_empty_values(self): + note = frappe.qb.DocType("Note") + query = func_in(note.name, ["n1", "n2", "n3"]) + sql_str = str(query).lower() + + self.assertIn("in", sql_str) + self.assertNotIn("coalesce", sql_str) + + def test_func_in_with_none_converts_to_empty_string(self): + note = frappe.qb.DocType("Note") + query = func_in(note.name, [None, "user1"]) + sql_str = str(query).lower() + + self.assertIn("coalesce", sql_str) + self.assertIn("''", sql_str) + + def test_func_in_with_empty_string_uses_coalesce(self): + note = frappe.qb.DocType("Note") + query = func_in(note.name, ["", "user1"]) + sql_str = str(query).lower() + + self.assertIn("coalesce", sql_str) + self.assertIn("''", sql_str) + + def test_func_in_with_mixed_none_and_values(self): + note = frappe.qb.DocType("Note") + query = func_in(note.name, ["val1", None, "val2"]) + sql_str = str(query).lower() + + self.assertIn("coalesce", sql_str) + + def test_in_filter_matches_null_and_empty_columns(self): + test_doctype = new_doctype( + fields=[ + { + "fieldname": "test_field", + "fieldtype": "Data", + "label": "Test Field", + }, + ], + ) + test_doctype.insert() + self.test_doctype_name = test_doctype.name + self.addCleanup(frappe.delete_doc, "DocType", self.test_doctype_name) + + doc_null = frappe.get_doc({"doctype": self.test_doctype_name, "test_field": None}) + doc_null.insert() + doc_empty = frappe.get_doc({"doctype": self.test_doctype_name, "test_field": ""}) + doc_empty.insert() + doc_user = frappe.get_doc({"doctype": self.test_doctype_name, "test_field": "user1"}) + doc_user.insert() + + results = frappe.get_all( + self.test_doctype_name, + filters={"test_field": ["in", [None, "user1"]]}, + pluck="test_field", + ) + + self.assertIn(None, results) + self.assertIn("", results) + self.assertIn("user1", results)