From 1cfae33fb0dd627b8560aecb56de72ea60380ad2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 19 Jun 2026 21:21:06 +0530 Subject: [PATCH] refactor(postgres): port transaction_base delete_events to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/utilities/test_transaction_base.py | 77 ++++++++++++++++++++++ erpnext/utilities/transaction_base.py | 21 +++--- 2 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 erpnext/utilities/test_transaction_base.py diff --git a/erpnext/utilities/test_transaction_base.py b/erpnext/utilities/test_transaction_base.py new file mode 100644 index 00000000000..6d297d02c38 --- /dev/null +++ b/erpnext/utilities/test_transaction_base.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import now_datetime, random_string + +from erpnext.tests.utils import ERPNextTestSuite +from erpnext.utilities.transaction_base import delete_events + + +class TestDeleteEvents(ERPNextTestSuite): + def _make_event(self, reference_doctype, reference_docname): + # Insert a bare Event, then attach the Event Participants child row directly. + # reference_docname is a Dynamic Link that would otherwise be validated against a + # real target doc on save; db_insert keeps the test self-contained with arbitrary + # (random, guaranteed-unique) docnames while still populating exactly the columns + # delete_events joins/filters on (parent, reference_doctype, reference_docname). + event = frappe.get_doc( + { + "doctype": "Event", + "subject": "Test Event " + random_string(10), + "starts_on": now_datetime(), + "event_type": "Private", + } + ).insert(ignore_permissions=True) + + participant = frappe.new_doc("Event Participants") + participant.name = frappe.generate_hash(length=10) + participant.flags.name_set = True + participant.parent = event.name + participant.parenttype = "Event" + participant.parentfield = "event_participants" + participant.idx = 1 + participant.reference_doctype = reference_doctype + participant.reference_docname = reference_docname + participant.db_insert() + + return event.name + + def test_delete_events_removes_matching_and_keeps_others(self): + # Two distinct, real reference_docnames so the filter has something to discriminate on. + match_name = "Match " + random_string(10) + other_name = "Other " + random_string(10) + event_match = self._make_event("Customer", match_name) + event_other = self._make_event("Customer", other_name) + + # Sanity: both exist before deletion (otherwise the assertions below are tautological). + self.assertTrue(frappe.db.exists("Event", event_match)) + self.assertTrue(frappe.db.exists("Event", event_other)) + + delete_events("Customer", match_name) + + # Only the Event whose participant matches BOTH reference_doctype and + # reference_docname must be deleted. + self.assertFalse(frappe.db.exists("Event", event_match)) + self.assertTrue(frappe.db.exists("Event", event_other)) + + def test_delete_events_no_match_is_noop(self): + # When nothing matches, no Event may be deleted. + event = self._make_event("Customer", "Present " + random_string(10)) + self.assertTrue(frappe.db.exists("Event", event)) + + delete_events("Customer", "Absent " + random_string(10)) + + self.assertTrue(frappe.db.exists("Event", event)) + + def test_delete_events_distinguishes_reference_doctype(self): + # Same docname under two different reference_doctypes: only the queried doctype + # is deleted, proving both predicates are ANDed together. + shared_name = "Shared " + random_string(10) + event_customer = self._make_event("Customer", shared_name) + event_supplier = self._make_event("Supplier", shared_name) + + delete_events("Customer", shared_name) + + self.assertFalse(frappe.db.exists("Event", event_customer)) + self.assertTrue(frappe.db.exists("Event", event_supplier)) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 4b51bc5ecd3..bd7bbcdd34b 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -582,19 +582,16 @@ class TransactionBase(StatusUpdater): def delete_events(ref_type, ref_name): + event = frappe.qb.DocType("Event") + participant = frappe.qb.DocType("Event Participants") events = ( - frappe.db.sql_list( - """ SELECT - distinct `tabEvent`.name - from - `tabEvent`, `tabEvent Participants` - where - `tabEvent`.name = `tabEvent Participants`.parent - and `tabEvent Participants`.reference_doctype = %s - and `tabEvent Participants`.reference_docname = %s - """, - (ref_type, ref_name), - ) + frappe.qb.from_(event) + .inner_join(participant) + .on(event.name == participant.parent) + .select(event.name) + .distinct() + .where((participant.reference_doctype == ref_type) & (participant.reference_docname == ref_name)) + .run(pluck="name") or [] )