Merge pull request #25730 from ankush/ulid_naming

feat: UUID naming support
This commit is contained in:
Ankush Menat 2024-03-29 21:25:07 +05:30 committed by GitHub
commit bb26d8f678
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 111 additions and 9 deletions

View file

@ -570,7 +570,7 @@
"fieldtype": "Select",
"label": "Naming Rule",
"length": 40,
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nUUID\nBy script"
},
{
"fieldname": "migration_hash",
@ -750,7 +750,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2024-03-23 16:03:21.405959",
"modified": "2024-03-29 16:09:26.114720",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -147,6 +147,7 @@ class DocType(Document):
"Expression",
"Expression (old style)",
"Random",
"UUID",
"By script",
]
nsm_parent_field: DF.Data | None

View file

@ -47,6 +47,9 @@ class MariaDBTable(DBTable):
# issue link: https://jira.mariadb.org/browse/MDEV-20070
name_column = "name bigint primary key"
elif not self.meta.issingle and self.meta.autoname == "UUID":
name_column = "name uuid primary key"
additional_definitions = ",\n".join(additional_definitions)
# create table
@ -76,6 +79,9 @@ class MariaDBTable(DBTable):
f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
for col in columns_to_modify
]
if alter_pk := self.alter_primary_key():
modify_column_query.append(alter_pk)
modify_column_query.extend(
[f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)" for col in self.add_unique]
)
@ -138,3 +144,20 @@ class MariaDBTable(DBTable):
)
raise
def alter_primary_key(self) -> str | None:
# If there are no values in table allow migrating to UUID from varchar
autoname = self.meta.autoname
if autoname == "UUID" and frappe.db.get_column_type(self.doctype, "name") != "uuid":
if not frappe.db.get_value(self.doctype, {}, order_by=None):
return "modify name uuid"
else:
frappe.throw(
_("Primary key of doctype {0} can not be changed as there are existing values.").format(
self.doctype
)
)
# Reverting from UUID to VARCHAR
if autoname != "UUID" and frappe.db.get_column_type(self.doctype, "name") == "uuid":
return f"modify name varchar({frappe.db.VARCHAR_LEN})"

View file

@ -34,6 +34,9 @@ class PostgresTable(DBTable):
frappe.db.create_sequence(self.doctype, check_not_exists=True)
name_column = "name bigint primary key"
elif not self.meta.issingle and self.meta.autoname == "UUID":
name_column = "name uuid primary key"
# TODO: set docstatus length
# create table
frappe.db.sql(
@ -91,6 +94,9 @@ class PostgresTable(DBTable):
)
)
if alter_pk := self.alter_primary_key():
query.append(alter_pk)
for col in self.set_default:
if col.fieldname == "name":
continue
@ -181,3 +187,20 @@ class PostgresTable(DBTable):
)
else:
raise e
def alter_primary_key(self) -> str | None:
# If there are no values in table allow migrating to UUID from varchar
autoname = self.meta.autoname
if autoname == "UUID" and frappe.db.get_column_type(self.doctype, "name") != "uuid":
if not frappe.db.get_value(self.doctype, {}, order_by=None):
return "alter column `name` TYPE uuid USING name::uuid"
else:
frappe.throw(
_("Primary key of doctype {0} can not be changed as there are existing values.").format(
self.doctype
)
)
# Reverting from UUID to VARCHAR
if autoname != "UUID" and frappe.db.get_column_type(self.doctype, "name") == "uuid":
return f"alter column `name` TYPE varchar({frappe.db.VARCHAR_LEN})"

View file

