diff --git a/sources/container/qetcontainer.h b/sources/container/qetcontainer.h index 8daa78424..33740306f 100644 --- a/sources/container/qetcontainer.h +++ b/sources/container/qetcontainer.h @@ -33,7 +33,7 @@ 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, + project.sqlite the engineering data (format 2 on, QetContainerDb): title-block fields, symbol information, links, wires folios/.xml one per folio @@ -56,7 +56,12 @@ namespace QetContainer using Pictures = QHash; /// The format written; files of a later format are refused by join() /// unless their min-reader allows this one. - constexpr int Format = 3; + constexpr int Format = 4; + /// 3: the database's rows keyed by the items' uuids, true/false as + /// 1/0, its PRAGMA user_version the format. + /// 4: what QElectroTech computes left out of the folios + /// (dropComputed()), the tables written without keys or indexes. + /// Formats 2 and 3 are still read. QList split(const QDomDocument &project, const QString &writer = QString(), @@ -75,7 +80,7 @@ namespace QetContainer /// The file join() left the picture of @p image in, or an empty string QString pictureFile(const QDomElement &image); - /// Take what QElectroTech computes out of a folio (format 3) + /// Take what QElectroTech computes out of a folio (format 4) void dropComputed(QDomElement diagram, bool terminal_lists); /// Parse @p xml as a .qet is read (QETProject::openFile) bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr); diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index 85349e9fc..aea1b4768 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -17,13 +17,15 @@ */ #include "qetcontainerdb.h" -#include +#include "qetcontainer.h" +#include #include #include #include #include #include +#include #include #include @@ -31,9 +33,9 @@ namespace { const QString KeyAttribute = QStringLiteral("qetz-key"); +const QString FolioAttribute = QStringLiteral("qetz-folio"); //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 //node: the parent would come back a child short. @@ -63,39 +65,63 @@ const QStringList FolioData = { QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"), QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"), QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")}; + //From format 3 on an item's uuid is its row's key, not a column of its + //own: it stays in the folio file, where it says which row is the item's const QStringList ElementData = { - QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"), - QStringLiteral("freezeLabel")}; + QStringLiteral("type"), QStringLiteral("prefix"), QStringLiteral("freezeLabel")}; const QStringList ConductorData = { - QStringLiteral("uuid"), QStringLiteral("element1"), QStringLiteral("terminal1"), + 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")}; + //Attributes held as 0 or 1 (format 3 on). A value QElectroTech does not + //write, "yes" say, stays in the folio file as it was. +const QStringList BooleanData = {QStringLiteral("freezeLabel")}; + QString columns(const QStringList &names) { QStringList quoted; - for (const QString &n : names) quoted << QStringLiteral("\"%1\" TEXT").arg(n); + for (const QString &n : names) + quoted << QStringLiteral("\"%1\" %2").arg(n, BooleanData.contains(n) ? QStringLiteral("INTEGER") + : QStringLiteral("TEXT")); return quoted.join(QStringLiteral(", ")); } + //@p text as attribute @p name is stored, or invalid if it cannot be +QVariant stored(const QString &name, const QString &text) +{ + if (!BooleanData.contains(name)) return text; + if (text == QLatin1String("true")) return 1; + if (text == QLatin1String("false")) return 0; + return {}; +} + + //A stored value as attribute @p name writes it +QString attributeText(const QString &name, const QVariant &value) +{ + if (BooleanData.contains(name) && value.typeId() == QMetaType::Int) + return value.toInt() ? QStringLiteral("true") : QStringLiteral("false"); + return value.toString(); +} + 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," + "CREATE TABLE folio (uuid 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," + "CREATE TABLE folio_property (folio_uuid TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL," + " name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (folio_uuid, ord));" + "CREATE TABLE element (uuid TEXT PRIMARY KEY, folio_uuid 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);" + "CREATE TABLE element_info (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL," + " name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (element_uuid, ord));" + "CREATE TABLE link (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL," + " linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_uuid, ord));" + "CREATE TABLE conductor (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio, %3);" "CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE," " installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);" "CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip," @@ -108,9 +134,9 @@ QString schema() .arg(columns(FolioData), columns(ElementData), columns(ConductorData)); } - //The tables of a document's engineering data. From format 3 on they are - //named doc_, beside the project database's own tables in the same - //file (stage D3) + //The tables of a document's engineering data. While the project + //database's own tables were in the same file they were named + //doc_ (stage D3) const QStringList DocTables = { QStringLiteral("strip_bridge_terminal"), QStringLiteral("strip_bridge"), QStringLiteral("strip_terminal"), QStringLiteral("terminal_strip"), @@ -119,13 +145,15 @@ const QStringList DocTables = { QStringLiteral("conductor"), QStringLiteral("meta")}; //Owns an sqlite3 handle; its SQL names the document's tables by their - //format 2 names, given their prefix here + //plain names, given their prefix here, and its columns as the format of + //the database read names them class Database { public: ~Database() { if (m_db) sqlite3_close(m_db); } sqlite3 *m_db = nullptr; QString m_prefix; + int m_format = QetContainer::Format; QString sql(const QString &text) const { if (m_prefix.isEmpty()) return text; static const QRegularExpression names(QStringLiteral("(? children(const QDomElement &parent, const QString &tag) return list; } - //uuid when present and unique among @p items, else prefix + count -QStringList keys(const QList &items, const QString &prefix) + //The key of each of @p items: its uuid, when it has one no other of + //@p items has and @p reserved does not hold, so the uuid the folio file + //already holds says which row is the item's (@p own true). Else (F100: + //symbols copied with their uuid) a uuid made from the item itself -- + //the same on every save, never from where the item is -- kept in the + //folio file as qetz-key. @p reserved: the keys of rows copied from + //another database (a folio kept as it was) +QStringList keys(const QList &items, QList *own, + const QSet &reserved = QSet()) { + static const QUuid key_namespace(QStringLiteral("{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}")); QHash seen; for (const QDomElement &e : items) - if (e.hasAttribute(QStringLiteral("uuid"))) ++seen[e.attribute(QStringLiteral("uuid"))]; + if (!QUuid(e.attribute(QStringLiteral("uuid"))).isNull()) ++seen[e.attribute(QStringLiteral("uuid"))]; + QSet taken(seen.keyBegin(), seen.keyEnd()); + taken.unite(reserved); + QHash twins; 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)); + if (seen.value(uuid) == 1 && !reserved.contains(uuid)) { + out << uuid; + *own << true; + continue; + } + QString text; + QTextStream stream(&text); + e.save(stream, -1); + stream.flush(); + //Items alike to the byte: how many came before tells them apart + const int twin = twins[text]++; + QString key; + for (int salt = 0 ; key.isEmpty() || taken.contains(key) ; ++salt) + key = QUuid::createUuidV5(key_namespace, QStringLiteral("%1\n%2\n%3").arg(text).arg(twin).arg(salt)) + .toString(); + taken.insert(key); + out << key; + *own << false; } return out; } -} -namespace { - //@p database opened read only in @p db, its tables checked to be plain - //tables: a file is untrusted //Every table of @p db written again without its keys and indexes, in //the order of its rows, and the file compacted: the file a .qetz holds - //is read whole, its lookups indexed once read (openDatabase(), - //projectDataBase::fillFromIndex()), and keys and indexes of text uuids - //were half of it (stage D3b) + //is read whole, its lookups indexed once read (openDatabase()), and keys + //and indexes of text uuids were half of it (stage D3b) bool plainTables(sqlite3 *db) { QStringList tables; @@ -498,6 +563,8 @@ bool plainTables(sqlite3 *db) return ok && run(QStringLiteral("VACUUM")); } + //@p database opened in @p db, its tables checked to be plain tables and + //its format one this reads: a file is untrusted bool openDatabase(Database &db, const QByteArray &database, QString *error) { if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) { @@ -516,7 +583,8 @@ bool openDatabase(Database &db, const QByteArray &database, QString *error) //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")); - //Format 3 names the document's tables doc_, format 2 not + //Beside the project database's own tables the document's are + //named doc_ { Statement doc(db, QStringLiteral("SELECT count(*) FROM sqlite_master WHERE name = 'doc_meta'")); if (doc.next() && doc.value(0).toInt() > 0) db.m_prefix = QStringLiteral("doc_"); @@ -530,11 +598,24 @@ bool openDatabase(Database &db, const QByteArray &database, QString *error) setError(error, QStringLiteral("the project database has no table %1").arg(QLatin1String(table))); return false; } + } + { + Statement format(db, QStringLiteral("SELECT value FROM meta WHERE key = 'format'")); + const QString text = format.next() ? format.value(0).toString() : QString(); + db.m_format = text.toInt(); + if (db.m_format < 2 || db.m_format > QetContainer::Format) { + setError(error, QStringLiteral("the project database is of format %1, this reads 2 to %2") + .arg(text).arg(QetContainer::Format)); + return false; + } } //What the folios are read by: a file holds no index (plainTables()) - for (const char *index : {"folio(id)", "folio_property(folio_id)", "element(key)", "element(folio_id)", - "element_info(element_key)", "link(element_key)", "conductor(key)", "meta(key)"}) { - const QString on = QString::fromLatin1(index); + const QStringList indexes = { + QStringLiteral("folio(%1)").arg(db.folioKey()), QStringLiteral("folio_property(%1)").arg(db.folioRef()), + QStringLiteral("element(%1)").arg(db.itemKey()), QStringLiteral("element(%1)").arg(db.folioRef()), + QStringLiteral("element_info(%1)").arg(db.elementRef()), QStringLiteral("link(%1)").arg(db.elementRef()), + QStringLiteral("conductor(%1)").arg(db.itemKey()), QStringLiteral("meta(key)")}; + for (const QString &on : indexes) { const QString name = QStringLiteral("qetz_read_") + QString(on).replace(QLatin1Char('('), QLatin1Char('_')).remove(QLatin1Char(')')); if (!db.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS %1 ON %2").arg(name, on))) { setError(error, QStringLiteral("the project database cannot be read: %1").arg(db.error())); @@ -546,29 +627,46 @@ bool openDatabase(Database &db, const QByteArray &database, QString *error) } QDomElement entryElement(QDomDocument &document, const QVariant &name_attr, const QString &name, - const QVariant &value, const QVariant &attrs, const QString &default_tag) + const QVariant &value, const QVariant &attrs, const QString &default_tag, + const QVariant &show = QVariant()) { 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()); + if (!show.isNull()) e.setAttribute(QStringLiteral("show"), show.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 + //An item's key: the one it was given, else (format 3 on) its uuid +QString keyOf(const Database &db, QDomElement e, const QString &attribute) +{ + if (e.hasAttribute(attribute)) { + const QString key = e.attribute(attribute); + e.removeAttribute(attribute); + return key; + } + return db.v2() ? QString() : e.attribute(QStringLiteral("uuid")); +} + +QString quoted(const QStringList &names) +{ + return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\""); +} + + //The engineering data of the folio @p d 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 QVariant &attrs, const QString &default_tag, const QVariant &show = QVariant()) { + return entryElement(document, name_attr, name, value, attrs, default_tag, show); }; - const QString id = d.attribute(FolioAttribute); + const QString id = keyOf(db, d, FolioAttribute); if (id.isEmpty()) return true; - d.removeAttribute(FolioAttribute); - Statement f(db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE id = ?") - .arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\""))); + Statement f(db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE %2 = ?") + .arg(quoted(FolioData), db.folioKey())); f.bind({id}); if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; } QVariantList values; @@ -577,7 +675,8 @@ bool restoreDiagram(Database &db, QDomElement d, QString *error) if (!values.at(FolioData.size()).isNull()) { QDomElement props = document.createElement(QStringLiteral("properties")); setAttributesJson(props, values.at(FolioData.size() + 1)); - Statement p(db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE folio_id = ? ORDER BY ord")); + Statement p(db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE %1 = ? ORDER BY ord") + .arg(db.folioRef())); p.bind({id}); while (p.next()) props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2), @@ -589,34 +688,37 @@ bool restoreDiagram(Database &db, QDomElement d, QString *error) } } + const QStringList element_data = db.elementData(); + const QStringList conductor_data = db.conductorData(); 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, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE key = ?") - .arg(QStringLiteral("\"") + ElementData.join(QStringLiteral("\", \"")) + QStringLiteral("\""))); + const QString key = keyOf(db, e, KeyAttribute); + if (key.isEmpty()) continue; + Statement s(db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE %2 = ?") + .arg(quoted(element_data), db.itemKey())); 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(); + for (int i = 0 ; i < element_data.size() + 4 ; ++i) v << s.value(i); + put(e, element_data, v, 0); + const int n = element_data.size(); QDomElement info, links; if (!v.at(n).isNull()) { info = document.createElement(QStringLiteral("elementInformations")); setAttributesJson(info, v.at(n + 1)); - Statement i(db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord")); + Statement i(db, QStringLiteral("SELECT name, value, attrs, %1 FROM element_info WHERE %2 = ? ORDER BY ord") + .arg(db.showColumn(), db.elementRef())); i.bind({key}); while (i.next()) info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2), - QStringLiteral("elementInformation"))); + QStringLiteral("elementInformation"), i.value(3))); 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, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord")); + Statement l(db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE %1 = ? ORDER BY ord") + .arg(db.elementRef())); l.bind({key}); while (l.next()) links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1), @@ -632,16 +734,15 @@ bool restoreDiagram(Database &db, QDomElement d, QString *error) } 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, QStringLiteral("SELECT %1 FROM conductor WHERE key = ?") - .arg(QStringLiteral("\"") + ConductorData.join(QStringLiteral("\", \"")) + QStringLiteral("\""))); + const QString key = keyOf(db, c, KeyAttribute); + if (key.isEmpty()) continue; + Statement s(db, QStringLiteral("SELECT %1 FROM conductor WHERE %2 = ?") + .arg(quoted(conductor_data), db.itemKey())); 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); + for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i); + put(c, conductor_data, v, 0); } } return true; @@ -658,14 +759,14 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB const QByteArray &kept_from) { Database db; - db.m_prefix = QStringLiteral("doc_"); if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) { setError(error, QStringLiteral("cannot create the project database")); return {}; } //The project database's own tables first, when given: one file - //holds both (stage D3) + //holds both (stage D3), the document's named doc_ if (!index.isEmpty()) { + db.m_prefix = QStringLiteral("doc_"); unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(index.size()))); if (!copy) { setError(error, QStringLiteral("out of memory")); return {}; } memcpy(copy, index.constData(), size_t(index.size())); @@ -680,7 +781,7 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB return {}; } //The database of the .qetz this project was read from, for the folios - //kept as they were (stage D4) + //kept as they were (stage D4): of this format (currentFormat()) if (!kept_from.isEmpty()) { unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(kept_from.size()))); if (!copy) { setError(error, QStringLiteral("out of memory")); return {}; } @@ -708,13 +809,14 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB Statement folio(db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?,?)").arg(placeholders(FolioData.size()))); Statement folio_property(db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?)")); Statement element(db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?,?,?)").arg(placeholders(ElementData.size()))); - Statement element_info(db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?)")); + Statement element_info(db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?,?)")); Statement link(db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)")); Statement conductor(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("3")}); + ok = ok && meta.run({QStringLiteral("format"), QString::number(QetContainer::Format)}) + && db.exec(QStringLiteral("PRAGMA user_version = %1").arg(QetContainer::Format)); // of a block, as rows of @p rows (with @p prefix values) auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) { @@ -724,7 +826,24 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB 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)); + << value << attributesJson(p, {QStringLiteral("name")}, QStringLiteral("property"))); + } + }; + //The entries of @p block, as the rows of the + //symbol @p key + auto moveInformation = [&](const QDomElement &block, const QString &key) { + int ord = 0; + for (QDomElement i : children(block, QStringLiteral("elementInformation"))) { + QVariant value; + simpleValue(i, &value); + const QString show = i.attribute(QStringLiteral("show")); + const bool show_stored = show == QLatin1String("0") || show == QLatin1String("1"); + QStringList kept{QStringLiteral("name")}; + if (show_stored) kept << QStringLiteral("show"); + ok = ok && element_info.run({key, ord++, i.hasAttribute(QStringLiteral("name")) + ? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(), + value, show_stored ? QVariant(show.toInt()) : QVariant(), + attributesJson(i, kept, QStringLiteral("elementInformation"))}); } }; @@ -739,7 +858,7 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB 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)}); + value, attributesJson(p, {QStringLiteral("name")}, QStringLiteral("property"))}); } leaveMarker(project_props); } @@ -752,87 +871,78 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB 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")); + //The keys of the rows the folios kept as they were bring, which no + //other item may have + QSet kept_elements, kept_conductors; + for (const QDomElement &d : diagrams) { + if (!d.hasAttribute(KeptPartAttribute)) continue; + Statement e(db, QStringLiteral("SELECT uuid FROM old.element WHERE folio_uuid = ?")); + e.bind({d.attribute(QStringLiteral("uuid"))}); + while (e.next()) kept_elements.insert(e.value(0).toString()); + Statement c(db, QStringLiteral("SELECT uuid FROM old.conductor WHERE folio_uuid = ?")); + c.bind({d.attribute(QStringLiteral("uuid"))}); + while (c.next()) kept_conductors.insert(c.value(0).toString()); + } + QList folio_own, element_own, conductor_own; + const QStringList folio_keys = keys(diagrams, &folio_own); + const QStringList element_keys = keys(all_elements, &element_own, kept_elements); + const QStringList conductor_keys = keys(all_conductors, &conductor_own, kept_conductors); 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 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 = folio_keys.at(pos); + const bool own_id = folio_own.at(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 + //Kept as it was: its rows from the database it was read from, + //under its uuid if (d.hasAttribute(KeptPartAttribute)) { - if (kept_from.isEmpty() || !uuid_ids) { + if (kept_from.isEmpty() || !own_id) { 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("\""))); + Statement copy(db, QStringLiteral("INSERT INTO folio SELECT uuid, ?, %1, props_at, props_attrs " + "FROM old.folio WHERE uuid = ?").arg(quoted(FolioData))); 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")}) { + QStringLiteral("INSERT INTO folio_property SELECT * FROM old.folio_property WHERE folio_uuid = ? ORDER BY rowid"), + QStringLiteral("INSERT INTO element SELECT * FROM old.element WHERE folio_uuid = ? ORDER BY rowid"), + QStringLiteral("INSERT INTO element_info SELECT * FROM old.element_info WHERE element_uuid IN " + "(SELECT uuid FROM old.element WHERE folio_uuid = ?) ORDER BY rowid"), + QStringLiteral("INSERT INTO link SELECT * FROM old.link WHERE element_uuid IN " + "(SELECT uuid FROM old.element WHERE folio_uuid = ?) ORDER BY rowid"), + QStringLiteral("INSERT INTO conductor SELECT * FROM old.conductor WHERE folio_uuid = ? ORDER BY rowid")}) { Statement rows(db, sql); ok = ok && rows.run({id}); } //Its symbols whose information changed since (QETProject::toXml()): //their information rows written as a symbol's block writes them for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-element"))) { - Statement find(db, QStringLiteral("SELECT key FROM element WHERE folio_id = ? AND uuid = ? AND info_at IS NOT NULL")); + Statement find(db, QStringLiteral("SELECT uuid FROM element WHERE folio_uuid = ? AND uuid = ? AND info_at IS NOT NULL")); find.bind({id, edited.attribute(QStringLiteral("uuid"))}); if (!find.next()) { setError(error, QStringLiteral("a symbol changed on folio %1 kept as it was is not in its database").arg(id)); return {}; } const QString key = find.value(0).toString(); - Statement drop(db, QStringLiteral("DELETE FROM element_info WHERE element_key = ?")); + Statement drop(db, QStringLiteral("DELETE FROM element_info WHERE element_uuid = ?")); ok = ok && drop.run({key}); - int ord = 0; - for (QDomElement i : children(edited.firstChildElement(QStringLiteral("elementInformations")), - 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)}); - } + moveInformation(edited.firstChildElement(QStringLiteral("elementInformations")), key); } //Its wires whose properties changed since: their columns for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-conductor"))) { QStringList set; QVariantList values; for (const QString &column : ConductorData) { - if (column == QLatin1String("uuid") || column.startsWith(QLatin1String("element")) - || column.startsWith(QLatin1String("terminal")) || column == QLatin1String("freezeLabel") - || !edited.hasAttribute(column)) + if (column.startsWith(QLatin1String("element")) || column.startsWith(QLatin1String("terminal")) + || BooleanData.contains(column) || !edited.hasAttribute(column)) continue; set << QStringLiteral("\"%1\" = ?").arg(column); values << edited.attribute(column); } values << id << edited.attribute(QStringLiteral("uuid")); - Statement update(db, QStringLiteral("UPDATE conductor SET %1 WHERE folio_id = ? AND uuid = ?") + Statement update(db, QStringLiteral("UPDATE conductor SET %1 WHERE folio_uuid = ? AND uuid = ?") .arg(set.join(QStringLiteral(", ")))); ok = ok && !set.isEmpty() && update.run(values) && sqlite3_changes(db.m_db) == 1; } @@ -853,11 +963,12 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB moveProperties(props, folio_property, {id}); leaveMarker(props); } - d.setAttribute(FolioAttribute, id); + if (!own_id) 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++); + const QString key = element_keys.at(e_index); + const bool own_key = element_own.at(e_index++); QVariantList erow{key, id}; erow += take(e, ElementData); QDomElement info = e.firstChildElement(QStringLiteral("elementInformations")); @@ -871,35 +982,28 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB << (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 (info_at >= 0) + moveInformation(info, key); 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)}); + attributesJson(l, {QStringLiteral("uuid")}, QStringLiteral("link_uuid"))}); } if (info_at >= 0) leaveMarker(info); if (links_at >= 0) leaveMarker(links); - e.setAttribute(KeyAttribute, key); + if (!own_key) 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++); + const QString key = conductor_keys.at(c_index); + const bool own_key = conductor_own.at(c_index++); QVariantList crow{key, id}; crow += take(c, ConductorData); ok = ok && conductor.run(crow); - c.setAttribute(KeyAttribute, key); + if (!own_key) c.setAttribute(KeyAttribute, key); } } } @@ -955,16 +1059,6 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS if (!openDatabase(db, database, error)) 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; { @@ -976,8 +1070,8 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS setAttributesJson(props, meta.value(QStringLiteral("props_attrs"))); Statement s(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"))); + props.appendChild(entryElement(document, 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")); @@ -1064,16 +1158,13 @@ bool QetContainerDb::FolioReader::restore(QDomElement diagram, QString *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) + @brief QetContainerDb::currentFormat + @return true if @p database, a project.sqlite, is of the format this + writes: a folio written as it was read keeps its rows, copied as they + are (stage D4) */ -bool QetContainerDb::foliosByUuid(const QByteArray &database) +bool QetContainerDb::currentFormat(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"); + return openDatabase(db, database, nullptr) && db.m_format == QetContainer::Format; } diff --git a/sources/container/qetcontainerdb.h b/sources/container/qetcontainerdb.h index 1b616acf2..5e34f307d 100644 --- a/sources/container/qetcontainerdb.h +++ b/sources/container/qetcontainerdb.h @@ -32,8 +32,10 @@ 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 + A row's key is its item's uuid, which stays in the folio file. An item + without a uuid of its own -- none, or one another item shares -- gets a + uuid made from what it holds, left in the document as qetz-key or + qetz-folio. 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. */ @@ -55,8 +57,8 @@ namespace QetContainerDb struct Private; std::unique_ptr d; }; - /// true if @p database names its folios by their uuid (format 3) - bool foliosByUuid(const QByteArray &database); + /// true if @p database is of the format this writes + bool currentFormat(const QByteArray &database); } #endif // QETCONTAINERDB_H diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 31c603181..e5600445c 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -3485,7 +3485,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project) m_kept_parts_trusted = m_opened_from_index && unloadedFolioCount() > 0 && m_project_qet_version == QetVersion::currentVersion() && m_data_base.rowsFromDocument() - && QetContainerDb::foliosByUuid(m_container_files.value(QStringLiteral("project.sqlite"))); + && QetContainerDb::currentFormat(m_container_files.value(QStringLiteral("project.sqlite"))); m_kept_folios_stale = false; m_edited_since_open = false; if (m_kept_folios_trusted || m_kept_parts_trusted) diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 2418a9ef0..18f285472 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -1253,4 +1253,5 @@ 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 SQLite3::SQLite3) target_compile_definitions(tst_qetcontainer PRIVATE - "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"" + "QET_FIXTURES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures\"") diff --git a/tests/qttest/fixtures/qetz_format2.qetz b/tests/qttest/fixtures/qetz_format2.qetz new file mode 100644 index 000000000..fdf8d066b Binary files /dev/null and b/tests/qttest/fixtures/qetz_format2.qetz differ diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp index 630dca6db..9e0d0e1cb 100644 --- a/tests/qttest/lazy_folios_probe.cpp +++ b/tests/qttest/lazy_folios_probe.cpp @@ -288,7 +288,7 @@ static QStringList savedLinks(const QString &qetz, const QString &path) db.setDatabaseName(db_path); check(db.open(), "a .qetz's database opened"); QSqlQuery query(db); - check(query.exec(QStringLiteral("SELECT element_key, linked_uuid FROM doc_link ORDER BY element_key, ord")), + check(query.exec(QStringLiteral("SELECT element_uuid, linked_uuid FROM doc_link ORDER BY element_uuid, ord")), "saved links read"); while (query.next()) links[query.value(0).toString()] << query.value(1).toString(); diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index 93676ead3..dd818596a 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -81,6 +81,57 @@ class tst_qetcontainer : public QObject return copy; } + static QByteArray entry(const QList &entries, const QString &name) + { + for (const QetZip::Entry &e : entries) + if (e.name == name) return e.data; + return {}; + } + + static QByteArray folioFiles(const QList &entries) + { + QByteArray all; + for (const QetZip::Entry &e : entries) + if (e.name.startsWith(QStringLiteral("folios/"))) all += e.data; + return all; + } + + // The first column of the first row @p sql gives, from @p database + static QString query(const QByteArray &database, const char *sql) + { + sqlite3 *db = nullptr; + sqlite3_open(":memory:", &db); + unsigned char *copy = static_cast(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 *st = nullptr; + QString out; + if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) + out = QString::fromUtf8(reinterpret_cast(sqlite3_column_text(st, 0))); + sqlite3_finalize(st); + sqlite3_close(db); + return out; + } + + // grafcet.qet as QElectroTech saves it: every folio, symbol and wire + // with a uuid of its own, which the examples' wires and folios lack + static QDomDocument savedProject() + { + QList entries; + QDomDocument project; + QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries); + QetContainer::join(entries, &project); + return project; + } + + static QString backDifference(const QDomDocument &original, const QList &entries) + { + QDomDocument back; + QString error; + if (!QetContainer::join(entries, &back, &error)) return error; + return difference(withoutComputed(original).documentElement(), back.documentElement(), QString()); + } + private slots: void sameDocumentBack_data() { @@ -185,14 +236,14 @@ private slots: return n; }; const QDomNodeList diagrams = original.elementsByTagName(QStringLiteral("diagram")); - QCOMPARE(count("doc_folio"), int(diagrams.size())); - QCOMPARE(count("doc_element"), int(original.elementsByTagName(QStringLiteral("element")).size()) + 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("doc_element_info"), int(original.elementsByTagName(QStringLiteral("elementInformation")).size())); - QCOMPARE(count("doc_link"), int(original.elementsByTagName(QStringLiteral("link_uuid")).size())); - QCOMPARE(count("doc_conductor"), int(original.elementsByTagName(QStringLiteral("conductor")).size())); - QVERIFY(count("doc_folio_property") + count("doc_project_property") > 0); + 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("doc_project_property") > 0); sqlite3_close(db); } @@ -388,8 +439,8 @@ private slots: memcpy(copy, e.data.constData(), size_t(e.data.size())); sqlite3_deserialize(db, "main", copy, e.data.size(), e.data.size(), SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE); - QCOMPARE(sqlite3_exec(db, "ALTER TABLE doc_element RENAME TO element_data;" - "CREATE VIEW doc_element AS SELECT * FROM element_data;", + QCOMPARE(sqlite3_exec(db, "ALTER TABLE element RENAME TO element_data;" + "CREATE VIEW element AS SELECT * FROM element_data;", nullptr, nullptr, nullptr), SQLITE_OK); sqlite3_int64 size = 0; unsigned char *bytes = sqlite3_serialize(db, "main", &size, 0); @@ -403,6 +454,104 @@ private slots: QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error)); } + // A row's key is its item's uuid, which the folio file keeps: no key + // made up from where an item is (format 2 had f1, f2...), no key + // attribute in the folio files of a project as QElectroTech saves it, + // true/false held as 1/0, and the format in the database's + // user_version. + void keysAreTheUuids() + { + const QDomDocument original = savedProject(); + original.elementsByTagName(QStringLiteral("conductor")).at(0).toElement() + .setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("true")); + QString error; + const QList entries = QetContainer::split(original, QString(), &error); + QVERIFY2(!entries.isEmpty(), qPrintable(error)); + const QByteArray database = entry(entries, QStringLiteral("project.sqlite")); + const QByteArray folios = folioFiles(entries); + QVERIFY(!folios.contains("qetz-key")); + QVERIFY(!folios.contains("qetz-folio")); + QCOMPARE(query(database, "PRAGMA user_version"), QString::number(QetContainer::Format)); + const QString first = original.elementsByTagName(QStringLiteral("diagram")).at(0).toElement() + .attribute(QStringLiteral("uuid")); + QCOMPARE(query(database, "SELECT uuid FROM folio WHERE pos = 1"), first); + QCOMPARE(query(database, "SELECT count(*) FROM folio WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0")); + QCOMPARE(query(database, "SELECT count(*) FROM element WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0")); + QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0")); + QCOMPARE(query(database, "SELECT count(*) FROM element WHERE typeof(freezeLabel) <> 'integer'"), + QStringLiteral("0")); + QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE typeof(freezeLabel) <> 'integer'"), + QStringLiteral("0")); + QCOMPARE(query(database, "SELECT sum(freezeLabel) FROM conductor"), QStringLiteral("1")); + const QString d = backDifference(original, entries); + QVERIFY2(d.isEmpty(), qPrintable(d)); + QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE typeof(show) <> 'integer'"), + QStringLiteral("0")); + QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE attrs LIKE '%\"tag\"%'"), + QStringLiteral("0")); + } + + // Two symbols sharing a uuid (F100) each get a key made from what they + // hold, the same on every save, and come back as they were. + void sharedUuidKeyed() + { + QDomDocument original = savedProject(); + const QDomNodeList elements = original.elementsByTagName(QStringLiteral("element")); + QDomElement a, b; + for (int i = 0 ; i < elements.size() && b.isNull() ; ++i) { + const QDomElement e = elements.at(i).toElement(); + if (e.parentNode().nodeName() != QLatin1String("elements")) continue; + if (a.isNull()) a = e; else b = e; + } + QVERIFY(!b.isNull()); + b.setAttribute(QStringLiteral("uuid"), a.attribute(QStringLiteral("uuid"))); + QString error; + const QList once = QetContainer::split(original, QString(), &error); + const QList twice = QetContainer::split(original, QString(), &error); + QVERIFY2(!once.isEmpty(), qPrintable(error)); + QCOMPARE(folioFiles(once).count("qetz-key"), 2); + QCOMPARE(entry(once, QStringLiteral("project.sqlite")), entry(twice, QStringLiteral("project.sqlite"))); + const QString d = backDifference(original, once); + QVERIFY2(d.isEmpty(), qPrintable(d)); + } + + // A true/false attribute QElectroTech would not write stays in the + // folio file as it was. + void oddBooleanKept() + { + QDomDocument original; + QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")), &original)); + const QDomNodeList conductors = original.elementsByTagName(QStringLiteral("conductor")); + QVERIFY(conductors.size() > 0); + conductors.at(0).toElement().setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("yes")); + QString error; + const QList entries = QetContainer::split(original, QString(), &error); + QVERIFY2(!entries.isEmpty(), qPrintable(error)); + QVERIFY(folioFiles(entries).contains("freezeLabel=\"yes\"")); + const QString d = backDifference(original, entries); + QVERIFY2(d.isEmpty(), qPrintable(d)); + } + + // A format 2 file, as QElectroTech 0.200.1 development builds wrote it + // (grafcet.qet saved as .qetz), still opens, and saved again as the + // current format comes back the same. + void format2Read() + { + QList entries; + QString error; + QVERIFY2(QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries, &error), + qPrintable(error)); + QVERIFY(entry(entries, QStringLiteral("manifest.xml")).contains("format=\"2\"")); + QDomDocument project; + QVERIFY2(QetContainer::join(entries, &project, &error), qPrintable(error)); + QVERIFY(project.toString().contains(QStringLiteral(" resaved = QetContainer::split(project, QString(), &error); + QVERIFY2(!resaved.isEmpty(), qPrintable(error)); + const QString d = backDifference(project, resaved); + QVERIFY2(d.isEmpty(), qPrintable(d)); + } + void notAProjectRefused() { QDomDocument back;