Repository: frappe/frappe

Issue:
Implement `like` in `compare` function to simplify evaluating filters
Avoids things like

https://github.com/frappe/frappe/pull/21908/files#diff-5f06af4d31e78fad18b50ca2229b6b931453eab12f30b74348cb29f7ab56dc0cR111 


Discussion:
@GursheenK if you want to attempt, probably very small change + tests. 

Resolving patch:
diff --git a/frappe/core/doctype/recorder/recorder.py b/frappe/core/doctype/recorder/recorder.py
--- a/frappe/core/doctype/recorder/recorder.py
+++ b/frappe/core/doctype/recorder/recorder.py
@@ -4,7 +4,7 @@
 import frappe
 from frappe.model.document import Document
 from frappe.recorder import get as get_recorder_data
-from frappe.utils import cint, compare, make_filter_dict
+from frappe.utils import cint, evaluate_filters, make_filter_dict
 
 
 class Recorder(Document):
@@ -47,7 +47,7 @@ def get_list(args):
 				order_by_statment = order_by_statment.split(".")[1]
 
 			if " " in order_by_statment:
-				sort_key, sort_order = order_by_statment.split(" ")
+				sort_key, sort_order = order_by_statment.split(" ", 1)
 			else:
 				sort_key = order_by_statment
 				sort_order = "desc"
@@ -63,9 +63,9 @@ def get_count(args):
 
 	@staticmethod
 	def get_filtered_requests(args):
-		filters = make_filter_dict(args.get("filters"))
+		filters = args.get("filters")
 		requests = [serialize_request(request) for request in get_recorder_data()]
-		return [req for req in requests if _evaluate_filters(req, filters)]
+		return [req for req in requests if evaluate_filters(req, filters)]
 
 	@staticmethod
 	def get_stats(args):
@@ -100,20 +100,3 @@ def serialize_request(request):
 	)
 
 	return request
-
-
-def _evaluate_filters(row, filters) -> bool:
-	for field in filters:
-		value = row[field]
-		operand = filters[field][1]
-		operator = filters[field][0]
-
-		if operator == "like":
-			operator = "in"  # python equivalent.
-			operand = operand.strip("%")
-			# Swap because like is "reverse IN"
-			value, operand = operand, value
-
-		if not compare(value, operator, operand):
-			return False
-	return True
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -1692,6 +1692,20 @@ def get_url_to_report_with_filters(name, filters, report_type=None, doctype=None
 	return get_url(uri=f"/app/query-report/{quoted(name)}?{filters}")
 
 
+def sql_like(value: str, pattern: str) -> bool:
+	if not isinstance(pattern, str) and isinstance(value, str):
+		return False
+	if pattern.startswith("%") and pattern.endswith("%"):
+		return pattern.strip("%") in value
+	elif pattern.startswith("%"):
+		return value.endswith(pattern.lstrip("%"))
+	elif pattern.endswith("%"):
+		return value.startswith(pattern.rstrip("%"))
+	else:
+		# assume default as wrapped in '%'
+		return pattern in value
+
+
 operator_map = {
 	# startswith
 	"^": lambda a, b: (a or "").startswith(b),
@@ -1707,6 +1721,8 @@ def get_url_to_report_with_filters(name, filters, report_type=None, doctype=None
 	"<=": operator.le,
 	"not None": lambda a, b: a is not None,
 	"None": lambda a, b: a is None,
+	"like": sql_like,
+	"not like": lambda a, b: not sql_like(a, b),
 }
 
 
@@ -1812,7 +1828,7 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr
 					break
 
 	try:
-		df = frappe.get_meta(f.doctype).get_field(f.fieldname)
+		df = frappe.get_meta(f.doctype).get_field(f.fieldname) if f.doctype else None
 	except frappe.exceptions.DoesNotExistError:
 		df = None
 


Test patch:
diff --git a/frappe/core/doctype/recorder/test_recorder.py b/frappe/core/doctype/recorder/test_recorder.py
--- a/frappe/core/doctype/recorder/test_recorder.py
+++ b/frappe/core/doctype/recorder/test_recorder.py
@@ -15,6 +15,9 @@ class TestRecorder(FrappeTestCase):
 	def setUp(self):
 		self.start_recoder()
 
+	def tearDown(self) -> None:
+		frappe.recorder.stop()
+
 	def start_recoder(self):
 		frappe.recorder.stop()
 		frappe.recorder.delete()
diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -173,6 +173,23 @@ def test_date_time(self):
 			)
 		)
 
+	def test_like_not_like(self):
+		doc = {"doctype": "User", "username": "test_abc", "prefix": "startswith", "suffix": "endswith"}
+
+		test_cases = [
+			([["username", "like", "test"]], True),
+			([["username", "like", "user1"]], False),
+			([["username", "not like", "test"]], False),
+			([["username", "not like", "user1"]], True),
+			([["prefix", "like", "start%"]], True),
+			([["prefix", "not like", "end%"]], True),
+			([["suffix", "like", "%with"]], True),
+			([["suffix", "not like", "%end"]], True),
+		]
+
+		for filter, expected_result in test_cases:
+			self.assertEqual(evaluate_filters(doc, filter), expected_result)
+
 
 class TestMoney(FrappeTestCase):
 	def test_money_in_words(self):