diff --git a/src/database/database-sqlite3.cpp b/src/database/database-sqlite3.cpp index e86e0c7466492..6c3380f847aa8 100644 --- a/src/database/database-sqlite3.cpp +++ b/src/database/database-sqlite3.cpp @@ -167,6 +167,43 @@ void Database_SQLite3::verifyDatabase() m_initialized = true; } +bool Database_SQLite3::checkTable(const char *table) +{ + assert(m_database); + + sqlite3_stmt *m_stmt_tmp = nullptr; + PREPARE_STATEMENT(tmp, "PRAGMA table_list;"); + + bool ret = false; + while (sqlite3_step(m_stmt_tmp) == SQLITE_ROW) { + ret |= sqlite_to_string_view(m_stmt_tmp, 1) == table; + if (ret) + break; + } + + FINALIZE_STATEMENT(tmp) + return ret; +} + +bool Database_SQLite3::checkColumn(const char *table, const char *column) +{ + assert(m_database); + + sqlite3_stmt *m_stmt_tmp = nullptr; + auto query_str = std::string("PRAGMA table_info(").append(table).append(");"); + PREPARE_STATEMENT(tmp, query_str.c_str()); + + bool ret = false; + while (sqlite3_step(m_stmt_tmp) == SQLITE_ROW) { + ret |= sqlite_to_string_view(m_stmt_tmp, 1) == column; + if (ret) + break; + } + + FINALIZE_STATEMENT(tmp) + return ret; +} + Database_SQLite3::~Database_SQLite3() { FINALIZE_STATEMENT(begin) @@ -198,26 +235,53 @@ void MapDatabaseSQLite3::createDatabase() { assert(m_database); - SQLOK(sqlite3_exec(m_database, + // Note: before 5.12.0 the format was blocks(pos INT, data BLOB). + // This function only runs for fresh databases so we don't mind it here. + + const char *schema = "CREATE TABLE IF NOT EXISTS `blocks` (\n" - " `pos` INT PRIMARY KEY,\n" - " `data` BLOB\n" - ");\n", - NULL, NULL, NULL), + "`x` INT," + "`y` INT," + "`z` INT," + "`data` BLOB," + "PRIMARY KEY (`x`, `y`, `z`)" + ");\n" + ; + SQLOK(sqlite3_exec(m_database, schema, NULL, NULL, NULL), "Failed to create database table"); } void MapDatabaseSQLite3::initStatements() { - PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1"); - PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); - PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?"); - PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`"); + assert(checkTable("blocks")); + m_new_format = checkColumn("blocks", "z"); + infostream << "MapDatabaseSQLite3: split column format = " + << (m_new_format ? "yes" : "no") << std::endl; + + if (m_new_format) { + PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `x` = ? AND `y` = ? AND `z` = ? LIMIT 1"); + PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`x`, `y`, `z`, `data`) VALUES (?, ?, ?, ?)"); + PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `x` = ? AND `y` = ? AND `z` = ?"); + PREPARE_STATEMENT(list, "SELECT `x`, `y`, `z` FROM `blocks`"); + } else { + PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1"); + PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); + PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?"); + PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`"); + } } -inline void MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index) +inline int MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, v3s16 pos, int index) { - int64_to_sqlite(stmt, index, getBlockAsInteger(pos)); + if (m_new_format) { + int_to_sqlite(stmt, index, pos.X); + int_to_sqlite(stmt, index + 1, pos.Y); + int_to_sqlite(stmt, index + 2, pos.Z); + return index + 3; + } else { + int64_to_sqlite(stmt, index, getBlockAsInteger(pos)); + return index + 1; + } } bool MapDatabaseSQLite3::deleteBlock(const v3s16 &pos) @@ -240,8 +304,8 @@ bool MapDatabaseSQLite3::saveBlock(const v3s16 &pos, std::string_view data) { verifyDatabase(); - bindPos(m_stmt_write, pos); - str_to_sqlite(m_stmt_write, 2, data); + int col = bindPos(m_stmt_write, pos); + str_to_sqlite(m_stmt_write, col, data); SQLRES(sqlite3_step(m_stmt_write), SQLITE_DONE, "Failed to save block") sqlite3_reset(m_stmt_write); @@ -271,8 +335,17 @@ void MapDatabaseSQLite3::listAllLoadableBlocks(std::vector &dst) { verifyDatabase(); - while (sqlite3_step(m_stmt_list) == SQLITE_ROW) - dst.push_back(getIntegerAsBlock(sqlite3_column_int64(m_stmt_list, 0))); + v3s16 p; + while (sqlite3_step(m_stmt_list) == SQLITE_ROW) { + if (m_new_format) { + p.X = sqlite_to_int(m_stmt_list, 0); + p.Y = sqlite_to_int(m_stmt_list, 1); + p.Z = sqlite_to_int(m_stmt_list, 2); + } else { + p = getIntegerAsBlock(sqlite_to_int64(m_stmt_list, 0)); + } + dst.push_back(p); + } sqlite3_reset(m_stmt_list); } diff --git a/src/database/database-sqlite3.h b/src/database/database-sqlite3.h index 4a26bc3456d8d..c0a97862fadc3 100644 --- a/src/database/database-sqlite3.h +++ b/src/database/database-sqlite3.h @@ -30,6 +30,12 @@ class Database_SQLite3 : public Database // Open and initialize the database if needed (not thread-safe) void verifyDatabase(); + // Check if a specific table exists + bool checkTable(const char *table); + + // Check if a table has a specific column + bool checkColumn(const char *table, const char *column); + /* Value conversion helpers */ inline void str_to_sqlite(sqlite3_stmt *s, int iCol, std::string_view str) const @@ -167,9 +173,12 @@ class MapDatabaseSQLite3 : private Database_SQLite3, public MapDatabase virtual void initStatements(); private: - void bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index = 1); + /// @brief Bind block position into statement at column index + /// @return index of next column after position + int bindPos(sqlite3_stmt *stmt, v3s16 pos, int index = 1); + + bool m_new_format = false; - // Map sqlite3_stmt *m_stmt_read = nullptr; sqlite3_stmt *m_stmt_write = nullptr; sqlite3_stmt *m_stmt_list = nullptr;