From 3daff9955639dc518b7f99ace2823186d85efd0f Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Fri, 2 Oct 2026 17:57:24 -0500 Subject: [PATCH] feat(db): measure the database's size per table and keep the samples IDatabaseUsage: MySQL/MariaDB from information_schema.TABLES (fresh statistics on MySQL 8), SQLite from its page counts and the dbstat table (now compiled in) with counted rows. Samples go to database_usage and database_usage_tables (migrations 105 / 88). Co-Authored-By: Claude Opus 5.5 --- dDatabase/GameDatabase/GameDatabase.h | 3 +- .../GameDatabase/ITables/IDatabaseUsage.h | 57 ++++++++++ dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 6 + .../GameDatabase/MySQL/Tables/CMakeLists.txt | 1 + .../MySQL/Tables/DatabaseUsage.cpp | 60 ++++++++++ .../GameDatabase/SQLite/SQLiteDatabase.h | 6 + .../GameDatabase/SQLite/Tables/CMakeLists.txt | 1 + .../SQLite/Tables/DatabaseUsage.cpp | 103 ++++++++++++++++++ .../GameDatabase/TestSQL/TestSQLDatabase.h | 5 + migrations/dlu/mysql/105_database_usage.sql | 21 ++++ migrations/dlu/sqlite/88_database_usage.sql | 19 ++++ .../dDatabaseTests/ChatHistorySqliteTests.cpp | 29 +++++ thirdparty/SQLite/CMakeLists.txt | 3 + 13 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 dDatabase/GameDatabase/ITables/IDatabaseUsage.h create mode 100644 dDatabase/GameDatabase/MySQL/Tables/DatabaseUsage.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/DatabaseUsage.cpp create mode 100644 migrations/dlu/mysql/105_database_usage.sql create mode 100644 migrations/dlu/sqlite/88_database_usage.sql diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index fd7af7f1d..8a5435ceb 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -54,6 +54,7 @@ #include "IPropertyReputation.h" #include "IBbbAutosave.h" #include "IServerTraffic.h" +#include "IDatabaseUsage.h" #include "IApiKeys.h" #include "IUgcLookup.h" #include "IPermissionGrants.h" @@ -71,7 +72,7 @@ class GameDatabase : public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, - public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IChatFlags, public IClientSysInfo, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants, public IGuilds { + public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IChatFlags, public IClientSysInfo, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants, public IGuilds, public IDatabaseUsage { public: virtual ~GameDatabase() = default; // TODO: These should be made private. diff --git a/dDatabase/GameDatabase/ITables/IDatabaseUsage.h b/dDatabase/GameDatabase/ITables/IDatabaseUsage.h new file mode 100644 index 000000000..d0e294481 --- /dev/null +++ b/dDatabase/GameDatabase/ITables/IDatabaseUsage.h @@ -0,0 +1,57 @@ +#ifndef __IDATABASEUSAGE__H__ +#define __IDATABASEUSAGE__H__ + +#include +#include +#include + +/** + * How much room the game database takes, measured by the dashboard's Database usage task (hourly by default) and + * kept as history for database_usage_days: the whole database and every table's data, indexes and rows. + */ +class IDatabaseUsage { +public: + struct TableUsage { + std::string name; + uint64_t dataBytes{}; + uint64_t indexBytes{}; + uint64_t rows{}; // MySQL/MariaDB: the server's estimate (information_schema); SQLite: counted + }; + + // The database as it is now + struct Measurement { + std::vector tables; // by name + uint64_t totalBytes{}; // SQLite: the file's pages; MySQL: every table's data and indexes + uint64_t freeBytes{}; // allocated but unused: SQLite free pages, MySQL data_free (both reusable) + std::string location; // SQLite: the database file; MySQL: the server's data directory (@@datadir) + bool sizesKnown{ true }; // false: SQLite without the dbstat table, so only the totals and rows are known + }; + + // Reads sizes only; slow on a big SQLite database (counts every table's rows), so run it in the background + virtual Measurement MeasureDatabaseUsage() = 0; + + // One sample of the whole database + struct UsageTotals { + int64_t time{}; + uint64_t totalBytes{}; + uint64_t freeBytes{}; + uint64_t dataBytes{}; // the tables' + uint64_t indexBytes{}; + uint64_t rows{}; + uint64_t diskFree{}; // on the database's volume; diskTotal 0: not known (a database on another machine) + uint64_t diskTotal{}; + }; + + virtual void InsertDatabaseUsage(const UsageTotals& totals, const std::vector& tables) = 0; + + // The samples between from and to (inclusive), oldest first + virtual std::vector GetDatabaseUsageTotals(int64_t from, int64_t to) = 0; + + // The tables of the sample taken at `time`, by name + virtual std::vector GetDatabaseUsageTables(int64_t time) = 0; + + // Removes samples (and their tables) older than beforeTime; returns how many samples + virtual uint32_t PruneDatabaseUsage(int64_t beforeTime) = 0; +}; + +#endif //!__IDATABASEUSAGE__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index be6336acd..65f229ec3 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -220,6 +220,12 @@ public: void InsertTrafficMinutes(const std::vector& minutes) override; std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; uint32_t PruneTrafficMinutes(int64_t beforeTime) override; + // IDatabaseUsage + Measurement MeasureDatabaseUsage() override; + void InsertDatabaseUsage(const UsageTotals& totals, const std::vector& tables) override; + std::vector GetDatabaseUsageTotals(int64_t from, int64_t to) override; + std::vector GetDatabaseUsageTables(int64_t time) override; + uint32_t PruneDatabaseUsage(int64_t beforeTime) override; // IUgcLookup std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt index 37b313acb..32765ee33 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt @@ -26,6 +26,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES "PropertyReputation.cpp" "BbbAutosave.cpp" "ServerTraffic.cpp" + "DatabaseUsage.cpp" "MessageCaptures.cpp" "ChatLog.cpp" "ChatFlags.cpp" diff --git a/dDatabase/GameDatabase/MySQL/Tables/DatabaseUsage.cpp b/dDatabase/GameDatabase/MySQL/Tables/DatabaseUsage.cpp new file mode 100644 index 000000000..28195ca0e --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/DatabaseUsage.cpp @@ -0,0 +1,60 @@ +#include "MySQLDatabase.h" + +IDatabaseUsage::Measurement MySQLDatabase::MeasureDatabaseUsage() { + Measurement measurement; + // MySQL 8 caches information_schema sizes for a day by default; this connection asks for fresh ones (MariaDB has no cache) + { + auto expiry = ExecuteSelect("SHOW VARIABLES LIKE 'information_schema_stats_expiry';"); + if (expiry->next()) ExecuteCustomQuery("SET SESSION information_schema_stats_expiry = 0;"); + } + auto result = ExecuteSelect("SELECT TABLE_NAME AS name, COALESCE(DATA_LENGTH, 0) AS data_bytes, COALESCE(INDEX_LENGTH, 0) AS index_bytes, " + "COALESCE(TABLE_ROWS, 0) AS row_count, COALESCE(DATA_FREE, 0) AS free_bytes FROM information_schema.TABLES " + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME;"); + while (result->next()) { + TableUsage table{ result->getString("name").c_str(), result->getUInt64("data_bytes"), result->getUInt64("index_bytes"), result->getUInt64("row_count") }; + measurement.totalBytes += table.dataBytes + table.indexBytes; + measurement.freeBytes += result->getUInt64("free_bytes"); + measurement.tables.push_back(std::move(table)); + } + auto dir = ExecuteSelect("SELECT @@datadir AS dir;"); + if (dir->next() && !dir->isNull("dir")) measurement.location = dir->getString("dir").c_str(); + return measurement; +} + +void MySQLDatabase::InsertDatabaseUsage(const UsageTotals& t, const std::vector& tables) { + const auto prevCommit = GetAutoCommit(); + SetAutoCommit(false); + ExecuteInsert("REPLACE INTO database_usage (time, total_bytes, free_bytes, data_bytes, index_bytes, row_count, disk_free, disk_total) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);", t.time, t.totalBytes, t.freeBytes, t.dataBytes, t.indexBytes, t.rows, t.diskFree, t.diskTotal); + for (const auto& table : tables) { + ExecuteInsert("REPLACE INTO database_usage_tables (time, table_name, data_bytes, index_bytes, row_count) VALUES (?, ?, ?, ?, ?);", + t.time, table.name, table.dataBytes, table.indexBytes, table.rows); + } + Commit(); + SetAutoCommit(prevCommit); +} + +std::vector MySQLDatabase::GetDatabaseUsageTotals(int64_t from, int64_t to) { + std::vector samples; + auto r = ExecuteSelect("SELECT time, total_bytes, free_bytes, data_bytes, index_bytes, row_count, disk_free, disk_total FROM database_usage " + "WHERE time >= ? AND time <= ? ORDER BY time;", from, to); + while (r->next()) { + samples.push_back({ r->getInt64("time"), r->getUInt64("total_bytes"), r->getUInt64("free_bytes"), r->getUInt64("data_bytes"), + r->getUInt64("index_bytes"), r->getUInt64("row_count"), r->getUInt64("disk_free"), r->getUInt64("disk_total") }); + } + return samples; +} + +std::vector MySQLDatabase::GetDatabaseUsageTables(int64_t time) { + std::vector tables; + auto r = ExecuteSelect("SELECT table_name, data_bytes, index_bytes, row_count FROM database_usage_tables WHERE time = ? ORDER BY table_name;", time); + while (r->next()) { + tables.push_back({ r->getString("table_name").c_str(), r->getUInt64("data_bytes"), r->getUInt64("index_bytes"), r->getUInt64("row_count") }); + } + return tables; +} + +uint32_t MySQLDatabase::PruneDatabaseUsage(int64_t beforeTime) { + ExecuteUpdate("DELETE FROM database_usage_tables WHERE time < ?;", beforeTime); + return static_cast(ExecuteUpdate("DELETE FROM database_usage WHERE time < ?;", beforeTime)); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index e010a2994..a9d9b87a0 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -204,6 +204,12 @@ public: void InsertTrafficMinutes(const std::vector& minutes) override; std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; uint32_t PruneTrafficMinutes(int64_t beforeTime) override; + // IDatabaseUsage + Measurement MeasureDatabaseUsage() override; + void InsertDatabaseUsage(const UsageTotals& totals, const std::vector& tables) override; + std::vector GetDatabaseUsageTotals(int64_t from, int64_t to) override; + std::vector GetDatabaseUsageTables(int64_t time) override; + uint32_t PruneDatabaseUsage(int64_t beforeTime) override; // IUgcLookup std::vector SearchUgc(const UgcSearch& search, const uint32_t limit) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt index 95fcb5c81..3f78c51b3 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -26,6 +26,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES "PropertyReputation.cpp" "BbbAutosave.cpp" "ServerTraffic.cpp" + "DatabaseUsage.cpp" "MessageCaptures.cpp" "ChatLog.cpp" "ChatFlags.cpp" diff --git a/dDatabase/GameDatabase/SQLite/Tables/DatabaseUsage.cpp b/dDatabase/GameDatabase/SQLite/Tables/DatabaseUsage.cpp new file mode 100644 index 000000000..44a9e10e5 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/DatabaseUsage.cpp @@ -0,0 +1,103 @@ +#include "SQLiteDatabase.h" + +#include +#include + +namespace { + uint64_t U64(CppSQLite3Query& result, const char* field) { + const auto value = result.getInt64Field(field); + return value > 0 ? static_cast(value) : 0; + } + + uint64_t Pragma(CppSQLite3Query& result) { + return result.eof() ? 0 : static_cast(std::max(result.getInt64Field(0), 0)); + } + + std::string Quoted(const std::string& name) { + std::string quoted = "\""; + for (const char c : name) quoted += c == '"' ? std::string("\"\"") : std::string(1, c); + return quoted + "\""; + } +} + +IDatabaseUsage::Measurement SQLiteDatabase::MeasureDatabaseUsage() { + Measurement measurement; + uint64_t pageSize = 0, pages = 0, freePages = 0; + { auto [s, r] = ExecuteSelect("PRAGMA page_size;"); pageSize = Pragma(r); } + { auto [s, r] = ExecuteSelect("PRAGMA page_count;"); pages = Pragma(r); } + { auto [s, r] = ExecuteSelect("PRAGMA freelist_count;"); freePages = Pragma(r); } + measurement.totalBytes = pageSize * pages; + measurement.freeBytes = pageSize * freePages; + { + auto [s, r] = ExecuteSelect("PRAGMA database_list;"); + for (; !r.eof(); r.nextRow()) { + if (std::string(r.getStringField("name")) == "main") measurement.location = r.getStringField("file"); + } + } + + std::map tables; + { + auto [s, r] = ExecuteSelect("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"); + for (; !r.eof(); r.nextRow()) { + const std::string name = r.getStringField("name"); + tables[name].name = name; + } + } + // Bytes of each table and its indexes, from the dbstat table (SQLITE_ENABLE_DBSTAT_VTAB) + try { + auto [s, r] = ExecuteSelect("SELECT m.tbl_name AS tbl, m.type AS type, SUM(d.pgsize) AS bytes FROM dbstat d JOIN sqlite_master m ON m.name = d.name " + "GROUP BY m.tbl_name, m.type;"); + for (; !r.eof(); r.nextRow()) { + const auto it = tables.find(r.getStringField("tbl")); + if (it == tables.end()) continue; + if (std::string(r.getStringField("type")) == "index") it->second.indexBytes += U64(r, "bytes"); + else it->second.dataBytes += U64(r, "bytes"); + } + } catch (const std::exception&) { + measurement.sizesKnown = false; + } + for (auto& [name, table] : tables) { + auto [s, r] = ExecuteSelect("SELECT COUNT(*) AS n FROM " + Quoted(name) + ";"); + if (!r.eof()) table.rows = U64(r, "n"); + measurement.tables.push_back(table); + } + return measurement; +} + +void SQLiteDatabase::InsertDatabaseUsage(const UsageTotals& t, const std::vector& tables) { + const auto prevCommit = GetAutoCommit(); + SetAutoCommit(false); + ExecuteInsert("INSERT OR REPLACE INTO database_usage (time, total_bytes, free_bytes, data_bytes, index_bytes, row_count, disk_free, disk_total) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);", t.time, t.totalBytes, t.freeBytes, t.dataBytes, t.indexBytes, t.rows, t.diskFree, t.diskTotal); + for (const auto& table : tables) { + ExecuteInsert("INSERT OR REPLACE INTO database_usage_tables (time, table_name, data_bytes, index_bytes, row_count) VALUES (?, ?, ?, ?, ?);", + t.time, table.name, table.dataBytes, table.indexBytes, table.rows); + } + Commit(); + SetAutoCommit(prevCommit); +} + +std::vector SQLiteDatabase::GetDatabaseUsageTotals(int64_t from, int64_t to) { + std::vector samples; + auto [s, r] = ExecuteSelect("SELECT time, total_bytes, free_bytes, data_bytes, index_bytes, row_count, disk_free, disk_total FROM database_usage " + "WHERE time >= ? AND time <= ? ORDER BY time;", from, to); + for (; !r.eof(); r.nextRow()) { + samples.push_back({ r.getInt64Field("time"), U64(r, "total_bytes"), U64(r, "free_bytes"), U64(r, "data_bytes"), U64(r, "index_bytes"), + U64(r, "row_count"), U64(r, "disk_free"), U64(r, "disk_total") }); + } + return samples; +} + +std::vector SQLiteDatabase::GetDatabaseUsageTables(int64_t time) { + std::vector tables; + auto [s, r] = ExecuteSelect("SELECT table_name, data_bytes, index_bytes, row_count FROM database_usage_tables WHERE time = ? ORDER BY table_name;", time); + for (; !r.eof(); r.nextRow()) { + tables.push_back({ r.getStringField("table_name"), U64(r, "data_bytes"), U64(r, "index_bytes"), U64(r, "row_count") }); + } + return tables; +} + +uint32_t SQLiteDatabase::PruneDatabaseUsage(int64_t beforeTime) { + ExecuteUpdate("DELETE FROM database_usage_tables WHERE time < ?;", beforeTime); + return static_cast(ExecuteUpdate("DELETE FROM database_usage WHERE time < ?;", beforeTime)); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 2a97cbeca..c8a4de4c4 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -258,6 +258,11 @@ class TestSQLDatabase : public GameDatabase { void InsertTrafficMinutes(const std::vector& minutes) override {} std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override { return {}; } uint32_t PruneTrafficMinutes(int64_t beforeTime) override { return 0; } + Measurement MeasureDatabaseUsage() override { return {}; } + void InsertDatabaseUsage(const UsageTotals& totals, const std::vector& tables) override {} + std::vector GetDatabaseUsageTotals(int64_t from, int64_t to) override { return {}; } + std::vector GetDatabaseUsageTables(int64_t time) override { return {}; } + uint32_t PruneDatabaseUsage(int64_t beforeTime) override { return 0; } void InsertInstanceSamples(const std::vector& samples) override {} std::vector GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) override { return {}; } void InsertPositionSamples(const std::vector& samples) override {} diff --git a/migrations/dlu/mysql/105_database_usage.sql b/migrations/dlu/mysql/105_database_usage.sql new file mode 100644 index 000000000..490b1edf9 --- /dev/null +++ b/migrations/dlu/mysql/105_database_usage.sql @@ -0,0 +1,21 @@ +/* How much room the game database takes, sampled by the dashboard's Database usage task and kept for + database_usage_days. database_usage: the whole database per sample (sizes in bytes; disk_total 0 when the database's + volume is not known); database_usage_tables: each table at that sample. row_count is the server's estimate on MySQL. */ +CREATE TABLE IF NOT EXISTS database_usage ( + time BIGINT NOT NULL PRIMARY KEY, + total_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + free_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + data_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + index_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + row_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + disk_free BIGINT UNSIGNED NOT NULL DEFAULT 0, + disk_total BIGINT UNSIGNED NOT NULL DEFAULT 0 +); +CREATE TABLE IF NOT EXISTS database_usage_tables ( + time BIGINT NOT NULL, + table_name VARCHAR(64) NOT NULL, + data_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + index_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0, + row_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (time, table_name) +); diff --git a/migrations/dlu/sqlite/88_database_usage.sql b/migrations/dlu/sqlite/88_database_usage.sql new file mode 100644 index 000000000..f461c776f --- /dev/null +++ b/migrations/dlu/sqlite/88_database_usage.sql @@ -0,0 +1,19 @@ +/* How much room the game database takes, per sample and per table. See the MySQL migration. */ +CREATE TABLE IF NOT EXISTS database_usage ( + time BIGINT NOT NULL PRIMARY KEY, + total_bytes BIGINT NOT NULL DEFAULT 0, + free_bytes BIGINT NOT NULL DEFAULT 0, + data_bytes BIGINT NOT NULL DEFAULT 0, + index_bytes BIGINT NOT NULL DEFAULT 0, + row_count BIGINT NOT NULL DEFAULT 0, + disk_free BIGINT NOT NULL DEFAULT 0, + disk_total BIGINT NOT NULL DEFAULT 0 +); +CREATE TABLE IF NOT EXISTS database_usage_tables ( + time BIGINT NOT NULL, + table_name TEXT NOT NULL, + data_bytes BIGINT NOT NULL DEFAULT 0, + index_bytes BIGINT NOT NULL DEFAULT 0, + row_count BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (time, table_name) +); diff --git a/tests/dDatabaseTests/ChatHistorySqliteTests.cpp b/tests/dDatabaseTests/ChatHistorySqliteTests.cpp index 82fd0db12..ee3044d55 100644 --- a/tests/dDatabaseTests/ChatHistorySqliteTests.cpp +++ b/tests/dDatabaseTests/ChatHistorySqliteTests.cpp @@ -426,3 +426,32 @@ TEST(ClientSysInfoSqlite, ListsAcrossAccounts) { ASSERT_EQ(rows.size(), 1u); EXPECT_EQ(rows[0].buildNumber, 2600u); } + +// The Database usage task's measurement and history (IDatabaseUsage) on SQLite +TEST(DatabaseUsageSqlite, MeasuresTablesAndKeepsHistory) { + const auto measurement = g_Db->MeasureDatabaseUsage(); + EXPECT_TRUE(measurement.sizesKnown); // built with the dbstat table + EXPECT_GT(measurement.totalBytes, 0u); + EXPECT_EQ(std::filesystem::weakly_canonical(measurement.location), std::filesystem::weakly_canonical(g_Path)); + const auto it = std::ranges::find(measurement.tables, std::string("database_usage"), &IDatabaseUsage::TableUsage::name); + ASSERT_NE(it, measurement.tables.end()); + EXPECT_GT(it->dataBytes, 0u); + EXPECT_EQ(std::ranges::count_if(measurement.tables, [](const auto& t) { return t.name.starts_with("sqlite_"); }), 0); + + g_Db->InsertDatabaseUsage({ 1000, 500, 10, 300, 200, 7, 40, 100 }, { { "a", 100, 50, 3 }, { "b", 200, 150, 4 } }); + g_Db->InsertDatabaseUsage({ 2000, 900, 0, 600, 300, 9, 30, 100 }, { { "a", 400, 100, 5 } }); + const auto totals = g_Db->GetDatabaseUsageTotals(0, 5000); + ASSERT_EQ(totals.size(), 2u); + EXPECT_EQ(totals[0].time, 1000); + EXPECT_EQ(totals[1].totalBytes, 900u); + EXPECT_EQ(totals[1].diskFree, 30u); + const auto tables = g_Db->GetDatabaseUsageTables(1000); + ASSERT_EQ(tables.size(), 2u); + EXPECT_EQ(tables[1].name, "b"); + EXPECT_EQ(tables[1].indexBytes, 150u); + EXPECT_EQ(tables[1].rows, 4u); + + EXPECT_EQ(g_Db->PruneDatabaseUsage(1500), 1u); + EXPECT_TRUE(g_Db->GetDatabaseUsageTables(1000).empty()); + EXPECT_EQ(g_Db->GetDatabaseUsageTotals(0, 5000).size(), 1u); +} diff --git a/thirdparty/SQLite/CMakeLists.txt b/thirdparty/SQLite/CMakeLists.txt index 4a9e9311b..d89308d04 100644 --- a/thirdparty/SQLite/CMakeLists.txt +++ b/thirdparty/SQLite/CMakeLists.txt @@ -17,3 +17,6 @@ if(UNIX) target_compile_options(sqlite3 PRIVATE "-Wno-return-stack-address" "-Wno-uninitialized" "-Wno-deprecated-declarations") endif() endif() + +# The dashboard measures each table's bytes (Database usage) with the dbstat table +target_compile_definitions(sqlite3 PRIVATE SQLITE_ENABLE_DBSTAT_VTAB)