Merge pull request #25730 from ankush/ulid_naming
feat: UUID naming support
This commit is contained in:
commit
bb26d8f678
10 changed files with 111 additions and 9 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ class DocType(Document):
|
|||
"Expression",
|
||||
"Expression (old style)",
|
||||
"Random",
|
||||
"UUID",
|
||||
"By script",
|
||||
]
|
||||
nsm_parent_field: DF.Data | None
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue