diff --git a/src/gismo/application.py b/src/gismo/application.py index 9602da6..849c55e 100644 --- a/src/gismo/application.py +++ b/src/gismo/application.py @@ -3,8 +3,10 @@ from cpl_core.configuration import ConfigurationABC from cpl_core.console import Console from cpl_core.dependency_injection import ServiceProviderABC from cpl_core.logging import LoggerABC + from gismo_core.abc.bot_service_abc import BotServiceABC from gismo_core.service.bot_service import BotService +from gismo_data.service.migration_service import MigrationService class Gismo(ApplicationABC): @@ -14,12 +16,14 @@ class Gismo(ApplicationABC): self._bot: BotService = services.get_service(BotServiceABC) self._logger: LoggerABC = services.get_service(LoggerABC) + self._migrations: MigrationService = services.get_service(MigrationService) async def configure(self): pass async def main(self): try: + self._migrations.migrate() self._logger.trace(__name__, f'Try to start {BotService}') await self._bot.start_async() except Exception as e: diff --git a/src/gismo/startup.py b/src/gismo/startup.py index 307aa35..1e5e73e 100644 --- a/src/gismo/startup.py +++ b/src/gismo/startup.py @@ -15,12 +15,17 @@ from gismo_core.abc.message_service_abc import MessageServiceABC from gismo_core.service.bot_service import BotService from gismo_core.service.message_service import MessageService from gismo_data.abc.known_user_repository_abc import KnownUserRepositoryABC +from gismo_data.abc.migration_abc import MigrationABC from gismo_data.abc.server_repository_abc import ServerRepositoryABC from gismo_data.abc.user_repository_abc import UserRepositoryABC from gismo_data.db_context import DBContext +from gismo_data.migration.initial_migration import InitialMigration +from gismo_data.migration.migration_0_3 import Migration_0_3 from gismo_data.service.client_repository_service import ( ClientRepositoryABC, ClientRepositoryService) -from gismo_data.service.known_user_repository_service import KnownUserRepositoryService +from gismo_data.service.known_user_repository_service import \ + KnownUserRepositoryService +from gismo_data.service.migration_service import MigrationService from gismo_data.service.server_repository_service import \ ServerRepositoryService from gismo_data.service.user_repository_service import UserRepositoryService @@ -58,18 +63,28 @@ class Startup(StartupABC): services.add_db_context(DBContext, self._config.get_configuration(DatabaseSettings)) + # modules services.add_singleton(ModuleServiceABC, ModuleService) services.add_singleton(BotServiceABC, BotService) services.add_transient(MessageServiceABC, MessageService) + # services + services.add_transient(MigrationService) + + # data services services.add_transient(ServerRepositoryABC, ServerRepositoryService) services.add_transient(UserRepositoryABC, UserRepositoryService) services.add_transient(ClientRepositoryABC, ClientRepositoryService) services.add_transient(KnownUserRepositoryABC, KnownUserRepositoryService) - + + # modules services.add_transient(ModuleABC, Database) services.add_transient(ModuleABC, Base) services.add_transient(ModuleABC, BootLog) + + # migrations + services.add_transient(MigrationABC, InitialMigration) + services.add_transient(MigrationABC, Migration_0_3) provider: ServiceProviderABC = services.build_service_provider() diff --git a/src/gismo_data/abc/migration_abc.py b/src/gismo_data/abc/migration_abc.py new file mode 100644 index 0000000..53b1696 --- /dev/null +++ b/src/gismo_data/abc/migration_abc.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + + +class MigrationABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def upgrade(self): pass + + @abstractmethod + def downgrade(self): pass diff --git a/src/gismo_data/db_context.py b/src/gismo_data/db_context.py index aae38ca..c45dafa 100644 --- a/src/gismo_data/db_context.py +++ b/src/gismo_data/db_context.py @@ -16,10 +16,6 @@ class DBContext(DatabaseContext): try: self._logger.debug(__name__, "Connecting to database") self._db.connect(database_settings) - for table in self._tables: - self._logger.debug(__name__, f"Create table if not exists: {table}") - self._logger.trace(__name__, f'Send SQL command: {table.get_create_string()}') - self._db.cursor.execute(table.get_create_string()) self.save_changes() self._logger.info(__name__, "Connected to database") diff --git a/src/gismo_data/migration/__init__.py b/src/gismo_data/migration/__init__.py new file mode 100644 index 0000000..425ab6c --- /dev/null +++ b/src/gismo_data/migration/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/src/gismo_data/migration/initial_migration.py b/src/gismo_data/migration/initial_migration.py new file mode 100644 index 0000000..b2882c5 --- /dev/null +++ b/src/gismo_data/migration/initial_migration.py @@ -0,0 +1,57 @@ +from cpl_core.logging import LoggerABC + +from gismo_data.abc.migration_abc import MigrationABC +from gismo_data.db_context import DBContext + + +class InitialMigration(MigrationABC): + + def __init__(self, logger: LoggerABC, db: DBContext): + self._logger = logger + self._db = db + self._cursor = db.cursor + + def upgrade(self): + self._logger.debug(__name__, 'Running upgrade') + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `MigrationHistory` ( + `MigrationId` VARCHAR(255), + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`MigrationId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Servers` ( + `ServerId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordServerId` BIGINT NOT NULL, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`ServerId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Users` ( + `UserId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordId` BIGINT NOT NULL, + `XP` BIGINT NOT NULL DEFAULT 0, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`ServerId`) REFERENCES Servers(`ServerId`), + PRIMARY KEY(`UserId`) + ); + """) + ) + + def downgrade(self): + self._cursor.execute('DROP TABLE `Servers`;') + self._cursor.execute('DROP TABLE `Users`;') diff --git a/src/gismo_data/migration/migration_0_3.py b/src/gismo_data/migration/migration_0_3.py new file mode 100644 index 0000000..138de96 --- /dev/null +++ b/src/gismo_data/migration/migration_0_3.py @@ -0,0 +1,65 @@ +from cpl_core.logging import LoggerABC + +from gismo_data.abc.migration_abc import MigrationABC +from gismo_data.db_context import DBContext + +class Migration_0_3(MigrationABC): + + def __init__(self, logger: LoggerABC, db: DBContext): + self._logger = logger + self._db = db + self._cursor = db.cursor + + def upgrade(self): + self._logger.debug(__name__, 'Running upgrade') + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Clients` ( + `ClientId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordClientId` BIGINT NOT NULL, + `SentMessageCount` BIGINT NOT NULL DEFAULT 0, + `ReceivedMessageCount` BIGINT NOT NULL DEFAULT 0, + `DeletedMessageCount` BIGINT NOT NULL DEFAULT 0, + `ReceivedCommandsCount` BIGINT NOT NULL DEFAULT 0, + `MovedUsersCount` BIGINT NOT NULL DEFAULT 0, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`ServerId`) REFERENCES Servers(`ServerId`), + PRIMARY KEY(`ClientId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `KnownUsers` ( + `KnownUserId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordId` BIGINT NOT NULL, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`KnownUserId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `UserJoinedServers` ( + `JoinId` BIGINT NOT NULL AUTO_INCREMENT, + `UserId` BIGINT NOT NULL, + `JoinedOn` DATETIME(6) NOT NULL, + `LeavedOn` DATETIME(6), + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`UserId`) REFERENCES Users(`UserId`), + PRIMARY KEY(`JoinId`) + ); + """) + ) + + def downgrade(self): + self._cursor.execute('DROP TABLE `Clients`;') + self._cursor.execute('DROP TABLE `KnownUsers`;') + self._cursor.execute('DROP TABLE `UserJoinedServers`;') diff --git a/src/gismo_data/model/migration_history.py b/src/gismo_data/model/migration_history.py new file mode 100644 index 0000000..c00ba7a --- /dev/null +++ b/src/gismo_data/model/migration_history.py @@ -0,0 +1,47 @@ +from cpl_core.database import TableABC + + +class MigrationHistory(TableABC): + + def __init__(self, id: str): + self._id = id + + TableABC.__init__(self) + + @property + def migration_id(self) -> str: + return self._id + + @staticmethod + def get_select_by_id_string(id: str) -> str: + return str(f""" + SELECT * FROM `MigrationHistory` + WHERE `MigrationId` = '{id}'; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `MigrationHistory` ( + `MigrationId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + '{self._id}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `MigrationHistory` + SET LastModifiedAt` = '{self._modified_at}' + WHERE `MigrationId` = '{self._id}'; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `MigrationHistory` + WHERE `MigrationId` = '{self._id}'; + """) diff --git a/src/gismo_data/service/migration_service.py b/src/gismo_data/service/migration_service.py new file mode 100644 index 0000000..cd34dd3 --- /dev/null +++ b/src/gismo_data/service/migration_service.py @@ -0,0 +1,43 @@ +from cpl_core.database.context import DatabaseContextABC +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.logging import LoggerABC + +from gismo_data.abc.migration_abc import MigrationABC +from gismo_data.model.migration_history import MigrationHistory + + +class MigrationService: + + def __init__(self, logger: LoggerABC, services: ServiceProviderABC, db: DatabaseContextABC): + self._logger = logger + self._services = services + + self._db = db + self._cursor = db.cursor + + self._migrations: list[MigrationABC] = MigrationABC.__subclasses__() + + def migrate(self): + self._logger.info(__name__, f"Running Migrations") + for migration in self._migrations: + migration_id = migration.__name__ + try: + # check if table exists + self._cursor.execute("SHOW TABLES LIKE 'MigrationHistory'") + result = self._cursor.fetchone() + if result: + # there is a table named "tableName" + self._logger.trace(__name__, f"Running SQL Command: {MigrationHistory.get_select_by_id_string(migration_id)}") + migration_from_db: MigrationHistory = self._db.select(MigrationHistory.get_select_by_id_string(migration_id)) + self._logger.trace(__name__, migration_from_db) + if migration_from_db is not None and len(migration_from_db) > 0: + continue + + self._logger.debug(__name__, f"Running Migration {migration}") + migration_as_service: MigrationABC = self._services.get_service(migration) + migration_as_service.upgrade() + self._cursor.execute(MigrationHistory(migration_id).insert_string) + self._db.save_changes() + + except Exception as e: + self._logger.error(__name__, f'Cannot get migration with id {migration}', e)