.qetz: key database rows by uuid, type true/false, version the schema

Answers the points arummler raised in discussion #1440 about the
project.sqlite that .qetz files carry (format 2, PR #1462):

- Folio rows were keyed "f1", "f2"... by position, and every symbol and
  wire carried a separate key beside its uuid. A row's key is now the
  item's own uuid, which stays in the folio file; no key attribute is
  added to the folio files of a project as QElectroTech saves it. An item
  without a usable uuid (none, or one another item shares, F100) gets a
  uuid made from its own content, the same on every save, never from
  where it is.
- freezeLabel is stored as 0/1 (INTEGER) and an element information's
  "show" in its own column; a value QElectroTech would not write stays in
  the folio file verbatim. The tag is kept in the JSON only when it is
  not the usual one.
- The database's PRAGMA user_version holds the format.

Format 3. Format 2 files, written by development builds since #1462,
still open: tst_qetcontainer reads one (fixtures/qetz_format2.qetz,
grafcet.qet saved by master aa503aec8) and saves it again.

Checked: tst_qetcontainer 38/38, and 343/343 readable projects of the
legacy corpus round-trip exactly. Every example saved as .qetz then .qet
equals its plain resave, and every format 2 file read by this build
equals what master writes from it. Each new test goes red with the
behaviour it checks broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-14 00:34:35 +13:00
parent aa503aec81
commit 8d118faf36
6 changed files with 318 additions and 69 deletions
+4 -2
View File
@@ -31,7 +31,7 @@
manifest.xml format and oldest format allowed to read it
project.xml the project, each part replaced by
<qetz-part file="..."/>
project.sqlite the engineering data (format 2,
project.sqlite the engineering data (format 2 on,
QetContainerDb): title-block fields, symbol
information, links, wires
folios/<uuid>.xml one per folio
@@ -47,7 +47,9 @@ 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 = 2;
constexpr int Format = 3;
/// 3: the database's rows keyed by the items' uuids, true/false as
/// 1/0, its PRAGMA user_version the format. Format 2 is still read.
QList<QetZip::Entry> split(const QDomDocument &project,
const QString &writer = QString(),
+159 -64
View File
@@ -17,11 +17,15 @@
*/
#include "qetcontainerdb.h"
#include "qetcontainer.h"
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <QTextStream>
#include <QUuid>
#include <QVariant>
#include <sqlite3.h>
@@ -58,39 +62,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,"
@@ -165,15 +193,16 @@ QString placeholders(int n)
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)
//The attributes of @p e but @p skip as JSON, and its tag if it is not
//@p usual_tag
QVariant attributesJson(const QDomElement &e, const QStringList &skip, const QString &usual_tag)
{
QJsonObject o;
if (with_tag) o.insert(QStringLiteral("tag"), e.tagName());
if (e.tagName() != usual_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 (!skip.contains(a.name())) o.insert(a.name(), a.value());
}
if (o.isEmpty()) return QVariant();
return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
@@ -266,12 +295,15 @@ int childIndex(const QDomNode &child)
}
//The attributes @p names of @p e, taken out of it; one that cannot be
//stored stays, its value null
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);
const QVariant v = e.hasAttribute(n) ? stored(n, e.attribute(n)) : QVariant();
values << (v.isValid() ? v : QVariant());
if (v.isValid()) e.removeAttribute(n);
}
return values;
}
@@ -279,7 +311,8 @@ QVariantList take(QDomElement e, const QStringList &names)
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());
if (!values.at(from + i).isNull())
e.setAttribute(names.at(i), attributeText(names.at(i), values.at(from + i)));
}
//Same tags, attributes, text and child order
@@ -417,18 +450,40 @@ QList<QDomElement> children(const QDomElement &parent, const QString &tag)
return list;
}
//uuid when present and unique among @p items, else prefix + count
QStringList keys(const QList<QDomElement> &items, const QString &prefix)
//The key of each of @p items: its uuid, when it has one no other of
//@p items has, 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.
QStringList keys(const QList<QDomElement> &items, QList<bool> *own)
{
static const QUuid key_namespace(QStringLiteral("{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}"));
QHash<QString, int> 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<QString> taken(seen.keyBegin(), seen.keyEnd());
QHash<QString, int> 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) {
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;
}
@@ -454,13 +509,14 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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 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")});
ok = ok && meta.run({QStringLiteral("format"), QString::number(QetContainer::Format)})
&& db.exec(QStringLiteral("PRAGMA user_version = %1").arg(QetContainer::Format));
//<properties> of a block, as rows of @p rows (with @p prefix values)
auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) {
@@ -470,7 +526,7 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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")));
}
};
@@ -485,7 +541,7 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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);
}
@@ -498,14 +554,18 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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"));
QList<bool> folio_own, element_own, conductor_own;
const QStringList folio_keys = keys(diagrams, &folio_own);
const QStringList element_keys = keys(all_elements, &element_own);
const QStringList conductor_keys = keys(all_conductors, &conductor_own);
int e_index = 0, c_index = 0;
int pos = 0;
for (QDomElement d : diagrams)
{
const QString id = QStringLiteral("f%1").arg(++pos);
const QString id = folio_keys.at(pos);
const bool own_id = folio_own.at(pos);
++pos;
QVariantList row{id, pos};
row += take(d, FolioData);
QDomElement props = d.firstChildElement(QStringLiteral("properties"));
@@ -517,11 +577,12 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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"));
@@ -540,9 +601,14 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
for (QDomElement i : children(info, 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, attributesJson(i, QStringLiteral("name"), true)});
value, show_stored ? QVariant(show.toInt()) : QVariant(),
attributesJson(i, kept, QStringLiteral("elementInformation"))});
}
}
if (links_at >= 0) {
@@ -550,20 +616,21 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
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);
}
}
}
@@ -637,10 +704,11 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
}
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
const QVariant &attrs, const QString &default_tag) {
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;
@@ -652,6 +720,35 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta"));
while (s.next()) meta.insert(s.value(0).toString(), s.value(1));
}
//Format 2 (QElectroTech 0.200.1 development builds) keyed its rows
//apart from the items' uuids: "f1", "e1", "c1" where an item had
//none of its own, and every item's key in the folio file
const int format = meta.value(QStringLiteral("format")).toString().toInt();
if (format < 2 || format > QetContainer::Format) {
setError(error, QStringLiteral("the project database is of format %1, this reads 2 to %2")
.arg(meta.value(QStringLiteral("format")).toString()).arg(QetContainer::Format));
return false;
}
const bool v2 = format == 2;
const QString folio_key = v2 ? QStringLiteral("id") : QStringLiteral("uuid");
const QString folio_ref = v2 ? QStringLiteral("folio_id") : QStringLiteral("folio_uuid");
const QString item_key = v2 ? QStringLiteral("key") : QStringLiteral("uuid");
const QString element_ref = v2 ? QStringLiteral("element_key") : QStringLiteral("element_uuid");
const QString show_column = v2 ? QStringLiteral("NULL") : QStringLiteral("show");
const QStringList element_data = v2 ? QStringList{QStringLiteral("uuid")} + ElementData : ElementData;
const QStringList conductor_data = v2 ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData;
auto quoted = [](const QStringList &names) {
return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\"");
};
//An item's key: the one it was given, else (format 3) its uuid
auto keyOf = [v2](QDomElement e, const QString &attribute) {
if (e.hasAttribute(attribute)) {
const QString key = e.attribute(attribute);
e.removeAttribute(attribute);
return key;
}
return v2 ? QString() : e.attribute(QStringLiteral("uuid"));
};
if (meta.contains(QStringLiteral("props_at"))) {
QDomElement props = document.createElement(QStringLiteral("properties"));
setAttributesJson(props, meta.value(QStringLiteral("props_attrs")));
@@ -688,11 +785,10 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
for (QDomElement d : children(project, QStringLiteral("diagram")))
{
const QString id = d.attribute(FolioAttribute);
const QString id = keyOf(d, 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("\"")));
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE %2 = ?")
.arg(quoted(FolioData), folio_key));
f.bind({id});
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
QVariantList values;
@@ -701,7 +797,7 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
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"));
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE %1 = ? ORDER BY ord").arg(folio_ref));
p.bind({id});
while (p.next())
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
@@ -715,32 +811,32 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
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("\"")));
const QString key = keyOf(e, KeyAttribute);
if (key.isEmpty()) continue;
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE %2 = ?")
.arg(quoted(element_data), item_key));
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.m_db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord"));
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM element_info WHERE %2 = ? ORDER BY ord")
.arg(show_column, element_ref));
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.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord"));
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE %1 = ? ORDER BY ord").arg(element_ref));
l.bind({key});
while (l.next())
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
@@ -756,16 +852,15 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
}
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("\"")));
const QString key = keyOf(c, KeyAttribute);
if (key.isEmpty()) continue;
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE %2 = ?")
.arg(quoted(conductor_data), item_key));
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);
}
}
}
+4 -2
View File
@@ -30,8 +30,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.
*/
+2 -1
View File
@@ -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\"")
Binary file not shown.
+149
View File
@@ -69,6 +69,57 @@ class tst_qetcontainer : public QObject
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
}
static QByteArray entry(const QList<QetZip::Entry> &entries, const QString &name)
{
for (const QetZip::Entry &e : entries)
if (e.name == name) return e.data;
return {};
}
static QByteArray folioFiles(const QList<QetZip::Entry> &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<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
memcpy(copy, database.constData(), size_t(database.size()));
sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
sqlite3_stmt *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<const char *>(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<QetZip::Entry> 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<QetZip::Entry> &entries)
{
QDomDocument back;
QString error;
if (!QetContainer::join(entries, &back, &error)) return error;
return difference(original.documentElement(), back.documentElement(), QString());
}
private slots:
void sameDocumentBack_data()
{
@@ -300,6 +351,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<QetZip::Entry> 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<QetZip::Entry> once = QetContainer::split(original, QString(), &error);
const QList<QetZip::Entry> 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<QetZip::Entry> 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<QetZip::Entry> 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("<elementInformation ")));
QVERIFY(!project.toString().contains(QStringLiteral("qetz-")));
const QList<QetZip::Entry> 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;