From f6e181494581b62fa19ae152eaefa9b5fd92f404 Mon Sep 17 00:00:00 2001 From: grendervill Date: Mon, 7 Sep 2026 16:36:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20=D1=81=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B1=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=20+=20=D1=84=D0=B8=D0=BA=D1=81=20syncFile=20(=D0=BD?= =?UTF-8?q?=D0=B5=20=D1=80=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B2=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D1=8F=D1=89=D0=B8=D0=B5=20=D1=80=D1=91=D0=B1=D1=80=D0=B0?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db_test: граф index->a, index->b, a->b + изолят c; прямые/обратные ссылки, достижимость через WITH RECURSIVE, ~sql из одного файла в другие, межфайловые свойства, подключение изолята, удаление - Database::clearFileData: syncFile больше не удаляет строку files и входящие links (id сохраняется, граф целый) --- src/db/Database.cpp | 20 +++++-- src/db/Database.h | 3 + tests/db_test.cpp | 139 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/src/db/Database.cpp b/src/db/Database.cpp index c261bb5..536eae0 100644 --- a/src/db/Database.cpp +++ b/src/db/Database.cpp @@ -244,6 +244,20 @@ int Database::tagId(const QString &tag, bool create) return q.lastInsertId().toInt(); } +void Database::clearFileData(int fid) +{ + QSqlQuery q(m_db); + q.prepare(QStringLiteral("DELETE FROM links WHERE src_id = ?")); + q.addBindValue(fid); + q.exec(); + q.prepare(QStringLiteral("DELETE FROM file_tags WHERE file_id = ?")); + q.addBindValue(fid); + q.exec(); + q.prepare(QStringLiteral("DELETE FROM properties WHERE file_id = ?")); + q.addBindValue(fid); + q.exec(); +} + void Database::deleteFileRows(int fid) { QSqlQuery q(m_db); @@ -374,10 +388,8 @@ void Database::syncFile(const QString &path) } const int fid = fileId(clean, false); if (fid >= 0) - deleteFileRows(fid); - else { - // строки нет (например, после ручной чистки) — создастся в indexOneFile - } + clearFileData(fid); + // строки нет (например, после ручной чистки) — создастся в indexOneFile indexOneFile(clean); deleteOrphanTags(); emit changed(); diff --git a/src/db/Database.h b/src/db/Database.h index ebdc0f7..8cc60c6 100644 --- a/src/db/Database.h +++ b/src/db/Database.h @@ -70,6 +70,9 @@ private: int fileId(const QString &path, bool create); int tagId(const QString &tag, bool create); void deleteFileRows(int fileId); + // Снести только собственные данные файла (исходящие ссылки, теги, свойства). + // Строка files и входящие ссылки целы — id сохраняется, граф не рвётся. + void clearFileData(int fileId); void deleteOrphanTags(); void indexOneFile(const QString &path); static bool isReadOnlySql(const QString &sql, QString *error); diff --git a/tests/db_test.cpp b/tests/db_test.cpp index 5325af5..63dfa9c 100644 --- a/tests/db_test.cpp +++ b/tests/db_test.cpp @@ -235,6 +235,145 @@ int main(int argc, char *argv[]) return fail("plain body kept", t.left(200)); } + // --- Связность: вызовы из одного файла в другие через базу --- + // Граф: index -> a, index -> b, a -> b, c — изолят. + QTemporaryDir dir2; + if (!dir2.isValid()) + return fail("temp dir2"); + const QString v2 = dir2.path(); + writeFile(v2 + "/index.md", + "---\nтип: хаб\n---\n\n# Хаб\n\n[[a]] [[b]]\n\n" + "```~sql\n" + "SELECT f2.title AS связанные FROM files f1\n" + " JOIN links l ON l.src_id = f1.id\n" + " JOIN files f2 ON f2.id = l.dst_id\n" + "WHERE f1.path LIKE '%index.md' ORDER BY f2.title;\n" + "```\n"); + writeFile(v2 + "/a.md", "# A #общее\n\n[[b]]\n"); + writeFile(v2 + "/b.md", "# B #общее\n"); + writeFile(v2 + "/c.md", "# C\n\nИзолят\n"); + VaultIndex index2; + index2.rebuild(v2); + if (index2.fileCount() != 4) + return fail("link index count", QString::number(index2.fileCount())); + Database db2; + db2.setIndex(&index2); + if (!db2.open(v2)) + return fail("link db open"); + QVariantMap st2 = db2.stats(); + if (st2.value("files").toInt() != 4) + return fail("link files"); + if (st2.value("links").toInt() != 3) // index->a, index->b, a->b + return fail("link edges", QString::number(st2.value("links").toInt())); + if (st2.value("tags").toInt() != 1) + return fail("link tags"); + + auto titles = [&](const QVariantMap &r) { + QStringList out; + for (const QVariant &row : r.value("rows").toList()) + out.append(row.toList().value(0).toString()); + return out; + }; + // Прямые соседи index.md + { + QVariantMap r = db2.queryTable( + "SELECT f2.title FROM files f1 JOIN links l ON l.src_id = f1.id " + "JOIN files f2 ON f2.id = l.dst_id " + "WHERE f1.path LIKE '%index.md' ORDER BY f2.title"); + if (!r.value("error").toString().isEmpty()) + return fail("fwd err", r.value("error").toString()); + if (titles(r) != QStringList({"a", "b"})) + return fail("fwd index", titles(r).join(",")); + } + // Обратные ссылки на b.md (кто ссылается) + { + QVariantMap r = db2.queryTable( + "SELECT f.title FROM files f JOIN links l ON l.src_id = f.id " + "JOIN files d ON d.id = l.dst_id " + "WHERE d.path LIKE '%b.md' ORDER BY f.title"); + if (titles(r) != QStringList({"a", "index"})) + return fail("backlinks b", titles(r).join(",")); + } + // Достижимость из index.md за любое число шагов (рекурсия) — c.md вне + { + QVariantMap r = db2.queryTable( + "WITH RECURSIVE reach(id) AS (" + " SELECT id FROM files WHERE path LIKE '%index.md'" + " UNION" + " SELECT l.dst_id FROM links l JOIN reach r ON l.src_id = r.id)" + " SELECT f.title FROM files f JOIN reach r ON r.id = f.id" + " WHERE f.path NOT LIKE '%index.md' ORDER BY f.title"); + if (!r.value("error").toString().isEmpty()) + return fail("reach err", r.value("error").toString()); + if (titles(r) != QStringList({"a", "b"})) + return fail("reach index", titles(r).join(",")); + } + // Изолят: у c.md нет рёбер ни в одну сторону + { + QVariantMap r = db2.queryTable( + "SELECT COUNT(*) FROM links l JOIN files f ON f.id IN (l.src_id, l.dst_id)" + " WHERE f.path LIKE '%c.md'"); + if (r.value("rows").toList().value(0).toList().value(0).toString() != "0") + return fail("isolate c"); + } + // ~sql внутри index.md видит другие файлы (и не видит изолят) + { + MdParser p; + p.setDatabaseForVault(v2, &db2); + QFile f(v2 + "/index.md"); + f.open(QIODevice::ReadOnly); + const QString h = p.render(QString::fromUtf8(f.readAll()), v2, v2); + if (!h.contains(">a") || !h.contains(">b")) + return fail("xfile table", h.left(300)); + if (h.contains(">c")) + return fail("xfile leak isolate"); + if (h.contains("тип: хаб")) + return fail("xfile fm leak"); + } + // Межфайловые свойства: статусы соседей index.md + { + QVariantMap pa; + pa.insert("статус", "в работе"); + if (!db2.saveFileProperties(v2 + "/a.md", pa).isEmpty()) + return fail("xfile props save"); + index2.updateFile(v2 + "/a.md"); + db2.syncFile(v2 + "/a.md"); + QVariantMap r = db2.queryTable( + "SELECT f2.title, p.value FROM files f1 JOIN links l ON l.src_id = f1.id " + "JOIN files f2 ON f2.id = l.dst_id " + "JOIN properties p ON p.file_id = f2.id " + "WHERE f1.path LIKE '%index.md' AND p.key = 'статус'"); + const QVariantList rows = r.value("rows").toList(); + if (rows.size() != 1 || rows[0].toList().value(0).toString() != "a" + || rows[0].toList().value(1).toString() != "в работе") + return fail("xfile props query", r.value("error").toString()); + } + // Динамика: a.md ссылается на c.md — изолят подключается + { + writeFile(v2 + "/a.md", "# A #общее\n\n[[b]] [[c]]\n"); + index2.updateFile(v2 + "/a.md"); + db2.syncFile(v2 + "/a.md"); + if (db2.stats().value("links").toInt() != 4) + return fail("dyn link+"); + QVariantMap r = db2.queryTable( + "SELECT f.title FROM files f JOIN links l ON l.src_id = f.id " + "JOIN files d ON d.id = l.dst_id " + "WHERE d.path LIKE '%c.md'"); + if (titles(r) != QStringList({"a"})) + return fail("dyn backlinks c", titles(r).join(",")); + } + // Динамика: удаление c.md чистит рёбра + { + QFile::remove(v2 + "/c.md"); + index2.removeFile(v2 + "/c.md"); + db2.syncRemove(v2 + "/c.md"); + QVariantMap s = db2.stats(); + if (s.value("files").toInt() != 3 || s.value("links").toInt() != 3) + return fail("dyn remove", + QString("files=%1 links=%2").arg(s.value("files").toInt()) + .arg(s.value("links").toInt())); + } + std::printf("db_test OK\n"); return 0; }