diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 8993a81fa..7281bf1ca 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -168,6 +168,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/container/miniz/miniz.c ${QET_DIR}/sources/container/miniz/miniz.h ${QET_DIR}/sources/container/qetcontainer.cpp + ${QET_DIR}/sources/container/qetcontainerdb.cpp + ${QET_DIR}/sources/container/qetcontainerdb.h ${QET_DIR}/sources/container/qetcontainer.h ${QET_DIR}/sources/container/qetzip.cpp ${QET_DIR}/sources/container/qetzip.h diff --git a/sources/container/qetcontainer.cpp b/sources/container/qetcontainer.cpp index 631bf0bed..7f27789c4 100644 --- a/sources/container/qetcontainer.cpp +++ b/sources/container/qetcontainer.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "qetcontainer.h" +#include "qetcontainerdb.h" #include #include @@ -180,6 +181,11 @@ QList QetContainer::split(const QDomDocument &project, const QStr return {}; } + //The engineering data out of the document, into project.sqlite + const QByteArray database = QetContainerDb::extract(root, error); + if (database.isEmpty()) + return {}; + QList parts; QSet taken; @@ -245,6 +251,7 @@ QList QetContainer::split(const QDomDocument &project, const QStr entries.append(QetZip::Entry{QStringLiteral("mimetype"), QByteArray(MimeType), false}); entries.append(QetZip::Entry{QStringLiteral("manifest.xml"), serialize(qetz), true}); entries.append(QetZip::Entry{QStringLiteral("project.xml"), serialize(root), true}); + entries.append(QetZip::Entry{QStringLiteral("project.sqlite"), database, true}); entries += parts; for (auto it = pictures.cbegin() ; it != pictures.cend() ; ++it) entries.append(QetZip::Entry{it.key(), it.value(), false}); //PNG is compressed already @@ -318,5 +325,11 @@ bool QetContainer::join(const QList &entries, QDomDocument *proje image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())), image.firstChild()); } + + //The engineering data back from project.sqlite (format 2 on) + if (files.contains(QStringLiteral("project.sqlite")) + && !QetContainerDb::restore(project->documentElement(), + files.value(QStringLiteral("project.sqlite")), error)) + return false; return true; } diff --git a/sources/container/qetcontainer.h b/sources/container/qetcontainer.h index 686818877..d92df084c 100644 --- a/sources/container/qetcontainer.h +++ b/sources/container/qetcontainer.h @@ -31,6 +31,9 @@ manifest.xml format and oldest format allowed to read it project.xml the project, each part replaced by + project.sqlite the engineering data (format 2, + QetContainerDb): title-block fields, symbol + information, links, wires folios/.xml one per folio elements/.../ the project's symbol definitions titleblocks/.titleblock @@ -44,7 +47,7 @@ namespace QetContainer extern const char MimeType[]; /// The format written; files of a later format are refused by join() /// unless their min-reader allows this one. - constexpr int Format = 1; + constexpr int Format = 2; QList split(const QDomDocument &project, const QString &writer = QString(), diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp new file mode 100644 index 000000000..2d90b80c2 --- /dev/null +++ b/sources/container/qetcontainerdb.cpp @@ -0,0 +1,600 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "qetcontainerdb.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace { +const QString KeyAttribute = QStringLiteral("qetz-key"); +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 + //node: the parent would come back a child short. +const QString MarkerTag = QStringLiteral("qetz-db"); +const QString MarkerBlock = QStringLiteral("block"); + +void leaveMarker(QDomElement block) +{ + QDomElement marker = block.ownerDocument().createElement(MarkerTag); + marker.setAttribute(MarkerBlock, block.tagName()); + block.parentNode().replaceChild(marker, block); +} + + //Put @p block where its marker is in @p parent +bool replaceMarker(QDomElement parent, const QDomElement &block) +{ + for (QDomElement m = parent.firstChildElement(MarkerTag) ; !m.isNull() ; m = m.nextSiblingElement(MarkerTag)) { + if (m.attribute(MarkerBlock) == block.tagName()) { + parent.replaceChild(block, m); + return true; + } + } + return false; +} + +const QStringList FolioData = { + QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"), + QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"), + QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")}; +const QStringList ElementData = { + QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"), + QStringLiteral("freezeLabel")}; +const QStringList ConductorData = { + QStringLiteral("uuid"), QStringLiteral("element1"), QStringLiteral("terminal1"), + QStringLiteral("element2"), QStringLiteral("terminal2"), QStringLiteral("num"), + QStringLiteral("formula"), QStringLiteral("function"), QStringLiteral("tension_protocol"), + QStringLiteral("conductor_color"), QStringLiteral("conductor_section"), + QStringLiteral("cable"), QStringLiteral("bus"), QStringLiteral("freezeLabel")}; + +QString columns(const QStringList &names) +{ + QStringList quoted; + for (const QString &n : names) quoted << QStringLiteral("\"%1\" TEXT").arg(n); + return quoted.join(QStringLiteral(", ")); +} + +QString schema() +{ + return QStringLiteral( + "CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);" + "CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, attrs TEXT);" + "CREATE TABLE folio (id TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE, %1," + " props_at INTEGER, props_attrs TEXT);" + "CREATE TABLE folio_property (folio_id TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL," + " name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (folio_id, ord));" + "CREATE TABLE element (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %2," + " info_at INTEGER, info_attrs TEXT, links_at INTEGER, links_attrs TEXT);" + "CREATE TABLE element_info (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL," + " name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));" + "CREATE TABLE link (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL," + " linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));" + "CREATE TABLE conductor (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %3);") + .arg(columns(FolioData), columns(ElementData), columns(ConductorData)); +} + + //Owns an sqlite3 handle +class Database +{ + public: + ~Database() { if (m_db) sqlite3_close(m_db); } + sqlite3 *m_db = nullptr; + bool exec(const QString &sql) { + return sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, nullptr) == SQLITE_OK; + } + QString error() const { return QString::fromUtf8(sqlite3_errmsg(m_db)); } +}; + + //Owns a prepared statement; binds and reads QVariants (null = SQL NULL) +class Statement +{ + public: + Statement(sqlite3 *db, const QString &sql) { + sqlite3_prepare_v2(db, sql.toUtf8().constData(), -1, &m_stmt, nullptr); + } + ~Statement() { sqlite3_finalize(m_stmt); } + bool ok() const { return m_stmt; } + void bind(const QVariantList &values) { + sqlite3_reset(m_stmt); + sqlite3_clear_bindings(m_stmt); + for (int i = 0 ; i < values.size() ; ++i) { + const QVariant &v = values.at(i); + if (v.isNull()) { + sqlite3_bind_null(m_stmt, i + 1); + } else if (v.typeId() == QMetaType::Int) { + sqlite3_bind_int(m_stmt, i + 1, v.toInt()); + } else { + const QByteArray utf8 = v.toString().toUtf8(); + sqlite3_bind_text(m_stmt, i + 1, utf8.constData(), utf8.size(), SQLITE_TRANSIENT); + } + } + } + bool run(const QVariantList &values) { + bind(values); + return sqlite3_step(m_stmt) == SQLITE_DONE; + } + bool next() { return sqlite3_step(m_stmt) == SQLITE_ROW; } + QVariant value(int column) const { + switch (sqlite3_column_type(m_stmt, column)) { + case SQLITE_NULL: return QVariant(); + case SQLITE_INTEGER: return QVariant(sqlite3_column_int(m_stmt, column)); + default: return QVariant(QString::fromUtf8( + reinterpret_cast(sqlite3_column_text(m_stmt, column)), + sqlite3_column_bytes(m_stmt, column))); + } + } + sqlite3_stmt *m_stmt = nullptr; +}; + +void setError(QString *error, const QString &text) { if (error) *error = text; } + +QString placeholders(int n) +{ + QStringList q; + for (int i = 0 ; i < n ; ++i) q << QStringLiteral("?"); + return q.join(QLatin1Char(',')); +} + + //The attributes of @p e but @p skip, and its tag, as JSON +QVariant attributesJson(const QDomElement &e, const QString &skip = QString(), bool with_tag = false) +{ + QJsonObject o; + if (with_tag) o.insert(QStringLiteral("tag"), e.tagName()); + const QDomNamedNodeMap map = e.attributes(); + for (int i = 0 ; i < map.count() ; ++i) { + const QDomAttr a = map.item(i).toAttr(); + if (a.name() != skip) o.insert(a.name(), a.value()); + } + if (o.isEmpty()) return QVariant(); + return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact)); +} + +void setAttributesJson(QDomElement e, const QVariant &json) +{ + if (json.isNull()) return; + const QJsonObject o = QJsonDocument::fromJson(json.toString().toUtf8()).object(); + for (auto it = o.begin() ; it != o.end() ; ++it) + if (it.key() != QLatin1String("tag") && it.key() != QLatin1String("qetz-ws")) + e.setAttribute(it.key(), it.value().toString()); +} + + //A list block's attributes, and the whitespace between its entries if + //it has any (a document read from a file does; a save has none): + //"qetz-ws" holds, for each entry and after the last, the text before it. +QVariant blockJson(const QDomElement &block) +{ + QJsonObject o; + const QDomNamedNodeMap map = block.attributes(); + for (int i = 0 ; i < map.count() ; ++i) { + const QDomAttr a = map.item(i).toAttr(); + o.insert(a.name(), a.value()); + } + QJsonArray ws; + QString pending; + bool any = false; + for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) { + if (n.isText()) { pending += n.nodeValue(); any = true; } + else { ws.append(pending); pending.clear(); } + } + ws.append(pending); + if (any) o.insert(QStringLiteral("qetz-ws"), ws); + if (o.isEmpty()) return QVariant(); + return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact)); +} + + //The whitespace blockJson() kept, back between the entries of @p block +void restoreWhitespace(QDomElement block, const QVariant &json) +{ + if (json.isNull()) return; + const QJsonArray ws = QJsonDocument::fromJson(json.toString().toUtf8()).object() + .value(QStringLiteral("qetz-ws")).toArray(); + if (ws.isEmpty()) return; + QDomDocument document = block.ownerDocument(); + QList entries; + for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) entries << n; + for (int i = 0 ; i < entries.size() && i < ws.size() ; ++i) + if (!ws.at(i).toString().isEmpty()) + block.insertBefore(document.createTextNode(ws.at(i).toString()), entries.at(i)); + if (ws.size() > entries.size() && !ws.last().toString().isEmpty()) + block.appendChild(document.createTextNode(ws.last().toString())); +} + + //The value of a list entry: its only child, a text node, or none +bool simpleValue(const QDomElement &e, QVariant *value) +{ + const QDomNodeList kids = e.childNodes(); + if (kids.size() == 0) { *value = QVariant(); return true; } + if (kids.size() == 1 && kids.at(0).isText() && !kids.at(0).isCDATASection()) { + *value = kids.at(0).nodeValue(); + return true; + } + return false; +} + + //A list block -- , , -- + //this can move: entries named @p entry with simple values, and + //whitespace between them that a save would write anyway. +bool listBlock(const QDomElement &block, const QString &entry) +{ + for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) { + if (n.isElement()) { + QVariant v; + if (n.toElement().tagName() != entry || !simpleValue(n.toElement(), &v)) return false; + } else if (!(n.isText() && !n.isCDATASection() && n.nodeValue().trimmed().isEmpty())) { + return false; + } + } + return true; +} + +int childIndex(const QDomNode &child) +{ + int i = 0; + for (QDomNode n = child.parentNode().firstChild() ; !n.isNull() ; n = n.nextSibling(), ++i) + if (n == child) return i; + return -1; +} + + +QVariantList take(QDomElement e, const QStringList &names) +{ + QVariantList values; + for (const QString &n : names) { + values << (e.hasAttribute(n) ? QVariant(e.attribute(n)) : QVariant()); + e.removeAttribute(n); + } + return values; +} + +void put(QDomElement e, const QStringList &names, const QVariantList &values, int from) +{ + for (int i = 0 ; i < names.size() ; ++i) + if (!values.at(from + i).isNull()) e.setAttribute(names.at(i), values.at(from + i).toString()); +} + +QList children(const QDomElement &parent, const QString &tag) +{ + QList list; + for (QDomElement e = parent.firstChildElement(tag) ; !e.isNull() ; e = e.nextSiblingElement(tag)) + list << e; + return list; +} + + //uuid when present and unique among @p items, else prefix + count +QStringList keys(const QList &items, const QString &prefix) +{ + QHash seen; + for (const QDomElement &e : items) + if (e.hasAttribute(QStringLiteral("uuid"))) ++seen[e.attribute(QStringLiteral("uuid"))]; + QStringList out; + int made_up = 0; + for (const QDomElement &e : items) { + const QString uuid = e.attribute(QStringLiteral("uuid")); + out << ((!uuid.isEmpty() && seen.value(uuid) == 1) + ? uuid : prefix + QString::number(++made_up)); + } + return out; +} +} + +/** + @brief QetContainerDb::extract + Move the engineering data of @p project (the element, its + 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) +{ + Database db; + if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK || !db.exec(schema())) { + setError(error, QStringLiteral("cannot create the project database")); + return {}; + } + db.exec(QStringLiteral("BEGIN")); + + Statement meta(db.m_db, QStringLiteral("INSERT INTO meta VALUES (?,?)")); + Statement project_property(db.m_db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?)")); + Statement folio(db.m_db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?,?)").arg(placeholders(FolioData.size()))); + Statement folio_property(db.m_db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?)")); + Statement element(db.m_db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?,?,?)").arg(placeholders(ElementData.size()))); + Statement element_info(db.m_db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?)")); + Statement link(db.m_db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)")); + Statement conductor(db.m_db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size()))); + bool ok = meta.ok() && project_property.ok() && folio.ok() && folio_property.ok() + && element.ok() && element_info.ok() && link.ok() && conductor.ok(); + + ok = ok && meta.run({QStringLiteral("format"), QStringLiteral("2")}); + + // of a block, as rows of @p rows (with @p prefix values) + auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) { + int ord = 0; + for (QDomElement p : children(block, QStringLiteral("property"))) { + QVariant value; + simpleValue(p, &value); + ok = ok && rows.run(QVariantList(prefix) << ord++ << (p.hasAttribute(QStringLiteral("name")) + ? QVariant(p.attribute(QStringLiteral("name"))) : QVariant()) + << value << attributesJson(p, QStringLiteral("name"), true)); + } + }; + + //Project properties + const QDomElement project_props = project.firstChildElement(QStringLiteral("properties")); + if (!project_props.isNull() && listBlock(project_props, QStringLiteral("property"))) { + ok = ok && meta.run({QStringLiteral("props_at"), QString::number(childIndex(project_props))}); + ok = ok && meta.run({QStringLiteral("props_attrs"), blockJson(project_props)}); + int ord = 0; + for (QDomElement p : children(project_props, QStringLiteral("property"))) { + QVariant value; + simpleValue(p, &value); + ok = ok && project_property.run({ord++, p.hasAttribute(QStringLiteral("name")) + ? QVariant(p.attribute(QStringLiteral("name"))) : QVariant(), + value, attributesJson(p, QStringLiteral("name"), true)}); + } + leaveMarker(project_props); + } + + const QList diagrams = children(project, QStringLiteral("diagram")); + QList all_elements, all_conductors; + for (const QDomElement &d : diagrams) { + for (const QDomElement &list : children(d, QStringLiteral("elements"))) + all_elements += children(list, QStringLiteral("element")); + for (const QDomElement &list : children(d, QStringLiteral("conductors"))) + all_conductors += children(list, QStringLiteral("conductor")); + } + const QStringList element_keys = keys(all_elements, QStringLiteral("e")); + const QStringList conductor_keys = keys(all_conductors, QStringLiteral("c")); + int e_index = 0, c_index = 0; + + int pos = 0; + for (QDomElement d : diagrams) + { + const QString id = QStringLiteral("f%1").arg(++pos); + QVariantList row{id, pos}; + row += take(d, FolioData); + QDomElement props = d.firstChildElement(QStringLiteral("properties")); + const bool move_props = !props.isNull() && listBlock(props, QStringLiteral("property")); + row << (move_props ? QVariant(childIndex(props)) : QVariant()) + << (move_props ? blockJson(props) : QVariant()); + ok = ok && folio.run(row); + if (move_props) { + moveProperties(props, folio_property, {id}); + leaveMarker(props); + } + d.setAttribute(FolioAttribute, id); + + for (const QDomElement &list : children(d, QStringLiteral("elements"))) { + for (QDomElement e : children(list, QStringLiteral("element"))) { + const QString key = element_keys.at(e_index++); + QVariantList erow{key, id}; + erow += take(e, ElementData); + QDomElement info = e.firstChildElement(QStringLiteral("elementInformations")); + QDomElement links = e.firstChildElement(QStringLiteral("links_uuids")); + if (!info.isNull() && !listBlock(info, QStringLiteral("elementInformation"))) info = QDomElement(); + if (!links.isNull() && !listBlock(links, QStringLiteral("link_uuid"))) links = QDomElement(); + const int info_at = info.isNull() ? -1 : childIndex(info); + const int links_at = links.isNull() ? -1 : childIndex(links); + erow << (info_at < 0 ? QVariant() : QVariant(info_at)) + << (info_at < 0 ? QVariant() : blockJson(info)) + << (links_at < 0 ? QVariant() : QVariant(links_at)) + << (links_at < 0 ? QVariant() : blockJson(links)); + ok = ok && element.run(erow); + if (info_at >= 0) { + int ord = 0; + for (QDomElement i : children(info, QStringLiteral("elementInformation"))) { + QVariant value; + simpleValue(i, &value); + ok = ok && element_info.run({key, ord++, i.hasAttribute(QStringLiteral("name")) + ? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(), + value, attributesJson(i, QStringLiteral("name"), true)}); + } + } + if (links_at >= 0) { + int ord = 0; + for (QDomElement l : children(links, QStringLiteral("link_uuid"))) + ok = ok && link.run({key, ord++, l.hasAttribute(QStringLiteral("uuid")) + ? QVariant(l.attribute(QStringLiteral("uuid"))) : QVariant(), + attributesJson(l, QStringLiteral("uuid"), true)}); + } + if (info_at >= 0) leaveMarker(info); + if (links_at >= 0) leaveMarker(links); + e.setAttribute(KeyAttribute, key); + } + } + for (const QDomElement &list : children(d, QStringLiteral("conductors"))) { + for (QDomElement c : children(list, QStringLiteral("conductor"))) { + const QString key = conductor_keys.at(c_index++); + QVariantList crow{key, id}; + crow += take(c, ConductorData); + ok = ok && conductor.run(crow); + c.setAttribute(KeyAttribute, key); + } + } + } + + if (!ok || !db.exec(QStringLiteral("COMMIT"))) { + setError(error, QStringLiteral("cannot fill the project database: %1").arg(db.error())); + return {}; + } + + sqlite3_int64 size = 0; + unsigned char *bytes = sqlite3_serialize(db.m_db, "main", &size, 0); + if (!bytes) { + setError(error, QStringLiteral("cannot write the project database")); + return {}; + } + const QByteArray file(reinterpret_cast(bytes), qsizetype(size)); + sqlite3_free(bytes); + return file; +} + +/** + @brief QetContainerDb::restore + Put the engineering data in @p database back into @p project (the + element, its folios joined back already). +*/ +bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QString *error) +{ + QDomDocument document = project.ownerDocument(); + Database db; + if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) { + setError(error, QStringLiteral("cannot open the project database")); + return false; + } + unsigned char *copy = static_cast(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"}) { + 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) { + 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; + }; + + //Project properties + QHash meta; + { + Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta")); + while (s.next()) meta.insert(s.value(0).toString(), s.value(1)); + } + if (meta.contains(QStringLiteral("props_at"))) { + QDomElement props = document.createElement(QStringLiteral("properties")); + setAttributesJson(props, meta.value(QStringLiteral("props_attrs"))); + Statement s(db.m_db, QStringLiteral("SELECT name, value, attrs FROM project_property ORDER BY ord")); + while (s.next()) + props.appendChild(entry(s.value(0), QStringLiteral("name"), s.value(1), s.value(2), + QStringLiteral("property"))); + restoreWhitespace(props, meta.value(QStringLiteral("props_attrs"))); + if (!replaceMarker(project, props)) { + setError(error, QStringLiteral("the project's properties have no place")); + return false; + } + } + + 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); + } + } + } + return true; +} diff --git a/sources/container/qetcontainerdb.h b/sources/container/qetcontainerdb.h new file mode 100644 index 000000000..a70dfb1fe --- /dev/null +++ b/sources/container/qetcontainerdb.h @@ -0,0 +1,44 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QETCONTAINERDB_H +#define QETCONTAINERDB_H + +#include +#include + +/** + @brief The QetContainerDb namespace + The engineering data of a project, moved between its document and the + project.sqlite of a .qetz: what a folio's title block says, a symbol's + information, a coil's contacts, a wire's ends and number. In a .qetz + these live only in the database; the folio files keep where things are + drawn. extract() takes them out of the document into a database, + restore() puts them back, so that restore(extract(d)) is d. + + Each moved item leaves its key in the document (qetz-key, qetz-folio): + its uuid where that is unique, else a made-up one. A block of a shape + this does not model -- a value holding elements, say -- stays in the + document, so nothing can be lost by moving. +*/ +namespace QetContainerDb +{ + QByteArray extract(QDomElement project, QString *error = nullptr); + bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr); +} + +#endif // QETCONTAINERDB_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 569b8c195..8d9cbaa61 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -1180,10 +1180,11 @@ add_executable( tst_qetcontainer tst_qetcontainer.cpp ${QET_DIR}/sources/container/qetcontainer.cpp + ${QET_DIR}/sources/container/qetcontainerdb.cpp ${QET_DIR}/sources/container/qetzip.cpp ${QET_DIR}/sources/container/miniz/miniz.c) add_test(NAME tst_qetcontainer COMMAND tst_qetcontainer) target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources) -target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml) +target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3) target_compile_definitions(tst_qetcontainer PRIVATE "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index b124a1854..9a91f3c60 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -8,6 +8,8 @@ #include "container/qetcontainer.h" +#include + // A project document split into the parts of a .qetz, zipped, unzipped and // joined back is the document it was: every example, and every .qet in the // folder QET_CONTAINER_CORPUS names, if set -- exactly, whitespace text @@ -132,6 +134,53 @@ private slots: QCOMPARE(whitespace_nodes(back), 0); } + // The engineering data is in project.sqlite and nowhere else: the folio + // files hold no symbol information, links, wire numbers or title-block + // fields, and the database holds them all (industrial.qet: 50 folios). + void databaseHoldsTheData() + { + QDomDocument original; + const QList entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original); + QVERIFY(!entries.isEmpty()); + QByteArray database; + for (const QetZip::Entry &e : entries) { + if (e.name == QLatin1String("project.sqlite")) database = e.data; + if (e.name.startsWith(QStringLiteral("folios/")) || e.name == QLatin1String("project.xml")) { + QVERIFY2(!e.data.contains("]* num="))), + qPrintable(e.name)); + } + } + QVERIFY(database.startsWith("SQLite format 3")); + + sqlite3 *db = nullptr; + QCOMPARE(sqlite3_open(":memory:", &db), SQLITE_OK); + unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(database.size()))); + memcpy(copy, database.constData(), size_t(database.size())); + QCOMPARE(sqlite3_deserialize(db, "main", copy, database.size(), database.size(), + SQLITE_DESERIALIZE_FREEONCLOSE), SQLITE_OK); + auto count = [db](const char *table) { + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, (QByteArray("SELECT count(*) FROM ") + table).constData(), -1, &st, nullptr); + sqlite3_step(st); + const int n = sqlite3_column_int(st, 0); + sqlite3_finalize(st); + return n; + }; + const QDomNodeList diagrams = original.elementsByTagName(QStringLiteral("diagram")); + QCOMPARE(count("folio"), int(diagrams.size())); + QCOMPARE(count("element"), int(original.elementsByTagName(QStringLiteral("element")).size()) + - int(original.elementsByTagName(QStringLiteral("collection")).at(0).toElement() + .elementsByTagName(QStringLiteral("element")).size())); + QCOMPARE(count("element_info"), int(original.elementsByTagName(QStringLiteral("elementInformation")).size())); + QCOMPARE(count("link"), int(original.elementsByTagName(QStringLiteral("link_uuid")).size())); + QCOMPARE(count("conductor"), int(original.elementsByTagName(QStringLiteral("conductor")).size())); + QVERIFY(count("folio_property") + count("project_property") > 0); + sqlite3_close(db); + } + // The folio pictures leave the folios, once each void picturesAreFiles() { @@ -161,7 +210,8 @@ private slots: QVERIFY(!entries.isEmpty()); for (QetZip::Entry &e : entries) if (e.name == QLatin1String("manifest.xml")) - e.data.replace("min-reader=\"1\"", "min-reader=\"9\""); + e.data = QString::fromUtf8(e.data).replace(QRegularExpression(QStringLiteral("min-reader=\"\\d+\"")), + QStringLiteral("min-reader=\"9\"")).toUtf8(); QDomDocument back; QString error; QVERIFY(!QetContainer::join(entries, &back, &error));