@ -4,10 +4,12 @@
import base64
import datetime
import re
import struct
import time
from collections.abc import Callable
from typing import TYPE_CHECKING, Optional
from uuid import UUID
import uuid_utils
import frappe
from frappe import _
@ -39,6 +41,10 @@ class InvalidNamingSeriesError(frappe.ValidationError):
pass
class InvalidUUIDValue(frappe.ValidationError):
pass
class NamingSeries:
__slots__ = ("series",)
@ -142,13 +148,25 @@ def set_new_name(doc):
meta = frappe.get_meta(doc.doctype)
autoname = meta.autoname or ""
if autoname.lower() != "prompt" and not frappe.flags.in_import:
if autoname.lower() not in ("prompt", "uuid") and not frappe.flags.in_import:
doc.name = None
if is_autoincremented(doc.doctype, meta):
doc.name = frappe.db.get_next_sequence_val(doc.doctype)
return
if meta.autoname == "UUID":
if not doc.name:
doc.name = str(uuid_utils.uuid7())
elif isinstance(doc.name, UUID | uuid_utils.UUID):
doc.name = str(doc.name)
elif isinstance(doc.name, str): # validate
try:
UUID(doc.name)
except ValueError:
frappe.throw(_("Invalid value specified for UUID: {}").format(doc.name), InvalidUUIDValue)
return
if getattr(doc, "amended_from", None):
_set_amended_name(doc)
if doc.name:
@ -179,10 +197,7 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
if not meta:
meta = frappe.get_meta(doctype)
if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement":
return True
return False
return not getattr(meta, "issingle", False) and meta.autoname == "autoincrement"
def set_name_from_naming_options(autoname, doc):

View file

@ -78,6 +78,7 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.
Expression: "format:",
"Expression (sld style)": "",
Random: "hash",
UUID: "UUID",
"By script": "",
};
this.frm.set_value(

View file

@ -139,6 +139,20 @@ class TestDBUpdate(FrappeTestCase):
doctype.delete()
frappe.db.commit()
def test_uuid_varchar_migration(self):
doctype = new_doctype().insert()
doctype.autoname = "UUID"
doctype.save()
self.assertEqual(frappe.db.get_column_type(doctype.name, "name"), "uuid")
doc = frappe.new_doc(doctype.name).insert()
doctype.autoname = "hash"
doctype.save()
varchar = "varchar" if frappe.db.db_type == "mariadb" else "character varying"
self.assertIn(varchar, frappe.db.get_column_type(doctype.name, "name"))
doc.reload() # ensure that docs are still accesible
def get_fieldtype_from_def(field_def):
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ("", 0))

View file

@ -9,7 +9,7 @@ from frappe.app import make_form_dict
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.tests.utils import FrappeTestCase, timeout
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint, now_datetime, set_request
from frappe.website.serve import get_response

View file

@ -2,13 +2,16 @@
# License: MIT. See LICENSE
import time
from uuid import UUID
import uuid_utils
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_full_jitter
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.model.naming import (
InvalidNamingSeriesError,
InvalidUUIDValue,
NamingSeries,
append_number_if_name_exists,
determine_consecutive_week_number,
@ -407,6 +410,27 @@ class TestNaming(FrappeTestCase):
names.append(make_autoname("hash"))
self.assertEqual(names, sorted(names))
def test_uuid_naming(self):
uuid_doctype = new_doctype(autoname="UUID").insert().name
self.assertEqual("uuid", frappe.db.get_column_type(uuid_doctype, "name"))
# Auto set names
document = frappe.new_doc(uuid_doctype).insert()
uid = UUID(document.name)
self.assertEqual(uid.version, 7) # Default version
# Applications can specify UUID themselves, useful for APIs to set name themselves.
for uid in (uuid_utils.uuid4(), uuid_utils.uuid7()):
doc = frappe.new_doc(uuid_doctype, name=uid).insert()
self.assertEqual(doc.name, str(uid))
# Can specify valid UUID strings too
for uid in (uuid_utils.uuid4(), uuid_utils.uuid7()):
doc = frappe.new_doc(uuid_doctype, name=str(uid)).insert()
self.assertEqual(doc.name, str(uid))
self.assertRaises(InvalidUUIDValue, frappe.new_doc(uuid_doctype, name="XYZ").insert)
def parse_naming_series_variable(doc, variable):
if variable == "PM":

View file

@ -73,6 +73,7 @@ dependencies = [
"terminaltables~=3.1.10",
"traceback-with-variables~=2.0.4",
"typing_extensions>=4.6.1,<5",
"uuid-utils~=0.6.1",
"xlrd~=2.0.1",
"zxcvbn~=4.4.28",
"markdownify~=0.11.6",