Save a .qetz without building the folios not touched (stage D4)

With QET_LAZY_FOLIOS=1, a project opened from a .qetz's index saves a
.qetz without building the folios it never built, when building them
would write the same: their parts are copied as they were and their rows
of the document's engineering data from the project.sqlite they were read
from (QetContainerDb::extract() with kept_from, QetContainer::split() with
the old entries, pictures included). Whether a folio may be kept follows
the rules of stage 5.5 (QETProject::toXml() with kept_parts): nothing
project-wide changed since opening; a folio whose own data changed is
built with its partners; after an edit, folios holding a table filled
from the database are built; the partners still waiting of every built
folio are built. Trusted only for a project opened from a format 3 index
written by this version whose folios are named by their uuid.

Format 3 names a folio by its uuid in the database (doc_meta folio_ids =
uuid) when every folio has one of its own, so that a kept folio keeps its
rows whatever its place; copies keep the rows' order (ORDER BY rowid).
A folio not built keeps the name of its part until it is built
(Diagram::deferredPart()).

Measured, the 136-folio f2099 opened from its .qetz and saved as a .qetz:
4.75 s with no folio built, 33 s with every folio built; its 136 folio
parts copied byte for byte; a kept save of a kept save is the same bytes.
industrial.qetz untouched: 28 of its 47 folios not built are built to save
(link chains, as in 5.5); after everyday edits on its busiest folio, 7
stay not built.

tst_lazyfolios: an untouched .qetz saved without building, read back,
holds the project; edits saved without building hold, read back, what the
same edits saved with every folio built hold (read back too: a wire
numbered from its potential is saved with no sequential numbers whichever
the way -- reading it back renumbers it, a separate issue); fails when a
kept folio's symbol information is not copied. Corpus, 342 projects: a
.qetz saved without building, read and saved as .qet = eager = master on
all 342. ctest
96/96 with and without QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-11 21:21:42 +13:00
parent 8ea051acfc
commit 8ad33f6281
9 changed files with 227 additions and 21 deletions
+86 -2
View File
@@ -24,12 +24,15 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <QUuid>
#include <QVariant>
#include <sqlite3.h>
namespace {
const QString KeyAttribute = QStringLiteral("qetz-key");
//A folio written as it was read (stage D4): its part
const QString KeptPartAttribute = QStringLiteral("qetz-kept-part");
const QString FolioAttribute = QStringLiteral("qetz-folio");
//What a moved block leaves where it was. Removing it instead would leave
//the whitespace before and after it side by side, read back as one text
@@ -604,7 +607,8 @@ bool restoreDiagram(Database &db, QDomElement d, QString *error)
folios still in it) into a new database.
@return the database file, or an empty array with @p error set
*/
QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QByteArray &index)
QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QByteArray &index,
const QByteArray &kept_from)
{
Database db;
db.m_prefix = QStringLiteral("doc_");
@@ -627,6 +631,28 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB
if (!db.exec(schema())) {
setError(error, QStringLiteral("cannot create the project database"));
return {};
}
//The database of the .qetz this project was read from, for the folios
//kept as they were (stage D4)
if (!kept_from.isEmpty()) {
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(kept_from.size())));
if (!copy) { setError(error, QStringLiteral("out of memory")); return {}; }
memcpy(copy, kept_from.constData(), size_t(kept_from.size()));
if (!db.exec(QStringLiteral("ATTACH DATABASE ':memory:' AS old"))
|| sqlite3_deserialize(db.m_db, "old", copy, kept_from.size(), kept_from.size(),
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) {
setError(error, QStringLiteral("cannot read the database the kept folios come from"));
return {};
}
db.exec(QStringLiteral("PRAGMA trusted_schema = OFF"));
for (const QString &table : DocTables) {
Statement kind(db, QStringLiteral("SELECT type FROM old.sqlite_master WHERE name = ?"));
kind.bind({db.table(table)});
if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) {
setError(error, QStringLiteral("the kept folios' database has no table %1").arg(table));
return {};
}
}
}
db.exec(QStringLiteral("BEGIN"));
@@ -683,10 +709,53 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB
const QStringList conductor_keys = keys(all_conductors, QStringLiteral("c"));
int e_index = 0, c_index = 0;
//A folio's id is its uuid when every folio has one of its own, so
//that a folio kept as it was keeps its rows (format 3, D4)
bool uuid_ids = true;
{
QSet<QString> seen;
for (const QDomElement &d : diagrams) {
const QString uuid = d.attribute(QStringLiteral("uuid"));
if (QUuid(uuid).isNull() || seen.contains(uuid)) { uuid_ids = false; break; }
seen.insert(uuid);
}
}
if (uuid_ids) {
ok = ok && meta.run({QStringLiteral("folio_ids"), QStringLiteral("uuid")});
}
int pos = 0;
for (QDomElement d : diagrams)
{
const QString id = QStringLiteral("f%1").arg(++pos);
++pos;
const QString id = uuid_ids ? d.attribute(QStringLiteral("uuid")) : QStringLiteral("f%1").arg(pos);
//Kept as it was: its rows from the database it was read from
if (d.hasAttribute(KeptPartAttribute)) {
if (kept_from.isEmpty() || !uuid_ids) {
setError(error, QStringLiteral("a folio kept as it was, without its database"));
return {};
}
Statement copy(db, QStringLiteral("INSERT INTO folio SELECT id, ?, %1, props_at, props_attrs "
"FROM old.folio WHERE id = ?")
.arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
ok = ok && copy.run({pos, id}) && sqlite3_changes(db.m_db) == 1;
for (const QString &sql : {
QStringLiteral("INSERT INTO folio_property SELECT * FROM old.folio_property WHERE folio_id = ? ORDER BY rowid"),
QStringLiteral("INSERT INTO element SELECT * FROM old.element WHERE folio_id = ? ORDER BY rowid"),
QStringLiteral("INSERT INTO element_info SELECT * FROM old.element_info WHERE element_key IN "
"(SELECT key FROM old.element WHERE folio_id = ?) ORDER BY rowid"),
QStringLiteral("INSERT INTO link SELECT * FROM old.link WHERE element_key IN "
"(SELECT key FROM old.element WHERE folio_id = ?) ORDER BY rowid"),
QStringLiteral("INSERT INTO conductor SELECT * FROM old.conductor WHERE folio_id = ? ORDER BY rowid")}) {
Statement rows(db, sql);
ok = ok && rows.run({id});
}
if (!ok) {
setError(error, QStringLiteral("folio %1 kept as it was is not in its database").arg(id));
return {};
}
continue;
}
QVariantList row{id, pos};
row += take(d, FolioData);
QDomElement props = d.firstChildElement(QStringLiteral("properties"));
@@ -859,3 +928,18 @@ bool QetContainerDb::restoreFolio(QDomElement diagram, const QByteArray &databas
Database db;
return openDatabase(db, database, error) && restoreDiagram(db, diagram, error);
}
/**
@brief QetContainerDb::foliosByUuid
@return true if @p database, a format 3 project.sqlite, names its
folios by their uuid: a folio written as it was read keeps its rows
under it (stage D4)
*/
bool QetContainerDb::foliosByUuid(const QByteArray &database)
{
Database db;
if (!openDatabase(db, database, nullptr) || db.m_prefix.isEmpty())
return false;
Statement s(db, QStringLiteral("SELECT value FROM meta WHERE key = 'folio_ids'"));
return s.next() && s.value(0).toString() == QLatin1String("uuid");
}