diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 4b14ecc95937..b55fadd32350 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -81,6 +81,9 @@ def restore_database(verbose: bool, target: str, source: str, user: str, passwor # Newer versions of MariaDB add in a line that'll break on older versions, so remove it command.extend(["sed", r"'/\/\*M\{0,1\}!999999\\- enable the sandbox mode \*\//d'", "|"]) + # Remove view security definers + command.extend(["sed", r"'/\/\*![0-9]* DEFINER=[^ ]* SQL SECURITY DEFINER \*\//d'", "|"]) + # Generate the restore command bin, args, bin_name = get_command( socket=frappe.conf.db_socket, diff --git a/frappe/hooks.py b/frappe/hooks.py index ed09ad6dedfd..c28895ffb911 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -570,4 +570,5 @@ "insert_queue_for_*", # Deferred Insert "recorder-*", # Recorder "global_search_queue", + "monitor-transactions", ] diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 258d4bdb32da..3560f354d9f2 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -18,6 +18,7 @@ table_fields, ) from frappe.model.docstatus import DocStatus +from frappe.model.dynamic_links import invalidate_distinct_link_doctypes from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module @@ -831,6 +832,8 @@ def get_msg(df, docname): if not doctype: frappe.throw(_("{0} must be set first").format(self.meta.get_label(df.options))) + invalidate_distinct_link_doctypes(df.parent, df.options, doctype) + # MySQL is case insensitive. Preserve case of the original docname in the Link Field. # get a map of values ot fetch along with this link query diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index caddb6b77eec..160e2bcca781 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -28,6 +28,7 @@ get_time, get_timespan_date_range, make_filter_tuple, + sanitize_column, ) from frappe.utils.data import DateTimeLikeObject, get_datetime, getdate, sbool @@ -600,7 +601,7 @@ def build_filter_conditions(self, filters, conditions: list, ignore_permissions= for f in filters: if isinstance(f, str): - conditions.append(f) + conditions.append(sanitize_column(f)) else: conditions.append(self.prepare_filter_condition(f)) diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index ccd6baacc3d0..cc1f1e520296 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -42,9 +42,7 @@ def get_dynamic_link_map(for_delete=False): dynamic_link_map.setdefault(meta.name, []).append(df) else: try: - links = frappe.db.sql_list( - """select distinct `{options}` from `tab{parent}`""".format(**df) - ) + links = fetch_distinct_link_doctypes(df.parent, df.options) for doctype in links: dynamic_link_map.setdefault(doctype, []).append(df) except frappe.db.TableMissingError: @@ -61,3 +59,41 @@ def get_dynamic_links(): for query in dynamic_link_queries: df += frappe.db.sql(query, as_dict=True) return df + + +def _dynamic_link_map_key(doctype, fieldname): + return f"dynamic_link_map::{doctype}::{fieldname}" + + +def fetch_distinct_link_doctypes(doctype: str, fieldname: str): + """Return all unique doctypes a dynamic link is linking against. + Note: + - results are cached and can *possibly be outdated* + - cache gets updated when a document with different document link is discovered + - raw queries adding dynamic link won't update this cache + - cache miss can often be VERY expensive on large table. + """ + + key = _dynamic_link_map_key(doctype, fieldname) + doctypes = frappe.cache.get_value(key) + + if doctypes is None: + doctypes = frappe.db.sql(f"""select distinct `{fieldname}` from `tab{doctype}`""", pluck=True) + frappe.cache.set_value(key, doctypes, expires_in_sec=12 * 60 * 60) + + return doctypes + + +def invalidate_distinct_link_doctypes(doctype: str, fieldname: str, linked_doctype: str): + """If new linked doctype is discovered for a dynamic link then cache is evicted.""" + + key = _dynamic_link_map_key(doctype, fieldname) + doctypes = frappe.cache.get_value(key) + + if doctypes is None or not isinstance(doctypes, list): + return + + if linked_doctype not in doctypes: + # Note: Do NOT "update" cache because it can lead to concurrency bugs. + frappe.cache.delete_value(key) + frappe.db.after_commit.add(lambda: frappe.cache.delete_value(key)) \ No newline at end of file diff --git a/frappe/monitor.py b/frappe/monitor.py index 522b743c4c3e..415054577d24 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -128,7 +128,7 @@ def flush(): logs = frappe.cache.lrange(MONITOR_REDIS_KEY, 0, -1) if logs: logs = list(map(frappe.safe_decode, logs)) - with open(log_file(), "a", os.O_NONBLOCK) as f: + with open(log_file(), "a") as f: f.write("\n".join(logs)) f.write("\n") # Remove fetched entries from cache diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 27fe08439c67..734377a3fea2 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -128,7 +128,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { if (invalid.length && check_invalid) { frappe.msgprint({ - title: __("Inavlid Values"), + title: __("Invalid Values"), message: __("Following fields have invalid values:") + "