Тесты связности базы + фикс syncFile (не рвать входящие рёбра)
- db_test: граф index->a, index->b, a->b + изолят c; прямые/обратные ссылки, достижимость через WITH RECURSIVE, ~sql из одного файла в другие, межфайловые свойства, подключение изолята, удаление - Database::clearFileData: syncFile больше не удаляет строку files и входящие links (id сохраняется, граф целый)
This commit is contained in:
+16
-4
@@ -244,6 +244,20 @@ int Database::tagId(const QString &tag, bool create)
|
|||||||
return q.lastInsertId().toInt();
|
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)
|
void Database::deleteFileRows(int fid)
|
||||||
{
|
{
|
||||||
QSqlQuery q(m_db);
|
QSqlQuery q(m_db);
|
||||||
@@ -374,10 +388,8 @@ void Database::syncFile(const QString &path)
|
|||||||
}
|
}
|
||||||
const int fid = fileId(clean, false);
|
const int fid = fileId(clean, false);
|
||||||
if (fid >= 0)
|
if (fid >= 0)
|
||||||
deleteFileRows(fid);
|
clearFileData(fid);
|
||||||
else {
|
// строки нет (например, после ручной чистки) — создастся в indexOneFile
|
||||||
// строки нет (например, после ручной чистки) — создастся в indexOneFile
|
|
||||||
}
|
|
||||||
indexOneFile(clean);
|
indexOneFile(clean);
|
||||||
deleteOrphanTags();
|
deleteOrphanTags();
|
||||||
emit changed();
|
emit changed();
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ private:
|
|||||||
int fileId(const QString &path, bool create);
|
int fileId(const QString &path, bool create);
|
||||||
int tagId(const QString &tag, bool create);
|
int tagId(const QString &tag, bool create);
|
||||||
void deleteFileRows(int fileId);
|
void deleteFileRows(int fileId);
|
||||||
|
// Снести только собственные данные файла (исходящие ссылки, теги, свойства).
|
||||||
|
// Строка files и входящие ссылки целы — id сохраняется, граф не рвётся.
|
||||||
|
void clearFileData(int fileId);
|
||||||
void deleteOrphanTags();
|
void deleteOrphanTags();
|
||||||
void indexOneFile(const QString &path);
|
void indexOneFile(const QString &path);
|
||||||
static bool isReadOnlySql(const QString &sql, QString *error);
|
static bool isReadOnlySql(const QString &sql, QString *error);
|
||||||
|
|||||||
@@ -235,6 +235,145 @@ int main(int argc, char *argv[])
|
|||||||
return fail("plain body kept", t.left(200));
|
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</td>") || !h.contains(">b</td>"))
|
||||||
|
return fail("xfile table", h.left(300));
|
||||||
|
if (h.contains(">c</td>"))
|
||||||
|
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");
|
std::printf("db_test OK\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user