async def list(
    connection: asyncpg.Connection,
    table_schema: str = constants.MIGRATIONS_SCHEMA,
    table_name: str = constants.MIGRATIONS_TABLE,
) -> model.MigrationHistory:
    logger.debug('Getting a history of migrations')

    history = model.MigrationHistory()

    await connection.reload_schema_state()
    async with connection.transaction():
        async for record in connection.cursor("""
                select revision, label, timestamp, direction from
                    {table_schema}.{table_name}
                    order by timestamp asc;
                """.format(
                table_schema=table_schema,
                table_name=table_name,
        )):
            history.append(
                model.MigrationHistoryEntry(
                    revision=model.Revision(record['revision']),
                    label=record['label'],
                    timestamp=record['timestamp'],
                    direction=model.MigrationDir(record['direction']),
                ), )

    return history
Exemple #2
0
async def test_get_revision_migration_table_exists_with_entries(
    db_connection: asyncpg.Connection,
    table_schema: str,
    table_name: str,
    mocker: ptm.MockFixture,
) -> None:
    max_revisions = 10
    await migration.create_table(
        connection=db_connection,
        table_schema=table_schema,
        table_name=table_name,
    )
    for i in range(1, max_revisions + 1):
        await migration.save(
            connection=db_connection,
            migration=model.Migration(
                revision=model.Revision(i),
                label=__name__,
                path=mocker.stub(),
                upgrade=mocker.stub(),
                downgrade=mocker.stub(),
            ),
            direction=secrets.choice([
                model.MigrationDir.DOWN,
                model.MigrationDir.UP,
            ]),
            table_schema=table_schema,
            table_name=table_name,
        )

    assert (await migration.latest_revision(
        connection=db_connection,
        table_schema=table_schema,
        table_name=table_name,
    )) == max_revisions
async def latest_revision(
    connection: asyncpg.Connection,
    table_schema: str = constants.MIGRATIONS_SCHEMA,
    table_name: str = constants.MIGRATIONS_TABLE,
) -> t.Optional[model.Revision]:
    await connection.reload_schema_state()
    val = await connection.fetchval(
        """
            select revision from {table_schema}.{table_name} order
            by timestamp desc limit 1;
        """.format(
            table_schema=table_schema,
            table_name=table_name,
        ), )
    return model.Revision(val) if val is not None else None
Exemple #4
0
def test_db_history(
    cli_runner: testing.CliRunner,
    mocker: ptm.MockFixture,
    entries_count: int,
) -> None:
    @dataclass
    class MockedConfig:
        database_dsn: str
        database_name: str

    entries = model.MigrationHistory([
        model.MigrationHistoryEntry(
            revision=model.Revision(rev),
            timestamp=model.Timestamp(dt.datetime.today()),
            label=mocker.stub(),
            direction=model.MigrationDir.UP if rev %
            2 else model.MigrationDir.DOWN,
        ) for rev in range(entries_count)
    ])

    mocker.patch(
        'asyncpg_migrate.loader.load_configuration',
        return_value=MockedConfig(
            'postgres://*****:*****@test:5432/test',
            'test',
        ),
    )
    mocker.patch(
        'asyncpg.connect',
        side_effect=asyncio.coroutine(lambda dsn: object()),
    )
    mocker.patch(
        'asyncpg_migrate.engine.migration.list',
        side_effect=asyncio.coroutine(lambda *args, **kwargs: entries),
    )

    from asyncpg_migrate import main

    result = cli_runner.invoke(main.db, 'history')
    if not entries_count:
        assert result.exception is not None
        assert result.exit_code == 1
        assert 'No revisions found, you might want to run some migrations first :)' in \
            result.output
    else:
        assert result.exception is None
        assert result.exit_code == 0
        assert len(result.output.split('\n')[3:]) - 1 == entries_count
