diff --git a/frappe/database/database.py b/frappe/database/database.py index 9fa1ff161c..c833bdeed3 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -177,6 +177,8 @@ class Database(object): raise frappe.QueryTimeoutError(e) elif frappe.conf.db_type == 'postgres': + # TODO: added temporarily + print(e) raise if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)): diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 8380161050..ea4264212b 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -389,12 +389,24 @@ class BaseDocument(object): fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype] return fieldname[0] if fieldname else None - def db_insert(self): - """INSERT the document (with valid columns) in the database.""" + 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 @@ -405,10 +417,11 @@ class BaseDocument(object): columns = list(d) try: frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns}) - VALUES ({values})""".format( - doctype = self.doctype, - columns = ", ".join("`"+c+"`" for c in columns), - values = ", ".join(["%s"] * len(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): @@ -421,8 +434,11 @@ class BaseDocument(object): self.db_insert() return - 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) + 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 diff --git a/frappe/model/document.py b/frappe/model/document.py index 66a0cef7dd..cb36c18b47 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -249,11 +249,7 @@ class Document(BaseDocument): if getattr(self.meta, "issingle", 0): self.update_single(self.get_valid_dict()) else: - try: - self.db_insert() - except frappe.DuplicateEntryError as e: - if not ignore_if_duplicate: - raise e + self.db_insert(ignore_if_duplicate=ignore_if_duplicate) # children for d in self.get_all_children(): diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 6e96849b35..bbd09590be 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -291,6 +291,16 @@ class TestDB(unittest.TestCase): frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION + def test_pk_collision_ignoring(self): + # note has `name` generated from title + for _ in range(3): + frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True) + + with savepoint(): + self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert) + # recover transaction to continue other tests + raise Exception + @run_only_if(db_type_is.MARIADB) class TestDDLCommandsMaria(unittest.TestCase):