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:") +
"