async def latest_revision(
    connection: asyncpg.Connection,
    table_schema: str = constants.MIGRATIONS_SCHEMA,
    table_name: str = constants.MIGRATIONS_TABLE,
) -> t.Optional[model.Revision]:
    try:
        await connection.reload_schema_state()

        table_name_in_db = await connection.fetchval(
            """
            select to_regclass('{schema}.{table}')
            """.format(
                schema=table_schema,
                table=table_name,
            ), )
        db_revision = None
        if table_name_in_db is None:
            raise MigrationTableMissing(f'{table_name} table does not exist')
        else:
            val = await connection.fetchval(
                """
                    select revision from {table_schema}.{table_name} order
                    by timestamp desc limit 1;
                    """.format(
                    table_schema=table_schema,
                    table_name=table_name,
                ), )
            db_revision = model.Revision(val) if val is not None else None

        return db_revision
    except MigrationTableMissing:
        logger.exception('Migration table seems to be missing')
        raise
    except Exception as ex:
        logger.exception(
            'Unknown error occurred while getting latest revision')
        raise MigrationProcessingError() from ex
Exemple #6
0
async def run(
    config: model.Config,
    target_revision: t.Union[str, int],
    connection: asyncpg.Connection,
) -> t.Optional[model.Revision]:
    logger.info(
        'Downgrading to revision {target_revision} has been triggered',
        target_revision=target_revision,
    )

    await migration.create_table(connection)

    migrations = loader.load_migrations(config)
    if not migrations:
        logger.info('There are no migrations scripts, skipping')
        return None
    elif str(target_revision).lower() == 'head':
        # although revision can be decoded from 'head' string
        # in downgraded only 'base' is supported
        raise ValueError('Cannot downgrade using "head"')
    else:
        to_revision = 1 if str(
            target_revision, ).lower() == 'base' else int(target_revision)

    maybe_db_revision = await migration.latest_revision(connection)

    if maybe_db_revision is None:
        logger.debug('No migration has ever happened, skipping...')
        return None
    elif maybe_db_revision == 0:
        logger.debug(
            'Dowgraded everything there could have been migrated, skipping...')
        return None
    else:
        db_revision = maybe_db_revision
        to_revision = 1 if abs(to_revision) == 0 else to_revision

        if to_revision > 0:
            if to_revision > len(migrations):
                logger.error(
                    'Cannot downgrade further than I know scripts for')
                return None
        else:
            previous_db_revision = db_revision - (abs(to_revision) - 1)
            if previous_db_revision <= 0:
                to_revision = 1
            else:
                to_revision = previous_db_revision

        if db_revision == 1:
            # special case, we are about to go back to the state as-if no
            # migration has happened, we will remove all the scripts apart
            # from first one
            migrations_to_apply = migrations.slice(start=1, end=1)
        else:
            migrations_to_apply = migrations.slice(start=to_revision,
                                                   end=db_revision)

        logger.debug(
            f'Applying migrations {sorted(migrations_to_apply.keys(), reverse=True)}',
        )

        async with connection.transaction():
            try:
                for mig in migrations_to_apply.downgrade_iterator():
                    logger.debug(f'Applying {mig.revision}/{mig.label}')

                    await mig.downgrade(connection)
                    await migration.save(
                        migration=mig,
                        direction=model.MigrationDir.DOWN,
                        connection=connection,
                    )
                    await asyncio.sleep(1)
                    last_completed_revision = mig.revision - 1

                logger.info(
                    'Upgraded did manage to finish at {last_completed_revision} revision',
                    last_completed_revision=last_completed_revision,
                )
                return model.Revision(last_completed_revision)
            except Exception as ex:
                logger.exception('Failed to downgrade...')
                raise RuntimeError(str(ex))
Exemple #7
0
@pytest.mark.parametrize(
    'test_revision,expected_revision,all_revisions',
    [
        ('HEAD', 2, [1, 2]),
        ('HEAD', ValueError, []),
        ('base', 1, [1, 2, 3, 4, 5]),
        ('base', ValueError, []),
        ('head', 3, [1, 2, 3]),
        ('BASE', 1, [1]),
        (1, 1, [1, 2]),
        (2, 2, [1, 2]),
        (-1, ValueError, [1, 2]),
        (-1, ValueError, []),
        (0, 0, [1, 2, 3]),
        (0, 0, []),
        (model.Revision(1), 1, [1, 2]),
        ('foo', ValueError, []),
        ('bar', ValueError, [1, 2]),
    ],
)
def test_revision_decoding(
    test_revision: t.Union[str, int],
    expected_revision: t.Union[model.Revision, Exception],
    all_revisions: t.Sequence[model.Revision],
) -> None:
    if type(expected_revision) == int:
        assert model.Revision.decode(
            test_revision,
            all_revisions,
        ) == expected_revision
    else: