From 19a6c5aa50bea29e6bf2a16a293c2ed4b4b2a375 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas Date: Wed, 18 Mar 2026 16:57:49 +0530 Subject: [PATCH 1/3] fix(query): handle none in IN filter value list via Coalesce --- frappe/core/doctype/user/user.js | 2 +- frappe/database/operator_map.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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) From 6281eac44a3a2af69abdf54f2a22d14dfd46e81c Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas Date: Fri, 20 Mar 2026 12:27:54 +0530 Subject: [PATCH 2/3] test: add unit test --- frappe/tests/test_query_builder.py | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index 9375d17ef2..a8befd0541 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,70 @@ 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) + + frappe.db.sql(f"DELETE FROM `tab{self.test_doctype_name}`") + frappe.db.commit() + + 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) From c8ce8cdc231a0a85f0708abe07a11ab010d0cc9a Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas Date: Thu, 26 Mar 2026 12:45:05 +0530 Subject: [PATCH 3/3] test(query): remove manual commit from test --- frappe/tests/test_query_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index a8befd0541..0635198fdc 100644 --- a/frappe/tests/test_query_builder.py +++ b/frappe/tests/test_query_builder.py @@ -552,9 +552,6 @@ class TestOperatorIn(IntegrationTestCase): self.test_doctype_name = test_doctype.name self.addCleanup(frappe.delete_doc, "DocType", self.test_doctype_name) - frappe.db.sql(f"DELETE FROM `tab{self.test_doctype_name}`") - frappe.db.commit() - 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": ""})