Link a symbol built to its partners from the project database's rows

A symbol built after opening (lazy folios) links to the partners its
link rows name, and to the symbols whose rows name it; its file's list
only adds what the rows miss. The rows keep a partner on a folio not
built until it is built, in its saved place, so a symbol no longer keeps
a list of its own: the list read from the file is dropped once opening
has filled the database. pendingLinks(), waitingLinks(),
keepsSavedLinks(), linksToSave() and QETProject::waitingLinkTargets()
go; the callers that asked a symbol for its waiting partners ask the
database (waitingPartners()).

When its links change, a symbol's rows are written from its built
partners and the waiting partners its rows held, in their order
(rowsToWrite()), so a full rewrite of the table keeps them too.

A symbol built alone as a partner on a folio not built
(Diagram::buildSymbols()) counts as built: its rows are kept, it is not
waiting (waitingSymbols(), waitingPartners()), so saving does not build
its folio, and removing it does not remove the rows naming it. A row
naming a placed symbol that has not linked yet (its folio being built)
is kept too. At opening the database holds no link until its fill
(forgetLinks()); the symbols read their files' lists until then, and the
fill takes their order: a symbol's own rows, its list, then the rows
naming it. The symbols waiting on folios not built are cached and
updated as folios are built.

QET_CHECK_LINK_ROWS now checks the rows against the links: each built
partner in its order, no other row but to a folio not built.

tst_resaveunchanged::undoKeepsLinksToUnbuiltFolios: m_000 opened with
lazy folios, a contact, its folio or its coil's folio (not built)
removed, then undone: every link saved as before.

