diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 8afe0ed55..15a10ce7b 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -121,6 +121,7 @@ void projectDataBase::updateDB() if (!m_content_changed) { flushDrawingItems(); + flushLinks(); emit dataBaseUpdated(); return; } @@ -130,6 +131,7 @@ void projectDataBase::updateDB() populateElementTable(); populateElementInfoTable(); populateConductorTable(); + populateLinkTable(); populateDrawingItemTables(); m_content_changed = false; @@ -140,8 +142,8 @@ void projectDataBase::updateDB() @brief projectDataBase::updateDB updateDB() for a project just read from @p document. - The diagram, diagram_info, element, element_info, terminal and conductor - tables are filled from the document itself when it carries everything + The diagram, diagram_info, element, element_info, terminal, conductor + and link tables are filled from the document itself when it carries everything they need -- see populateFromDocument() -- and from the built folios otherwise, as updateDB() does. Shapes, texts and pictures always come from the built folios: their boxes need the fonts and pens a folio renders with. @@ -229,6 +231,7 @@ struct DocumentElement DiagramContext informations; QString label; QHash terminals; + QList> links; //linked uuid, group index }; //The sequential values an element or a conductor was saved with, as @@ -256,8 +259,8 @@ struct DocumentConductor /** @brief projectDataBase::populateFromDocument - Fill the diagram, diagram_info, element, element_info, terminal and - conductor tables from @p document, the project as read from its file, + Fill the diagram, diagram_info, element, element_info, terminal, + conductor and link tables from @p document, the project as read from its file, with the same values the built folios give -- using the same code: a BorderTitleBlock read from each folio's XML gives the title-block values and each element's grid cell, the project's embedded collection @@ -270,8 +273,10 @@ struct DocumentConductor a folio number uses %autonum, a conductor ends on a terminal that shows its master's contact label, an element's definition is missing or not one the folios could build, two elements on a folio number their - terminals alike, or sequential numbers are saved as the attributes - older files carry. A file saved by a current QElectroTech carries + terminals alike, sequential numbers are saved as the attributes + older files carry, or a link is listed by one side only, joins kinds + that cannot link, or gives a contact or a folio report a second + partner. A file saved by a current QElectroTech carries everything else. A label or a conductor text made from a formula is worked out again, @@ -491,6 +496,13 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString element.label = autonum::AssignVariables::formulaToLabel(formula, sequence, context); } + for (const QDomElement &link : QET::findInDomElement( + element_xml, QStringLiteral("links_uuids"), QStringLiteral("link_uuid"))) { + element.links << qMakePair(QUuid(link.attribute(QStringLiteral("uuid"))).toString(), + link.attribute(QStringLiteral("group_index"), + QStringLiteral("-1")).toInt()); + } + on_this_folio.insert(uuid, int(elements.size())); elements << element; } @@ -585,13 +597,58 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString diagram_dates << border->date(); } + //The links the folios make (Element::initLink()): a link to an + //element the project does not have is dropped there too. Anything + //else they would resolve one way or another -- a link only one side + //lists, between kinds that cannot link, or a second coil for a + //contact -- is left to them. + QHash element_index; + for (int i = 0 ; i < elements.size() ; ++i) { + element_index.insert(elements.at(i).uuid, i); + } + auto canLink = [](const QString &a, const QString &b) { + return (a == QLatin1String("master") && b == QLatin1String("slave")) + || (a == QLatin1String("slave") && b == QLatin1String("master")) + || (a == QLatin1String("next_report") && b == QLatin1String("previous_report")) + || (a == QLatin1String("previous_report") && b == QLatin1String("next_report")); + }; + for (DocumentElement &element : elements) + { + QList> kept; + QSet seen; + for (const auto &link : std::as_const(element.links)) + { + const int other = element_index.value(link.first, -1); + if (other < 0) { + continue; + } + const DocumentElement &partner = elements.at(other); + bool listed_back = false; + for (const auto &back : partner.links) { + listed_back |= back.first == element.uuid; + } + if (seen.contains(link.first) || !listed_back + || !canLink(element.type, partner.type)) { + return refuse(QStringLiteral("a link is not one the folios would make as saved")); + } + seen.insert(link.first); + kept << link; + } + if (kept.size() > 1 && element.type != QLatin1String("master")) { + return refuse(QStringLiteral("a link is not one the folios would make as saved")); + } + element.links = kept; + } + //Everything could be read: write it. QSqlQuery query(m_data_base); for (const QString &table : {QStringLiteral("diagram"), QStringLiteral("diagram_info"), QStringLiteral("element"), QStringLiteral("element_info"), - QStringLiteral("conductor"), QStringLiteral("terminal")}) { + QStringLiteral("conductor"), QStringLiteral("terminal"), + QStringLiteral("link")}) { query.exec(QStringLiteral("DELETE FROM ") + table); } + m_dirty_link_elements.clear(); for (int i = 0 ; i < diagram_uuids.size() ; ++i) { @@ -625,10 +682,19 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } } - QHash element_index; - for (int i = 0 ; i < elements.size() ; ++i) { - element_index.insert(elements.at(i).uuid, i); + query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " + "VALUES (:element_uuid, :linked_uuid, :group_index)")); + for (const DocumentElement &element : std::as_const(elements)) { + for (const auto &link : element.links) { + query.bindValue(QStringLiteral(":element_uuid"), element.uuid); + query.bindValue(QStringLiteral(":linked_uuid"), link.first); + query.bindValue(QStringLiteral(":group_index"), link.second >= 0 ? QVariant(link.second) : QVariant()); + if (!query.exec()) { + qDebug() << "projectDataBase::populateFromDocument link insert error : " << query.lastError(); + } + } } + for (const DocumentConductor &conductor : std::as_const(conductors)) { for (const auto &end : {std::make_pair(conductor.element1, conductor.terminal1), @@ -758,7 +824,9 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) { //Drawing-item rows are rewritten lazily, see drawingItemChanged(). //Every read from outside comes through here, so this is the one //place the queue has to be emptied for a reader to see current rows. + //The same goes for link rows, see linksChanged(). flushDrawingItems(); + flushLinks(); // First gate: which kind of statement is acceptable here at all. A // textual check is the right tool for that and the wrong tool for @@ -850,6 +918,8 @@ void projectDataBase::addElement(Element *element) if (!m_insert_elements_query.exec()) { qDebug() << "projectDataBase::addElement insert element error : " << m_insert_elements_query.lastError(); } + connect(element, &Element::linkedElementChanged, + this, &projectDataBase::linksChanged, Qt::UniqueConnection); bindElementInfoValues(m_insert_element_info_query, element); if (!m_insert_element_info_query.exec()) { @@ -882,6 +952,13 @@ void projectDataBase::removeElement(Element *element) qDebug() << "projectDataBase::removeElement remove element_info error : " << m_remove_element_info_query.lastError(); } + QSqlQuery remove_links(m_data_base); + remove_links.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid OR linked_uuid = :uuid")); + remove_links.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); + if (!remove_links.exec()) { + qDebug() << "projectDataBase::removeElement remove link error : " << remove_links.lastError(); + } + if (changed) { emit dataBaseUpdated(); } @@ -972,6 +1049,18 @@ void projectDataBase::removeDiagram(Diagram *diagram) //element/terminal/element_info/conductor rows survive. m_data_base.transaction(); + QSqlQuery cascade_links(m_data_base); + cascade_links.prepare(QStringLiteral( + "DELETE FROM link WHERE element_uuid IN (SELECT uuid FROM element WHERE diagram_uuid = :uuid) " + "OR linked_uuid IN (SELECT uuid FROM element WHERE diagram_uuid = :uuid)")); + cascade_links.bindValue(QStringLiteral(":uuid"), uuid_str); + if (!cascade_links.exec()) { + qDebug() << "projectDataBase::removeDiagram link cascade error : " + << cascade_links.lastError(); + m_data_base.rollback(); + return; + } + m_cascade_remove_element_info_query.bindValue(":uuid", uuid_str); if (!m_cascade_remove_element_info_query.exec()) { qDebug() << "projectDataBase::removeDiagram element_info cascade error : " @@ -1605,6 +1694,20 @@ bool projectDataBase::createDataBase() qDebug() << " element_info_table query : " << query_.lastError(); } + //Create the link table: one row per element and element it is + //linked to -- a coil and its contacts, a pair of folio reports -- + //from each side, with the contact group the element saved for it. + const QString link_table(QStringLiteral( + "CREATE TABLE link (" + "element_uuid VARCHAR(50) NOT NULL, " + "linked_uuid VARCHAR(50) NOT NULL, " + "group_index INTEGER, " + "PRIMARY KEY (element_uuid, linked_uuid), " + "FOREIGN KEY (element_uuid) REFERENCES element (uuid))")); + if (!query_.exec(link_table)) { + qDebug() << " link_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 @@ -2001,6 +2104,96 @@ void projectDataBase::populateElementInfoTable() } } +/** + @brief projectDataBase::populateLinkTable + Populate the link table from the built folios +*/ +void projectDataBase::populateLinkTable() +{ + m_dirty_link_elements.clear(); + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("DELETE FROM link")); + query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " + "VALUES (:element_uuid, :linked_uuid, :group_index)")); + + for (const auto &diagram : m_project->diagrams()) + { + const ElementProvider ep(diagram); + for (const auto &elmt : ep.find(allElementTypes())) + { + for (Element *linked : elmt->linkedElements()) + { + const int group = elmt->groupIndexForElement(linked); + query.bindValue(QStringLiteral(":element_uuid"), elmt->uuid().toString()); + query.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString()); + query.bindValue(QStringLiteral(":group_index"), group >= 0 ? QVariant(group) : QVariant()); + if (!query.exec()) { + qDebug() << "projectDataBase::populateLinkTable insert error : " << query.lastError(); + } + } + } + } +} + +/** + @brief projectDataBase::linksChanged + A link of the sender() was made or undone: its rows are written again + by the next flushLinks(). +*/ +void projectDataBase::linksChanged() +{ + m_content_changed = true; + auto *element = qobject_cast(sender()); + if (element && !m_dirty_link_elements.contains(element)) { + m_dirty_link_elements << element; + } +} + +/** + @brief projectDataBase::flushLinks + Write the link rows of every element queued by linksChanged() +*/ +void projectDataBase::flushLinks() +{ + if (m_dirty_link_elements.isEmpty()) { + return; + } + + const auto dirty = m_dirty_link_elements; + m_dirty_link_elements.clear(); + const bool own_transaction = m_data_base.transaction(); + QSqlQuery remove(m_data_base); + remove.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid")); + QSqlQuery insert(m_data_base); + insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " + "VALUES (:element_uuid, :linked_uuid, :group_index)")); + for (const QPointer &element : dirty) + { + if (!element) { + continue; + } + remove.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); + if (!remove.exec()) { + qDebug() << "projectDataBase::flushLinks remove error : " << remove.lastError(); + } + //An element taken off its folio is unlinked first (Diagram:: + //removeItem()), so it writes no rows here. + for (Element *linked : element->linkedElements()) + { + const int group = element->groupIndexForElement(linked); + insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); + insert.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString()); + insert.bindValue(QStringLiteral(":group_index"), group >= 0 ? QVariant(group) : QVariant()); + if (!insert.exec()) { + qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); + } + } + } + if (own_transaction) { + m_data_base.commit(); + } +} + void projectDataBase::populateDiagramInfoTable() { QSqlQuery query(m_data_base); @@ -2403,6 +2596,7 @@ void projectDataBase::exportDb(projectDataBase *db, // requiring access to the SQLite driver's native connection handle. const auto escaped_path = path_.replace("'", "''"); db->flushDrawingItems(); + db->flushLinks(); QSqlQuery query(db->m_data_base); if (!query.exec("VACUUM INTO '" % escaped_path % "'")) { qWarning() << "Unable to export project database:" << query.lastError().text(); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 1029fcc3d..b15cd9286 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -124,6 +124,8 @@ class projectDataBase : public QObject //Queue the sender()'s drawing-item row for rewriting. void drawingItemChanged(); void drawingItemDestroyed(QObject *object); + //Queue the sender()'s link rows for rewriting. + void linksChanged(); public: @@ -141,6 +143,8 @@ class projectDataBase : public QObject void populateElementInfoTable(); void populateDiagramInfoTable(); void populateConductorTable(); + void populateLinkTable(); + void flushLinks(); void populateDrawingItemTables(); bool populateFromDocument(const QDomDocument &document, QString *why = nullptr); bool writeDrawingItem(QObject *object); @@ -206,6 +210,10 @@ class projectDataBase : public QObject QHash m_drawing_item_row; QHash m_drawing_row_owner; QSet m_dirty_drawing_items; + //Elements whose links changed since their link rows were last + //written: a link is made first and its contact group set after, + //so the rows are written when next read, as drawing items are. + QList> m_dirty_link_elements; #ifdef QET_EXPORT_PROJECT_DB public: diff --git a/tests/qttest/tst_databasefromdocument.cpp b/tests/qttest/tst_databasefromdocument.cpp index d2febebf4..1660803d8 100644 --- a/tests/qttest/tst_databasefromdocument.cpp +++ b/tests/qttest/tst_databasefromdocument.cpp @@ -15,7 +15,7 @@ // what the same database filled from the built folios holds. Every example // is saved once (so that it carries the uuids a current QElectroTech // writes), then opened twice through the real binary's --run: once as is, -// once with QET_DATABASE_FROM_FOLIOS=1, and the six tables compared. +// once with QET_DATABASE_FROM_FOLIOS=1, and the seven tables compared. class tst_databasefromdocument : public QObject { Q_OBJECT @@ -50,7 +50,7 @@ class tst_databasefromdocument : public QObject return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); } - // The six tables, each as a sorted list of its rows, and which way the + // The seven tables, each as a sorted list of its rows, and which way the // database was filled. QJsonObject dump(const QString &project, bool from_folios, QString *how) { @@ -102,7 +102,7 @@ class tst_databasefromdocument : public QObject QVERIFY2(how_document == QLatin1String("Project database filled from the document"), qPrintable(how_document)); QVERIFY2(how_folios.contains(QStringLiteral("QET_DATABASE_FROM_FOLIOS")), qPrintable(how_folios)); - QCOMPARE(document.keys().size(), 6); + QCOMPARE(document.keys().size(), 7); for (const QString &table : folios.keys()) { const QJsonArray a = document.value(table).toArray(), b = folios.value(table).toArray(); QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows from the document, %3 from the folios") @@ -121,7 +121,7 @@ private slots: QFile js(m_dir.filePath(QStringLiteral("dump.js"))); QVERIFY(js.open(QIODevice::WriteOnly)); js.write("var out = {};\n" - "['diagram', 'diagram_info', 'element', 'element_info', 'terminal', 'conductor']" + "['diagram', 'diagram_info', 'element', 'element_info', 'terminal', 'conductor', 'link']" ".forEach(function (t) { out[t] = qet.query('SELECT * FROM ' + t); });\n" "qet.log('DUMP ' + JSON.stringify(out));\n"); } @@ -280,6 +280,71 @@ private slots: "two elements on a folio number their terminals alike")); } + // m_000.qet saved once, as a document to change + QDomDocument resavedWithLinks(const QString &saved) + { + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"), saved}); + QFile file(saved); + QDomDocument document; + if (file.open(QIODevice::ReadOnly)) + document.setContent(&file); + return document; + } + + // A coil and its contacts are in the link table, from each side, with + // both fills: m_000.qet's coil b02216df and its contact 998190fa. + void linksAreListed() + { + const QString saved = m_dir.filePath(QStringLiteral("links.qet")); + QDomDocument document = resavedWithLinks(saved); + const QString coil = QStringLiteral("{b02216df-0851-4732-893b-901fea80703e}"), + contact = QStringLiteral("{998190fa-5b4c-4be3-bc67-8a828eaab2b4}"); + //The coil puts this contact in its contact group 1. + bool grouped = false; + const QDomNodeList elements = document.elementsByTagName(QStringLiteral("element")); + for (int i = 0 ; i < elements.size() ; ++i) { + const QDomElement e = elements.at(i).toElement(); + if (e.attribute(QStringLiteral("uuid")) != coil) continue; + for (QDomElement l = e.firstChildElement(QStringLiteral("links_uuids")).firstChildElement(QStringLiteral("link_uuid")) ; + !l.isNull() ; l = l.nextSiblingElement(QStringLiteral("link_uuid"))) { + if (l.attribute(QStringLiteral("uuid")) == contact) { + l.setAttribute(QStringLiteral("group_index"), 1); + grouped = true; + } + } + } + QVERIFY(grouped); + QVERIFY(write(saved, document)); + compareBothWays(saved); + bool coil_side = false, contact_side = false; + for (const QJsonValue &row : m_last.value(QStringLiteral("link")).toArray()) { + const QJsonObject link = QJsonDocument::fromJson(row.toString().toUtf8()).object(); + const QString from = link.value(QStringLiteral("element_uuid")).toString(), + to = link.value(QStringLiteral("linked_uuid")).toString(); + coil_side |= from == coil && to == contact + && link.value(QStringLiteral("group_index")).toVariant().toInt() == 1; + contact_side |= from == contact && to == coil; + } + QVERIFY2(coil_side, "the coil's link to its contact, in group 1, is missing"); + QVERIFY2(contact_side, "the contact's link to its coil is missing"); + } + + // A link only one side lists: the folios decide what it becomes. + void oneSidedLinkFallsBack() + { + const QString saved = m_dir.filePath(QStringLiteral("onesided.qet")); + QDomDocument document = resavedWithLinks(saved); + const QDomNodeList links = document.elementsByTagName(QStringLiteral("link_uuid")); + QVERIFY(links.size() > 0); + QDomNode link = links.at(0); + link.parentNode().removeChild(link); + QVERIFY(write(saved, document)); + QString how; + dump(saved, false, &how); + QCOMPARE(how, QStringLiteral("Project database filled from the folios: " + "a link is not one the folios would make as saved")); + } + // A file whose items carry no saved uuid is filled from the folios, // which work those uuids out as they are built, and says why. void olderFileFallsBack()