Return PRIMARY key in `Duplicated entry` error when both PRIMARY and UNIQUE indexes are conflicting
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Enhancement
When there are conflicting entries for both `PRIMARY` and `UNIQUE` keys. Always return PRIMARY key in `Duplicated entry` error to make it behave the same as MySQL and MariaDB.
Applications depending on the returned error message to run different logic would fail on TiDB in this case. For example,
```Python
def db_insert(self, ignore_if_duplicate=False):
"""INSERT the document (with valid columns) in the database.
args:
ignore_if_duplicate: ignore primary key collision
at database level (postgres)
in python (mariadb)
"""
if not self.name:
# name will be set by document class in most cases
set_new_name(self)
conflict_handler = ""
# On postgres we can't implcitly ignore PK collision
# So instruct pg to ignore `name` field conflicts
if ignore_if_duplicate and frappe.db.db_type == "postgres":
conflict_handler = "on conflict (name) do nothing"
if not self.creation:
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(
convert_dates_to_str=True,
ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE,
ignore_virtual=True,
)
columns = list(d)
try:
frappe.db.sql(
"""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values}) {conflict_handler}""".format(
doctype=self.doctype,
columns=", ".join("`" + c + "`" for c in columns),
values=", ".join(["%s"] * len(columns)),
conflict_handler=conflict_handler,
),
list(d.values()),
)
except Exception as e:
if frappe.db.is_primary_key_violation(e):
if self.meta.autoname == "hash":
# hash collision? try again
frappe.flags.retry_count = (frappe.flags.retry_count or 0) + 1
if frappe.flags.retry_count > 5 and not frappe.flags.in_test:
raise
self.name = None
self.db_insert()
return
if not ignore_if_duplicate:
frappe.msgprint(
_("{0} {1} already exists").format(_(self.doctype), frappe.bold(self.name)),
title=_("Duplicate Name"),
indicator="red",
)
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
# unique constraint
self.show_unique_validation_message(e)
else:
raise
self.set("__islocal", False)
```
The code snippet fails with unique validation when `ignore_if_duplicate` is `True`
To reproduce the difference run the following SQL statements:
```SQL
CREATE TABLE `tabRole` (
`name` varchar(140) NOT NULL,
`creation` datetime(6) DEFAULT NULL,
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) DEFAULT NULL,
`owner` varchar(140) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`idx` int(8) NOT NULL DEFAULT 0,
`role_name` varchar(140) DEFAULT NULL,
`home_page` varchar(140) DEFAULT NULL,
`restrict_to_domain` varchar(140) DEFAULT NULL,
`disabled` int(1) NOT NULL DEFAULT 0,
`is_custom` int(1) NOT NULL DEFAULT 0,
`desk_access` int(1) NOT NULL DEFAULT 1,
`two_factor_auth` int(1) NOT NULL DEFAULT 0,
`search_bar` int(1) NOT NULL DEFAULT 1,
`notifications` int(1) NOT NULL DEFAULT 1,
`list_sidebar` int(1) NOT NULL DEFAULT 1,
`bulk_actions` int(1) NOT NULL DEFAULT 1,
`view_switcher` int(1) NOT NULL DEFAULT 1,
`form_sidebar` int(1) NOT NULL DEFAULT 1,
`timeline` int(1) NOT NULL DEFAULT 1,
`dashboard` int(1) NOT NULL DEFAULT 1,
`_user_tags` text DEFAULT NULL,
`_comments` text DEFAULT NULL,
`_assign` text DEFAULT NULL,
`_liked_by` text DEFAULT NULL,
PRIMARY KEY (`name`),
UNIQUE KEY `role_name` (`role_name`),
KEY `modified` (`modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
```
For insert with violations to both PRIMARY key and `role_name` column.
```SQL
INSERT INTO `tabRole` (`name`, `owner`, `creation`, `modified`, `modified_by`, `docstatus`, `idx`, `role_name`, `home_page`, `restrict_to_domain`, `disabled`, `is_custom`, `desk_access`, `two_factor_auth`, `search_bar`, `notifications`, `list_sidebar`, `bulk_actions`, `view_switcher`, `form_sidebar`, `timeline`, `dashboard`) VALUES ('Report Manager', 'Administrator', '2023-12-26 12:12:51.355082', '2023-12-26 12:12:51.355082', 'Administrator', 0, 0, 'Report Manager', null, null, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1);
INSERT INTO `tabRole` (`name`, `owner`, `creation`, `modified`, `modified_by`, `docstatus`, `idx`, `role_name`, `home_page`, `restrict_to_domain`, `disabled`, `is_custom`, `desk_access`, `two_factor_auth`, `search_bar`, `notifications`, `list_sidebar`, `bulk_actions`, `view_switcher`, `form_sidebar`, `timeline`, `dashboard`) VALUES ('Report Manager', 'Administrator', '2023-12-26 12:14:50.123841', '2023-12-26 12:14:50.123841', 'Administrator', 0, 0, 'Report Manager', null, null, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1);
```
Both MariaDB 10.6 and MySQL 5.7 always returns this error
```SHELL
ERROR 1062 (23000): Duplicate entry 'Report Manager' for key 'PRIMARY'
```
MySQL 8.0 returns this slightly different error with table name but still indicating the violation is in `PRIMARY` key.
```SHELL
ERROR 1062 (23000): Duplicate entry 'Report Manager' for key 'tabRole.PRIMARY'
```
On the other hand, TiDB returns this error indicating the violation is in `UNIQUE` key `role_name` instead.
```SHELL
ERROR 1062 (23000): Duplicate entry 'Report Manager' for key 'tabRole.role_name'
```
Contributor guide
Assessment
This issue has not been assessed yet.