Merge #1473 into feat/qetz-db-truth-v2: format 4

#1473 keys the .qetz database's rows by the items' uuids, stores
true/false as 1/0 and writes the format to PRAGMA user_version, as
format 3. This branch's own changes (computed text left out of the
folios, tables without keys, folios kept as they were) become format 4;
formats 2 and 3 are still read.

Folios kept as they were copy their rows by folio uuid, and the keys of
those rows are kept from the other items' keys. The document's tables
are named doc_<name> only beside the project database's own tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-14 07:12:57 +13:00
8 changed files with 425 additions and 177 deletions
+8 -3
View File
@@ -33,7 +33,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
@@ -56,7 +56,12 @@ namespace QetContainer
using Pictures = QHash<QString, QByteArray>;
/// 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<QetZip::Entry> 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);
+250 -159
View File
@@ -17,13 +17,15 @@
*/
#include "qetcontainerdb.h"
#include <QRegularExpression>
#include "qetcontainer.h"
#include <QRegularExpression>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <QTextStream>
#include <QUuid>
#include <QVariant>
@@ -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_<name>, 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_<name> (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("(?<![\"\\w])(%1)(?![\\w\"])")
@@ -138,6 +166,17 @@ class Database
return sqlite3_exec(m_db, sql(text).toUtf8().constData(), nullptr, nullptr, nullptr) == SQLITE_OK;
}
QString error() const { return QString::fromUtf8(sqlite3_errmsg(m_db)); }
//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
bool v2() const { return m_format == 2; }
QString folioKey() const { return v2() ? QStringLiteral("id") : QStringLiteral("uuid"); }
QString folioRef() const { return v2() ? QStringLiteral("folio_id") : QStringLiteral("folio_uuid"); }
QString itemKey() const { return v2() ? QStringLiteral("key") : QStringLiteral("uuid"); }
QString elementRef() const { return v2() ? QStringLiteral("element_key") : QStringLiteral("element_uuid"); }
QString showColumn() const { return v2() ? QStringLiteral("NULL") : QStringLiteral("show"); }
QStringList elementData() const { return v2() ? QStringList{QStringLiteral("uuid")} + ElementData : ElementData; }
QStringList conductorData() const { return v2() ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData; }
};
//Owns a prepared statement; binds and reads QVariants (null = SQL NULL)
@@ -196,15 +235,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));
@@ -297,12 +337,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;
}
@@ -310,7 +353,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
@@ -448,31 +492,52 @@ 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 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<QDomElement> &items, QList<bool> *own,
const QSet<QString> &reserved = QSet<QString>())
{
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());
taken.unite(reserved);
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 && !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_<name>, format 2 not
//Beside the project database's own tables the document's are
//named doc_<name>
{
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_<name>
if (!index.isEmpty()) {
db.m_prefix = QStringLiteral("doc_");
unsigned char *copy = static_cast<unsigned char *>(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<unsigned char *>(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));
//<properties> 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 <elementInformation> 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<QString> 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<bool> 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<QString> seen;
for (const QDomElement &d : diagrams) {
const QString uuid = d.attribute(QStringLiteral("uuid"));
if (QUuid(uuid).isNull() || seen.contains(uuid)) { uuid_ids = false; break; }
seen.insert(uuid);
}
}
if (uuid_ids) {
ok = ok && meta.run({QStringLiteral("folio_ids"), QStringLiteral("uuid")});
}
int pos = 0;
for (QDomElement d : diagrams)
{
const QString id = 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<QString, QVariant> 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;
}
+6 -4
View File
@@ -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<Private> 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
+1 -1
View File
@@ -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)
+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.
+1 -1
View File
@@ -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();
+157 -8
View File
@@ -81,6 +81,57 @@ class tst_qetcontainer : public QObject
return copy;
}
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(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<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;