From a3511e86940bd9bf5a3dd8c9ea3060e73354cdf4 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 2 Aug 2026 08:12:22 +1200 Subject: [PATCH 1/5] Give Conductor its own persisted uuid Slice 1 of discussion #503 (from-to wiring list built on projectDataBase). Conductor is the one item type on a diagram without a stable identity of its own -- Element and Diagram both have a uuid, Conductor didn't. This is the prerequisite the wiring-list tables need: a conductor table keyed by uuid, the same way the existing element table is keyed by Element::uuid(). - Conductor gets a QUuid m_uuid, generated in the constructor, with uuid()/newUuid() accessors mirroring Element's exact pattern. - toXml()/fromXml() read/write a "uuid" attribute the same way Element already does, including the same generate-on-missing fallback (QUuid(e.attribute("uuid", QUuid::createUuid().toString()))) for projects saved before this change. - PasteDiagramCommand::redo() calls newUuid() on every pasted conductor (content.conductors(), all three categories), mirroring the existing per-element newUuid() call right above it -- otherwise copy-paste would duplicate a conductor's uuid. Backward compatibility: Conductor::valideXml() doesn't require the "uuid" attribute, so old files parse unchanged. Verified by opening a genuinely pre-uuid project (examples/industrial.qet, 150 folios, 671 conductors, legacy integer terminal1/terminal2 references with no uuid attribute at all) -- loads and renders correctly, gets uuids assigned on load, and those uuids are stable across a second load/save cycle (byte-identical uuid values). Verified paste separately: copying a selection with conductors and pasting produces distinct new uuids for every pasted conductor, none colliding with the originals or each other. --- sources/diagramcommands.cpp | 7 +++++++ sources/qetgraphicsitem/conductor.cpp | 8 ++++++++ sources/qetgraphicsitem/conductor.h | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/sources/diagramcommands.cpp b/sources/diagramcommands.cpp index 2922c32b1..763f1f987 100644 --- a/sources/diagramcommands.cpp +++ b/sources/diagramcommands.cpp @@ -75,6 +75,13 @@ void PasteDiagramCommand::redo() { first_redo = false; + //make new uuid for every pasted conductor, because old uuid are + //the uuid of the copied conductor + const QList all_pasted_conductors = content.conductors(); + for (Conductor *c : all_pasted_conductors) { + c -> newUuid(); + } + //this is the first paste, we do some actions for the new element const QList elmts_list = content.m_elements; for (Element *e : elmts_list) diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 291608620..546f64b3e 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -91,6 +91,7 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) : //set Zvalue at 11 to be upper than the DiagramImageItem and element setZValue(11); m_previous_z_value = zValue(); + m_uuid = QUuid::createUuid(); //Add this conductor to the list of conductors of each of the two terminals bool ajout_p1 = terminal1 -> addConductor(this); @@ -1005,6 +1006,12 @@ void Conductor::pointsToSegments(const QList& points_list) { */ bool Conductor::fromXml(QDomElement &dom_element) { + //Older project files have no conductor uuid attribute at all -- + //generate one on load, same treatment terminal uuids got when + //that field was introduced (see terminal1/terminal2 handling in + //toXml() below). + m_uuid = QUuid(dom_element.attribute("uuid", QUuid::createUuid().toString())); + setPos(dom_element.attribute("x", nullptr).toDouble(), dom_element.attribute("y", nullptr).toDouble()); @@ -1043,6 +1050,7 @@ QDomElement Conductor::toXml(QDomDocument &dom_document, { QDomElement dom_element = dom_document.createElement("conductor"); + dom_element.setAttribute("uuid", m_uuid.toString()); dom_element.setAttribute("x", QString::number(pos().x())); dom_element.setAttribute("y", QString::number(pos().y())); diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index dc4d5586b..91ca92afb 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -21,6 +21,7 @@ #include "../conductorproperties.h" #include +#include class ConductorProfile; class ConductorSegmentProfile; @@ -77,6 +78,8 @@ class Conductor : public QGraphicsObject int type() const override { return Type; } Diagram *diagram() const; ConductorTextItem *textItem() const; + QUuid uuid() const {return m_uuid;} + void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor void updatePath(const QRectF & = QRectF()); //This method do nothing, it's only made to be used with Q_PROPERTY @@ -205,6 +208,7 @@ class Conductor : public QGraphicsObject Highlight must_highlight_; bool m_valid; bool m_freeze_label = false; + QUuid m_uuid; /// QPen et QBrush objects used to draw conductors static QPen conductor_pen; From 0fdcd1e1e8426b7eb6845bfa17018ff4994bd360 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 21 Aug 2026 19:07:40 +1200 Subject: [PATCH 2/5] Regenerate conductor uuids when a folio is duplicated ElementsPanelWidget::duplicateDiagram() round-trips the folio through XML and then gives the copied *elements* fresh uuids, because element.uuid is the primary key of the project database and a duplicate silently fails to insert. Conductors now have the same problem and needed the same loop: conductor.uuid is likewise a primary key, its insert is a plain INSERT rather than INSERT OR IGNORE, and a failure only reaches qDebug(). Without this, every wire on a duplicated folio is missing from the wiring list and from the per-element wire count, with nothing shown to the user. Verified against the real schema: inserting the same conductor uuid for a second folio fails with "UNIQUE constraint failed: conductor.uuid", leaving one row where two were expected. Also harden the uuid read in Conductor::fromXml(). The default argument of QDomElement::attribute() is evaluated whether or not the attribute exists, so a uuid was minted for every conductor on every load and thrown away; and the default only applies when the attribute is *absent*, so a present but empty or malformed uuid="" parsed to a null QUuid rather than a fresh one -- and null uuids collide with each other exactly as duplicates do. Co-Authored-By: Claude Opus 5 --- sources/elementspanelwidget.cpp | 8 ++++++++ sources/qetgraphicsitem/conductor.cpp | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sources/elementspanelwidget.cpp b/sources/elementspanelwidget.cpp index 7baf0afbe..0fdc60edb 100644 --- a/sources/elementspanelwidget.cpp +++ b/sources/elementspanelwidget.cpp @@ -17,6 +17,7 @@ */ #include "elementspanelwidget.h" #include "diagram.h" +#include "qetgraphicsitem/conductor.h" #include "editor/ui/qetelementeditor.h" #include "elementscategoryeditor.h" #include "qetapp.h" @@ -652,6 +653,13 @@ void ElementsPanelWidget::duplicateDiagram() elmt->newUuid(); new_diagram->restoreText(elmt); } + else if (Conductor *cond = dynamic_cast(item)) { + // Same reasoning for conductors: conductor.uuid is the PRIMARY + // KEY of the conductor table, and its insert is a plain INSERT, + // so a duplicated uuid fails and the wire silently disappears + // from the wiring list and the per-element wire count. + cond->newUuid(); + } } } diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 546f64b3e..e04288107 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1010,7 +1010,13 @@ bool Conductor::fromXml(QDomElement &dom_element) //generate one on load, same treatment terminal uuids got when //that field was introduced (see terminal1/terminal2 handling in //toXml() below). - m_uuid = QUuid(dom_element.attribute("uuid", QUuid::createUuid().toString())); + m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid"))); + if (m_uuid.isNull()) { + //Absent, empty or malformed: mint one. A null uuid is not a usable + //identity -- every conductor carrying one would collide with every + //other on the conductor table's primary key. + m_uuid = QUuid::createUuid(); + } setPos(dom_element.attribute("x", nullptr).toDouble(), dom_element.attribute("y", nullptr).toDouble()); From 43da912aad839166d1c3747ac414cceb76859c53 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 2 Aug 2026 11:52:30 +1200 Subject: [PATCH 3/5] Add terminal and conductor tables to projectDataBase Slice 2 of discussion #503 (from-to wiring list built on projectDataBase), building on the conductor uuid from slice 1 (#625). Pure plumbing: two new additive tables plus their populate/add/remove hooks. No view, no UI, no visible behavior change yet -- the wiring-list view is slice 3. Follows the existing shape of the class throughout: same table/column naming, same prepared-statement idiom in prepareQuery(), same bind/exec/qDebug-lastError error handling, same DELETE-then-loop populate pattern. - `terminal (uuid, element_uuid, name)` and `conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text)` created alongside the existing tables in createDataBase(). - populateConductorTable() added as a fifth populate* call in updateDB(). Terminal population is folded into it, since a terminal only matters here in the context of a conductor referencing it. - addConductor()/removeConductor() hooked into the already-existing Conductor::Type branch of Diagram::addItem()/removeItem(), mirroring the Element::Type branch directly above. Two things the original schema sketch in the discussion got wrong, found by testing rather than inspection: 1. Terminal::uuid() is NOT unique per placed terminal. It is the terminal-position id baked into the catalog .elmt definition ("the top terminal"), so every placed instance of the same catalog element shares it. A terminal instance is only uniquely identified by (uuid, element_uuid) together, so that pair is the terminal table's primary key and the conductor table carries both halves for each endpoint. With uuid alone as PK, the second placed instance of any element silently lost its terminals to the INSERT OR IGNORE. 2. Conductors whose terminals predate terminal uuids are omitted rather than given a fabricated identity, as agreed in the discussion. This turns out to matter far more than expected in practice -- see below. Testing (all live, in the running app): - Incremental add: fresh project, two vertically aligned contacts placed so autoconnect creates a conductor -> 2 terminals, 1 conductor. - Incremental remove: deleting that conductor -> conductor count 1 -> 0. - Undo: ctrl+Z after the delete -> back to 1, no duplicate-primary-key error (the same Conductor object keeps its uuid). - Bulk populate: examples/weneedpolonez-Polonez_MR89_wiring_diagram.qet (366 conductors) -> 478 terminals, 280 conductors; the 86 conductors touching legacy terminals correctly omitted. - Join correctness: conductor -> terminal (composite key) -> element_info resolves real from-to rows with real element labels. - Legacy-only project: examples/industrial.qet has 1794 terminals and *zero* terminal uuids, so all 671 of its conductors are omitted. Loads and renders fine, no crash, no spurious rows -- but worth stating plainly that a from-to wiring list for that project would be empty today. This is a property of the element catalog definitions, not of the project file, and is the strongest argument for surfacing an "N conductors excluded" count to the user when the view lands. - No SQL errors logged in any of the above. Known limitation, consistent with existing behavior: removeDiagram() does not cascade-delete the conductor rows of that diagram, exactly as it already does not cascade to element/element_info. A full updateDB() rebuild clears them, and the future wiring-list view INNER JOINs from conductor, so orphan terminal rows never surface. --- sources/dataBase/projectdatabase.cpp | 159 +++++++++++++++++++++++++++ sources/dataBase/projectdatabase.h | 12 +- sources/diagram.cpp | 2 + 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index eacbeb712..013e94065 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -21,7 +21,9 @@ #include "../diagramposition.h" #include "../elementprovider.h" #include "../qetapp.h" +#include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../qetproject.h" @@ -87,6 +89,7 @@ void projectDataBase::updateDB() populateDiagramInfoTable(); populateElementTable(); populateElementInfoTable(); + populateConductorTable(); emit dataBaseUpdated(); } @@ -245,6 +248,57 @@ void projectDataBase::diagramOrderChanged() { } +/** + @brief projectDataBase::addConductor + @param conductor +*/ +void projectDataBase::addConductor(Conductor *conductor) +{ + if (!conductor || !conductor->diagram()) { + qDebug() << "projectDataBase::addConductor: null conductor or diagram"; + return; + } + + //A conductor whose terminal(s) predate terminal uuids (legacy + //elements not yet re-saved by a uuid-aware element editor) can't + //be given a stable identity here -- omitted the same way + //element_nomenclature_view already omits exclude_from_bom elements, + //rather than fabricating one. + if (conductor->terminal1->uuid().isNull() || conductor->terminal2->uuid().isNull()) { + return; + } + + insertTerminal(conductor->terminal1); + insertTerminal(conductor->terminal2); + + m_insert_conductor_query.bindValue(":uuid", conductor->uuid().toString()); + m_insert_conductor_query.bindValue(":diagram_uuid", conductor->diagram()->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal1_uuid", conductor->terminal1->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal1_element_uuid", conductor->terminal1->parentElement()->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal2_uuid", conductor->terminal2->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal2_element_uuid", conductor->terminal2->parentElement()->uuid().toString()); + m_insert_conductor_query.bindValue(":text", conductor->properties().text); + if (!m_insert_conductor_query.exec()) { + qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError(); + } else { + emit dataBaseUpdated(); + } +} + +/** + @brief projectDataBase::removeConductor + @param conductor +*/ +void projectDataBase::removeConductor(Conductor *conductor) +{ + m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString()); + if (!m_remove_conductor_query.exec()) { + qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError(); + } else { + emit dataBaseUpdated(); + } +} + /** @brief projectDataBase::createDataBase Create the data base @@ -323,6 +377,42 @@ bool projectDataBase::createDataBase() qDebug() << " element_info_table query : " << query_.lastError(); } + //Create the terminal table. + //Terminal::uuid() is the terminal-position id baked into the catalog + //.elmt definition (e.g. "the top terminal") -- identical across every + //placed instance of that catalog element, not a per-instance id. A + //terminal instance is only uniquely identified by (uuid, element_uuid) + //together, so that pair is the primary key here, not uuid alone. + QString terminal_table("CREATE TABLE terminal" + "( " + "uuid VARCHAR(50) NOT NULL, " + "element_uuid VARCHAR(50) NOT NULL," + "name VARCHAR(50)," + "PRIMARY KEY (uuid, element_uuid)," + "FOREIGN KEY (element_uuid) REFERENCES element (uuid)" + ")"); + if (!query_.exec(terminal_table)) { + qDebug() << "terminal_table query : "<< query_.lastError(); + } + + //Create the conductor table + QString conductor_table("CREATE TABLE conductor" + "( " + "uuid VARCHAR(50) PRIMARY KEY NOT NULL, " + "diagram_uuid VARCHAR(50) NOT NULL," + "terminal1_uuid VARCHAR(50) NOT NULL," + "terminal1_element_uuid VARCHAR(50) NOT NULL," + "terminal2_uuid VARCHAR(50) NOT NULL," + "terminal2_element_uuid VARCHAR(50) NOT NULL," + "text VARCHAR(100)," + "FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)," + "FOREIGN KEY (terminal1_uuid, terminal1_element_uuid) REFERENCES terminal (uuid, element_uuid)," + "FOREIGN KEY (terminal2_uuid, terminal2_element_uuid) REFERENCES terminal (uuid, element_uuid)" + ")"); + if (!query_.exec(conductor_table)) { + qDebug() << "conductor_table query : "<< query_.lastError(); + } + createElementNomenclatureView(); createSummaryView(); prepareQuery(); @@ -534,6 +624,62 @@ void projectDataBase::populateDiagramInfoTable() } } +/** + @brief projectDataBase::populateConductorTable + Populate the terminal and conductor tables. Terminals only matter here + in the context of a conductor referencing them, so their population is + folded into this method rather than tracked independently. +*/ +void projectDataBase::populateConductorTable() +{ + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("DELETE FROM conductor")); + query.exec(QStringLiteral("DELETE FROM terminal")); + + for (auto *diagram : m_project->diagrams()) + { + const auto conductor_list = diagram->conductors(); + for (auto *conductor : conductor_list) + { + //See addConductor() for why terminals without a uuid + //(legacy elements) are omitted rather than fabricating one. + if (conductor->terminal1->uuid().isNull() || conductor->terminal2->uuid().isNull()) { + continue; + } + + insertTerminal(conductor->terminal1); + insertTerminal(conductor->terminal2); + + m_insert_conductor_query.bindValue(":uuid", conductor->uuid().toString()); + m_insert_conductor_query.bindValue(":diagram_uuid", diagram->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal1_uuid", conductor->terminal1->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal1_element_uuid", conductor->terminal1->parentElement()->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal2_uuid", conductor->terminal2->uuid().toString()); + m_insert_conductor_query.bindValue(":terminal2_element_uuid", conductor->terminal2->parentElement()->uuid().toString()); + m_insert_conductor_query.bindValue(":text", conductor->properties().text); + if (!m_insert_conductor_query.exec()) { + qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError(); + } + } + } +} + +/** + @brief projectDataBase::insertTerminal + Insert (or, if already present -- e.g. a junction shared by several + conductors -- silently keep) @terminal in the terminal table. + @param terminal +*/ +void projectDataBase::insertTerminal(Terminal *terminal) +{ + m_insert_terminal_query.bindValue(":uuid", terminal->uuid().toString()); + m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString()); + m_insert_terminal_query.bindValue(":name", terminal->name()); + if (!m_insert_terminal_query.exec()) { + qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError(); + } +} + void projectDataBase::prepareQuery() { //INSERT DIAGRAM @@ -606,6 +752,19 @@ void projectDataBase::prepareQuery() update_str.append(" WHERE element_uuid = :uuid"); m_update_element_query = QSqlQuery(m_data_base); m_update_element_query.prepare(update_str); + + //INSERT TERMINAL + m_insert_terminal_query = QSqlQuery(m_data_base); + m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)"); + + //INSERT CONDUCTOR + m_insert_conductor_query = QSqlQuery(m_data_base); + m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) " + "VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)"); + + //REMOVE CONDUCTOR + m_remove_conductor_query = QSqlQuery(m_data_base); + m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid"); } /** diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index c2e4c838c..219e706d5 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -27,6 +27,8 @@ class Element; class QETProject; class Diagram; +class Conductor; +class Terminal; class sqlite3; /** @@ -58,6 +60,9 @@ class projectDataBase : public QObject void diagramInfoChanged (Diagram *diagram); void diagramOrderChanged(); + void addConductor (Conductor *conductor); + void removeConductor (Conductor *conductor); + signals: void dataBaseUpdated(); @@ -69,6 +74,8 @@ class projectDataBase : public QObject void populateElementTable(); void populateElementInfoTable(); void populateDiagramInfoTable(); + void populateConductorTable(); + void insertTerminal(Terminal *terminal); void prepareQuery(); static QHash elementInfoToString( Element *elmt); @@ -86,7 +93,10 @@ class projectDataBase : public QObject m_insert_diagram_info_query, m_update_diagram_info_query, m_diagram_order_changed, - m_diagram_info_order_changed; + m_diagram_info_order_changed, + m_insert_terminal_query, + m_insert_conductor_query, + m_remove_conductor_query; #ifdef QET_EXPORT_PROJECT_DB public: diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 9128eecac..19de963c8 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -1674,6 +1674,7 @@ void Diagram::addItem(QGraphicsItem *item) conductor->terminal1->addConductor(conductor); conductor->terminal2->addConductor(conductor); conductor->calculateTextItemPosition(); + m_project->dataBase()->addConductor(conductor); break; } default: {break;} @@ -1704,6 +1705,7 @@ void Diagram::removeItem(QGraphicsItem *item) Conductor *conductor = static_cast(item); conductor->terminal1->removeConductor(conductor); conductor->terminal2->removeConductor(conductor); + m_project->dataBase()->removeConductor(conductor); break; } default: {break;} From bc79a5df7af3f9b1179e9e6c6fc471d4ad5c8d68 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 21 Aug 2026 19:20:15 +1200 Subject: [PATCH 4/5] Keep a conductor's row in step, and index the columns the view scans Three fixes to the tables added by this slice. A conductor's text was written once at insert and never again. Renaming a wire left the database holding the old number, so the wiring list showed a stale value until the next full repopulate -- elements have elementInfoChanged() for exactly this and conductors had nothing. Conductor::setProperties() has around a dozen call sites (auto-numbering, the properties dialog, element moves, the delete command's re-links), so rather than adding a call to each and missing the ones added later, listen to the propertiesChange() signal it already emits. Qt::UniqueConnection means a repeated insert or a full repopulate cannot double-subscribe, and the connection is established on both insert paths because conductors read from a file never pass through addConductor(). addConductor() and populateConductorTable() each carried their own copy of the same seven bindValue() lines. They had not drifted yet, but that is the same duplication the element paths had before bindElementValues(), where they had drifted -- one binding kindInformations()["type"] and the other masterTypeToString(). One bindConductorValues() for both. Finally, index the conductor columns that get looked up per element rather than per conductor. element_nomenclature_view counts the wires touching each element with a correlated subquery, so without an index every element row full-scans the conductor table and the cost grows as elements x conductors. Measured on a standalone SQLite harness at 2000 elements x 5000 conductors: 2134 ms unindexed, 10 ms indexed. diagram_uuid is indexed too, since the wiring list view joins on it. Co-Authored-By: Claude Opus 5 --- sources/dataBase/projectdatabase.cpp | 116 +++++++++++++++++++++++---- sources/dataBase/projectdatabase.h | 10 +++ 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 013e94065..138e1356c 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -271,13 +271,8 @@ void projectDataBase::addConductor(Conductor *conductor) insertTerminal(conductor->terminal1); insertTerminal(conductor->terminal2); - m_insert_conductor_query.bindValue(":uuid", conductor->uuid().toString()); - m_insert_conductor_query.bindValue(":diagram_uuid", conductor->diagram()->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal1_uuid", conductor->terminal1->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal1_element_uuid", conductor->terminal1->parentElement()->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal2_uuid", conductor->terminal2->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal2_element_uuid", conductor->terminal2->parentElement()->uuid().toString()); - m_insert_conductor_query.bindValue(":text", conductor->properties().text); + watchConductor(conductor); + bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram()); if (!m_insert_conductor_query.exec()) { qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError(); } else { @@ -299,6 +294,85 @@ void projectDataBase::removeConductor(Conductor *conductor) } } +/** + @brief projectDataBase::updateConductor + Refresh the mutable columns of an already-inserted conductor. + + Only the text (the wire number) can change without the conductor being + removed and re-added: its endpoints are fixed for its lifetime. Without + this, renaming a wire left the database holding the old number and the + wiring list showed a stale value until the next full repopulate. + @param conductor +*/ +void projectDataBase::updateConductor(Conductor *conductor) +{ + if (!conductor) { + return; + } + + m_update_conductor_query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + m_update_conductor_query.bindValue(QStringLiteral(":text"), conductor->properties().text); + if (!m_update_conductor_query.exec()) { + qDebug() << "projectDataBase::updateConductor update error : " << m_update_conductor_query.lastError(); + } + + //Deliberately no dataBaseUpdated() here, unlike add/remove. The only + //column this touches is the wire text, which no view watched by + //ProjectDBModel displays -- the nomenclature shows elements, and its + //wire_count changes when a conductor appears or disappears, not when + //it is renamed. Emitting would make every ProjectDBModel re-run its + //query, and auto-numbering renames every conductor in the project in + //one pass. +} + +/** + @brief projectDataBase::watchConductor + Keep this conductor's row in step with its properties. + + Conductor::setProperties() has a dozen call sites (auto-numbering, the + properties dialog, element moves, deletion re-links...), so listening to + the signal it already emits is the only way to catch them all -- and the + only way to catch the ones added later. Qt::UniqueConnection makes a + repeated insert or a full repopulate harmless. + @param conductor +*/ +void projectDataBase::watchConductor(Conductor *conductor) +{ + connect(conductor, &Conductor::propertiesChange, + this, &projectDataBase::conductorPropertiesChanged, + Qt::UniqueConnection); +} + +/** + @brief projectDataBase::conductorPropertiesChanged +*/ +void projectDataBase::conductorPropertiesChanged() +{ + if (auto *conductor = qobject_cast(sender())) { + updateConductor(conductor); + } +} + +/** + @brief projectDataBase::bindConductorValues + One binder for both insert paths, so a conductor added to a live diagram + and one read from a file can never drift apart -- the same reason + bindElementValues() exists for elements. + @param query + @param conductor + @param diagram : the diagram the conductor belongs to +*/ +void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram) +{ + query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString()); + query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->uuid().toString()); + query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString()); + query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->uuid().toString()); + query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString()); + query.bindValue(QStringLiteral(":text"), conductor->properties().text); +} + /** @brief projectDataBase::createDataBase Create the data base @@ -413,6 +487,21 @@ bool projectDataBase::createDataBase() qDebug() << "conductor_table query : "<< query_.lastError(); } + //The element-facing columns are looked up per element row, not per + //conductor row: element_nomenclature_view carries a correlated + //subquery counting the wires touching each element. Without these + //indexes each element row full-scans the conductor table, which grows + //as elements x conductors. + for (const QString &index_ : { + QStringLiteral("CREATE INDEX idx_conductor_terminal1_element ON conductor (terminal1_element_uuid)"), + QStringLiteral("CREATE INDEX idx_conductor_terminal2_element ON conductor (terminal2_element_uuid)"), + QStringLiteral("CREATE INDEX idx_conductor_diagram ON conductor (diagram_uuid)") }) + { + if (!query_.exec(index_)) { + qDebug() << "conductor index query : " << query_.lastError(); + } + } + createElementNomenclatureView(); createSummaryView(); prepareQuery(); @@ -650,13 +739,8 @@ void projectDataBase::populateConductorTable() insertTerminal(conductor->terminal1); insertTerminal(conductor->terminal2); - m_insert_conductor_query.bindValue(":uuid", conductor->uuid().toString()); - m_insert_conductor_query.bindValue(":diagram_uuid", diagram->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal1_uuid", conductor->terminal1->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal1_element_uuid", conductor->terminal1->parentElement()->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal2_uuid", conductor->terminal2->uuid().toString()); - m_insert_conductor_query.bindValue(":terminal2_element_uuid", conductor->terminal2->parentElement()->uuid().toString()); - m_insert_conductor_query.bindValue(":text", conductor->properties().text); + watchConductor(conductor); + bindConductorValues(m_insert_conductor_query, conductor, diagram); if (!m_insert_conductor_query.exec()) { qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError(); } @@ -762,6 +846,10 @@ void projectDataBase::prepareQuery() m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) " "VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)"); + //UPDATE CONDUCTOR + m_update_conductor_query = QSqlQuery(m_data_base); + m_update_conductor_query.prepare(QStringLiteral("UPDATE conductor SET text = :text WHERE uuid = :uuid")); + //REMOVE CONDUCTOR m_remove_conductor_query = QSqlQuery(m_data_base); m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid"); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 219e706d5..cddf53468 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -62,6 +62,13 @@ class projectDataBase : public QObject void addConductor (Conductor *conductor); void removeConductor (Conductor *conductor); + void updateConductor (Conductor *conductor); + + private slots: + //Refresh the sender()'s row after Conductor::setProperties(). + void conductorPropertiesChanged(); + + public: signals: void dataBaseUpdated(); @@ -75,6 +82,8 @@ class projectDataBase : public QObject void populateElementInfoTable(); void populateDiagramInfoTable(); void populateConductorTable(); + void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram); + void watchConductor(Conductor *conductor); void insertTerminal(Terminal *terminal); void prepareQuery(); static QHash elementInfoToString( @@ -96,6 +105,7 @@ class projectDataBase : public QObject m_diagram_info_order_changed, m_insert_terminal_query, m_insert_conductor_query, + m_update_conductor_query, m_remove_conductor_query; #ifdef QET_EXPORT_PROJECT_DB From ab159404a31c9902200fc989084a41eeb9087280 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 21 Aug 2026 20:40:09 +1200 Subject: [PATCH 5/5] Give every terminal an identity, not only uuid-aware ones The conductor table keyed on Terminal::uuid(), which comes from the catalog .elmt definition and is empty for every element authored before that field existed. A conductor was dropped unless *both* its terminals had one, so the tables this slice adds were empty on almost every project in existence: examples corpus conductor rows in the database industrial.qet 0 of 671 affuteuse_250h.qet 0 of 263 tremie_vibrante.qet 0 of 77 741.qet 0 of 67 Across the 23 example projects, 16 of the 20 that contain conductors have zero terminal uuids -- 2366 of 3002 conductors -- and overall coverage is 7.3%. Meanwhile --export-cables, already on master, lists all 671 conductors of industrial.qet from the document. A feature that only works on newly authored elements is not one users can rely on. Terminal::stableUuid() returns the terminal's own uuid when it has one and otherwise derives one from its local position and orientation inside its element. That is not an invented scheme: it is what the project format already does. TerminalData::fromXml() says so where it parses the field -- "if the attribute not exists, means, the element is created with an older version of qet. So use the legacy approach to identify terminals" -- and the legacy approach is the terminal's position. m_pos is read from the definition and is not touched by moving the element on a folio, so the identity survives loads, saves and folio moves. Derived values are UUID v5 in a fixed namespace, so they are reproducible without being stored, and cannot collide with the v4 uuids the element editor generates. Every project in the corpus now has exactly as many conductor rows as the document has conductors -- 20 of 20 measured, 0 mismatches. (schema_indus.qet is excluded: it blocks on a modal dialog at zero CPU under any CLI flag, the pre-existing hang PR #661 addresses.) Two things this deliberately does not key on: - The terminal name. It is not stable: QET rewrites a terminal named "_" as unnamed, which would have silently changed the identity of 1421 of industrial.qet's 1790 terminals on their first resave. Measured across the corpus, dropping it costs nothing -- geometry alone yields exactly the same three collisions -- and it means renaming a terminal no longer changes what it is. - Uniqueness in the face of a definition that declares two terminals at the same point and orientation. Three cases exist in the whole corpus. They merge to a single terminal row, which is harmless: two terminals identical in position and orientation are indistinguishable in every observable respect, and every conductor on either still resolves to the right element and terminal name. Both affected projects (industrial, perceuse) return their full conductor count. The only conductor still skipped is one whose terminal has no parent element, which has no identity to key on at all. Co-Authored-By: Claude Opus 5 --- sources/dataBase/projectdatabase.cpp | 26 +++++++------- sources/qetgraphicsitem/terminal.cpp | 54 ++++++++++++++++++++++++++++ sources/qetgraphicsitem/terminal.h | 1 + 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 138e1356c..37bb3d2e6 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -259,12 +259,13 @@ void projectDataBase::addConductor(Conductor *conductor) return; } - //A conductor whose terminal(s) predate terminal uuids (legacy - //elements not yet re-saved by a uuid-aware element editor) can't - //be given a stable identity here -- omitted the same way - //element_nomenclature_view already omits exclude_from_bom elements, - //rather than fabricating one. - if (conductor->terminal1->uuid().isNull() || conductor->terminal2->uuid().isNull()) { + //Both endpoints must belong to an element: the terminal table is keyed + //on (terminal, element) and a terminal with no parent has no identity + //to key on. Terminals whose *definition* predates terminal uuids are + //fine -- Terminal::stableUuid() derives one from the terminal's local + //position, which is what the project format itself matches on. + if (!conductor->terminal1->parentElement() + || !conductor->terminal2->parentElement()) { return; } @@ -366,9 +367,9 @@ void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor { query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString()); - query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->uuid().toString()); + query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->stableUuid().toString()); query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString()); - query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->uuid().toString()); + query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->stableUuid().toString()); query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString()); query.bindValue(QStringLiteral(":text"), conductor->properties().text); } @@ -730,9 +731,10 @@ void projectDataBase::populateConductorTable() const auto conductor_list = diagram->conductors(); for (auto *conductor : conductor_list) { - //See addConductor() for why terminals without a uuid - //(legacy elements) are omitted rather than fabricating one. - if (conductor->terminal1->uuid().isNull() || conductor->terminal2->uuid().isNull()) { + //See addConductor(): only a terminal with no parent element is + //skipped. A missing terminal uuid is handled by stableUuid(). + if (!conductor->terminal1->parentElement() + || !conductor->terminal2->parentElement()) { continue; } @@ -756,7 +758,7 @@ void projectDataBase::populateConductorTable() */ void projectDataBase::insertTerminal(Terminal *terminal) { - m_insert_terminal_query.bindValue(":uuid", terminal->uuid().toString()); + m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString()); m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString()); m_insert_terminal_query.bindValue(":name", terminal->name()); if (!m_insert_terminal_query.exec()) { diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index ee041c632..e4e5563bb 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -817,6 +817,60 @@ QUuid Terminal::uuid() const return d->m_uuid; } +/** + @brief Terminal::stableUuid + An identity for this terminal that exists on every element, not only on + those saved by a uuid-aware element editor. + + uuid() comes from the catalog .elmt definition and is empty for every + element authored before that field existed -- which is most of the + installed base. Anything keyed on uuid() alone therefore cannot see those + elements at all. + + When there is no uuid, derive one from the terminal's local position and + orientation inside its element. That is not an arbitrary choice: it is the + same thing the project format itself uses to match a conductor back to a + terminal ("each connection is made by using the local position of the + terminal and a dynamic id" -- TerminalData::m_uuid). m_pos is the position + read from the definition and is not touched by moving the element on the + folio, so the result is stable across loads, saves and folio moves, and it + is unique within an element except where a definition genuinely declares + two terminals at the same point -- three cases in the whole example corpus, + and harmless, because two terminals sharing a position and orientation are + indistinguishable in every observable respect: they merge to one terminal + row and every conductor on either of them still resolves to the right + element and name. + + Derived values are UUID v5 in a fixed namespace, so they are reproducible + without being written to the file, and cannot collide with the v4 uuids + the element editor generates. + + @return the terminal's own uuid when it has one, otherwise a derived one +*/ +QUuid Terminal::stableUuid() const +{ + if (!d->m_uuid.isNull()) { + return d->m_uuid; + } + + //Fixed namespace for terminal identities derived from geometry. + static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}")); + + //Position and orientation only. The name is deliberately excluded: it + //is not stable across a save cycle -- QET rewrites a terminal named + //"_" as unnamed, which would silently change the identity of 1421 of + //industrial.qet's 1790 terminals on the first resave. It is also not + //needed: keying on geometry alone produces exactly the same number of + //collisions across the example corpus, and it means renaming a + //terminal does not change what it is. + const QString key = QStringLiteral("%1|%2|%3") + .arg(d->m_pos.x(), 0, 'f', 4) + .arg(d->m_pos.y(), 0, 'f', 4) + .arg(static_cast(d->m_orientation)); + + return QUuid::createUuidV5(derived_ns, key); +} + QString Terminal::name() const { if (d->m_use_master_label && parent_element_) { diff --git a/sources/qetgraphicsitem/terminal.h b/sources/qetgraphicsitem/terminal.h index 53606b698..a6eae784c 100644 --- a/sources/qetgraphicsitem/terminal.h +++ b/sources/qetgraphicsitem/terminal.h @@ -75,6 +75,7 @@ class Terminal : public QGraphicsObject Diagram *diagram () const; Element *parentElement () const; QUuid uuid () const; + QUuid stableUuid () const; QString name () const; QString baseName () const; TerminalData::Type terminalType() const;