Open a .qetz from its database, each folio read when built (stage D2)

A .qetz now carries index.sqlite: the project database as QElectroTech
keeps it -- every table but the drawing items, which exist for built
folios only -- with each folio's own data (Diagram::headerXml()) and a
stamp of the schema it was written with (projectDataBase::indexFile(),
QETProject::writeContainerFile(): Save, --resave, --set-titleblock, a
script's and live mode's save). Written when every folio, symbol and wire
has a uuid of its own; a reader that does not know the entry skips it.

With QET_LAZY_FOLIOS=1, a .qetz with an index opens from it: the project
document is joined without its folios (QetContainer::join() with
folio_files), the database and the stores of symbol information and wire
properties are filled from the index (fillFromIndex()), each folio is made
from its own data and keeps the name of its part, read when the folio is
built (Diagram::deferredXml(), QETProject::readFolioPart(),
QetContainer::readFolio() -- pictures and engineering data back from
project.sqlite, QetContainerDb::restoreFolio()). A terminal strip finds its
terminals' folios in the database. Any other .qetz, or one whose index
cannot be used, is read whole as before.

The index is untrusted: refused unless written with this database's
schema, every table read a plain table (not a view), nothing in its schema
run (trusted_schema off); refused, the project is read whole. Checked: an
index of garbage, a wrong stamp, a table made a view -- each refused and
the project saved as a plain save.

The 136-folio f2099 as .qetz opens in 1.8 s (3.65 s read whole, 29 s
building every folio); what is left is mostly its 7 MB of symbol
definitions (0.8 s). The file grows from 2.6 to 5.2 MB, the index
overlapping project.sqlite until D3.

