diff --git a/sql/schema/schema.sql b/sql/schema/schema.sql index 9663e255..e58df240 100644 --- a/sql/schema/schema.sql +++ b/sql/schema/schema.sql @@ -602,3 +602,8 @@ CREATE TABLE `charamonsternote` ( `UPDATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(`CharacterId`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +CREATE TABLE `__Migration` ( + `MigrationName` VARCHAR(250) NOT NULL, + PRIMARY KEY (`MigrationName`) +) ENGINE=InnoDB; diff --git a/src/common/Util/Util.cpp b/src/common/Util/Util.cpp index d653e29c..2be5d946 100644 --- a/src/common/Util/Util.cpp +++ b/src/common/Util/Util.cpp @@ -141,3 +141,15 @@ void Util::valueToFlagByteIndexValue( uint32_t inVal, uint8_t& outVal, uint16_t& outVal = 1 << bitIndex; } + +std::string Util::fmtUtcTime( const std::string& fmt ) +{ + auto t = std::time( nullptr ); + auto tm = std::gmtime( &t ); + + std::stringstream ss; + + ss << std::put_time( tm, fmt.c_str() ); + + return ss.str(); +} diff --git a/src/common/Util/Util.h b/src/common/Util/Util.h index e42dd7ee..b498b5bc 100644 --- a/src/common/Util/Util.h +++ b/src/common/Util/Util.h @@ -19,6 +19,8 @@ namespace Sapphire::Common::Util std::string toLowerCopy( const std::string& inStr ); + std::string fmtUtcTime( const std::string& fmt ); + uint64_t getTimeMs(); /*! diff --git a/src/dbm/DbManager.cpp b/src/dbm/DbManager.cpp index b81790a4..371273a2 100644 --- a/src/dbm/DbManager.cpp +++ b/src/dbm/DbManager.cpp @@ -4,6 +4,15 @@ #include #include #include +#include +#include + +#include + +using namespace Sapphire; +using namespace Sapphire::Common; + +namespace fs = std::experimental::filesystem; DbManager::DbManager( const std::string& host, const std::string& database, const std::string& user, const std::string& pw, uint16_t port ) : m_host( host ), @@ -107,9 +116,14 @@ bool DbManager::performAction() case Mode::LIQUIDATE: result = modeLiquidate(); break; - case Mode::UPDATE: + case Mode::MIGRATE: + result = modeMigrate(); break; case Mode::CHECK: + result = modeCheck(); + break; + case Mode::ADD_MIGRATION: + result = modeAddMigration(); break; case Mode::CLEAN_CHARS: break; @@ -195,8 +209,8 @@ bool DbManager::modeInit() content.erase( 0, pos + delimiter.length() ); } - std::cout << "======================================================" << std::endl; - std::cout << "Inserting default values..." << std::endl; + Logger::info( "======================================================" ); + Logger::info( "Inserting default values..." ); std::ifstream t1( m_iFile ); @@ -273,7 +287,7 @@ bool DbManager::modeLiquidate() while( resultSet->next() ) { - std::cout << "DROP TABLE `" + resultSet->getString( 1 ) + "`;" << "\n"; + Logger::info( "DROP TABLE `{}`;", resultSet->getString( 1 ) ); if( !execute( "DROP TABLE `" + resultSet->getString( 1 ) + "`;" ) ) return false; } @@ -297,4 +311,157 @@ void DbManager::setSchemaFile( const std::string& sFile ) m_sFile = sFile; } +void DbManager::setMigratioName( const std::string& name ) +{ + m_migrationName = name; +} + +bool DbManager::modeCheck() +{ + if( !selectSchema() ) + return false; + + std::string query = "SELECT MigrationName FROM __Migration;"; + + std::vector< std::string > appliedMigrations; + + try + { + auto stmt = m_pConnection->createStatement(); + auto resultSet = stmt->executeQuery( query ); + + while( resultSet->next() ) + { + appliedMigrations.emplace_back( resultSet->getString( 1 ) ); + } + } + catch( std::runtime_error& e ) + { + m_lastError = e.what(); + return false; + } + + uint32_t missing = 0; + for( auto& entry : fs::directory_iterator( "sql/migrations" ) ) + { + auto& path = entry.path(); + + // just in case... + if( path.extension() != ".sql" ) + continue; + + if( std::find( appliedMigrations.begin(), appliedMigrations.end(), path.filename().string() ) == appliedMigrations.end() ) + { + Logger::info( "Missing migration: {}", path.filename().string() ); + missing++; + } + } + + if( missing > 0 ) + { + Logger::warn( "Database is missing {} migrations.", missing ); + } + else + { + Logger::info( "All available migrations have been applied." ); + } + + return true; +} + +bool DbManager::modeMigrate() +{ + if( !selectSchema() ) + return false; + + std::string query = "SELECT MigrationName FROM __Migration;"; + + std::vector< std::string > appliedMigrations; + + try + { + auto stmt = m_pConnection->createStatement(); + auto resultSet = stmt->executeQuery( query ); + + while( resultSet->next() ) + { + appliedMigrations.emplace_back( resultSet->getString( 1 ) ); + } + } + catch( std::runtime_error& e ) + { + m_lastError = e.what(); + return false; + } + + for( auto& entry : fs::directory_iterator( "sql/migrations" ) ) + { + auto& path = entry.path(); + + // just in case... + if( path.extension() != ".sql" ) + continue; + + if( std::find( appliedMigrations.begin(), appliedMigrations.end(), path.filename().string() ) == appliedMigrations.end() ) + { + Logger::info( "Applying migration: {}", path.filename().string() ); + + std::ifstream mFile( path.string() ); + if( !mFile.is_open() ) + { + m_lastError = "File " + path.string() + " does not exist!"; + return false; + } + std::string sql( ( std::istreambuf_iterator< char >( mFile ) ), + ( std::istreambuf_iterator< char >( ) ) ); + + try + { + auto stmt = m_pConnection->createStatement(); + stmt->executeQuery( sql ); + } + catch( std::runtime_error& e ) + { + m_lastError = e.what(); + return false; + } + + // insert into migrations table + if( !execute( fmt::format( "INSERT INTO __Migration (`MigrationName`) VALUES ('{}');", path.filename().string() ) ) ) + return false; + } + } + + return true; +} + +bool DbManager::modeAddMigration() +{ + if( !selectSchema() ) + return false; + + fs::create_directories( "sql/migrations" ); + + auto filename = fmt::format( "{}_{}.sql", Util::fmtUtcTime( "%Y%m%d%H%M%S" ), m_migrationName ); + + if( filename.size() > 250 ) + { + Logger::error( "Migration name '{}' is longer than 250 characters, please shorten its name.", filename ); + return false; + } + + auto path = fmt::format( "sql/migrations/{}", filename ); + + std::ofstream mFile( path ); + + mFile << fmt::format( "-- Migration generated at {}", Util::fmtUtcTime( "%Y/%m/%d %H:%M:%S" ) ) << std::endl; + mFile << fmt::format( "-- {}", filename ) << std::endl << std::endl; + + mFile.close(); + + Logger::info( "New migration created: {}", path ); + + return true; +} + diff --git a/src/dbm/DbManager.h b/src/dbm/DbManager.h index 302a5422..f5c1d632 100644 --- a/src/dbm/DbManager.h +++ b/src/dbm/DbManager.h @@ -13,9 +13,10 @@ enum class Mode { INIT, LIQUIDATE, - UPDATE, + MIGRATE, CHECK, - CLEAN_CHARS + CLEAN_CHARS, + ADD_MIGRATION, }; class DbManager @@ -37,6 +38,12 @@ class DbManager bool modeLiquidate(); + bool modeCheck(); + + bool modeMigrate(); + + bool modeAddMigration(); + virtual ~DbManager(); const std::string& getLastError(); @@ -49,6 +56,8 @@ class DbManager void setForceMode( bool mode ); + void setMigratioName( const std::string& name ); + private: std::string m_host; std::string m_database; @@ -61,6 +70,8 @@ class DbManager std::string m_iFile; std::string m_sFile; bool m_force; + + std::string m_migrationName; }; diff --git a/src/dbm/main.cpp b/src/dbm/main.cpp index dacf13f5..3eeebf2c 100644 --- a/src/dbm/main.cpp +++ b/src/dbm/main.cpp @@ -5,7 +5,10 @@ #include #include #include +#include +#include +Sapphire::Common::Util::CrashHandler crashHandler; namespace filesys = std::experimental::filesystem; @@ -77,13 +80,14 @@ std::string delChar( std::string &str, char del ) void printUsage() { - Logger::info( " Usage: sapphire_dbm " ); + Logger::info( " Usage: dbm " ); Logger::info( "\t --mode" ); Logger::info( "\t\t initialize -> Creates DB if not present and inserts default tables/data" ); Logger::info( "\t\t check -> Checks if Sapphire DB-Version matches your DB-Version" ); - Logger::info( "\t\t update -> Updates your DB-Version to Sapphire DB-Version" ); + Logger::info( "\t\t migrate -> Updates your DB-Version to Sapphire DB-Version" ); Logger::info( "\t\t clearchars -> Removes all character data from DB. Accounts will stay untouched" ); Logger::info( "\t\t liquidate -> Removes all tables and deletes the DB" ); + Logger::info( "\t\t add-migration -> Creates a new migration with the assoicated up/down sql files" ); Logger::info( "\t --user " ); Logger::info( "\t --pass ( default empty )" ); Logger::info( "\t --host ( default 127.0.0.1 )" ); @@ -91,6 +95,7 @@ void printUsage() Logger::info( "\t --database " ); Logger::info( "\t --sfile ( default sql/schema/schema.sql )" ); Logger::info( "\t --force ( skips user input / auto Yes )" ); + Logger::info( "\t --name " ); } int main( int32_t argc, char* argv[] ) @@ -109,8 +114,21 @@ int main( int32_t argc, char* argv[] ) std::string sFile; std::string iFile; + std::string migrationName; + bool force = false; + // load config first so it can still be overridden if required + Common::ConfigMgr configMgr; + Common::Config::GlobalConfig globalConfig; + if( configMgr.loadGlobalConfig( globalConfig ) ) + { + host = globalConfig.database.host; + database = globalConfig.database.database; + user = globalConfig.database.user; + pass = globalConfig.database.password; + } + std::vector< std::string > args( argv + 1, argv + argc ); for( uint32_t i = 0; i + 1 < args.size(); i += 2 ) { @@ -137,6 +155,8 @@ int main( int32_t argc, char* argv[] ) iFile = val; else if( arg == "force" ) force = true; + else if( arg == "name" ) + migrationName = val; } if( host.empty() ) @@ -155,6 +175,12 @@ int main( int32_t argc, char* argv[] ) dbm.setInsertFile( iFile ); dbm.setSchemaFile( sFile ); } + + if( !migrationName.empty() ) + { + dbm.setMigratioName( migrationName ); + } + if( force ) dbm.setForceMode( true ); //initialize|check|update|clearchars|liquidate @@ -166,9 +192,9 @@ int main( int32_t argc, char* argv[] ) { dbm.setMode( Mode::CHECK ); } - else if( mode.find( "update" ) != std::string::npos ) + else if( mode.find( "migrate" ) != std::string::npos ) { - dbm.setMode( Mode::UPDATE ); + dbm.setMode( Mode::MIGRATE ); } else if( mode.find( "clearchars" ) != std::string::npos ) { @@ -178,6 +204,10 @@ int main( int32_t argc, char* argv[] ) { dbm.setMode( Mode::LIQUIDATE ); } + else if( mode.find( "add-migration" ) != std::string::npos ) + { + dbm.setMode( Mode::ADD_MIGRATION ); + } else { Logger::fatal( "Not a valid mode: {0} !", mode );