From 84e4d080642db777d11c9d1a8d3dfc7a79732da4 Mon Sep 17 00:00:00 2001 From: Kevin Duff Date: Sun, 13 Oct 2024 13:01:17 +0100 Subject: [PATCH] Testing and some tweaks --- poetry.lock | 52 ++- pyproject.toml | 8 + src/flux/cli.py | 18 +- src/flux/runner.py | 24 +- tests/integration/postgres/conftest.py | 26 +- .../integration/postgres/test_postgres_cli.py | 375 +++++++++++++++++- .../postgres/test_postgres_migrations.py | 106 +++++ 7 files changed, 583 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index 08118fd..48ef482 100644 --- a/poetry.lock +++ b/poetry.lock @@ -288,6 +288,20 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.12.0,<2.13.0" pyflakes = ">=3.2.0,<3.3.0" +[[package]] +name = "freezegun" +version = "1.5.1" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "greenlet" version = "3.1.1" @@ -456,6 +470,17 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -744,6 +769,20 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "rich" version = "13.9.2" @@ -774,6 +813,17 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sqlalchemy" version = "2.0.35" @@ -932,4 +982,4 @@ postgres = ["aiopg", "asyncpg", "databases", "sqlparse"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "65d81c250abdd335acf269c68ca92b4230b7e581dfe7476a384a0ffec3c0d03e" +content-hash = "143b61a4f8ae28c205efc4e6ce222d4eb7e80dab181f1d48104b88f48d4fee0d" diff --git a/pyproject.toml b/pyproject.toml index cd71530..e2b05b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,8 @@ poethepoet = "^0.26.1" pytest = "^8.2.0" pytest-cov = "^5.0.0" pytest-asyncio = "^0.23.6" +nest-asyncio = "^1.6.0" +freezegun = "^1.5.1" [tool.poetry.extras] postgres = ["aiopg", "databases", "asyncpg", "sqlparse"] @@ -79,6 +81,10 @@ sequence = [ {cmd = "pytest -vv --cov-report xml --cov-report term --cov=flux tests/unit"}, ] +[tool.poe.tasks.unit.env] +FLUX_TESTING = "1" + + [tool.poe.tasks.integration] sequence = [ {cmd = "docker compose -f docker-compose.test.yml up -d --wait"}, @@ -90,6 +96,7 @@ ignore_fail = "return_non_zero" [tool.poe.tasks.integration.env] TEST_PG_CONNECTION_STRING = "postgresql+aiopg://postgres:postgres@localhost:55443" TEST_PG_MANAGEMENT_DB = "postgres" +FLUX_TESTING = "1" [tool.poe.tasks.integration-ci] @@ -101,6 +108,7 @@ ignore_fail = "return_non_zero" [tool.poe.tasks.integration-ci.env] TEST_PG_CONNECTION_STRING = "postgresql+aiopg://postgres:postgres@localhost:5432" TEST_PG_MANAGEMENT_DB = "postgres" +FLUX_TESTING = "1" [build-system] diff --git a/src/flux/cli.py b/src/flux/cli.py index 4cfe51f..cf3427f 100644 --- a/src/flux/cli.py +++ b/src/flux/cli.py @@ -1,6 +1,6 @@ +import asyncio import datetime as dt import os -from asyncio import run as asyncio_run from dataclasses import dataclass from enum import Enum from typing import Optional @@ -24,6 +24,16 @@ NOT_APPLIED_STATUS = "Not Applied" +def async_run(coro): + # Temp ugly workaround for testing until typer supports async + # See fastapi/typer#950 + if os.environ.get("FLUX_TESTING"): + import nest_asyncio + + nest_asyncio.apply() + asyncio.run(coro) + + @dataclass class _CliState: config: FluxConfig | None = None @@ -167,7 +177,7 @@ def new( name: Annotated[str, typer.Argument(help="Migration name and default comment")], kind: MigrationKind = MigrationKind.python, ): - asyncio_run(_new(ctx=ctx, name=name, kind=kind)) + async_run(_new(ctx=ctx, name=name, kind=kind)) async def _print_apply_report(runner: FluxRunner, n: int | None): @@ -249,7 +259,7 @@ def apply( ] = None, auto_approve: bool = False, ): - asyncio_run( + async_run( _apply(ctx, connection_uri=connection_uri, n=n, auto_approve=auto_approve) ) @@ -290,6 +300,6 @@ def rollback( ] = None, auto_approve: bool = False, ): - asyncio_run( + async_run( _rollback(ctx, connection_uri=connection_uri, n=n, auto_approve=auto_approve) ) diff --git a/src/flux/runner.py b/src/flux/runner.py index 5b54704..7e4b0da 100644 --- a/src/flux/runner.py +++ b/src/flux/runner.py @@ -165,7 +165,9 @@ async def apply_migrations(self, n: int | None = None): self.applied_migrations = await self.backend.get_applied_migrations() - def migrations_to_rollback(self, n: int | None = None): + def migrations_to_rollback(self, n: int | None = None) -> list[Migration]: + if n == 0: + return [] applied_migrations = self.list_applied_migrations() migrations_to_rollback = ( applied_migrations[-n:] if n is not None else applied_migrations @@ -201,3 +203,23 @@ async def rollback_migrations(self, n: int | None = None): await self._apply_post_apply_migrations() self.applied_migrations = await self.backend.get_applied_migrations() + + async def rollback_migration(self, migration_id: str): + """ + Rollback all migrations up to and including the given migration ID + """ + applied_migrations = self.list_applied_migrations() + target_migration_index = next( + ( + index + for index, migration in enumerate(applied_migrations) + if migration.id == migration_id + ), + None, + ) + if target_migration_index is None: + raise ValueError(f"Migration {migration_id!r} has not been applied") + + n = len(applied_migrations) - target_migration_index + + await self.rollback_migrations(n=n) diff --git a/tests/integration/postgres/conftest.py b/tests/integration/postgres/conftest.py index e2a2f49..8df5e4c 100644 --- a/tests/integration/postgres/conftest.py +++ b/tests/integration/postgres/conftest.py @@ -28,21 +28,31 @@ async def test_database() -> AsyncGenerator[str, None]: @pytest.fixture -async def postgres_backend(test_database): +def database_uri(test_database) -> str: + return f"{TEST_PG_CONNECTION_STRING}/{test_database}" + + +@pytest.fixture +async def postgres_backend(database_uri: str): return FluxPostgresBackend( - database_url=f"{TEST_PG_CONNECTION_STRING}/{test_database}", - migrations_schema="_migrations", + database_url=database_uri, migrations_table="_flux_migrations", ) @pytest.fixture -def example_migrations_dir() -> Generator[str, None, None]: +def example_project_dir() -> Generator[str, None, None]: + with TemporaryDirectory() as tempdir: + migrations_dir = os.path.join(tempdir, "migrations") + shutil.copytree(MIGRATIONS_1_DIR, migrations_dir) + yield tempdir + + +@pytest.fixture +def example_migrations_dir(example_project_dir: str) -> str: """ Create a temporary copy of the migrations directory that can be freely modified by tests """ - with TemporaryDirectory() as tempdir: - migrations_dir = os.path.join(tempdir, "migrations") - shutil.copytree(MIGRATIONS_1_DIR, migrations_dir) - yield migrations_dir + migrations_dir = os.path.join(example_project_dir, "migrations") + return migrations_dir diff --git a/tests/integration/postgres/test_postgres_cli.py b/tests/integration/postgres/test_postgres_cli.py index 414f8cb..40d319c 100644 --- a/tests/integration/postgres/test_postgres_cli.py +++ b/tests/integration/postgres/test_postgres_cli.py @@ -1,15 +1,18 @@ import os +import pytest from typer.testing import CliRunner +from flux.builtins.postgres import FluxPostgresBackend from flux.cli import app +from flux.runner import FluxRunner from tests.helpers import change_cwd +from tests.integration.postgres.helpers import postgres_config +from freezegun import freeze_time -def test_cli_init( - example_migrations_dir: str, -): - with change_cwd(example_migrations_dir): +def test_cli_init(example_project_dir: str): + with change_cwd(example_project_dir): runner = CliRunner() result = runner.invoke(app, ["init", "postgres"]) assert result.exit_code == 0, result.stdout @@ -31,10 +34,8 @@ def test_cli_init( ) -def test_cli_init_twice_errors( - example_migrations_dir: str, -): - with change_cwd(example_migrations_dir): +def test_cli_init_twice_errors(example_project_dir: str): + with change_cwd(example_project_dir): runner = CliRunner() runner.invoke(app, ["init", "postgres"]) result = runner.invoke(app, ["init", "postgres"]) @@ -43,12 +44,362 @@ def test_cli_init_twice_errors( assert "flux.toml already exists" in result.stdout -def test_cli_init_bad_backend( - example_migrations_dir: str, -): - with change_cwd(example_migrations_dir): +def test_cli_init_bad_backend(example_project_dir: str): + with change_cwd(example_project_dir): runner = CliRunner() result = runner.invoke(app, ["init", "postgres-2"]) assert result.exit_code == 1, result.stdout assert "Backend 'postgres-2' is not installed" in result.stdout + + +async def test_cli_apply_all_auto_approve( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table", + "20200102_001_add_timestamp_to_another_table", + "20200102_002_create_new_table", + } + + +async def test_cli_apply_all_manual_approve( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", database_uri], input="y\n") + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table", + "20200102_001_add_timestamp_to_another_table", + "20200102_002_create_new_table", + } + + +async def test_cli_apply_all_manual_reject( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", database_uri], input="n\n") + assert result.exit_code == 1, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == set() + + +@pytest.mark.parametrize("n", [0, 1, 2, 3, 4, 5]) +async def test_cli_apply_all_auto_approve_n( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, + n: int, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri, str(n)]) + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert ( + sorted([m.id for m in runner.applied_migrations]) + == [ + "20200101_001_add_description_to_simple_table", + "20200102_001_add_timestamp_to_another_table", + "20200102_002_create_new_table", + ][:n] + ) + + +async def test_cli_rollback_all_auto_approve( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["rollback", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == set() + + +async def test_cli_rollback_all_manual_approve( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["rollback", database_uri], input="y\n") + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == set() + + +async def test_cli_rollback_all_manual_reject( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["rollback", database_uri], input="n\n") + assert result.exit_code == 1, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert {m.id for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table", + "20200102_001_add_timestamp_to_another_table", + "20200102_002_create_new_table", + } + + +@pytest.mark.parametrize("n", [0, 1, 2, 3, 4, 5]) +async def test_cli_rollback_all_auto_approve_n( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, + n: int, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke(app, ["apply", "--auto-approve", database_uri]) + assert result.exit_code == 0, result.stdout + + result = runner.invoke( + app, ["rollback", "--auto-approve", database_uri, str(n)] + ) + assert result.exit_code == 0, result.stdout + + config = postgres_config(migration_directory=example_migrations_dir) + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert ( + sorted([m.id for m in runner.applied_migrations]) + == [ + "20200101_001_add_description_to_simple_table", + "20200102_001_add_timestamp_to_another_table", + "20200102_002_create_new_table", + ][: max(3 - n, 0)] + ) + + +async def test_cli_new_python( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + with freeze_time("2021-02-03T04:05:06Z"): + result = runner.invoke(app, ["new", "A new migration"]) + + assert result.exit_code == 0, result.stdout + + with freeze_time("2021-02-03T05:06:07Z"): + result = runner.invoke( + app, ["new", "--kind", "python", "Another new migration"] + ) + + assert result.exit_code == 0, result.stdout + + first_migration_file = os.path.join( + example_migrations_dir, "20210203_001_a-new-migration.py" + ) + second_migration_file = os.path.join( + example_migrations_dir, "20210203_002_another-new-migration.py" + ) + + assert os.path.exists(first_migration_file) + assert os.path.exists(second_migration_file) + + with open(first_migration_file, "r") as f: + content = f.read() + assert ( + content + == '''\ +""" +A new migration +""" + + +def apply(): + return """ """ + + +def undo(): + return """ """ + +''' + ) + + with open(second_migration_file, "r") as f: + content = f.read() + assert ( + content + == '''\ +""" +Another new migration +""" + + +def apply(): + return """ """ + + +def undo(): + return """ """ + +''' + ) + + +async def test_cli_new_sql( + example_project_dir: str, + example_migrations_dir: str, + postgres_backend: FluxPostgresBackend, + database_uri: str, +): + with change_cwd(example_project_dir): + runner = CliRunner() + result = runner.invoke(app, ["init", "postgres"]) + assert result.exit_code == 0, result.stdout + + with freeze_time("2021-02-03T04:05:06Z"): + result = runner.invoke(app, ["new", "--kind", "sql", "A new migration"]) + + assert result.exit_code == 0, result.stdout + + with freeze_time("2021-02-03T05:06:07Z"): + result = runner.invoke( + app, ["new", "--kind", "sql", "Another new migration"] + ) + + assert result.exit_code == 0, result.stdout + + first_up_migration_file = os.path.join( + example_migrations_dir, "20210203_001_a-new-migration.sql" + ) + first_down_migration_file = os.path.join( + example_migrations_dir, "20210203_001_a-new-migration.undo.sql" + ) + second_up_migration_file = os.path.join( + example_migrations_dir, "20210203_002_another-new-migration.sql" + ) + second_down_migration_file = os.path.join( + example_migrations_dir, "20210203_002_another-new-migration.undo.sql" + ) + + assert os.path.exists(first_up_migration_file) + assert os.path.exists(first_down_migration_file) + assert os.path.exists(second_up_migration_file) + assert os.path.exists(second_down_migration_file) + + with open(first_up_migration_file, "r") as f: + content = f.read() + assert content == "" + + with open(second_up_migration_file, "r") as f: + content = f.read() + assert content == "" + + with open(first_down_migration_file, "r") as f: + content = f.read() + assert content == "" + + with open(second_down_migration_file, "r") as f: + content = f.read() + assert content == "" diff --git a/tests/integration/postgres/test_postgres_migrations.py b/tests/integration/postgres/test_postgres_migrations.py index 1061528..35a46f5 100644 --- a/tests/integration/postgres/test_postgres_migrations.py +++ b/tests/integration/postgres/test_postgres_migrations.py @@ -554,6 +554,112 @@ async def test_postgres_migrations_apply_undo_2( ] +async def test_postgres_migrations_rollback_specific_migration( + postgres_backend: FluxPostgresBackend, + example_migrations_dir: str, +): + config = postgres_config(migration_directory=example_migrations_dir) + + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert runner.applied_migrations == set() + + await runner.apply_migrations() + + assert {m.id: m.hash for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table": "1ddd0147bd77f7dd8d9c064584a2559d", # noqa: E501 + "20200102_001_add_timestamp_to_another_table": "a78bba561022b845e60ac752288fdee4", # noqa: E501 + "20200102_002_create_new_table": "ee1ceca0e0080b8e32c131bb88bc573a", + } + + await runner.validate_applied_migrations() + + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert len(runner.applied_migrations) == 3 + + await runner.rollback_migration("20200102_001_add_timestamp_to_another_table") + + assert {m.id: m.hash for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table": "1ddd0147bd77f7dd8d9c064584a2559d", # noqa: E501 + } + + await runner.validate_applied_migrations() + + async with postgres_backend.connection(): + migrations_table_rows = await postgres_backend.get_all_migration_rows() + assert len(list(migrations_table_rows)) == 1 + assert {m[0]: m[1] for m in migrations_table_rows} == { + "20200101_001_add_description_to_simple_table": "1ddd0147bd77f7dd8d9c064584a2559d", # noqa: E501 + } + + simple_table_info = await postgres_backend.table_info("simple_table") + assert simple_table_info == [ + ("id", "integer"), + ("data", "text"), + ("description", "text"), + ] + + another_table_info = await postgres_backend.table_info("another_table") + assert another_table_info == [ + ("id", "integer"), + ("value", "integer"), + ] + + new_table_info = await postgres_backend.table_info("new_table") + assert new_table_info == [ + ("id", "integer"), + ("info", "text"), + ] + + view1_info = await postgres_backend.table_info("view1") + assert view1_info == [ + ("id", "integer"), + ("data", "text"), + ] + + view2_info = await postgres_backend.table_info("view2") + assert view2_info == [ + ("id", "integer"), + ("value", "integer"), + ] + + +async def test_postgres_migrations_rollback_specific_migration_invalid( + postgres_backend: FluxPostgresBackend, + example_migrations_dir: str, +): + config = postgres_config(migration_directory=example_migrations_dir) + + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert runner.applied_migrations == set() + + await runner.apply_migrations(n=2) + + assert {m.id: m.hash for m in runner.applied_migrations} == { + "20200101_001_add_description_to_simple_table": "1ddd0147bd77f7dd8d9c064584a2559d", # noqa: E501 + "20200102_001_add_timestamp_to_another_table": "a78bba561022b845e60ac752288fdee4", # noqa: E501 + } + + await runner.validate_applied_migrations() + + async with FluxRunner(config=config, backend=postgres_backend) as runner: + initialized = await postgres_backend.is_initialized() + assert initialized is True + + assert len(runner.applied_migrations) == 2 + + with pytest.raises(ValueError): + await runner.rollback_migration("20200102_002_create_new_table") + + @pytest.mark.parametrize( "file_to_remove", [