Merge pull request #628 from ispyisail/feature-wiring-db-tables

Add terminal and conductor tables to projectDataBase (discussion #503, slice 2)
This commit is contained in:
Laurent Trinques
2026-08-21 14:03:02 +02:00
committed by GitHub
9 changed files with 360 additions and 1 deletions
+249
View File
@@ -21,7 +21,9 @@
#include "../diagramposition.h" #include "../diagramposition.h"
#include "../elementprovider.h" #include "../elementprovider.h"
#include "../qetapp.h" #include "../qetapp.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h" #include "../qetinformation.h"
#include "../qetproject.h" #include "../qetproject.h"
@@ -87,6 +89,7 @@ void projectDataBase::updateDB()
populateDiagramInfoTable(); populateDiagramInfoTable();
populateElementTable(); populateElementTable();
populateElementInfoTable(); populateElementInfoTable();
populateConductorTable();
emit dataBaseUpdated(); emit dataBaseUpdated();
} }
@@ -245,6 +248,132 @@ 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;
}
//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;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
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 {
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::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<Conductor *>(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->stableUuid().toString());
query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->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);
}
/** /**
@brief projectDataBase::createDataBase @brief projectDataBase::createDataBase
Create the data base Create the data base
@@ -323,6 +452,57 @@ bool projectDataBase::createDataBase()
qDebug() << " element_info_table query : " << query_.lastError(); 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();
}
//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(); createElementNomenclatureView();
createSummaryView(); createSummaryView();
prepareQuery(); prepareQuery();
@@ -534,6 +714,58 @@ 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(): 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;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
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();
}
}
}
}
/**
@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->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()) {
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
}
}
void projectDataBase::prepareQuery() void projectDataBase::prepareQuery()
{ {
//INSERT DIAGRAM //INSERT DIAGRAM
@@ -606,6 +838,23 @@ void projectDataBase::prepareQuery()
update_str.append(" WHERE element_uuid = :uuid"); update_str.append(" WHERE element_uuid = :uuid");
m_update_element_query = QSqlQuery(m_data_base); m_update_element_query = QSqlQuery(m_data_base);
m_update_element_query.prepare(update_str); 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)");
//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");
} }
/** /**
+21 -1
View File
@@ -27,6 +27,8 @@
class Element; class Element;
class QETProject; class QETProject;
class Diagram; class Diagram;
class Conductor;
class Terminal;
class sqlite3; class sqlite3;
/** /**
@@ -58,6 +60,16 @@ class projectDataBase : public QObject
void diagramInfoChanged (Diagram *diagram); void diagramInfoChanged (Diagram *diagram);
void diagramOrderChanged(); void diagramOrderChanged();
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: signals:
void dataBaseUpdated(); void dataBaseUpdated();
@@ -69,6 +81,10 @@ class projectDataBase : public QObject
void populateElementTable(); void populateElementTable();
void populateElementInfoTable(); void populateElementInfoTable();
void populateDiagramInfoTable(); void populateDiagramInfoTable();
void populateConductorTable();
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
void watchConductor(Conductor *conductor);
void insertTerminal(Terminal *terminal);
void prepareQuery(); void prepareQuery();
static QHash<QString, QString> elementInfoToString( static QHash<QString, QString> elementInfoToString(
Element *elmt); Element *elmt);
@@ -86,7 +102,11 @@ class projectDataBase : public QObject
m_insert_diagram_info_query, m_insert_diagram_info_query,
m_update_diagram_info_query, m_update_diagram_info_query,
m_diagram_order_changed, m_diagram_order_changed,
m_diagram_info_order_changed; 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 #ifdef QET_EXPORT_PROJECT_DB
public: public:
+2
View File
@@ -1666,6 +1666,7 @@ void Diagram::addItem(QGraphicsItem *item)
conductor->terminal1->addConductor(conductor); conductor->terminal1->addConductor(conductor);
conductor->terminal2->addConductor(conductor); conductor->terminal2->addConductor(conductor);
conductor->calculateTextItemPosition(); conductor->calculateTextItemPosition();
m_project->dataBase()->addConductor(conductor);
break; break;
} }
default: {break;} default: {break;}
@@ -1696,6 +1697,7 @@ void Diagram::removeItem(QGraphicsItem *item)
Conductor *conductor = static_cast<Conductor *>(item); Conductor *conductor = static_cast<Conductor *>(item);
conductor->terminal1->removeConductor(conductor); conductor->terminal1->removeConductor(conductor);
conductor->terminal2->removeConductor(conductor); conductor->terminal2->removeConductor(conductor);
m_project->dataBase()->removeConductor(conductor);
break; break;
} }
default: {break;} default: {break;}
+7
View File
@@ -82,6 +82,13 @@ void PasteDiagramCommand::redo()
{ {
first_redo = false; first_redo = false;
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> 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 //this is the first paste, we do some actions for the new element
const QList <Element *> elmts_list = content.m_elements; const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list) for (Element *e : elmts_list)
+8
View File
@@ -17,6 +17,7 @@
*/ */
#include "elementspanelwidget.h" #include "elementspanelwidget.h"
#include "diagram.h" #include "diagram.h"
#include "qetgraphicsitem/conductor.h"
#include "editor/ui/qetelementeditor.h" #include "editor/ui/qetelementeditor.h"
#include "elementscategoryeditor.h" #include "elementscategoryeditor.h"
#include "qetapp.h" #include "qetapp.h"
@@ -684,6 +685,13 @@ void ElementsPanelWidget::duplicateDiagram()
elmt->newUuid(); elmt->newUuid();
new_diagram->restoreText(elmt); new_diagram->restoreText(elmt);
} }
else if (Conductor *cond = dynamic_cast<Conductor *>(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();
}
} }
} }
+14
View File
@@ -91,6 +91,7 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) :
//set Zvalue at 11 to be upper than the DiagramImageItem and element //set Zvalue at 11 to be upper than the DiagramImageItem and element
setZValue(11); setZValue(11);
m_previous_z_value = zValue(); m_previous_z_value = zValue();
m_uuid = QUuid::createUuid();
//Add this conductor to the list of conductors of each of the two terminals //Add this conductor to the list of conductors of each of the two terminals
bool ajout_p1 = terminal1 -> addConductor(this); bool ajout_p1 = terminal1 -> addConductor(this);
@@ -1005,6 +1006,18 @@ void Conductor::pointsToSegments(const QList<QPointF>& points_list) {
*/ */
bool Conductor::fromXml(QDomElement &dom_element) 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(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(), setPos(dom_element.attribute("x", nullptr).toDouble(),
dom_element.attribute("y", nullptr).toDouble()); dom_element.attribute("y", nullptr).toDouble());
@@ -1043,6 +1056,7 @@ QDomElement Conductor::toXml(QDomDocument &dom_document,
{ {
QDomElement dom_element = dom_document.createElement("conductor"); 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("x", QString::number(pos().x()));
dom_element.setAttribute("y", QString::number(pos().y())); dom_element.setAttribute("y", QString::number(pos().y()));
+4
View File
@@ -21,6 +21,7 @@
#include "../conductorproperties.h" #include "../conductorproperties.h"
#include <QGraphicsPathItem> #include <QGraphicsPathItem>
#include <QUuid>
class ConductorProfile; class ConductorProfile;
class ConductorSegmentProfile; class ConductorSegmentProfile;
@@ -77,6 +78,8 @@ class Conductor : public QGraphicsObject
int type() const override { return Type; } int type() const override { return Type; }
Diagram *diagram() const; Diagram *diagram() const;
ConductorTextItem *textItem() 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()); void updatePath(const QRectF & = QRectF());
//This method do nothing, it's only made to be used with Q_PROPERTY //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_; Highlight must_highlight_;
bool m_valid; bool m_valid;
bool m_freeze_label = false; bool m_freeze_label = false;
QUuid m_uuid;
/// QPen et QBrush objects used to draw conductors /// QPen et QBrush objects used to draw conductors
static QPen conductor_pen; static QPen conductor_pen;
+54
View File
@@ -820,6 +820,60 @@ QUuid Terminal::uuid() const
return d->m_uuid; 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<int>(d->m_orientation));
return QUuid::createUuidV5(derived_ns, key);
}
QString Terminal::name() const QString Terminal::name() const
{ {
if (d->m_use_master_label && parent_element_) { if (d->m_use_master_label && parent_element_) {
+1
View File
@@ -75,6 +75,7 @@ class Terminal : public QGraphicsObject
Diagram *diagram () const; Diagram *diagram () const;
Element *parentElement () const; Element *parentElement () const;
QUuid uuid () const; QUuid uuid () const;
QUuid stableUuid () const;
QString name () const; QString name () const;
QString baseName () const; QString baseName () const;
TerminalData::Type terminalType() const; TerminalData::Type terminalType() const;