#include #include "sqlitebackend.h" SQLiteSaveFile::SQLiteSaveFile(QObject *parent) : QObject(parent) , m_lastError(NoError) , m_lastErrorString(QString()) , m_open(false) , m_dirty(false) { clearNew(); } bool SQLiteSaveFile::open(const QString &filename) { auto dbg = qDebug() << "open"; { /* Emit signals only after unlocking mutex to allow accesses by receivers */ QMutexLocker l(&m_dbMut); QFile f(filename); if (!f.exists()) { setError(FileNotFoundError, QString("File \"%1\"does not exist.").arg(filename)); return false; } QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); new_db.setDatabaseName(f.fileName()); if (!new_db.open()) { qWarning() << "Cannot open db, closing" << filename; setDatabaseError(new_db); new_db.close(); return false; } m_db.close(); m_db = new_db; /* Try to load image, ignore if image is unset */ QSqlQuery q("SELECT data FROM blobs WHERE name = 'image'", m_db); if (!q.next()) return setDatabaseError(q); m_memory = false; m_dirty = false; m_open = true; m_image = q.value(0).toByteArray(); dbg << QString("Loaded %1 byte image").arg(m_image.size()); } fileReload(); return true; } bool SQLiteSaveFile::clearNew() { qDebug() << "clearNew"; { /* Emit signals only after unlocking mutex to allow accesses by receivers */ QMutexLocker l(&m_dbMut); QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); new_db.setDatabaseName(":memory:"); if (!new_db.open()) { setDatabaseError(new_db); qWarning() << "Cannot open new mem db, closing"; new_db.close(); return false; } QSqlDatabase old_db = m_db; m_db = new_db; if (!initDb(true)) { qWarning() << "Cannot init new mem db, closing"; m_db.close(); m_db = old_db; return false; } old_db.close(); m_memory = true; m_dirty = false; m_open = true; m_image = QByteArray(); } fileReload(); /* Call after unlocking mutex to allow accesses by receivers */ return true; } bool SQLiteSaveFile::initDb(bool setCreationDate, const QString &schema_name) { for (auto const &q: { "CREATE TABLE IF NOT EXISTS %1.metadata (key TEXT PRIMARY KEY, value TEXT)", "CREATE TABLE IF NOT EXISTS %1.tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, anchor_x REAL, anchor_y REAL, meta TEXT)", "CREATE TABLE IF NOT EXISTS %1.blobs (name TEXT, data BLOB)"}) { /* We cannot use sqlite's value binding for the table/schema name. This is an sqlite API restriction. * The schema_name parameter here is not user-supplied, so this is safe. */ if (!runSql(QString(q).arg(schema_name))) return false; } if (setCreationDate) { if (!setMetaLocked("creationTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch())) return false; } return true; } bool SQLiteSaveFile::saveAs(const QString &filename) { /* Using the SQLite backup API is a royal PITA on windows due to Qt not including an actual sqlite3.dll there, so we just emulate its functionality. */ qDebug() << "saveAs" << filename; QMutexLocker l(&m_dbMut); QFile::remove(filename); qInfo() << "removed existing file" << filename; if (!runSql("ATTACH DATABASE ? AS target", {filename})) return false; if (!initDb(false, "target")) { /* Fire and forget */ QSqlQuery q("DETACH DATABASE target"); return false; } for (const auto &table : {"metadata", "tags", "blobs"}) { /* We cannot use sqlite's value binding for the table/schema name. This is an sqlite API restriction. * The schema_name parameter here is not user-supplied, so this is safe. */ if (!runSql(QString("INSERT INTO target.%1 SELECT * FROM main.%1").arg(table))) { /* Fire and forget */ QSqlQuery q("DETACH DATABASE target"); return false; } } m_db.close(); /* Re-create DB. We do not want to make any assumptions about the insides of Qt's driver. * It might cache things internally and we might throw it off-balance if we swap out the DB attachment underneath. */ QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); new_db.setDatabaseName(filename); if (!new_db.open()) return setDatabaseError(new_db); m_db = new_db; m_memory = false; m_dirty = false; return true; } QList SQLiteSaveFile::getAllTags() { auto dbg = qDebug() << "getAllTags()"; QMutexLocker l(&m_dbMut); resetError(); QList rv; QSqlQuery q("SELECT id, name, anchor_x, anchor_y, meta FROM tags", m_db); if (!setDatabaseError(q)) return QList(); while (q.next()) { rv << Tag { q.value(0).toLongLong(), q.value(1).toString(), q.value(2).toFloat(), q.value(3).toFloat(), q.value(4).toByteArray() }; } if (!setDatabaseError(q)) return QList(); dbg << QString("%1 tags").arg(rv.size()); return rv; } bool SQLiteSaveFile::updateTag(Tag tag) { auto dbg = qDebug() << "updating tag" << tag.id << tag.name; { QMutexLocker l(&m_dbMut); QSqlQuery q(m_db); q.prepare("SELECT 1 FROM tags WHERE id=? AND name=?"); q.addBindValue(tag.id); q.addBindValue(tag.name); if (!q.exec()) { setDatabaseError(q); return false; } bool nameChanged = !q.next(); if (!setDatabaseError(q)) return false; if (nameChanged) setMetaLocked("lastTagName", tag.name); if (!runSql("UPDATE tags SET name=?, anchor_x=?, anchor_y=?, meta=? WHERE id=?", { tag.name, tag.anchor.x(), tag.anchor.y(), QJsonDocument::fromVariant(tag.metadata).toJson(), tag.id })) { return false; } m_dirty = true; } dbg << "calling handlers"; tagChange(TagChange::CHANGED, tag); return true; } bool SQLiteSaveFile::deleteTag(Tag tag) { { QMutexLocker l(&m_dbMut); if (!runSql("DELETE FROM tags WHERE id=?", {tag.id})) return false; m_dirty = true; } tagChange(TagChange::DELETED, tag); return true; } bool SQLiteSaveFile::createTag(Tag tag) { QSqlQuery q(m_db); auto dbg = qDebug() << "createTag"; { QMutexLocker l(&m_dbMut); resetError(); if (!runSql("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", { tag.name, tag.anchor.x(), tag.anchor.y(), QJsonDocument::fromVariant(tag.metadata).toJson() })) { return false; } m_dirty = true; } auto id = q.lastInsertId().toLongLong(); dbg << "id" << id; tagChange(TagChange::CREATED, Tag(id, tag)); return true; } QString SQLiteSaveFile::getNextAutoTagName() { QVariant lookupResult = getMeta("lastTagName"); QString lastTagName = "U0"; if (lookupResult.isValid() && !lookupResult.toString().isNull()) lastTagName = lookupResult.toString(); QString newName = "U1"; QRegularExpression name_re("^(.*?)(\\d+)$"); auto res = name_re.match(lastTagName); if (res.hasMatch()) { bool ok = false; int numericSuffix = res.captured(2).toInt(&ok); QString stringPrefix = res.captured(1); if (ok) { do { numericSuffix ++; newName = QString("%1%2").arg(stringPrefix).arg(numericSuffix); } while (numericSuffix<10000 && !tagNameIsFree(newName)); } } return newName; } bool SQLiteSaveFile::createTagAt(const QPointF &anchor) { QString newName = getNextAutoTagName(); if (!setMeta("lastTagName", newName)) return false; return createTag(Tag(newName, anchor)); } bool SQLiteSaveFile::tagNameIsFree(const QString &name) { QMutexLocker l(&m_dbMut); QSqlQuery q(m_db); q.prepare("SELECT name FROM tags WHERE name=?"); q.addBindValue(name); if (!q.exec()) { setDatabaseError(q); return true; } if (q.next()) return false; setDatabaseError(q); return true; } bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value) { qDebug() << QString("setMeta: %1=%2").arg(key).arg(value.toString()); return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value}); } bool SQLiteSaveFile::setMetaLocked(std::initializer_list> metas) { for (const auto &meta : metas) { if (!setMetaLocked(meta.first, meta.second)) return false; } return true; } bool SQLiteSaveFile::setMeta(const QString &key, const QVariant &value, bool setDirty) { QMutexLocker l(&m_dbMut); m_dirty = m_dirty || setDirty; return setMetaLocked(key, value); } bool SQLiteSaveFile::setMeta(std::initializer_list> metas, bool setDirty) { QMutexLocker l(&m_dbMut); m_dirty = m_dirty || setDirty; return setMetaLocked(metas); } const QVariant SQLiteSaveFile::getMeta(const QString &key) const { QMutexLocker l(&m_dbMut); return getMetaLocked(key); } const QVariant SQLiteSaveFile::getMetaLocked(const QString &key) const { resetError(); QSqlQuery q(m_db); q.prepare("SELECT value FROM metadata WHERE key=?"); q.addBindValue(key); if (!q.exec()) { setDatabaseError(q); return QVariant(); } if (!q.next()) { setDatabaseError(q); return QVariant(); } qDebug() << QString("getMeta: %1=%2").arg(key).arg(q.value(0).toString()); return q.value(0); } bool SQLiteSaveFile::runSql(QString query, std::initializer_list bindings) { qDebug() << "runSql:" << query << "db: open=" << m_db.isOpen() << "valid=" << m_db.isValid() << "error state" << m_db.lastError().text(); resetError(); QSqlQuery q(m_db); q.prepare(query); for (const QVariant &v : bindings) { q.addBindValue(v); } if (!q.exec()) { return setDatabaseError(q); } return true; } bool SQLiteSaveFile::loadImageFromDisk(const QString &filename) { { QMutexLocker l(&m_dbMut); QFile f(filename); resetError(); if (!f.open(QIODevice::ReadOnly)) { setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString())); return false; } m_image = f.readAll(); if (f.error() != QFileDevice::NoError) { setError(ImageReadError, QString("Failed to read image: %1").arg(f.errorString())); return false; } if (!setMetaLocked({ {"imagePathOriginal", f.fileName()}, {"imagePathAbsolute", QFileInfo(f).absoluteFilePath()}, {"imageLoadedTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()}})) return false; m_dirty = true; if (!runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {m_image})) return false; } /* Emit signal with mutex unlocked */ imageLoaded(m_image); return true; } bool SQLiteSaveFile::reloadImageFromDisk() { const QString &p = getMeta("imagePathOriginal").toString(); if (QFile(p).exists()) return loadImageFromDisk(p); const QString &q = getMeta("imagePathAbsolute").toString(); if (QFile(q).exists()) return loadImageFromDisk(q); return false; } Tag::Tag(long long id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata) : id(id) , name(name) , anchor(QPointF(anchor_x, anchor_y)) , metadata(QJsonDocument::fromJson(metadata).object().toVariantMap()) , valid(true) { } Tag::Tag(long long id, const Tag &other) : id(id) , name(other.name) , anchor(other.anchor) , metadata(other.metadata) , valid(true) { } Tag::Tag(QString name, const QPointF &anchor, const QVariantMap metadata) : id(-1) , name(name) , anchor(anchor) , metadata(metadata) , valid(false) { } bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) const { if (!q.lastError().isValid()) return true; qWarning() << "Query error: " << q.lastError().text(); setError(SQLiteError, QString("Project file database error executing %1: %2").arg(q.executedQuery()).arg(q.lastError().text())); return false; } bool SQLiteSaveFile::setDatabaseError(const QSqlDatabase &db) const { if (!db.lastError().isValid()) return true; qWarning() << "Database error: " << db.lastError().text(); setError(SQLiteError, QString("Project file database error: %1").arg(db.lastError().text())); return false; }