From db9fd783f56990410a69d83a26b9c3da11ad1fa1 Mon Sep 17 00:00:00 2001 From: Luna Mendes Date: Tue, 6 Nov 2018 20:46:17 -0300 Subject: [PATCH] manage.cmd.migration: add rudimentary implementation Also add table changes for future message embeds. --- manage/cmd/migration/command.py | 128 +++++++++++++++++- .../scripts/1_message_embed_type.sql | 6 + schema.sql | 13 +- 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 manage/cmd/migration/scripts/1_message_embed_type.sql diff --git a/manage/cmd/migration/command.py b/manage/cmd/migration/command.py index 0296df6..fd237fb 100644 --- a/manage/cmd/migration/command.py +++ b/manage/cmd/migration/command.py @@ -1,10 +1,134 @@ -async def migrate_cmd(app, args): +import inspect +from pathlib import Path +from dataclasses import dataclass +from collections import namedtuple +from typing import Dict + +import asyncpg +from logbook import Logger + +log = Logger(__name__) + + +Migration = namedtuple('Migration', 'id name path') + + +@dataclass +class MigrationContext: + """Hold information about migration.""" + migration_folder: Path + scripts: Dict[int, Migration] + + @property + def latest(self): + """Return the latest migration ID.""" + return max(self.scripts.keys()) + + +def make_migration_ctx() -> MigrationContext: + """Create the MigrationContext instance.""" + # taken from https://stackoverflow.com/a/6628348 + script_path = inspect.stack()[0][1] + script_folder = '/'.join(script_path.split('/')[:-1]) + script_folder = Path(script_folder) + + migration_folder = script_folder / 'scripts' + + mctx = MigrationContext(migration_folder, {}) + + for mig_path in migration_folder.glob('*.sql'): + mig_path_str = str(mig_path) + + # extract migration script id and name + mig_filename = mig_path_str.split('/')[-1].split('.')[0] + name_fragments = mig_filename.split('_') + + mig_id = int(name_fragments[0]) + mig_name = '_'.join(name_fragments[1:]) + + mctx.scripts[mig_id] = Migration( + mig_id, mig_name, mig_path) + + return mctx + + +async def _ensure_changelog(app, ctx): + # make sure we have the migration table up + + try: + await app.db.execute(""" + CREATE TABLE migration_log ( + change_num bigint NOT NULL, + + apply_ts timestamp without time zone default + (now() at time zone 'utc'), + + description text, + + PRIMARY KEY (change_num) + ); + """) + + # if we were able to create the + # migration_log table, insert that we are + # on the latest version. + await app.db.execute(""" + INSERT INTO migration_log (change_num, description) + VALUES ($1, $2) + """, ctx.latest, 'migration setup') + except asyncpg.DuplicateTableError: + log.debug('existing migration table') + + +async def apply_migration(app, migration: Migration): + """Apply a single migration.""" + migration_sql = migration.path.read_text(encoding='utf-8') + + try: + await app.db.execute(""" + INSERT INTO migration_log (change_num, description) + VALUES ($1, $2) + """, migration.id, f'migration: {migration.name}') + except asyncpg.UniqueViolationError: + log.warning('already applied {}', migration.id) + return + + await app.db.execute(migration_sql) + log.info('applied {}', migration.id) + + +async def migrate_cmd(app, _args): """Main migration command. This makes sure the database is updated. """ - print('not implemented yet') + + ctx = make_migration_ctx() + + await _ensure_changelog(app, ctx) + + # local point in the changelog + local_change = await app.db.fetchval(""" + SELECT max(change_num) + FROM migration_log + """) + + local_change = local_change or 0 + latest_change = ctx.latest + + if local_change == latest_change: + print('no changes to do, exiting') + return + + # we do local_change + 1 so we start from the + # next migration to do, end in latest_change + 1 + # because of how range() works. + for idx in range(local_change + 1, latest_change + 1): + migration = ctx.scripts.get(idx) + + print('applying', migration.id, migration.name) + await apply_migration(app, migration) def setup(subparser): diff --git a/manage/cmd/migration/scripts/1_message_embed_type.sql b/manage/cmd/migration/scripts/1_message_embed_type.sql new file mode 100644 index 0000000..8650558 --- /dev/null +++ b/manage/cmd/migration/scripts/1_message_embed_type.sql @@ -0,0 +1,6 @@ +-- unused tables +DROP TABLE message_embeds; +DROP TABLE embeds; + +ALTER TABLE messages + ADD COLUMN embeds jsonb DEFAULT '[]' diff --git a/schema.sql b/schema.sql index e2823ec..9f9bda9 100644 --- a/schema.sql +++ b/schema.sql @@ -486,11 +486,6 @@ CREATE TABLE IF NOT EXISTS bans ( ); -CREATE TABLE IF NOT EXISTS embeds ( - -- TODO: this table - id bigint PRIMARY KEY -); - CREATE TABLE IF NOT EXISTS messages ( id bigint PRIMARY KEY, channel_id bigint REFERENCES channels (id) ON DELETE CASCADE, @@ -511,6 +506,8 @@ CREATE TABLE IF NOT EXISTS messages ( tts bool default false, mention_everyone bool default false, + embeds jsonb DEFAULT '[]', + nonce bigint default 0, message_type int NOT NULL @@ -522,12 +519,6 @@ CREATE TABLE IF NOT EXISTS message_attachments ( PRIMARY KEY (message_id, attachment) ); -CREATE TABLE IF NOT EXISTS message_embeds ( - message_id bigint REFERENCES messages (id) UNIQUE, - embed_id bigint REFERENCES embeds (id), - PRIMARY KEY (message_id, embed_id) -); - CREATE TABLE IF NOT EXISTS message_reactions ( message_id bigint REFERENCES messages (id), user_id bigint REFERENCES users (id),