diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 400242ad72..f10a94df50 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -275,7 +275,10 @@ class DbColumn: return # type - if current_def["type"] != column_type: + if current_def["type"] != column_type and not ( + # XXX: MariaDB JSON is same as longtext and information schema still returns longtext + current_def["type"] == "longtext" and column_type == "json" and frappe.db.db_type == "mariadb" + ): self.table.change_type.append(self) # unique @@ -313,20 +316,25 @@ class DbColumn: else: cur_default = current_def.get("default") new_default = self.default - if cur_default == "NULL" or cur_default is None: + if cur_default == "NULL": cur_default = None else: # Strip quotes from default value # eg. database returns default value as "'System Manager'" - cur_default = cur_default.lstrip("'").rstrip("'") + cur_default = cur_default.lstrip("'").rstrip("'").replace("\\\\", "\\") fieldtype = self.fieldtype + db_field_type = frappe.db.type_map.get(fieldtype) if fieldtype in ["Int", "Check"]: cur_default = cint(cur_default) new_default = cint(new_default) elif fieldtype in ["Currency", "Float", "Percent"]: cur_default = flt(cur_default) new_default = flt(new_default) + elif db_field_type and db_field_type[0] in ("varchar", "longtext", "text"): + new_default = cstr(new_default) + if not current_def.get("not_nullable"): + cur_default = cstr(cur_default) return cur_default != new_default def default_changed_for_decimal(self, current_def): @@ -336,7 +344,7 @@ class DbColumn: elif ( current_def["default"] - and float(current_def["default"]) == 0.0 + and flt(current_def["default"]) == 0.0 and self.default in ("", None, 0.0) ): return False diff --git a/frappe/tests/classes/integration_test_case.py b/frappe/tests/classes/integration_test_case.py index ecb2a47e29..07e82a8992 100644 --- a/frappe/tests/classes/integration_test_case.py +++ b/frappe/tests/classes/integration_test_case.py @@ -4,6 +4,7 @@ from contextlib import AbstractContextManager, contextmanager from types import MappingProxyType import frappe +from frappe.database.utils import get_query_type from frappe.utils import cint from ..utils.generators import get_missing_records_module_overrides, make_test_records @@ -112,7 +113,7 @@ class IntegrationTestCase(UnitTestCase): self._secondary_connection.rollback() @contextmanager - def assertQueryCount(self, count: int) -> AbstractContextManager[None]: + def assertQueryCount(self, count: int, query_type: tuple[str] | None = None): queries = [] def _sql_with_count(*args, **kwargs): @@ -124,6 +125,8 @@ class IntegrationTestCase(UnitTestCase): orig_sql = frappe.db.__class__.sql frappe.db.__class__.sql = _sql_with_count yield + if query_type: + queries = [q for q in queries if get_query_type(q) in query_type] self.assertLessEqual(len(queries), count, msg="Queries executed: \n" + "\n\n".join(queries)) finally: frappe.db.__class__.sql = orig_sql diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index a62a6433a9..971243293d 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -175,6 +175,16 @@ class TestDBUpdate(IntegrationTestCase): self.assertEqual(frappe.db.get_column_type(referring_doctype.name, link), "uuid") +class TestDBUpdateSanityChecks(IntegrationTestCase): + @run_only_if(db_type_is.MARIADB) + def test_no_unnecessary_migrates(self): + for doctype in frappe.get_all("DocType", {"is_virtual": 0, "custom": 0}, pluck="name"): + with self.subTest(f"Check {doctype}"): + frappe.reload_doctype(doctype, force=True) + with self.assertQueryCount(0, query_type=("alter",)): + frappe.reload_doctype(doctype, force=True) + + def get_fieldtype_from_def(field_def): fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ("", 0)) fieldtype = fieldtuple[0]