From 3eea3059bf0b6907bb7d8ca4e7ce33607039a1e8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 23 Sep 2026 16:13:04 +1200 Subject: [PATCH] Fix element_info orphan row causing UNIQUE constraint errors on undo Reviving #664, closed 2026-09-18 purely to clear a maintainer review backlog (#630), not on merit; not superseded. Rewritten fresh against current master rather than merged from the old branch -- that branch predates the Qt6-only switch and much of dataBase/projectdatabase.cpp's later rewrite, and the two had diverged too far for a textual merge to be trustworthy. projectDataBase::removeElement() only ran DELETE FROM element WHERE uuid=:uuid. It never touched element_info, even though every element also has a row there (element_uuid is its PRIMARY KEY, with a FOREIGN KEY back to element.uuid that isn't enforced by this connection -- no ON DELETE CASCADE in effect). So deleting an element left its element_info row orphaned. Re-adding an element with that same uuid later -- undo of that same deletion, or a redo replaying it -- goes through addElement(), which INSERTs into both tables. The element insert succeeds (that row really was removed). The element_info insert hits the orphaned row's primary key and fails, silently: the error is logged and swallowed, so the element re-enters the scene with no element_info row at all, and nothing later re-syncs it. removeDiagram() already cascades this cleanup when a whole folio is removed (a later, unrelated addition) -- confirmed on current master -- but that path never runs for a single element removed on its own, which is the case this fixes. Verified on the built binary, not just read: placed an element, deleted it (Ctrl+A, Delete), undid the deletion (Ctrl+Z). Reverting just this fix and repeating the identical sequence reproduces the exact reported error: Debug: projectDataBase::addElement insert element info error : QSqlError("1555", "Unable to fetch row", "UNIQUE constraint failed: element_info.element_uuid") With the fix, the same sequence produces nothing. Qt 6.10.2, ctest 13/13. Co-Authored-By: Claude Sonnet 5 --- sources/dataBase/projectdatabase.cpp | 32 ++++++++++++++++++++++++++-- sources/dataBase/projectdatabase.h | 1 + 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index e1d8dbf82..51ccd6fe2 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -319,10 +319,23 @@ void projectDataBase::addElement(Element *element) void projectDataBase::removeElement(Element *element) { m_content_changed = true; + bool changed = false; + m_remove_element_query.bindValue(":uuid", element->uuid().toString()); - if(!m_remove_element_query.exec()) { - qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError(); + if (m_remove_element_query.exec()) { + changed = true; } else { + qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError(); + } + + m_remove_element_info_query.bindValue(":uuid", element->uuid().toString()); + if (m_remove_element_info_query.exec()) { + changed = true; + } else { + qDebug() << "projectDataBase::removeElement remove element_info error : " << m_remove_element_info_query.lastError(); + } + + if (changed) { emit dataBaseUpdated(); } } @@ -1159,6 +1172,21 @@ void projectDataBase::prepareQuery() m_remove_element_query = QSqlQuery(m_data_base); m_remove_element_query.prepare(remove_element); + //REMOVE ELEMENT INFO + //element_info has no ON DELETE CASCADE (foreign keys aren't + //enforced by this connection), so removeElement() must clear it + //explicitly. Without this, the row is orphaned under the removed + //element's uuid, and re-adding an element with that same uuid + //later -- undo of this same removal, or a redo replaying it -- + //hits element_info's PRIMARY KEY constraint on element_uuid: the + //element re-add succeeds, but its element_info insert silently + //fails and is lost. removeDiagram()'s cascade already clears this + //table when a whole folio goes, but that does not run for a + //single element removed on its own. + QString remove_element_info("DELETE FROM element_info WHERE element_uuid=:uuid"); + m_remove_element_info_query = QSqlQuery(m_data_base); + m_remove_element_info_query.prepare(remove_element_info); + //UPDATE ELEMENT INFO QString update_str("UPDATE element_info SET "); for (auto string : QETInformation::elementInfoKeys()) { diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 55aacedd5..01da46856 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -139,6 +139,7 @@ class projectDataBase : public QObject QSqlQuery m_insert_elements_query, m_insert_element_info_query, m_remove_element_query, + m_remove_element_info_query, m_update_element_query, m_insert_diagram_query, m_remove_diagram_query,