tst_lazyfolios: a .qetz opened from its index builds only the terminal
strip's folios, numbering read as when built, every folio shown in turn
draws and links as when built, the save as when built; fails when a folio
part's engineering data is not put back. Corpus, 342 projects: 340 .qetz
carry an index and open from it (2 have symbols sharing a uuid); saved
back as .qet = eager = master on all 342. Probe on industrial and f2099
.qetz: every folio as eager but the 3 render-noise folios of f2099. In a
real window the .qetz opens and pages building only shown folios and
partners. 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 18:58:35 +13:00
parent ecc7a39131
commit e89ccf2a40
15 changed files with 719 additions and 136 deletions
+3 -2
View File
@@ -1020,8 +1020,9 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid"))));
m_project->buildFoliosHoldingSymbols(terminals);
//Get all free elements terminal of the project
const ElementProvider ep(m_project);
//Get all free elements terminal of the project: its own are on
//the folios just built
const ElementProvider ep(m_project->builtFolios());
auto free_terminals = ep.freeTerminal();
//Read each physical terminal
+2 -4
View File
@@ -870,9 +870,8 @@ int exportLinks(QETProject &project, const QString &output)
int resaveProject(QETProject &project, const QString &output)
{
if (QETProject::isContainerPath(output)) {
const QDomDocument doc = project.toXml();
QString error;
if (!QETProject::writeContainer(doc, output, &error)) {
if (!project.writeContainerFile(output, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
@@ -968,9 +967,8 @@ int setTitleBlock(QETProject &project, const QString &output,
}
if (QETProject::isContainerPath(output)) {
const QDomDocument doc = project.toXml();
QString error;
if (!QETProject::writeContainer(doc, output, &error)) {
if (!project.writeContainerFile(output, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
+49 -1
View File
@@ -258,6 +258,43 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
return entries;
}
/**
@brief QetContainer::readFolio
The folio part @p file of the .qetz whose entries are @p files, as
join() puts it in the project: its pictures back as base64 and its
engineering data back from project.sqlite.
*/
bool QetContainer::readFolio(const QHash<QString, QByteArray> &files, const QString &file,
QDomDocument *folio, QString *error)
{
if (!files.contains(file)) {
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
return false;
}
QString part_error;
if (!parse(files.value(file), folio, &part_error)) {
setError(error, QStringLiteral("%1: %2").arg(file, part_error));
return false;
}
const QDomNodeList images = folio->elementsByTagName(QStringLiteral("image"));
for (int i = 0 ; i < images.size() ; ++i)
{
QDomElement image = images.at(i).toElement();
if (!image.hasAttribute(ImageFileAttribute)) continue;
const QString picture = image.attribute(ImageFileAttribute);
if (!files.contains(picture)) {
setError(error, QStringLiteral("picture \"%1\" is missing").arg(picture));
return false;
}
image.removeAttribute(ImageFileAttribute);
image.insertBefore(folio->createTextNode(QString::fromLatin1(files.value(picture).toBase64())),
image.firstChild());
}
return !files.contains(QStringLiteral("project.sqlite"))
|| QetContainerDb::restoreFolio(folio->documentElement(),
files.value(QStringLiteral("project.sqlite")), error);
}
/**
@brief QetContainer::join
Rebuild in @p project the document split() made @p entries from.
@@ -265,7 +302,8 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
(the manifest's min-reader), or a part missing or unreadable. Entries
nothing names are ignored, so a later format may add some.
*/
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error)
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error,
QStringList *folio_files)
{
QHash<QString, QByteArray> files;
for (const QetZip::Entry &entry : entries)
@@ -297,6 +335,16 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
for (QDomElement part : placeholders)
{
const QString file = part.attribute(FileAttribute);
//With @p folio_files, the folios stay parts, read when built
if (folio_files && part.parentNode() == project->documentElement()
&& file.startsWith(QLatin1String("folios/"))) {
if (!files.contains(file)) {
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
return false;
}
folio_files->append(file);
continue;
}
if (!files.contains(file)) {
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
return false;
+5 -1
View File
@@ -53,7 +53,11 @@ namespace QetContainer
const QString &writer = QString(),
QString *error = nullptr);
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
QString *error = nullptr);
QString *error = nullptr, QStringList *folio_files = nullptr);
/// A folio part of @p files, which join() left out, with its
/// pictures and its engineering data back (stage D2)
bool readFolio(const QHash<QString, QByteArray> &files, const QString &file,
QDomDocument *folio, QString *error = nullptr);
/// Parse @p xml as a .qet is read (QETProject::openFile)
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
+151 -107
View File
@@ -434,6 +434,143 @@ QStringList keys(const QList<QDomElement> &items, const QString &prefix)
}
}
namespace {
//@p database opened read only in @p db, its tables checked to be plain
//tables: a file is untrusted
bool openDatabase(Database &db, const QByteArray &database, QString *error)
{
if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) {
setError(error, QStringLiteral("cannot open the project database"));
return false;
}
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
if (!copy) { setError(error, QStringLiteral("out of memory")); return false; }
memcpy(copy, database.constData(), size_t(database.size()));
if (sqlite3_deserialize(db.m_db, "main", copy, database.size(), database.size(),
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) {
setError(error, QStringLiteral("the project database cannot be read"));
return false;
}
//A file is untrusted: nothing in its schema may run, and every name
//read must be a plain table.
db.exec(QStringLiteral("PRAGMA trusted_schema = OFF"));
db.exec(QStringLiteral("PRAGMA query_only = ON"));
for (const char *table : {"meta", "project_property", "folio", "folio_property",
"element", "element_info", "link", "conductor", "terminal_strip",
"strip_terminal", "strip_bridge", "strip_bridge_terminal"}) {
Statement kind(db.m_db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?"));
kind.bind({QString::fromLatin1(table)});
if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) {
setError(error, QStringLiteral("the project database has no table %1").arg(QLatin1String(table)));
return false;
}
}
return true;
}
QDomElement entryElement(QDomDocument &document, const QVariant &name_attr, const QString &name,
const QVariant &value, const QVariant &attrs, const QString &default_tag)
{
const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object();
QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag));
if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString());
setAttributesJson(e, attrs);
if (!value.isNull()) e.appendChild(document.createTextNode(value.toString()));
return e;
}
//The engineering data of the folio @p d (its qetz-folio id) back from @p db
bool restoreDiagram(Database &db, QDomElement d, QString *error)
{
QDomDocument document = d.ownerDocument();
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
const QVariant &attrs, const QString &default_tag) {
return entryElement(document, name_attr, name, value, attrs, default_tag);
};
const QString id = d.attribute(FolioAttribute);
if (id.isEmpty()) return true;
d.removeAttribute(FolioAttribute);
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE id = ?")
.arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
f.bind({id});
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
QVariantList values;
for (int i = 0 ; i < FolioData.size() + 2 ; ++i) values << f.value(i);
put(d, FolioData, values, 0);
if (!values.at(FolioData.size()).isNull()) {
QDomElement props = document.createElement(QStringLiteral("properties"));
setAttributesJson(props, values.at(FolioData.size() + 1));
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE folio_id = ? ORDER BY ord"));
p.bind({id});
while (p.next())
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
QStringLiteral("property")));
restoreWhitespace(props, values.at(FolioData.size() + 1));
if (!replaceMarker(d, props)) {
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
return false;
}
}
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
for (QDomElement e : children(list, QStringLiteral("element"))) {
if (!e.hasAttribute(KeyAttribute)) continue;
const QString key = e.attribute(KeyAttribute);
e.removeAttribute(KeyAttribute);
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE key = ?")
.arg(QStringLiteral("\"") + ElementData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
QVariantList v;
for (int i = 0 ; i < ElementData.size() + 4 ; ++i) v << s.value(i);
put(e, ElementData, v, 0);
const int n = ElementData.size();
QDomElement info, links;
if (!v.at(n).isNull()) {
info = document.createElement(QStringLiteral("elementInformations"));
setAttributesJson(info, v.at(n + 1));
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord"));
i.bind({key});
while (i.next())
info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2),
QStringLiteral("elementInformation")));
restoreWhitespace(info, v.at(n + 1));
}
if (!v.at(n + 2).isNull()) {
links = document.createElement(QStringLiteral("links_uuids"));
setAttributesJson(links, v.at(n + 3));
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord"));
l.bind({key});
while (l.next())
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
QStringLiteral("link_uuid")));
restoreWhitespace(links, v.at(n + 3));
}
if ((!info.isNull() && !replaceMarker(e, info))
|| (!links.isNull() && !replaceMarker(e, links))) {
setError(error, QStringLiteral("symbol %1's information has no place").arg(key));
return false;
}
}
}
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
if (!c.hasAttribute(KeyAttribute)) continue;
const QString key = c.attribute(KeyAttribute);
c.removeAttribute(KeyAttribute);
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE key = ?")
.arg(QStringLiteral("\"") + ConductorData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
QVariantList v;
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i);
put(c, ConductorData, v, 0);
}
}
return true;
}
}
/**
@brief QetContainerDb::extract
Move the engineering data of @p project (the <project> element, its
@@ -609,32 +746,8 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
{
QDomDocument document = project.ownerDocument();
Database db;
if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) {
setError(error, QStringLiteral("cannot open the project database"));
if (!openDatabase(db, database, error))
return false;
}
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
if (!copy) { setError(error, QStringLiteral("out of memory")); return false; }
memcpy(copy, database.constData(), size_t(database.size()));
if (sqlite3_deserialize(db.m_db, "main", copy, database.size(), database.size(),
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) {
setError(error, QStringLiteral("the project database cannot be read"));
return false;
}
//A file is untrusted: nothing in its schema may run, and every name
//read must be a plain table.
db.exec(QStringLiteral("PRAGMA trusted_schema = OFF"));
db.exec(QStringLiteral("PRAGMA query_only = ON"));
for (const char *table : {"meta", "project_property", "folio", "folio_property",
"element", "element_info", "link", "conductor", "terminal_strip",
"strip_terminal", "strip_bridge", "strip_bridge_terminal"}) {
Statement kind(db.m_db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?"));
kind.bind({QString::fromLatin1(table)});
if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) {
setError(error, QStringLiteral("the project database has no table %1").arg(QLatin1String(table)));
return false;
}
}
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
const QVariant &attrs, const QString &default_tag) {
@@ -687,87 +800,18 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
}
for (QDomElement d : children(project, QStringLiteral("diagram")))
{
const QString id = d.attribute(FolioAttribute);
if (id.isEmpty()) continue;
d.removeAttribute(FolioAttribute);
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE id = ?")
.arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
f.bind({id});
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
QVariantList values;
for (int i = 0 ; i < FolioData.size() + 2 ; ++i) values << f.value(i);
put(d, FolioData, values, 0);
if (!values.at(FolioData.size()).isNull()) {
QDomElement props = document.createElement(QStringLiteral("properties"));
setAttributesJson(props, values.at(FolioData.size() + 1));
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE folio_id = ? ORDER BY ord"));
p.bind({id});
while (p.next())
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
QStringLiteral("property")));
restoreWhitespace(props, values.at(FolioData.size() + 1));
if (!replaceMarker(d, props)) {
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
return false;
}
}
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
for (QDomElement e : children(list, QStringLiteral("element"))) {
if (!e.hasAttribute(KeyAttribute)) continue;
const QString key = e.attribute(KeyAttribute);
e.removeAttribute(KeyAttribute);
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE key = ?")
.arg(QStringLiteral("\"") + ElementData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
QVariantList v;
for (int i = 0 ; i < ElementData.size() + 4 ; ++i) v << s.value(i);
put(e, ElementData, v, 0);
const int n = ElementData.size();
QDomElement info, links;
if (!v.at(n).isNull()) {
info = document.createElement(QStringLiteral("elementInformations"));
setAttributesJson(info, v.at(n + 1));
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord"));
i.bind({key});
while (i.next())
info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2),
QStringLiteral("elementInformation")));
restoreWhitespace(info, v.at(n + 1));
}
if (!v.at(n + 2).isNull()) {
links = document.createElement(QStringLiteral("links_uuids"));
setAttributesJson(links, v.at(n + 3));
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord"));
l.bind({key});
while (l.next())
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
QStringLiteral("link_uuid")));
restoreWhitespace(links, v.at(n + 3));
}
if ((!info.isNull() && !replaceMarker(e, info))
|| (!links.isNull() && !replaceMarker(e, links))) {
setError(error, QStringLiteral("symbol %1's information has no place").arg(key));
return false;
}
}
}
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
if (!c.hasAttribute(KeyAttribute)) continue;
const QString key = c.attribute(KeyAttribute);
c.removeAttribute(KeyAttribute);
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE key = ?")
.arg(QStringLiteral("\"") + ConductorData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
QVariantList v;
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i);
put(c, ConductorData, v, 0);
}
}
}
if (!restoreDiagram(db, d, error))
return false;
return true;
}
/**
@brief QetContainerDb::restoreFolio
restore() for the one folio @p diagram, read from its own part (a folio
built after its project was opened from a .qetz, stage D2)
*/
bool QetContainerDb::restoreFolio(QDomElement diagram, const QByteArray &database, QString *error)
{
Database db;
return openDatabase(db, database, error) && restoreDiagram(db, diagram, error);
}
+1
View File
@@ -39,6 +39,7 @@ namespace QetContainerDb
{
QByteArray extract(QDomElement project, QString *error = nullptr);
bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr);
bool restoreFolio(QDomElement diagram, const QByteArray &database, QString *error = nullptr);
}
#endif // QETCONTAINERDB_H
+278
View File
@@ -49,6 +49,8 @@
#include <QMetaEnum>
#include <QTextDocument>
#include <QFile>
#include <QCryptographicHash>
#include <QTemporaryDir>
#include <QRegularExpression>
#include <QSqlDriver>
#include <QSqlError>
@@ -865,6 +867,282 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
return true;
}
/**
@brief projectDataBase::schemaStamp
@return a stamp of the tables of this database as created: an index
file (indexFile()) is read back only into a database with the same
*/
QString projectDataBase::schemaStamp() const
{
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("SELECT sql FROM sqlite_master WHERE type IN ('table', 'index') "
"AND name NOT LIKE 'sqlite_%' ORDER BY name"));
QCryptographicHash hash(QCryptographicHash::Sha256);
while (query.next())
hash.addData(query.value(0).toString().toUtf8() + ';');
return QString::fromLatin1(hash.result().toHex());
}
namespace {
//The tables an index file carries, all of this database's but the
//drawing items, which exist for built folios only
QStringList indexTables(const QSqlDatabase &db)
{
QStringList tables;
QSqlQuery query(db);
query.exec(QStringLiteral("SELECT name FROM sqlite_master WHERE type = 'table' "
"AND name NOT LIKE 'sqlite_%' ORDER BY name"));
while (query.next()) {
const QString name = query.value(0).toString();
if (name != QLatin1String("shape") && name != QLatin1String("independent_text")
&& name != QLatin1String("image"))
tables << name;
}
return tables;
}
}
/**
@brief projectDataBase::indexFile
This database written to an SQLite file, for a .qetz to be opened from
(stage D2): every table but the drawing items, brought
up to date first, with the folios' own data @p folio_headers (in folio
order, each as Diagram::headerXml() writes it) and the stamp of the
schema it was written with.
@return the file, or nothing if it could not be written
*/
QByteArray projectDataBase::indexFile(const QList<std::pair<QUuid, QString>> &folio_headers)
{
if (m_content_changed) {
updateDB();
}
flushLinks();
flushElementPositions();
flushConductorProperties();
QTemporaryDir dir;
if (!dir.isValid()) {
return QByteArray();
}
const QString path = dir.filePath(QStringLiteral("index.sqlite"));
QString escaped = path;
escaped.replace(QLatin1Char('\''), QStringLiteral("''"));
QSqlQuery vacuum(m_data_base);
if (!vacuum.exec(QStringLiteral("VACUUM INTO '%1'").arg(escaped))) {
qWarning() << "projectDataBase::indexFile:" << vacuum.lastError().text();
return QByteArray();
}
const QString stamp = schemaStamp();
const QString connection = QStringLiteral("qet_index_") + QUuid::createUuid().toString();
bool ok = false;
{
QSqlDatabase file = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connection);
file.setDatabaseName(path);
if (file.open())
{
QSqlQuery query(file);
//Its views read the drawing items too: an index holds tables
QStringList views;
query.exec(QStringLiteral("SELECT name FROM sqlite_master WHERE type = 'view'"));
while (query.next()) views << query.value(0).toString();
for (const QString &view : std::as_const(views))
query.exec(QStringLiteral("DROP VIEW \"%1\"").arg(view));
ok = query.exec(QStringLiteral("DROP TABLE shape"))
&& query.exec(QStringLiteral("DROP TABLE independent_text"))
&& query.exec(QStringLiteral("DROP TABLE image"))
&& query.exec(QStringLiteral("CREATE TABLE index_meta (key TEXT PRIMARY KEY, value TEXT)"))
&& query.exec(QStringLiteral("CREATE TABLE folio_header (pos INTEGER PRIMARY KEY, "
"uuid TEXT NOT NULL, xml TEXT NOT NULL)"));
file.transaction();
query.prepare(QStringLiteral("INSERT INTO index_meta (key, value) VALUES (?, ?)"));
for (const auto &meta : {std::pair<QString, QString>{QStringLiteral("schema"), stamp},
std::pair<QString, QString>{QStringLiteral("writer"),
QStringLiteral("QElectroTech ") + QetVersion::currentVersion().toString()}}) {
query.addBindValue(meta.first);
query.addBindValue(meta.second);
ok = ok && query.exec();
}
query.prepare(QStringLiteral("INSERT INTO folio_header (pos, uuid, xml) VALUES (?, ?, ?)"));
int pos = 0;
for (const auto &header : folio_headers) {
query.addBindValue(pos++);
query.addBindValue(header.first.toString());
query.addBindValue(header.second);
ok = ok && query.exec();
}
ok = file.commit() && ok;
ok = query.exec(QStringLiteral("VACUUM")) && ok;
file.close();
}
}
QSqlDatabase::removeDatabase(connection);
if (!ok) {
return QByteArray();
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return file.readAll();
}
/**
@brief projectDataBase::discardFill
Undo fillFromIndex(): the project is read from its document instead
*/
void projectDataBase::discardFill()
{
QSqlQuery query(m_data_base);
for (const QString &table : indexTables(m_data_base))
query.exec(QStringLiteral("DELETE FROM \"%1\"").arg(table));
m_element_information.clear();
m_folio_titleblocks.clear();
m_conductor_properties.clear();
m_conductor_attributes.clear();
m_prefilled_elements.clear();
m_prefilled_folios.clear();
m_prefilled_conductors.clear();
m_rows_from_document = false;
m_content_changed = true;
}
/**
@brief projectDataBase::fillFromIndex
Fill this database from @p index, a file indexFile() wrote, instead of
from the project's document: its tables, and the stores of symbol
information and wire properties (the folios' title blocks are filled
from @p folio_headers by the folios). The file is untrusted: read only
when written with this database's schema, its tables plain tables, and
nothing in its schema run.
@param folio_headers : the folios' own data, in folio order
@return false, with @p why set and nothing changed in the tables, if
the file cannot be read
*/
bool projectDataBase::fillFromIndex(const QByteArray &index,
QList<std::pair<QUuid, QString>> *folio_headers,
QString *why)
{
auto refuse = [why](const QString &reason) {
if (why) *why = reason;
return false;
};
QTemporaryDir dir;
if (!dir.isValid()) {
return refuse(QStringLiteral("no temporary folder"));
}
const QString path = dir.filePath(QStringLiteral("index.sqlite"));
{
QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(index) != index.size()) {
return refuse(QStringLiteral("cannot write the index to a temporary file"));
}
}
QString escaped = path;
escaped.replace(QLatin1Char('\''), QStringLiteral("''"));
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("PRAGMA trusted_schema = OFF"));
if (!query.exec(QStringLiteral("ATTACH DATABASE '%1' AS idx").arg(escaped))) {
return refuse(QStringLiteral("the index cannot be read"));
}
auto detach = [this]() {
QSqlQuery q(m_data_base);
q.exec(QStringLiteral("DETACH DATABASE idx"));
};
const QStringList tables = indexTables(m_data_base);
auto isTable = [this](const QString &name) {
QSqlQuery kind(m_data_base);
kind.prepare(QStringLiteral("SELECT type FROM idx.sqlite_master WHERE name = ?"));
kind.addBindValue(name);
return kind.exec() && kind.next() && kind.value(0).toString() == QLatin1String("table");
};
for (const QString &table : tables + QStringList{QStringLiteral("index_meta"),
QStringLiteral("folio_header")}) {
if (!isTable(table)) {
detach();
return refuse(QStringLiteral("the index has no table %1").arg(table));
}
}
query.exec(QStringLiteral("SELECT value FROM idx.index_meta WHERE key = 'schema'"));
if (!query.next() || query.value(0).toString() != schemaStamp()) {
detach();
return refuse(QStringLiteral("the index was written with another database schema"));
}
m_data_base.transaction();
bool ok = true;
for (const QString &table : tables) {
QSqlQuery columns(m_data_base);
columns.exec(QStringLiteral("PRAGMA main.table_info(\"%1\")").arg(table));
QStringList names;
while (columns.next())
names << QStringLiteral("\"%1\"").arg(columns.value(1).toString());
const QString list = names.join(QLatin1Char(','));
ok = ok && query.exec(QStringLiteral("DELETE FROM main.\"%1\"").arg(table))
&& query.exec(QStringLiteral("INSERT INTO main.\"%1\" (%2) SELECT %2 FROM idx.\"%1\"")
.arg(table, list));
}
if (!ok) {
m_data_base.rollback();
detach();
return refuse(QStringLiteral("the index's rows cannot be read"));
}
m_data_base.commit();
folio_headers->clear();
query.exec(QStringLiteral("SELECT uuid, xml FROM idx.folio_header ORDER BY pos"));
while (query.next())
folio_headers->append({QUuid(query.value(0).toString()), query.value(1).toString()});
detach();
//The stores, from their tables, as the document would fill them
m_prefilled_elements.clear();
m_prefilled_folios.clear();
m_prefilled_conductors.clear();
for (PrefillTally &tally : m_prefill_tally) tally = PrefillTally();
query.exec(QStringLiteral("SELECT element_uuid, name, value, show FROM element_information "
"ORDER BY element_uuid, ord"));
while (query.next()) {
const QUuid uuid(query.value(0).toString());
m_prefilled_elements[uuid].addValue(query.value(1).toString(), query.value(2).toString(),
query.value(3).toInt() != 0);
}
QHash<QUuid, QHash<QString, QString>> attributes;
query.exec(QStringLiteral("SELECT conductor_uuid, name, value FROM conductor_properties"));
while (query.next())
attributes[QUuid(query.value(0).toString())].insert(query.value(1).toString(),
query.value(2).toString());
QDomDocument scratch;
for (auto it = attributes.cbegin() ; it != attributes.cend() ; ++it) {
QDomElement e = scratch.createElement(QStringLiteral("conductor"));
for (auto a = it.value().cbegin() ; a != it.value().cend() ; ++a)
e.setAttribute(a.key(), a.value());
ConductorProperties properties;
properties.fromXml(e);
m_prefilled_conductors.insert(it.key(), properties);
}
for (auto it = m_prefilled_elements.cbegin() ; it != m_prefilled_elements.cend() ; ++it)
m_element_information.insert(it.key(), it.value());
for (auto it = m_prefilled_conductors.cbegin() ; it != m_prefilled_conductors.cend() ; ++it)
m_conductor_properties.insert(it.key(), it.value());
m_conductor_attributes = attributes;
for (const auto &header : std::as_const(*folio_headers)) {
QDomDocument document;
if (!document.setContent(header.second)) continue;
TitleBlockProperties properties;
properties.fromXml(document.documentElement());
properties.version = QetVersion::displayedVersion();
properties.collection = QET::QetCollection::Embedded;
m_prefilled_folios.insert(header.first, properties);
m_folio_titleblocks.insert(header.first, properties);
}
m_rows_from_document = true;
m_content_changed = false;
emit dataBaseUpdated();
return true;
}
/**
@brief projectDataBase::foliosWithUnjoinedWires
@return the uuids of the folios whose document has a wire that building
+7
View File
@@ -163,6 +163,13 @@ class projectDataBase : public QObject
void setBuildingFolio(bool building);
bool isBuildingFolio() const {return m_building_folios > 0;}
bool rowsFromDocument() const {return m_rows_from_document;}
QString schemaStamp() const;
QByteArray indexFile(const QList<std::pair<QUuid, QString>> &folio_headers);
void discardFill();
/// The tables are right as they are (filled from an index)
void markCurrent() {m_content_changed = false;}
bool fillFromIndex(const QByteArray &index, QList<std::pair<QUuid, QString>> *folio_headers,
QString *why = nullptr);
QString prefillReport() const;
QSet<QUuid> foliosWithUnjoinedWires() const;
QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) const;
+24 -3
View File
@@ -778,7 +778,7 @@ void Diagram::keyReleaseEvent(QKeyEvent *e)
*/
void Diagram::ensureLoaded()
{
if (m_deferred.isNull()) {
if (isLoaded()) {
return;
}
if (m_project) {
@@ -788,6 +788,23 @@ void Diagram::ensureLoaded()
}
}
/**
@brief Diagram::deferredXml
@return the XML of this folio's items while they are not built: kept
from the project's document, or read now from the folio's part of the
.qetz the project was opened from
*/
QDomElement Diagram::deferredXml()
{
if (m_deferred.isNull() && !m_deferred_part.isEmpty() && m_project) {
m_deferred = m_project->readFolioPart(m_deferred_part);
if (!m_deferred.isNull()) {
m_deferred_part.clear();
}
}
return m_deferred;
}
/**
@brief Diagram::buildDeferred
Build this folio's items from the XML kept for later, and nothing else.
@@ -797,11 +814,15 @@ void Diagram::ensureLoaded()
*/
bool Diagram::buildDeferred()
{
if (m_deferred.isNull()) {
if (isLoaded()) {
return false;
}
QDomElement xml = m_deferred;
QDomElement xml = deferredXml();
m_deferred = QDomElement();
m_deferred_part.clear();
if (xml.isNull()) {
return true; //its part could not be read: QETProject::readFolioPart() said why
}
m_kept_text.clear();
projectDataBase *db = m_project ? m_project->dataBase() : nullptr;
if (db) db->setBuildingFolio(true);
+7 -2
View File
@@ -142,6 +142,7 @@ class Diagram : public QGraphicsScene
QUuid m_uuid = QUuid::createUuid();
/// The XML of this folio's items, while they are not built yet
QDomElement m_deferred;
QString m_deferred_part;
QString m_opened_header;
QByteArray m_kept_text;
bool m_loading_deferred = false;
@@ -212,8 +213,12 @@ class Diagram : public QGraphicsScene
DiagramContent * = nullptr);
/// false while this folio's items wait in its XML to be
/// built (QET_LAZY_FOLIOS)
bool isLoaded() const {return m_deferred.isNull();}
QDomElement deferredXml() const {return m_deferred;}
bool isLoaded() const {return m_deferred.isNull() && m_deferred_part.isEmpty();}
QDomElement deferredXml();
/// The folio's items are in the part @p file of the .qetz the
/// project was opened from, read when they are first needed
/// (stage D2)
void setDeferredPart(const QString &file) {m_deferred = QDomElement(); m_deferred_part = file;}
QString headerXml();
/// The folio's text in the file it was read from, while it is
/// not built (QETProject::toXml())
+155 -9
View File
@@ -30,6 +30,7 @@
#include "autoNum/elementautonumschemecommand.h"
#include "diagram.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "qetgraphicsitem/ViewItem/projectdbmodel.h"
#include "qetapp.h"
@@ -462,7 +463,40 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
// QetContainer::parse() does exactly that, for a .qet and for each part
// of a .qetz alike.
QString read_error;
if (!QetContainer::readDocument(content, &xml_project, &read_error))
//A .qetz holding the index of its database opens from it, the
//folios' parts read when they are built (stage D2);
//any other, or one whose index cannot be used, is read whole
m_opened_from_index = false;
if (QetContainer::isZip(content) && qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1)
{
QList<QetZip::Entry> entries;
QString why;
if (QetZip::fromBytes(content, &entries, &why))
{
QHash<QString, QByteArray> files;
for (const QetZip::Entry &entry : std::as_const(entries))
files.insert(entry.name, entry.data);
QStringList folio_files;
QDomDocument document;
if (files.contains(QStringLiteral("index.sqlite"))
&& QetContainer::join(entries, &document, &why, &folio_files))
{
if (m_data_base.fillFromIndex(files.value(QStringLiteral("index.sqlite")),
&m_folio_headers, &why)
&& m_folio_headers.size() == folio_files.size()) {
xml_project = document;
m_container_files = files;
m_folio_files = folio_files;
m_opened_from_index = true;
} else {
m_data_base.discardFill();
m_folio_headers.clear();
qInfo().noquote() << "Not opened from the index of" << fi.fileName() << ":" << why;
}
}
}
}
if (!m_opened_from_index && !QetContainer::readDocument(content, &xml_project, &read_error))
{
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
if(opened_here) {
@@ -798,6 +832,16 @@ void QETProject::buildFoliosHoldingSymbols(const QSet<QUuid> &symbols)
return;
}
QList<Diagram *> to_build;
//Opened from an index, the database knows where every symbol is
//without reading the folios' parts
if (m_opened_from_index) {
const QSet<QUuid> folios = m_data_base.folioUuidsOfElements(symbols);
for (Diagram *d : std::as_const(m_diagrams_list))
if (!d->isLoaded() && folios.contains(d->uuid()))
to_build << d;
buildFolios(to_build);
return;
}
for (Diagram *d : std::as_const(m_diagrams_list))
{
if (d->isLoaded()) {
@@ -2471,15 +2515,59 @@ bool QETProject::isContainerPath(const QString &path)
document, in parts, zipped (#1440). The file is replaced only once
the whole archive is written.
*/
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error)
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error,
const QByteArray &index)
{
const QList<QetZip::Entry> entries = QetContainer::split(
QList<QetZip::Entry> entries = QetContainer::split(
project,
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
error);
//The project database, for opening without reading the folios
//(stage D2); a reader that does not know it skips it
if (!entries.isEmpty() && !index.isEmpty())
entries.append(QetZip::Entry{QStringLiteral("index.sqlite"), index, true});
return !entries.isEmpty() && QetZip::write(path, entries, error);
}
/**
@brief QETProject::writeContainerFile
Write this project to @p path as a .qetz, with the index of its
database a .qetz is opened from (stage D2) when every
folio, symbol and wire has a uuid of its own -- what opening from the
index relies on, as opening without building folios does.
*/
bool QETProject::writeContainerFile(const QString &path, QString *error)
{
const QDomDocument document = toXml(); //every folio built
return writeContainer(document, path, error, containerIndex());
}
/**
@brief QETProject::containerIndex
@return the index of this project's database for a .qetz, empty when a
folio, symbol or wire has no uuid of its own
*/
QByteArray QETProject::containerIndex()
{
loadFolios();
QSet<QUuid> seen;
QList<std::pair<QUuid, QString>> headers;
auto unique = [&seen](const QUuid &uuid) {
if (uuid.isNull() || seen.contains(uuid)) return false;
seen.insert(uuid);
return true;
};
for (Diagram *diagram : std::as_const(m_diagrams_list)) {
if (!unique(diagram->uuid())) return QByteArray();
for (Element *element : diagram->elements())
if (!unique(element->uuid())) return QByteArray();
for (Conductor *conductor : diagram->conductors())
if (!unique(conductor->uuid())) return QByteArray();
headers.append({diagram->uuid(), diagram->headerXml()});
}
return m_data_base.indexFile(headers);
}
/**
@brief QETProject::besideContainerPath
@return the .qetz a project read from a .qet is saved to: the same
@@ -2539,9 +2627,9 @@ QETResult QETProject::write()
QString error_message;
if (isContainerPath(m_file_path))
{
// A container must contain complete folios, not lazy placeholders.
QDomDocument xml_project(toXml());
if (!writeContainer(xml_project, m_file_path, &error_message))
//A container is written from the whole document: every folio
//built (stage D4 writes it from the database)
if (!writeContainerFile(m_file_path, &error_message))
return(error_message);
}
else
@@ -3059,7 +3147,9 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
//The stores of symbol information, folio title blocks and wire
//properties are filled from the document before the folios are
//built
m_data_base.prefillFromDocument(xml_project);
if (!m_opened_from_index) {
m_data_base.prefillFromDocument(xml_project);
}
//QET_LAZY_FOLIOS leaves the folios to be built when needed only
//when the database can be filled from the document: otherwise
@@ -3075,7 +3165,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
}
//Load the diagrams
readDiagramsXml(xml_project);
if (m_opened_from_index) {
readFoliosFromIndex();
} else {
readDiagramsXml(xml_project);
}
const qint64 diagrams_ms = phase_timer.restart();
//Tie the elements to the numbering schemes they follow
@@ -3104,7 +3198,14 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
m_data_base.blockSignals(false);
m_data_base.setUpdateBlocked(false);
m_data_base.updateDB(xml_project);
if (m_opened_from_index) {
//Filled from the index already: making the folios changed
//nothing their rows say
m_data_base.markCurrent();
m_data_base.updateDB();
} else {
m_data_base.updateDB(xml_project);
}
m_data_base.endPrefill(unloadedFolioCount() > 0);
//Saving may write the folios not built as they were read, when
//building them would write the same: a file written by this
@@ -3145,6 +3246,51 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
m_state = Ok;
}
/**
@brief QETProject::readFoliosFromIndex
readDiagramsXml() for a .qetz opened from its index: each folio made
from its own data as the index has it, its items left in its part
(stage D2)
*/
void QETProject::readFoliosFromIndex()
{
for (int i = 0 ; i < m_folio_headers.size() ; ++i)
{
QDomDocument header;
header.setContent(m_folio_headers.at(i).second);
auto diagram = new Diagram(this);
m_diagrams_list << diagram;
connect(&diagram->border_and_titleblock, &BorderTitleBlock::needFolioData,
this, &QETProject::updateDiagramsFolioData);
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
this, &QETProject::usedTitleBlockTemplateChanged);
m_deferring_folios = true;
QDomElement header_element = header.documentElement();
diagram->initFromXml(header_element);
m_deferring_folios = false;
diagram->setDeferredPart(m_folio_files.at(i));
m_data_base.placeFolio(diagram);
}
updateDiagramsFolioData();
}
/**
@brief QETProject::readFolioPart
@return the folio part @p file of the .qetz this project was opened
from, read now, its engineering data back from project.sqlite; null
if it cannot be read (said in the log)
*/
QDomElement QETProject::readFolioPart(const QString &file)
{
QDomDocument folio;
QString error;
if (!QetContainer::readFolio(m_container_files, file, &folio, &error)) {
qWarning().noquote() << "Unable to read the folio" << file << ":" << error;
return QDomElement();
}
return folio.documentElement();
}
/**
@brief QETProject::readDiagramsXml
Load the diagrams from the xml description of the project.
+12 -1
View File
@@ -443,7 +443,11 @@ class QETProject : public QObject
QString besideContainerPath() const;
QETResult writeCopyForOlderVersions(const QString &path);
static bool writeContainer(const QDomDocument &project, const QString &path,
QString *error = nullptr);
QString *error = nullptr, const QByteArray &index = QByteArray());
bool writeContainerFile(const QString &path, QString *error = nullptr);
QByteArray containerIndex();
QDomElement readFolioPart(const QString &file);
bool openedFromIndex() const {return m_opened_from_index;}
private:
static QUuid derivedUuid(const QByteArray &content);
void refresh();
@@ -528,6 +532,13 @@ class QETProject : public QObject
bool m_kept_folios_trusted = false;
/// Read from a .qet whose path nobody chose since: saved beside it
bool m_opened_from_qet = false;
/// Opened from a .qetz's index (stage D2): the
/// database filled from it, each folio's items in its part
bool m_opened_from_index = false;
QHash<QString, QByteArray> m_container_files;
QStringList m_folio_files;
QList<std::pair<QUuid, QString>> m_folio_headers;
void readFoliosFromIndex();
bool m_kept_folios_stale = false;
/// Something was edited since the project was opened: a table
/// filled from the database may have changed
+3 -3
View File
@@ -919,11 +919,11 @@ QJsonObject LiveServer::snapshot(const QString &raw_path)
if (raw_path.trimmed().isEmpty() || info.isRelative())
return failure(QStringLiteral("path must be absolute"));
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
QDomDocument xml(project->toXml());
QString error;
if (QETProject::isContainerPath(info.absoluteFilePath())
? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error)
: !QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
? !project->writeContainerFile(info.absoluteFilePath(), &error)
: ![&]() { QDomDocument xml(project->toXml());
return QET::writeXmlFile(xml, info.absoluteFilePath(), &error); }())
return failure(QStringLiteral("snapshot not written: %1").arg(error));
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
{QStringLiteral("folio"), e->currentDiagramView()
+3 -3
View File
@@ -434,11 +434,11 @@ bool QetScriptApi::save(const QString &output)
return true;
}
QDomDocument xml_doc(m_project->toXml());
QString error_message;
if (QETProject::isContainerPath(output)
? !QETProject::writeContainer(xml_doc, output, &error_message)
: !QET::writeXmlFile(xml_doc, output, &error_message)) {
? !m_project->writeContainerFile(output, &error_message)
: ![&]() { QDomDocument xml_doc(m_project->toXml());
return QET::writeXmlFile(xml_doc, output, &error_message); }()) {
log(QStringLiteral("qet.save: %1").arg(error_message));
return false;
}
+19
View File
@@ -607,6 +607,25 @@ int main(int argc, char **argv)
check(opened.state() == QETProject::Ok, "a project with a shared uuid opens");
check(opened.unloadedFolioCount() == 0, "a project with a shared uuid opens with every folio built");
check(built_late == 0, "a project with a shared uuid builds its folios as it reads them, not later");
}
//A .qetz opened from its index (stage D2): no
//folio read, each read from its part when shown, drawn and
//linked as when built, saved as the .qet is
{
const QString zipped = output + "/lazy_folios.qetz";
QString error;
check(eager.writeContainerFile(zipped, &error), "saved as a .qetz");
QETProject indexed(zipped);
check(indexed.state() == QETProject::Ok, "the .qetz opens");
check(indexed.openedFromIndex(), "the .qetz opens from its index");
std::printf(".qetz opened from its index: %d of %d folios not built (strip folios %d)\n",
indexed.unloadedFolioCount(), int(folios.size()), int(strip_folios.size()));
check(indexed.unloadedFolioCount() == folios.size() - strip_folios.size(),
"the .qetz opens building only the folios of the terminal strip's terminals");
check(numbering(indexed) == numbering(eager), "symbols' numbering from the index as when built");
for (int i = 0; i < folios.size(); ++i)
same(indexed, i, "shown from the .qetz");
check(indexed.toXml().toString() == eager.toXml().toString(), "the .qetz saves as when built");
}
std::printf("PASS: %d folios\n", int(folios.size()));
} catch (const std::exception &error) {