Merge pull request #36621 from frappe/fix-empty-list-in-operator-behavior

fix!: Enhance IN/NOT IN operator handling for empty lists
This commit is contained in:
Suraj Shetty 2026-02-09 15:17:54 +05:30 committed by GitHub
commit cefac6851e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 3 deletions

View file

@ -597,6 +597,20 @@ class Engine:
v.strip().strip("'") for v in get_between_date_filter(_value, df).split(" AND ")
)
# Handle empty lists for IN/NOT IN operators before conversion
# IN with empty list should return 0 results (always False)
# NOT IN with empty list should return all results (always True)
if _operator.lower() in ("in", "not in"):
if isinstance(_value, (list, tuple, set)) and len(_value) == 0:
if _operator.lower() == "in":
# Return a criterion that always evaluates to False (1=0)
# This ensures IN with empty list returns 0 results
return RawCriterion("1=0")
else: # not in
# Return a criterion that always evaluates to True (1=1)
# NOT IN with empty set matches all rows since nothing is excluded
return RawCriterion("1=1")
if not _value and isinstance(_value, list | tuple | set):
_value = ("",)

View file

@ -868,6 +868,15 @@ from {tables}
if f.operator.lower() == "in":
can_be_null &= not f.value or any(v is None or v == "" for v in f.value)
# Handle empty lists for IN/NOT IN operators before processing
# IN with empty list should return 0 results (always False: 1=0)
# NOT IN with empty list should return all results (always True: 1=1)
if isinstance(f.value, (list, tuple)) and len(f.value) == 0:
if f.operator.lower() == "in":
return "1=0"
else: # not in
return "1=1"
if value is None:
values = f.value or ""
if isinstance(values, str):

View file

@ -1050,15 +1050,21 @@ class TestDBQuery(IntegrationTestCase):
self.assertNotIn("IF", frappe.get_all("User", {"first_name": ("in", ["a", "b"])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("in", ["a", None])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("in", ["a", ""])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("in", [])}, run=0).get_sql())
# Empty list with IN should return 1=0, not use IFNULL
self.assertIn("1=0", frappe.get_all("User", {"first_name": ("in", [])}, run=0).get_sql())
self.assertNotIn("IFNULL", frappe.get_all("User", {"first_name": ("in", [])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("not in", ["a"])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("not in", [])}, run=0).get_sql())
# Empty list with NOT IN should return 1=1, not use IFNULL
self.assertIn("1=1", frappe.get_all("User", {"first_name": ("not in", [])}, run=0).get_sql())
self.assertNotIn("IFNULL", frappe.get_all("User", {"first_name": ("not in", [])}, run=0).get_sql())
self.assertIn("IFNULL", frappe.get_all("User", {"first_name": ("not in", [""])}, run=0).get_sql())
# primary key is never nullable
self.assertNotIn("IFNULL", frappe.get_all("User", {"name": ("in", ["a", None])}, run=0).get_sql())
self.assertNotIn("IFNULL", frappe.get_all("User", {"name": ("in", ["a", ""])}, run=0).get_sql())
self.assertNotIn("IFNULL", frappe.get_all("User", {"name": ("in", (""))}, run=0).get_sql())
# Empty tuple with IN should return 1=0, not use IFNULL
self.assertIn("1=0", frappe.get_all("User", {"name": ("in", ())}, run=0).get_sql())
self.assertNotIn("IFNULL", frappe.get_all("User", {"name": ("in", ())}, run=0).get_sql())
def test_coalesce_with_datetime_ops(self):

View file

@ -410,12 +410,22 @@ class TestQuery(IntegrationTestCase):
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('ToDo','Note')",
)
# Empty list with IN operator should return 0 results (1=0 condition)
self.assertQueryEqual(
frappe.qb.get_query(
"DocType",
filters={"name": ("in", [])},
).get_sql(),
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('')",
"SELECT `name` FROM `tabDocType` WHERE 1=0",
)
# Empty list with NOT IN operator should return all results (1=1 condition)
self.assertQueryEqual(
frappe.qb.get_query(
"DocType",
filters={"name": ("not in", [])},
).get_sql(),
"SELECT `name` FROM `tabDocType` WHERE 1=1",
)
self.assertQueryEqual(