QETZ-DB-FIRST stage 2a-3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-18 22:22:57 +13:00
parent b473a07b7d
commit 9a41e01187
10 changed files with 474 additions and 183 deletions
+242 -40
View File
@@ -17,7 +17,6 @@
*/
#include "projectdatabase.h"
#include "../qetversion.h"
#include "sqlreadonly.h"
#include "../autoNum/assignvariables.h"
@@ -54,6 +53,7 @@
#include <QRegularExpression>
#include <QSqlDriver>
#include <QSqlError>
#include <optional>
@@ -861,6 +861,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
}
}
}
m_holds_links = true;
for (const DocumentConductor &conductor : std::as_const(conductors))
{
@@ -1647,8 +1648,13 @@ void projectDataBase::addElement(Element *element)
*/
void projectDataBase::removeElement(Element *element)
{
m_content_changed = true;
const QUuid uuid = element->uuid();
//A symbol built alone as a partner (Diagram::buildSymbols()) was
//never placed: its rows are those of the symbol its folio builds
if (!m_placed_elements.contains(uuid, element)) {
return;
}
m_content_changed = true;
removeElementRows(uuid);
unplaceElement(element, uuid);
//Another symbol still holds the uuid (F100): the rows are its own.
@@ -3872,8 +3878,16 @@ QString projectDataBase::prefillReport() const
void projectDataBase::populateLinkTable()
{
m_dirty_link_elements.clear();
const QSet<QUuid> waiting = waitingSymbols();
const QSet<QUuid> waiting_symbols = waitingSymbols();
const auto waiting = [&waiting_symbols](const QUuid &uuid) {return waiting_symbols.contains(uuid);};
QSqlQuery query(m_data_base);
//The rows as they are: the order they give, and the partners on
//folios not built, which no built symbol knows
QHash<QUuid, QList<std::pair<QUuid, int>>> old_rows;
if (query.exec(QStringLiteral("SELECT element_uuid, linked_uuid, group_index FROM link ORDER BY ord, rowid")))
while (query.next())
old_rows[QUuid(query.value(0).toString())].append(
{QUuid(query.value(1).toString()), query.value(2).isNull() ? -1 : query.value(2).toInt()});
query.exec(QStringLiteral("DELETE FROM link"));
query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) "
"VALUES (:element_uuid, :linked_uuid, :group_index, :ord)"));
@@ -3890,8 +3904,11 @@ void projectDataBase::populateLinkTable()
}
const ElementProvider ep(diagram);
for (const auto &elmt : ep.find(allElementTypes()))
insertLinkRows(query, elmt, waiting);
insertLinkRows(query, elmt, waiting,
old_rows.contains(elmt->uuid()) ? old_rows.value(elmt->uuid())
: elmt->savedLinkList());
}
m_holds_links = true;
}
/**
@@ -3919,8 +3936,9 @@ void projectDataBase::queueLinks(Element *element)
/**
@brief projectDataBase::linkRowMismatches
A test and debugging aid (QET_CHECK_LINK_ROWS): the placed
symbols whose link rows are not the links they save, in order
(Element::linksToSave(), insertLinkRows())
symbols whose link rows are not their links: each built partner, in
the order the symbol has them, and no other, but partners on folios
not built (QETZ-DB-FIRST stage 2a-3: the rows are what is saved)
@return one entry per such symbol: its uuid
*/
QStringList projectDataBase::linkRowMismatches()
@@ -3928,26 +3946,29 @@ QStringList projectDataBase::linkRowMismatches()
flushLinks();
const QSet<QUuid> waiting = waitingSymbols();
QStringList mismatches;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link "
"WHERE element_uuid = :uuid ORDER BY ord, rowid"));
for (auto it = m_placed_elements.cbegin() ; it != m_placed_elements.cend() ; ++it)
{
Element *element = it.value();
if (!element) {
continue;
}
QList<std::pair<QString, int>> expected;
for (const Element::SavedLink &link : element->linksToSave(waiting))
if (!link.built || m_placed_elements.contains(link.uuid, link.built))
expected.append({link.uuid.toString(), link.group_index});
QList<std::pair<QString, int>> rows;
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
if (query.exec())
while (query.next())
rows.append({query.value(0).toString(),
query.value(1).isNull() ? -1 : query.value(1).toInt()});
if (rows != expected) {
QList<std::pair<QUuid, int>> expected;
for (Element *linked : element->linkedElements())
if (m_placed_elements.contains(linked->uuid(), linked) || builtAlone(linked))
expected.append({linked->uuid(), element->groupIndexForElement(linked)});
QList<std::pair<QUuid, int>> rows;
bool stray = false;
for (const auto &row : linkRowsOf(element->uuid())) {
bool built = false;
for (const auto &link : std::as_const(expected))
built |= link.first == row.first;
if (built) {
rows.append(row);
} else {
stray |= !waiting.contains(row.first);
}
}
if (stray || rows != expected) {
mismatches << element->uuid().toString();
}
}
@@ -3989,36 +4010,133 @@ bool projectDataBase::savedLinks(const Element *element, QList<std::pair<QUuid,
/**
@brief projectDataBase::waitingSymbols
@return the symbols on folios not built yet (lazy folios): partners a
symbol keeps its saved links to until they are built
@return the symbols on folios not built yet (lazy folios) and not
built alone as partners either (Diagram::buildSymbols()): partners a
symbol keeps its rows to until they are built
*/
QSet<QUuid> projectDataBase::waitingSymbols() const
{
return m_project && m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios()
: QSet<QUuid>();
if (!m_project || !m_project->unloadedFolioCount()) {
return QSet<QUuid>();
}
//Kept while the same folios are not built and as many symbols are
//built alone: finding them reads every such folio's rows
QList<Diagram *> unbuilt;
int alone = 0;
for (Diagram *diagram : m_project->folios())
if (!diagram->isLoaded()) {
unbuilt << diagram;
alone += int(diagram->partialElements().size());
}
if (unbuilt == m_waiting_folios && alone == m_waiting_alone) {
return m_waiting_symbols;
}
//Folios only built since: their symbols are not waiting any more
bool only_built = true;
for (Diagram *diagram : std::as_const(unbuilt))
only_built &= m_waiting_folios.contains(diagram);
for (Diagram *diagram : std::as_const(m_waiting_folios))
if (!unbuilt.contains(diagram))
only_built &= m_project->folios().contains(diagram) && diagram->isLoaded();
QSet<QUuid> waiting;
if (only_built && !m_waiting_folios.isEmpty()) {
waiting = m_waiting_symbols;
for (Diagram *diagram : std::as_const(m_waiting_folios))
if (!unbuilt.contains(diagram))
for (Element *element : diagram->elements())
waiting.remove(element->uuid());
} else {
waiting = m_project->symbolsOnUnbuiltFolios();
}
for (Diagram *diagram : std::as_const(unbuilt))
for (const QUuid &built : diagram->partialElements().keys())
waiting.remove(built);
m_waiting_folios = unbuilt;
m_waiting_alone = alone;
m_waiting_symbols = waiting;
return waiting;
}
/**
@brief projectDataBase::builtAlone
@return true if @p element is a symbol built alone, as a partner, on a
folio of the project not built (Diagram::buildSymbols())
*/
bool projectDataBase::builtAlone(Element *element) const
{
Diagram *diagram = element->diagram();
return m_project && diagram && !diagram->isLoaded()
&& m_project->folios().contains(diagram)
&& diagram->partialElements().value(element->uuid()) == element;
}
/**
@brief projectDataBase::linksLater
@return true if a symbol of uuid @p uuid is placed but not linked from
its rows yet (Element::initLink(), as its folio is being built): it
links to the partners its rows and theirs name, so these are kept
*/
bool projectDataBase::linksLater(const QUuid &uuid) const
{
for (const QPointer<Element> &element : m_placed_elements.values(uuid))
if (element && !element->savedLinkList().isEmpty())
return true;
return false;
}
/**
@brief projectDataBase::rowsToWrite
@return the link rows of @p element: its built partners, and the
partners on folios not built (@p waiting) that @p old_rows, its rows as
they were, name, and those placed but not linked yet (linksLater()).
The rows keep the order of @p old_rows, the order the file gave, and
partners linked since come after them: the order the symbol has its
partners in changes with the order symbols are built.
A built partner the project no longer has (on a removed folio, linked
still for the undo) gets no row.
*/
QList<std::pair<QUuid, int>> projectDataBase::rowsToWrite(Element *element, const std::function<bool(const QUuid &)> &waiting,
const QList<std::pair<QUuid, int>> &old_rows) const
{
QList<Element *> built;
for (Element *linked : element->linkedElements())
if (m_placed_elements.contains(linked->uuid(), linked) || builtAlone(linked))
built << linked;
QList<std::pair<QUuid, int>> rows;
QSet<Element *> written;
for (const auto &row : old_rows) {
Element *linked = nullptr;
for (Element *partner : std::as_const(built))
if (partner->uuid() == row.first && !written.contains(partner)) { linked = partner; break; }
if (linked) {
written.insert(linked);
rows.append({linked->uuid(), element->groupIndexForElement(linked)});
} else if (waiting(row.first) || linksLater(row.first)) {
rows.append(row);
}
}
for (Element *partner : std::as_const(built))
if (!written.contains(partner))
rows.append({partner->uuid(), element->groupIndexForElement(partner)});
return rows;
}
/**
@brief projectDataBase::insertLinkRows
Write @p element's link rows with @p insert (prepared with :element_uuid,
:linked_uuid, :group_index and :ord): its partners as it saves them
(Element::linksToSave()), in that order, so the rows and the file say
the same: @p waiting is the symbols on folios not built, as a save
takes them (waitingSymbols()). A built partner the project no longer has (on a removed
folio, linked still for the undo) gets no row.
:linked_uuid, :group_index and :ord), as rowsToWrite() gives them, in
that order: @p waiting is the symbols on folios not built
(waitingSymbols()) tells which, @p old_rows its rows before.
*/
void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element,
const QSet<QUuid> &waiting)
void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element, const std::function<bool(const QUuid &)> &waiting,
const QList<std::pair<QUuid, int>> &old_rows)
{
int ord = 0;
for (const Element::SavedLink &link : element->linksToSave(waiting))
for (const auto &link : rowsToWrite(element, waiting, old_rows))
{
if (link.built && !m_placed_elements.contains(link.uuid, link.built)) {
continue;
}
insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString());
insert.bindValue(QStringLiteral(":linked_uuid"), link.uuid.toString());
insert.bindValue(QStringLiteral(":group_index"), link.group_index >= 0 ? QVariant(link.group_index) : QVariant());
insert.bindValue(QStringLiteral(":linked_uuid"), link.first.toString());
insert.bindValue(QStringLiteral(":group_index"), link.second >= 0 ? QVariant(link.second) : QVariant());
insert.bindValue(QStringLiteral(":ord"), ord++);
if (!insert.exec()) {
qDebug() << "projectDataBase::insertLinkRows insert error : " << insert.lastError();
@@ -4026,6 +4144,65 @@ void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element,
}
}
/**
@brief projectDataBase::linkRowsOf
@return the link rows of @p element: each partner and its contact group
(-1 if none), in their order
*/
QList<std::pair<QUuid, int>> projectDataBase::linkRowsOf(const QUuid &element) const
{
QList<std::pair<QUuid, int>> rows;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link "
"WHERE element_uuid = :uuid ORDER BY ord, rowid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec())
while (query.next())
rows.append({QUuid(query.value(0).toString()),
query.value(1).isNull() ? -1 : query.value(1).toInt()});
return rows;
}
/**
@brief projectDataBase::partnersToLink
@return the partners a symbol built links to (Element::initLink()): its
link rows, in their order, then the symbols whose rows name it (no
contact group: the group is the row owner's)
*/
QList<std::pair<QUuid, int>> projectDataBase::partnersToLink(const QUuid &element)
{
flushLinks();
QList<std::pair<QUuid, int>> partners = linkRowsOf(element);
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT element_uuid FROM link WHERE linked_uuid = :uuid ORDER BY rowid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec())
while (query.next())
partners.append({QUuid(query.value(0).toString()), -1});
return partners;
}
/**
@brief projectDataBase::waitingPartners
@return the partners of @p element its link rows name among @p waiting,
the symbols on folios not built (waitingSymbols()): those it is not
linked to yet
*/
QSet<QUuid> projectDataBase::waitingPartners(const QUuid &element, const QSet<QUuid> &waiting)
{
QSet<QUuid> partners;
if (waiting.isEmpty()) {
return partners;
}
flushLinks();
//A partner built alone (Diagram::buildSymbols()) is built: its
//folio, not built, still holds it
for (const auto &row : linkRowsOf(element))
if (waiting.contains(row.first) && waitingSymbols().contains(row.first))
partners.insert(row.first);
return partners;
}
/**
@brief projectDataBase::flushLinks
Write the link rows of every element queued by linksChanged()
@@ -4038,18 +4215,42 @@ void projectDataBase::flushLinks()
const auto dirty = m_dirty_link_elements;
m_dirty_link_elements.clear();
const QSet<QUuid> waiting = waitingSymbols();
//The symbols on folios not built, looked up only when a row names
//a partner not linked: finding them reads every such folio's rows
std::optional<QSet<QUuid>> waiting_symbols;
const auto waiting = [this, &waiting_symbols](const QUuid &uuid) {
if (!waiting_symbols) {
waiting_symbols = waitingSymbols();
}
return waiting_symbols->contains(uuid);
};
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, ord) "
"VALUES (:element_uuid, :linked_uuid, :group_index, :ord)"));
QList<QPointer<Element>> not_linked;
for (const QPointer<Element> &element : dirty)
{
if (!element) {
continue;
}
const bool placed = m_placed_elements.contains(element->uuid(), element);
//A symbol not linked from its rows yet (Element::initLink())
//keeps them as they are: a partner linking to it first has not
//given it its contact groups. Written once it is linked.
if (placed && m_holds_links && !element->savedLinkList().isEmpty()) {
not_linked << element;
continue;
}
QList<std::pair<QUuid, int>> old_rows;
if (placed) {
old_rows = linkRowsOf(element->uuid());
if (old_rows.isEmpty()) {
old_rows = element->savedLinkList();
}
}
remove.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
if (!remove.exec()) {
qDebug() << "projectDataBase::flushLinks remove error : " << remove.lastError();
@@ -4057,14 +4258,15 @@ void projectDataBase::flushLinks()
//An element taken off its folio is unlinked first (Diagram::
//removeItem()), so it writes no rows here; one on a removed
//folio is not, and writes none either.
if (!m_placed_elements.contains(element->uuid(), element)) {
if (!placed) {
continue;
}
insertLinkRows(insert, element, waiting);
insertLinkRows(insert, element, waiting, old_rows);
}
if (own_transaction) {
m_data_base.commit();
}
m_dirty_link_elements << not_linked;
}
void projectDataBase::populateDiagramInfoTable()
+21 -2
View File
@@ -18,6 +18,7 @@
#ifndef PROJECTDATABASE_H
#define PROJECTDATABASE_H
#include <functional>
#include <QObject>
#include <QSqlDatabase>
#include <QSqlQuery>
@@ -156,6 +157,14 @@ class projectDataBase : public QObject
QStringList conductorPropertiesMismatches() const;
QStringList linkRowMismatches();
bool savedLinks(const Element *element, QList<std::pair<QUuid, int>> *links);
//The link rows are written (opening filled them): a symbol
//built links from them (Element::initLink())
bool holdsLinks() const {return m_holds_links;}
void forgetLinks() {m_holds_links = false;}
QList<std::pair<QUuid, int>> partnersToLink(const QUuid &element);
QList<std::pair<QUuid, int>> linkRowsOf(const QUuid &element) const;
QSet<QUuid> waitingPartners(const QUuid &element, const QSet<QUuid> &waiting);
QSet<QUuid> waitingSymbols() const;
//Fill the three stores from a project's document before its
//folios are built, then, once
@@ -255,8 +264,12 @@ class projectDataBase : public QObject
void populateConductorTable();
void populateLinkTable();
void flushLinks();
void insertLinkRows(QSqlQuery &insert, Element *element, const QSet<QUuid> &waiting);
QSet<QUuid> waitingSymbols() const;
void insertLinkRows(QSqlQuery &insert, Element *element, const std::function<bool(const QUuid &)> &waiting,
const QList<std::pair<QUuid, int>> &old_rows);
bool builtAlone(Element *element) const;
bool linksLater(const QUuid &uuid) const;
QList<std::pair<QUuid, int>> rowsToWrite(Element *element, const std::function<bool(const QUuid &)> &waiting,
const QList<std::pair<QUuid, int>> &old_rows) const;
void populateDrawingItemTables();
bool populateFromDocument(const QDomDocument &document, QString *why = nullptr);
bool writeDrawingItem(QObject *object);
@@ -332,6 +345,12 @@ class projectDataBase : public QObject
//so the rows are written when next read, as drawing items are.
QList<QPointer<Element>> m_dirty_link_elements;
QSqlQuery m_saved_links_query;
bool m_holds_links = false;
/// waitingSymbols() as it was last, for these folios not built
/// and this count of symbols built alone on them
mutable QList<Diagram *> m_waiting_folios;
mutable int m_waiting_alone = 0;
mutable QSet<QUuid> m_waiting_symbols;
//Elements moved since their folio cell was last written: a move
//sends xChanged and yChanged for every step of a drag, so the
//cell is written when next read, as drawing items are.
+3 -1
View File
@@ -22,6 +22,7 @@
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminalelement.h"
#include "qetproject.h"
#include "dataBase/projectdatabase.h"
#include <QAbstractItemModel>
@@ -111,7 +112,8 @@ QVector <QPointer<Element>> ElementProvider::freeElement(ElementData::Types filt
{
if (filter & elmt_->elementData().m_type &&
elmt_->isFree() &&
!elmt_->pendingLinks().intersects(waiting))
(waiting.isEmpty()
|| m_project->dataBase()->waitingPartners(elmt_->uuid(), waiting).isEmpty()))
{
free_elmt << elmt_;
}
+80 -96
View File
@@ -1438,16 +1438,14 @@ QDomElement Element::toXml(
//if this element is linked to other elements, save the uuid of each:
//as the project database's link rows list them (QETZ-DB-FIRST stage
//2a), or, for a symbol the database does not hold, as it lists them
//itself (linksToSave())
//2a), or, for a symbol the database does not hold, its partners
QList<std::pair<QUuid, int>> links;
projectDataBase *database = diagram() && diagram()->project()
? diagram()->project()->dataBase() : nullptr;
if (!database || !database->savedLinks(this, &links)) {
links.clear();
for (const SavedLink &link : linksToSave(diagram() && diagram()->project()
? diagram()->project()->waitingLinkTargets() : QSet<QUuid>()))
links.append({link.uuid, link.group_index});
for (Element *elmt : std::as_const(connected_elements))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)});
}
if (!links.isEmpty()) {
QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids"));
@@ -1815,10 +1813,14 @@ QList <QPair <Terminal *, Terminal *> > Element::AlignedFreeTerminals() const
/**
@brief Element::initLink
Initialise the link between this element and other elements.
This method can be call once because init the link according to
uuid store in a private list, after link, the list is clear, so
call another time do nothing.
Link this symbol, just read, to its partners already built. Once the
project database holds the link rows, the partners are those its rows
name (QETZ-DB-FIRST stage 2a-3): its own rows, in their order, then the
symbols whose rows name it, then any its file names that the rows miss.
A partner not built yet (lazy folios) is linked when it is, from the
rows, which keep it until then. While a project opens, before the
database is filled, the partners are those its file names, and the list
is kept for the fill.
@param prj :
ownership project of this element and other element to be linked
@@ -1828,88 +1830,84 @@ void Element::initLink(QETProject *prj)
// if nothing to link return now
if (tmp_uuids_link.isEmpty()) return;
projectDataBase *database = prj->dataBase();
const bool from_rows = database->holdsLinks();
QList<LinkInfo> links;
QSet<QUuid> listed;
const QList<std::pair<QUuid, int>> rows = from_rows ? database->partnersToLink(uuid())
: QList<std::pair<QUuid, int>>();
const QList<std::pair<QUuid, int>> own_rows = from_rows ? database->linkRowsOf(uuid())
: QList<std::pair<QUuid, int>>();
//Its own rows first, then its file's list, then the symbols whose
//rows name it: a partner's row gives no contact group, so it
//comes after the lists that do
for (const auto &row : own_rows) {
if (!listed.contains(row.first)) {
listed.insert(row.first);
links << LinkInfo(row.first, row.second);
}
}
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
if (!listed.contains(link_info.uuid)) {
listed.insert(link_info.uuid);
links << link_info;
}
}
for (const auto &row : rows) {
if (!listed.contains(row.first)) {
listed.insert(row.first);
links << LinkInfo(row.first, row.second);
}
}
//The symbols built: on the folios built, and those built alone as
//partners on folios not built (Diagram::buildSymbols()). A partner
//not built yet (lazy folios) is linked when it is, and the
//saved list is kept until then, for the order it gives
//partners on folios not built (Diagram::buildSymbols())
ElementProvider ep(prj->folios());
QList<QUuid> uuids;
for (const auto &linkInfo : tmp_uuids_link) {
for (const auto &linkInfo : std::as_const(links)) {
uuids.append(linkInfo.uuid);
}
QList<Element *> elements = ep.fromUuids(uuids);
for (int i = 0; i < tmp_uuids_link.size(); ++i) {
for (const auto &link_info : std::as_const(links)) {
for (Element *elmt : elements) {
if (elmt->uuid() == tmp_uuids_link[i].uuid) {
if (elmt->uuid() == link_info.uuid) {
elmt->linkToElement(this);
if (tmp_uuids_link[i].group_index >= 0) {
m_group_index_map[elmt] = tmp_uuids_link[i].group_index;
if (link_info.group_index >= 0) {
m_group_index_map[elmt] = link_info.group_index;
}
break;
}
}
}
restoreSavedLinkOrder();
if (pendingLinks().isEmpty() || !prj->unloadedFolioCount()) {
restoreLinkOrder(uuids);
if (from_rows) {
//Each partner puts its partners back in the order of its rows:
//this one was linked last
for (Element *partner : std::as_const(connected_elements)) {
QList<QUuid> order;
for (const auto &row : database->partnersToLink(partner->uuid()))
order << row.first;
partner->restoreLinkOrder(order);
}
tmp_uuids_link.clear();
}
//Otherwise the project opens: the list is kept for the database's
//fill, which takes its order and its partners not built from it
//(QETProject::readProjectXml() drops it after)
}
/**
@brief Element::linksToSave
The partners this symbol saves, in the order it saves them: the order
it was saved with (tmp_uuids_link), then partners linked since. A
partner on a folio not built yet (lazy folios), which this symbol will
be linked to, is in the list too: saving needs no partner built (stage
D5a). toXml() writes this list, and the project database's link rows
are written from it, so the two are the same (QETZ-DB-FIRST stage 2a).
@param waiting the symbols on folios not built: a saved partner among
them is kept (QETProject::symbolsOnUnbuiltFolios())
@return each partner's uuid, its contact group (-1 if none), and the
partner itself when it is built (nullptr for one waiting)
@brief Element::savedLinkList
@return the links this symbol was read with and keeps (initLink()):
each partner's uuid and contact group (-1 if none), in its file's order
*/
QList<Element::SavedLink> Element::linksToSave(const QSet<QUuid> &waiting) const
QList<std::pair<QUuid, int>> Element::savedLinkList() const
{
QList<SavedLink> links;
if (keepsSavedLinks() && diagram() && diagram()->project()) {
QSet<Element *> written;
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
Element *linked = nullptr;
for (Element *elmt : std::as_const(connected_elements))
if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; }
if (linked) {
written.insert(linked);
links.append({linked->uuid(), m_group_index_map.value(linked, -1), linked});
} else if (waiting.contains(link_info.uuid)) {
links.append({link_info.uuid, link_info.group_index, nullptr});
}
}
for (Element *elmt : std::as_const(connected_elements))
if (!written.contains(elmt))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt});
} else {
for (Element *elmt : std::as_const(connected_elements))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt});
}
return links;
}
/**
@brief Element::pendingLinks
@return the uuids of the partners this symbol was saved linked to and
is not linked to yet: those on a folio not built yet (lazy folios),
until initLink() runs again once it is
*/
QSet<QUuid> Element::pendingLinks() const
{
QSet<QUuid> pending;
QList<std::pair<QUuid, int>> list;
for (const auto &link_info : tmp_uuids_link) {
pending.insert(link_info.uuid);
list.append({link_info.uuid, link_info.group_index});
}
for (Element *elmt : connected_elements) {
pending.remove(elmt->uuid());
}
return pending;
return list;
}
/**
@@ -1936,41 +1934,27 @@ void Element::initLink(const QList<Element *> &candidates)
}
}
}
restoreSavedLinkOrder();
QList<QUuid> order;
for (const auto &link_info : std::as_const(tmp_uuids_link))
order << link_info.uuid;
restoreLinkOrder(order);
tmp_uuids_link.clear();
}
/**
@brief Element::waitingLinks
@return pendingLinks() in the order this symbol was saved with them,
each with its group index (-1 for none)
@brief Element::restoreLinkOrder
Put connected_elements back in the order @p order gives (the order this
symbol saves its partners in). Partners link in the order the folios'
items are visited, and for items at the same height that order changes
from run to run, so a master's contacts were saved in a different order
each time. Linked elements missing from @p order keep their place after it.
*/
QList<std::pair<QUuid, int>> Element::waitingLinks() const
{
const QSet<QUuid> pending = pendingLinks();
QList<std::pair<QUuid, int>> list;
for (const auto &link_info : tmp_uuids_link) {
if (pending.contains(link_info.uuid)) {
list.append({link_info.uuid, link_info.group_index});
}
}
return list;
}
/**
@brief Element::restoreSavedLinkOrder
Put connected_elements back in the order tmp_uuids_link saved them.
Partners link in the order the folios' items are visited, and for
items at the same height that order changes from run to run, so a
master's contacts were saved in a different order each time.
Linked elements missing from the saved list keep their place after it.
*/
void Element::restoreSavedLinkOrder()
void Element::restoreLinkOrder(const QList<QUuid> &order)
{
QList<Element *> ordered;
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
for (const QUuid &uuid : order) {
for (Element *elmt : std::as_const(connected_elements)) {
if (elmt->uuid() == link_info.uuid && !ordered.contains(elmt)) {
if (elmt->uuid() == uuid && !ordered.contains(elmt)) {
ordered << elmt;
break;
}
+6 -11
View File
@@ -226,16 +226,12 @@ class Element : public QetGraphicsItem
virtual void unlinkAllElements() {}
virtual void unlinkElement(Element *) {}
virtual void initLink(QETProject *);
QSet<QUuid> pendingLinks() const;
/// A partner as the symbol saves it (linksToSave())
struct SavedLink {
QUuid uuid;
int group_index = -1;
Element *built = nullptr; ///< nullptr: on a folio not built yet
};
QList<SavedLink> linksToSave(const QSet<QUuid> &waiting) const;
bool keepsSavedLinks() const {return !tmp_uuids_link.isEmpty();}
QList<std::pair<QUuid, int>> waitingLinks() const;
/// The links this symbol was read with (its file's links_uuids),
/// until it is linked: the project database writes its link rows
/// from them while it holds none for it (opening)
QList<std::pair<QUuid, int>> savedLinkList() const;
void dropSavedLinks() {tmp_uuids_link.clear();}
void restoreLinkOrder(const QList<QUuid> &order);
/**
Resolve tmp_uuids_link against a caller-supplied candidate
list instead of a project-wide search (bugtracker #607).
@@ -359,7 +355,6 @@ class Element : public QetGraphicsItem
QList<QPointF> plcTablePositions() const { return m_plc_table_positions; }
private:
void restoreSavedLinkOrder();
void storeInformation();
projectDataBase *placedStore() const;
bool m_must_highlight = false;
+4 -1
View File
@@ -19,6 +19,7 @@
#include "../ElementsCollection/terminaluuids.h"
#include "../qet.h"
#include "../qetproject.h"
#include "../dataBase/projectdatabase.h"
#include "../conductorautonumerotation.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
@@ -1104,8 +1105,10 @@ QList<Terminal *> relatedPotentialTerminal (
//folio is; not while folios are being read or built, which
//would build whole report chains
Element *report = terminal->parentElement();
const QSet<QUuid> waiting = report->pendingLinks();
QETProject *project = report->diagram() ? report->diagram()->project() : nullptr;
const QSet<QUuid> waiting = project && project->unloadedFolioCount()
? project->dataBase()->waitingPartners(report->uuid(), project->symbolsOnUnbuiltFolios())
: QSet<QUuid>();
if (!waiting.isEmpty() && project
&& project->state() != QETProject::ProjectParsingRunning) {
project->buildFoliosHoldingSymbols(waiting);
+23 -26
View File
@@ -793,15 +793,6 @@ void QETProject::buildFolios(const QList<Diagram *> &folios) const
if (!signals_were_blocked && !built.isEmpty()) {
emit self->m_data_base.dataBaseUpdated();
}
//A symbol built before still keeping the links it was saved with
//(a partner on a folio not built): its initLink() links those just
//built, puts its partners back in their saved order and drops what
//is done
for (Diagram *d : builtFolios())
if (!built.contains(d))
for (Element *elmt : d->elements())
if (elmt->keepsSavedLinks())
elmt->initLink(self);
//A linked folio report's texts show a wire's properties: the
//report's own wire, else its partner's. A report built alone
//first, as a partner (buildPartnerSymbols()), linked before it had
@@ -1051,9 +1042,17 @@ void QETProject::buildFolioToShow(Diagram *folio)
if (chained.contains(d))
first << d;
buildFolios(first);
//The partners its symbols' rows name, not linked: those on a
//folio not built are built alone below (the others are built)
QSet<QUuid> partners;
for (Element *elmt : folio->elements())
partners.unite(elmt->pendingLinks());
for (Element *elmt : folio->elements()) {
QSet<QUuid> linked;
for (Element *partner : elmt->linkedElements())
linked.insert(partner->uuid());
for (const auto &row : m_data_base.linkRowsOf(elmt->uuid()))
if (!linked.contains(row.first))
partners.insert(row.first);
}
const QSet<QUuid> folio_uuids = partners.isEmpty()
? QSet<QUuid>()
: m_data_base.folioUuidsOfElements(partners);
@@ -1095,12 +1094,6 @@ void QETProject::buildPartnerSymbols(const QList<Diagram *> &folios, const QSet<
for (auto &text : element->dynamicTextItems())
text->refreshLabelConnection();
}
//A built symbol linked to one of them puts its partners back in
//their saved order
for (Diagram *d : builtFolios())
for (Element *element : d->elements())
if (element->keepsSavedLinks())
element->initLink(this);
m_state = state;
}
@@ -2456,10 +2449,11 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
built = unloadedFolioCount();
QSet<QUuid> waiting;
QSet<Diagram *> chained;
const QSet<QUuid> unbuilt = kept_folios ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
for (Diagram *diagram : builtFolios()) {
if (kept_folios)
for (Element *element : diagram->elements())
waiting.unite(element->pendingLinks());
waiting.unite(m_data_base.waitingPartners(element->uuid(), unbuilt));
for (Diagram *other : tableChainFolios(diagram))
if (!other->isLoaded()) chained.insert(other);
}
@@ -2473,7 +2467,6 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
} else {
loadFolios();
}
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
//QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol
//information, folio title blocks and wire properties agree with every placed symbol, folio and
//wire, at each save. A test and debugging aid; it changes nothing.
@@ -2661,7 +2654,6 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
<< links.mid(0, 5).join(QStringLiteral(", "));
}
m_waiting_link_targets.clear();
return(xml_doc);
}
@@ -3215,15 +3207,12 @@ bool QETProject::usesElement(const ElementsLocation &location) const
/**
@brief QETProject::folioXml
@return the built folio @p folio written as saving writes it: its
symbols keep the links to partners on folios not built yet
(lazy folios), which Diagram::toXml() alone leaves out
symbols' links are their link rows, partners on folios not built yet
(lazy folios) included (Element::toXml())
*/
QDomDocument QETProject::folioXml(Diagram *folio)
{
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
const QDomDocument xml = folio->toXml();
m_waiting_link_targets.clear();
return xml;
return folio->toXml();
}
/**
@@ -3519,6 +3508,9 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
//complete rebuild of it. The content being loaded is the same for all
//of them, so a single rebuild once everything is in place is enough.
m_data_base.setUpdateBlocked(true);
//The symbols read link from their files' lists until the fill
//below writes the link rows (Element::initLink())
m_data_base.forgetLinks();
//Load the project-wide properties
readProjectPropertiesXml(xml_project);
@@ -3602,6 +3594,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
m_data_base.setUpdateBlocked(false);
m_data_base.updateDB(xml_project);
m_data_base.endPrefill(unloadedFolioCount() > 0);
//The link rows now hold the partners on folios not built: a
//symbol keeps no list of its own (QETZ-DB-FIRST stage 2a-3)
for (Diagram *diagram : builtFolios())
for (Element *element : diagram->elements())
element->dropSavedLinks();
//Saving may write the folios not built as they were read, when
//building them would write the same: a file written by this
//version, whose database rows came from it (toXml()). A .qet does
-4
View File
@@ -143,9 +143,6 @@ class QETProject : public QObject
void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols);
void buildFoliosHoldingTables();
QSet<QUuid> symbolsOnUnbuiltFolios() const;
/// While saving: the symbols on folios not built a built symbol
/// may be saved linked to (Element::toXml())
QSet<QUuid> waitingLinkTargets() const {return m_waiting_link_targets;}
QDomDocument folioXml(Diagram *folio);
QSet<Diagram *> tableChainFolios(Diagram *folio) const;
void loadFolios() const;
@@ -585,7 +582,6 @@ class QETProject : public QObject
/// Saving a .qetz may copy the part of a folio not built
/// (toXml(), stage D4)
bool m_kept_parts_trusted = false;
QSet<QUuid> m_waiting_link_targets;
/// The entries of the .qetz opened, and the part each folio
/// was read from, in order
QHash<QString, QByteArray> m_container_files;
+4 -2
View File
@@ -90,9 +90,11 @@ QDomDocument WiringListExport::projectDocument() const
//Conductor::toXml(): the label of the last of the symbol's partners.
//A symbol still waiting for a partner not built has its links as
//saved, in the database
auto linked = [&built, database, &label](const QUuid &uuid) -> std::optional<QString> {
const QSet<QUuid> waiting = m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios()
: QSet<QUuid>();
auto linked = [&built, database, &label, &waiting](const QUuid &uuid) -> std::optional<QString> {
Element *element = built.value(uuid);
if (element && element->pendingLinks().isEmpty()) {
if (element && database->waitingPartners(uuid, waiting).isEmpty()) {
if (element->linkedElements().isEmpty()) return std::nullopt;
return element->linkedElements().last()->actualLabel();
}
+91
View File
@@ -522,6 +522,97 @@ private slots:
QCOMPARE(dangling, 0);
}
// Each placed symbol's saved links in the .qetz @p path, as "symbol
// partner" lines, sorted: the link table of its database
static QStringList qetzLinks(const QString &path)
{
QList<QetZip::Entry> entries;
if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")};
QByteArray database;
for (const QetZip::Entry &entry : std::as_const(entries))
if (entry.name == QLatin1String("project.sqlite")) database = entry.data;
sqlite3 *db = nullptr;
sqlite3_open(":memory:", &db);
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
memcpy(copy, database.constData(), size_t(database.size()));
sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
sqlite3_stmt *list = nullptr;
QStringList links;
sqlite3_prepare_v2(db, "SELECT element_uuid, linked_uuid FROM link", -1, &list, nullptr);
while (list && sqlite3_step(list) == SQLITE_ROW)
links << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(list, 0)))
+ QLatin1Char(' ')
+ QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(list, 1)));
sqlite3_finalize(list);
sqlite3_close(db);
links.sort();
return links;
}
// Removing a symbol or a folio and undoing it, with lazy folios, gives
// back every link, also those to symbols on folios not built, which
// only the project database's link rows hold (QETZ-DB-FIRST stage
// 2a-3). m_000 as a .qetz saved with every folio built; a contact of
// its middle folio, its folio, or its coil's folio (not built: the coil
// is built alone) removed, then undone.
void undoKeepsLinksToUnbuiltFolios_data()
{
QTest::addColumn<QString>("edit");
QTest::newRow("symbol") << QStringLiteral("qet.deleteElement(f, u)");
QTest::newRow("folio") << QStringLiteral("qet.removeFolio(f)");
QTest::newRow("coil folio") << QStringLiteral("qet.removeFolio(folio_of[m])");
}
void undoKeepsLinksToUnbuiltFolios()
{
#ifndef QET_HAS_SCRIPTING
QSKIP("needs --run: this QElectroTech is built without Qt Qml");
#endif
QFETCH(QString, edit);
//Each symbol's folio, from the file: asking QElectroTech would
//build the folios
QFile source(QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"));
QVERIFY(source.open(QIODevice::ReadOnly));
const QStringList folios = QString::fromUtf8(source.readAll()).split(QStringLiteral("<diagram "));
QStringList folio_of;
static const QRegularExpression placed(QStringLiteral("<element [^>]*\\buuid=\"(\\{[0-9a-f-]+\\})\""));
for (int i = 1 ; i < folios.size() ; ++i) {
auto it = placed.globalMatch(folios.at(i));
while (it.hasNext()) folio_of << QStringLiteral("'%1': %2").arg(it.next().captured(1)).arg(i - 1);
}
const QString qetz = m_dir.filePath(QStringLiteral("undo%1.qetz").arg(m_run));
QVERIFY(!runChecked({QStringLiteral("--resave"),
QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"), qetz}, true).isEmpty());
auto save = [&](const QString &name, const QString &body) {
const QString saved = m_dir.filePath(name + QString::number(m_run) + QStringLiteral(".qetz"));
const QString js = m_dir.filePath(name + QString::number(m_run) + QStringLiteral(".js"));
QFile script(js);
if (!script.open(QIODevice::WriteOnly)) return std::pair<QString, QString>();
script.write(QStringLiteral(
"var folio_of = {%3};\n"
"var n = qet.folioCount(), f = -1, u = '', m = '';\n"
"for (var k = 0 ; k < n && !u ; ++k) {\n"
" var g = (Math.floor(n / 2) + k) % n;\n"
" var all = qet.elementUuids(g).slice().sort();\n"
" for (var i = 0 ; i < all.length && !u ; ++i)\n"
" if (qet.elementLinkType(g, all[i]) == 'slave' && qet.linkedElements(g, all[i]).length) { f = g; u = all[i]; m = qet.linkedElements(g, u)[0]; }\n"
"}\n"
"%1\n"
"qet.save('%2');\n").arg(body, saved, folio_of.join(QStringLiteral(", "))).toUtf8());
script.close();
return std::pair<QString, QString>(saved, runChecked({QStringLiteral("--run"), js, qetz}, false,
"QET_CHECK_LINK_ROWS"));
};
const auto plain = save(QStringLiteral("undo-plain"), QStringLiteral("qet.log('FOUND ' + !!u);"));
QVERIFY2(plain.second.contains(QStringLiteral("FOUND true")), qPrintable(plain.second.right(600)));
const auto undone = save(QStringLiteral("undo-undone"),
QStringLiteral("qet.log('UNDONE ' + (%1) + ' ' + qet.undo());").arg(edit));
QVERIFY2(undone.second.contains(QStringLiteral("UNDONE true true")), qPrintable(undone.second.right(600)));
QVERIFY2(undone.second.contains(QStringLiteral("link rows: 0 differ")), qPrintable(undone.second.right(600)));
const QStringList before = qetzLinks(plain.first);
QVERIFY(before.size() > 10);
QCOMPARE(qetzLinks(undone.first), before);
}
// The stores filled from the document before the folios are built
// hold what building them gives, for a
// project saved by this version (every symbol, folio and wire carries