diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 6bd7f2306f..4e21f3bcb4 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -100,6 +100,7 @@ class DocType(Document):
self.set_default_in_list_view()
self.set_default_translatable()
validate_series(self)
+ self.set("can_change_name_type", validate_autoincrement_autoname(self))
self.validate_document_type()
validate_fields(self)
@@ -124,12 +125,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_("Standard DocType cannot have default print format, use Customize Form"))
- if check_if_can_change_name_type(self):
- change_name_column_type(
- self.name,
- "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
- )
-
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@@ -374,6 +369,10 @@ class DocType(Document):
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
+
+ if self.get("can_change_name_type"):
+ self.setup_autoincrement_and_sequence()
+
try:
frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
@@ -413,6 +412,17 @@ class DocType(Document):
clear_linked_doctype_cache()
+ def setup_autoincrement_and_sequence(self):
+ """Changes name type and makes sequence on change (if required)"""
+
+ name_type = f"varchar({frappe.db.VARCHAR_LEN})"
+
+ if self.autoname == "autoincrement":
+ name_type = "bigint"
+ frappe.db.create_sequence(self.name, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
+
+ change_name_column_type(self.name, name_type)
+
def sync_global_search(self):
"""If global search settings are changed, rebuild search properties for this table"""
global_search_fields_before_update = [
@@ -903,26 +913,25 @@ def validate_series(dt, autoname=None, name=None):
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
-def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
- def get_autoname_before_save(doctype: str, to_be_customized_dt: str) -> str:
- if doctype == "Customize Form":
- property_value = frappe.db.get_value(
- "Property Setter", {"doc_type": to_be_customized_dt, "property": "autoname"}, "value"
- )
+def validate_autoincrement_autoname(dt: DocType) -> bool:
+ """Checks if can doctype can change to/from autoincrement autoname"""
+ def get_autoname_before_save(dt: DocType) -> str:
+ if dt.name == "Customize Form":
+ property_value = frappe.db.get_value(
+ "Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value"
+ )
# initially no property setter is set,
# hence getting autoname value from the doctype itself
if not property_value:
- return frappe.db.get_value("DocType", to_be_customized_dt, "autoname") or ""
+ return frappe.db.get_value("DocType", dt.doc_type, "autoname") or ""
return property_value
return getattr(dt.get_doc_before_save(), "autoname", "")
- doctype_name = dt.doc_type if dt.doctype == "Customize Form" else dt.name
-
if not dt.is_new():
- autoname_before_save = get_autoname_before_save(dt.doctype, doctype_name)
+ autoname_before_save = get_autoname_before_save(dt)
is_autoname_autoincrement = dt.autoname == "autoincrement"
if (
@@ -930,23 +939,35 @@ def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
and autoname_before_save != "autoincrement"
or (not is_autoname_autoincrement and autoname_before_save == "autoincrement")
):
- if not frappe.get_all(doctype_name, limit=1):
+
+ if frappe.get_meta(dt.name).issingle:
+ if dt.name == "Customize Form":
+ frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form"))
+
+ return False
+
+ if not frappe.get_all(dt.name, limit=1):
# allow changing the column type if there is no data
return True
- if raise_err:
- frappe.throw(
- _("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
- )
+ frappe.throw(
+ _("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
+ )
return False
def change_name_column_type(doctype_name: str, type: str) -> None:
- return frappe.db.change_column_type(
- doctype_name, "name", type, True if frappe.db.db_type == "mariadb" else False
+ """Changes name column type"""
+
+ args = (
+ (doctype_name, "name", type, False, True)
+ if (frappe.db.db_type == "postgres")
+ else (doctype_name, "name", type, True)
)
+ frappe.db.change_column_type(*args)
+
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 59475a95a7..11f5ef8a69 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -38,6 +38,52 @@ class TestDocType(unittest.TestCase):
doc = new_doctype(name).insert()
doc.delete()
+ def test_making_sequence_on_change(self):
+ frappe.delete_doc_if_exists("DocType", self._testMethodName)
+ dt = new_doctype(self._testMethodName).insert(ignore_permissions=True)
+ autoname = dt.autoname
+
+ # change autoname
+ dt.autoname = "autoincrement"
+ dt.save()
+
+ # check if name type has been changed
+ self.assertEqual(
+ frappe.db.sql(
+ f"""select data_type FROM information_schema.columns
+ where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
+ )[0][0],
+ "bigint",
+ )
+
+ if frappe.db.db_type == "mariadb":
+ table_name = "information_schema.tables"
+ conditions = f"table_type = 'sequence' and table_name = '{self._testMethodName}_id_seq'"
+ else:
+ table_name = "information_schema.sequences"
+ conditions = f"sequence_name = '{self._testMethodName}_id_seq'"
+
+ # check if sequence table is created
+ self.assertTrue(
+ frappe.db.sql(
+ f"""select * from {table_name}
+ where {conditions}"""
+ )
+ )
+
+ # change the autoname/naming rule back to original
+ dt.autoname = autoname
+ dt.save()
+
+ # check if name type has changed
+ self.assertEqual(
+ frappe.db.sql(
+ f"""select data_type FROM information_schema.columns
+ where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
+ )[0][0],
+ "varchar" if frappe.db.db_type == "mariadb" else "character varying",
+ )
+
def test_doctype_unique_constraint_dropped(self):
if frappe.db.exists("DocType", "With_Unique"):
frappe.delete_doc("DocType", "With_Unique")
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index a0bc994c45..4c40b80f53 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -287,7 +287,7 @@
"label": "Naming"
},
{
- "description": "Naming Options:\n
- field:[fieldname] - By Field
\n- autoincrement - Uses Databases' Auto Increment feature
- naming_series: - By Naming Series (field called naming_series must be present
- Prompt - Prompt user for a name
- [series] - Series by prefix (separated by a dot); for example PRE.#####
\n- format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
+ "description": "Naming Options:\n- field:[fieldname] - By Field
- naming_series: - By Naming Series (field called naming_series must be present)
- Prompt - Prompt user for a name
- [series] - Series by prefix (separated by a dot); for example PRE.#####
\n- format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name"
@@ -319,7 +319,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-04-21 15:36:16.772277",
+ "modified": "2022-05-12 15:36:16.772277",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 262542fd4b..903d7fc27d 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -11,9 +11,8 @@ import frappe
import frappe.translate
from frappe import _
from frappe.core.doctype.doctype.doctype import (
- change_name_column_type,
check_email_append_to,
- check_if_can_change_name_type,
+ validate_autoincrement_autoname,
validate_fields_for_doctype,
validate_series,
)
@@ -163,7 +162,7 @@ class CustomizeForm(Document):
return
validate_series(self, self.autoname, self.doc_type)
- can_change_name_type = check_if_can_change_name_type(self)
+ validate_autoincrement_autoname(self)
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
self.set_property_setters()
@@ -172,12 +171,6 @@ class CustomizeForm(Document):
validate_fields_for_doctype(self.doc_type)
check_email_append_to(self)
- if can_change_name_type:
- change_name_column_type(
- self.doc_type,
- "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
- )
-
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 5a1f629beb..b00f45f5d2 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -396,3 +396,10 @@ class TestCustomizeForm(unittest.TestCase):
d.label = ""
d.run_method("save_customization")
self.assertEqual(d.label, "")
+
+ def test_change_to_autoincrement_autoname(self):
+ d = self.get_customize_form("Event")
+ d.autoname = "autoincrement"
+
+ with self.assertRaises(frappe.ValidationError):
+ d.run_method("save_customization")
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 28d78471d2..7505ef3a7f 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -19,6 +19,15 @@ class MariaDBDatabase(Database):
DataError = pymysql.err.DataError
REGEX_CHARACTER = "regexp"
+ # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
+ # it drops the cache and uses the next non cached value in setval query and
+ # puts that in the backup file, which will start the counter
+ # from that value when inserting any new record in the doctype.
+ # By default the cache is 1000 which will mess up the sequence when
+ # using the system after a restore.
+ # issue link: https://jira.mariadb.org/browse/MDEV-21786
+ SEQUENCE_CACHE = 50
+
def setup_type_map(self):
self.db_type = "mariadb"
self.type_map = {
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 784fa23c13..f402b4ec74 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -40,14 +40,7 @@ class MariaDBTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
- # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
- # it drops the cache and uses the next non cached value in setval func and
- # puts that in the backup file, which will start the counter
- # from that value when inserting any new record in the doctype.
- # By default the cache is 1000 which will mess up the sequence when
- # using the system after a restore.
- # issue link: https://jira.mariadb.org/browse/MDEV-21786
- frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=50)
+ frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
# NOTE: not used nextval func as default as the ability to restore
# database with sequences has bugs in mariadb and gives a scary error.
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index d69e0bea94..8bd4113823 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -31,6 +31,12 @@ class PostgresDatabase(Database):
InterfaceError = psycopg2.InterfaceError
REGEX_CHARACTER = "~"
+ # NOTE; The sequence cache for postgres is per connection.
+ # Since we're opening and closing connections for every transaction this results in skipping the cache
+ # to the next non-cached value hence not using cache in postgres.
+ # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
+ SEQUENCE_CACHE = 0
+
def setup_type_map(self):
self.db_type = "postgres"
self.type_map = {
@@ -209,18 +215,19 @@ class PostgresDatabase(Database):
)
def change_column_type(
- self, doctype: str, column: str, type: str, nullable: bool = False
+ self, doctype: str, column: str, type: str, nullable: bool = False, use_cast: bool = False
) -> Union[List, Tuple]:
table_name = get_table_name(doctype)
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL"
+ using_cast = f'using "{column}"::{type}' if use_cast else ""
# postgres allows ddl in transactions but since we've currently made
# things same as mariadb (raising exception on ddl commands if the transaction has any writes),
# hence using sql_ddl here for committing and then moving forward.
return self.sql_ddl(
f"""ALTER TABLE "{table_name}"
- ALTER COLUMN "{column}" TYPE {type},
- ALTER COLUMN "{column}" {null_constraint}"""
+ ALTER COLUMN "{column}" TYPE {type} {using_cast},
+ ALTER COLUMN "{column}" {null_constraint}"""
)
def create_auth_table(self):
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index 2abd5f37c7..ef7ba33e12 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -34,11 +34,7 @@ class PostgresTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
- # The sequence cache is per connection.
- # Since we're opening and closing connections for every transaction this results in skipping the cache
- # to the next non-cached value hence not using cache in postgres.
- # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
- frappe.db.create_sequence(self.doctype, check_not_exists=True)
+ frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
name_column = "name bigint primary key"
# TODO: set docstatus length
diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py
index ede4689485..6a352d20d1 100644
--- a/frappe/database/sequence.py
+++ b/frappe/database/sequence.py
@@ -5,7 +5,7 @@ def create_sequence(
doctype_name: str,
*,
slug: str = "_id_seq",
- temporary=False,
+ temporary: bool = False,
check_not_exists: bool = False,
cycle: bool = False,
cache: int = 0,
@@ -51,7 +51,7 @@ def create_sequence(
else:
query += " cycle"
- db.sql(query)
+ db.sql_ddl(query)
return sequence_name
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 2f67c36fc0..bce3b1e65a 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -277,7 +277,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2021-11-18 05:06:24.881742",
+ "modified": "2022-05-12 05:43:27.935510",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
@@ -312,6 +312,7 @@
"sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"subject_field": "subject",
"title_field": "subject",
"track_changes": 1,
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index acb63b5bfa..005b7e3741 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -920,12 +920,12 @@ def cast_name(column: str) -> str:
kwargs = {"string": column, "flags": re.IGNORECASE}
if "cast(" not in column.lower() and "::" not in column:
- if re.search(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", **kwargs):
+ if re.search(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", **kwargs):
return re.sub(
- r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\)", r"locate(\1, cast(\2 as varchar))", **kwargs
+ r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", r"locate(\1, cast(\2 as varchar))", **kwargs
)
- elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*([`\"]?name[`\"]?)\s*,", **kwargs):
+ elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", **kwargs):
func = match.groups()[0]
return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs)
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 5a5af389ee..b7fe61b385 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -65,7 +65,11 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
};
var update_input = function() {
- me.set_input(me.value);
+ if (me.doctype && me.docname) {
+ me.set_input(me.value);
+ } else {
+ me.set_input(me.value || null);
+ }
};
if (me.disp_status != "None") {
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 4d105ef28e..338bde7502 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -551,10 +551,16 @@ class TestDDLCommandsMaria(unittest.TestCase):
)
def test_change_type(self) -> None:
+ def get_table_description():
+ return frappe.db.sql(f"DESC `tab{self.test_table_name}`")
+
+ # try changing from int to varchar
frappe.db.change_column_type("TestNotes", "id", "varchar(255)")
- test_table_description = frappe.db.sql(f"DESC tab{self.test_table_name};")
- self.assertGreater(len(test_table_description), 0)
- self.assertIn("varchar(255)", test_table_description[0])
+ self.assertIn("varchar(255)", get_table_description()[0])
+
+ # try changing from varchar to bigint
+ frappe.db.change_column_type("TestNotes", "id", "bigint")
+ self.assertIn("bigint(20)", get_table_description()[0])
def test_add_index(self) -> None:
index_name = "test_index"
@@ -763,21 +769,34 @@ class TestDDLCommandsPost(unittest.TestCase):
self.assertEqual([("id",), ("content",)], frappe.db.describe(self.test_table_name))
def test_change_type(self) -> None:
+ from psycopg2.errors import DatatypeMismatch
+
+ def get_table_description():
+ return frappe.db.sql(
+ f"""
+ SELECT
+ table_name,
+ column_name,
+ data_type
+ FROM
+ information_schema.columns
+ WHERE
+ table_name = 'tab{self.test_table_name}'"""
+ )
+
+ # try changing from int to varchar
frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)")
- check_change = frappe.db.sql(
- f"""
- SELECT
- table_name,
- column_name,
- data_type
- FROM
- information_schema.columns
- WHERE
- table_name = 'tab{self.test_table_name}'
- """
- )
- self.assertGreater(len(check_change), 0)
- self.assertIn("character varying", check_change[0])
+ self.assertIn("character varying", get_table_description()[0])
+
+ # try changing from varchar to int
+ try:
+ frappe.db.change_column_type(self.test_table_name, "id", "bigint")
+ except DatatypeMismatch:
+ frappe.db.rollback()
+
+ # try changing from varchar to int (using cast)
+ frappe.db.change_column_type(self.test_table_name, "id", "bigint", use_cast=True)
+ self.assertIn("bigint", get_table_description()[0])
def test_add_index(self) -> None:
index_name = "test_index"
@@ -792,7 +811,6 @@ class TestDDLCommandsPost(unittest.TestCase):
)
self.assertEqual(len(indexs_in_table), 1)
- @run_only_if(db_type_is.POSTGRES)
def test_modify_query(self):
from frappe.database.postgres.database import modify_query
@@ -810,7 +828,6 @@ class TestDDLCommandsPost(unittest.TestCase):
modify_query(query),
)
- @run_only_if(db_type_is.POSTGRES)
def test_modify_values(self):
from frappe.database.postgres.database import modify_values