diff --git a/src/dipdup/cli.py b/src/dipdup/cli.py index 9d49d9657..1a00cdc98 100644 --- a/src/dipdup/cli.py +++ b/src/dipdup/cli.py @@ -503,6 +503,7 @@ async def schema_wipe(ctx: click.Context, immune: bool, force: bool) -> None: models=models, timeout=config.database.connection_timeout, decimal_precision=config.advanced.decimal_precision, + unsafe_sqlite=config.advanced.unsafe_sqlite, ): conn = get_connection() await wipe_schema( diff --git a/src/dipdup/database.py b/src/dipdup/database.py index cff3cf138..da66b20f3 100644 --- a/src/dipdup/database.py +++ b/src/dipdup/database.py @@ -214,12 +214,40 @@ async def _pg_create_functions(conn: AsyncpgClient) -> None: await execute_sql(conn, sql_path) +async def get_tables() -> set[str]: + conn = get_connection() + if isinstance(conn, SqliteClient): + _, sqlite_res = await conn.execute_query('SELECT name FROM sqlite_master WHERE type = "table";') + return {row[0] for row in sqlite_res} + if isinstance(conn, AsyncpgClient): + _, postgres_res = await conn.execute_query( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'" + ) + return {row[0] for row in postgres_res} + + raise NotImplementedError + + async def _pg_create_views(conn: AsyncpgClient) -> None: sql_path = Path(__file__).parent / 'sql' / 'dipdup_head_status.sql' # TODO: Configurable interval await execute_sql(conn, sql_path, HEAD_STATUS_TIMEOUT) +# FIXME: Private but used in dipdup.hasura +async def _pg_get_views(conn: AsyncpgClient, schema_name: str) -> list[str]: + return [ + row[0] + for row in ( + await conn.execute_query( + "SELECT table_name FROM information_schema.views WHERE table_schema =" + f" '{schema_name}' UNION SELECT matviewname as table_name FROM pg_matviews" + f" WHERE schemaname = '{schema_name}'" + ) + )[1] + ] + + async def _pg_wipe_schema( conn: AsyncpgClient, schema_name: str, @@ -257,8 +285,11 @@ async def _sqlite_wipe_schema( # NOTE: Copy immune tables to the new database. master_query = 'SELECT name FROM sqlite_master WHERE type = "table"' result = await conn.execute_query(master_query) - for name in result[1]: - if name not in immune_tables: # type: ignore[comparison-overlap] + for row in result[1]: + name = row[0] + if name == 'sqlite_sequence': + continue + if name not in immune_tables: continue expr = f'CREATE TABLE {namespace}.{name} AS SELECT * FROM {name}' diff --git a/src/dipdup/hasura.py b/src/dipdup/hasura.py index 2fe04b260..f54885dde 100644 --- a/src/dipdup/hasura.py +++ b/src/dipdup/hasura.py @@ -19,6 +19,8 @@ from dipdup.config import HttpConfig from dipdup.config import PostgresDatabaseConfig from dipdup.config import ResolvedHttpConfig +from dipdup.database import AsyncpgClient +from dipdup.database import _pg_get_views from dipdup.database import get_connection from dipdup.database import iter_models from dipdup.exceptions import ConfigurationError @@ -328,18 +330,9 @@ async def _apply_custom_metadata(self) -> None: async def _get_views(self) -> list[str]: conn = get_connection() - views = [ - row[0] - for row in ( - await conn.execute_query( - "SELECT table_name FROM information_schema.views WHERE table_schema =" - f" '{self._database_config.schema_name}' UNION SELECT matviewname as table_name FROM pg_matviews" - f" WHERE schemaname = '{self._database_config.schema_name}'" - ) - )[1] - ] - self._logger.info('Found %s regular and materialized views', len(views)) - return views + if not isinstance(conn, AsyncpgClient): + raise HasuraError('Hasura integration requires `postgres` database client') + return await _pg_get_views(conn, self._database_config.schema_name) def _iterate_graphql_queries(self) -> Iterator[tuple[str, str]]: graphql_path = env.get_package_path(self._package) / 'graphql' diff --git a/src/dipdup/test.py b/src/dipdup/test.py index a71ec9957..ec5262d85 100644 --- a/src/dipdup/test.py +++ b/src/dipdup/test.py @@ -1,10 +1,30 @@ +"""This module contains helper functions for testing DipDup projects. + +These helpers are not part of the public API and can be changed without prior notice. +""" import asyncio +import atexit +import os +import tempfile +from collections.abc import AsyncIterator from contextlib import AsyncExitStack +from contextlib import asynccontextmanager +from pathlib import Path +from shutil import which +from typing import TYPE_CHECKING from typing import Any from dipdup.config import DipDupConfig +from dipdup.config import HasuraConfig +from dipdup.config import PostgresDatabaseConfig from dipdup.dipdup import DipDup +from dipdup.exceptions import FrameworkException from dipdup.index import Index +from dipdup.project import get_default_answers +from dipdup.yaml import DipDupYAMLConfig + +if TYPE_CHECKING: + from docker.client import DockerClient # type: ignore[import-untyped] async def create_dummy_dipdup( @@ -47,3 +67,142 @@ async def spawn_index(dipdup: DipDup, name: str) -> Index[Any, Any, Any]: index: Index[Any, Any, Any] = await dispatcher._ctx._spawn_index(name) dispatcher._indexes[name] = dispatcher._ctx._pending_indexes.pop() return index + + +def get_docker_client() -> 'DockerClient': + """Get Docker client instance if socket is available; skip test otherwise.""" + import _pytest.outcomes + from docker.client import DockerClient + + docker_socks = ( + Path('/var/run/docker.sock'), + Path.home() / 'Library' / 'Containers' / 'com.docker.docker' / 'Data' / 'vms' / '0' / 'docker.sock', + Path.home() / 'Library' / 'Containers' / 'com.docker.docker' / 'Data' / 'docker.sock', + ) + for path in docker_socks: + if path.exists(): + return DockerClient(base_url=f'unix://{path}') + + raise _pytest.outcomes.Skipped( # pragma: no cover + 'Docker socket not found', + allow_module_level=True, + ) + + +async def run_postgres_container() -> PostgresDatabaseConfig: + """Run Postgres container (destroyed on exit) and return database config with its IP.""" + docker = get_docker_client() + postgres_container = docker.containers.run( + image=get_default_answers()['postgres_image'], + environment={ + 'POSTGRES_USER': 'test', + 'POSTGRES_PASSWORD': 'test', + 'POSTGRES_DB': 'test', + }, + detach=True, + remove=True, + ) + atexit.register(postgres_container.stop) + postgres_container.reload() + postgres_ip = postgres_container.attrs['NetworkSettings']['IPAddress'] + + while not postgres_container.exec_run('pg_isready').exit_code == 0: + await asyncio.sleep(0.1) + + return PostgresDatabaseConfig( + kind='postgres', + host=postgres_ip, + port=5432, + user='test', + database='test', + password='test', + ) + + +async def run_hasura_container(postgres_ip: str) -> HasuraConfig: + """Run Hasura container (destroyed on exit) and return config with its IP.""" + docker = get_docker_client() + hasura_container = docker.containers.run( + image=get_default_answers()['hasura_image'], + environment={ + 'HASURA_GRAPHQL_DATABASE_URL': f'postgres://test:test@{postgres_ip}:5432', + }, + detach=True, + remove=True, + ) + atexit.register(hasura_container.stop) + hasura_container.reload() + hasura_ip = hasura_container.attrs['NetworkSettings']['IPAddress'] + + return HasuraConfig( + url=f'http://{hasura_ip}:8080', + source='new_source', + create_source=True, + ) + + +@asynccontextmanager +async def tmp_project( + config_paths: list[Path], + package: str, + exists: bool, + env: dict[str, str] | None = None, +) -> AsyncIterator[tuple[Path, dict[str, str]]]: + """Create a temporary isolated DipDup project.""" + with tempfile.TemporaryDirectory() as tmp_package_path: + # NOTE: Dump config + config, _ = DipDupYAMLConfig.load(config_paths, environment=False) + tmp_config_path = Path(tmp_package_path) / 'dipdup.yaml' + tmp_config_path.write_text(config.dump()) + + # NOTE: Symlink packages and executables + tmp_bin_path = Path(tmp_package_path) / 'bin' + tmp_bin_path.mkdir() + for executable in ('dipdup', 'datamodel-codegen'): + if (executable_path := which(executable)) is None: + raise FrameworkException(f'Executable `{executable}` not found') # pragma: no cover + os.symlink(executable_path, tmp_bin_path / executable) + + os.symlink( + Path(__file__).parent.parent / 'dipdup', + Path(tmp_package_path) / 'dipdup', + ) + + # NOTE: Ensure that `run` uses existing package and `init` creates a new one + if exists: + os.symlink( + Path(__file__).parent.parent / package, + Path(tmp_package_path) / package, + ) + + # NOTE: Prepare environment + env = { + **os.environ, + **(env or {}), + 'PATH': str(tmp_bin_path), + 'PYTHONPATH': str(tmp_package_path), + 'DIPDUP_TEST': '1', + 'DIPDUP_DEBUG': '1', + } + + yield Path(tmp_package_path), env + + +async def run_in_tmp( + tmp_path: Path, + env: dict[str, str], + *args: str, +) -> None: + """Run DipDup in existing temporary project.""" + tmp_config_path = Path(tmp_path) / 'dipdup.yaml' + + proc = await asyncio.subprocess.create_subprocess_shell( + f'dipdup -c {tmp_config_path} {" ".join(args)}', + cwd=tmp_path, + shell=True, + env=env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + assert proc.returncode == 0 diff --git a/tests/__init__.py b/tests/__init__.py index a3f76569c..48e92b0f7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -10,9 +10,7 @@ env.set_test() -CONFIGS_PATH = Path(__file__).parent / 'configs' -REPLAYS_PATH = Path(__file__).parent / 'replays' -SRC_PATH = Path(__file__).parent.parent / 'src' +TEST_CONFIGS = Path(__file__).parent / 'configs' @asynccontextmanager diff --git a/tests/configs/test_postgres.yaml b/tests/configs/test_postgres.yaml new file mode 100644 index 000000000..98615dd3e --- /dev/null +++ b/tests/configs/test_postgres.yaml @@ -0,0 +1,6 @@ +database: + kind: postgres + host: ${POSTGRES_HOST} + user: test + password: test + database: test \ No newline at end of file diff --git a/tests/configs/test_postgres_immune.yaml b/tests/configs/test_postgres_immune.yaml new file mode 100644 index 000000000..8fd521d66 --- /dev/null +++ b/tests/configs/test_postgres_immune.yaml @@ -0,0 +1,11 @@ +database: + kind: postgres + host: ${POSTGRES_HOST} + user: test + password: test + database: test + + immune_tables: + - tld + - domain + - test diff --git a/tests/configs/sqlite.yaml b/tests/configs/test_sqlite.yaml similarity index 100% rename from tests/configs/sqlite.yaml rename to tests/configs/test_sqlite.yaml diff --git a/tests/configs/test_sqlite_immune.yaml b/tests/configs/test_sqlite_immune.yaml new file mode 100644 index 000000000..95232dd9e --- /dev/null +++ b/tests/configs/test_sqlite_immune.yaml @@ -0,0 +1,11 @@ +database: + kind: sqlite + path: db.sqlite3 + + immune_tables: + - tld + - domain + - test + +advanced: + unsafe_sqlite: true \ No newline at end of file diff --git a/tests/test_demos.py b/tests/test_demos.py index 93d810cb9..28f92246a 100644 --- a/tests/test_demos.py +++ b/tests/test_demos.py @@ -1,80 +1,16 @@ -import os -import subprocess -import tempfile -from collections.abc import AsyncIterator from collections.abc import Awaitable from collections.abc import Callable -from contextlib import AbstractAsyncContextManager from contextlib import AsyncExitStack -from contextlib import asynccontextmanager from decimal import Decimal from functools import partial -from pathlib import Path -from shutil import which import pytest -from dipdup.database import get_connection from dipdup.database import tortoise_wrapper -from dipdup.exceptions import FrameworkException from dipdup.models.tezos_tzkt import TzktOperationType -from tests import CONFIGS_PATH -from tests import SRC_PATH - - -@asynccontextmanager -async def tmp_project(config_path: Path, package: str, exists: bool) -> AsyncIterator[tuple[Path, dict[str, str]]]: - with tempfile.TemporaryDirectory() as tmp_package_path: - # NOTE: Symlink configs, packages and executables - tmp_config_path = Path(tmp_package_path) / 'dipdup.yaml' - os.symlink(config_path, tmp_config_path) - - tmp_bin_path = Path(tmp_package_path) / 'bin' - tmp_bin_path.mkdir() - for executable in ('dipdup', 'datamodel-codegen'): - if (executable_path := which(executable)) is None: - raise FrameworkException(f'Executable `{executable}` not found') - os.symlink(executable_path, tmp_bin_path / executable) - - os.symlink( - SRC_PATH / 'dipdup', - Path(tmp_package_path) / 'dipdup', - ) - - # NOTE: Ensure that `run` uses existing package and `init` creates a new one - if exists: - os.symlink( - SRC_PATH / package, - Path(tmp_package_path) / package, - ) - - # NOTE: Prepare environment - env = { - **os.environ, - 'PATH': str(tmp_bin_path), - 'PYTHONPATH': str(tmp_package_path), - 'DIPDUP_TEST': '1', - } - - yield Path(tmp_package_path), env - - -async def run_in_tmp( - tmp_path: Path, - env: dict[str, str], - *cmd: str, -) -> None: - sqlite_config_path = Path(__file__).parent / 'configs' / 'sqlite.yaml' - tmp_config_path = Path(tmp_path) / 'dipdup.yaml' - - subprocess.run( - f'dipdup -c {tmp_config_path} -c {sqlite_config_path} {" ".join(cmd)}', - cwd=tmp_path, - check=True, - shell=True, - env=env, - capture_output=True, - ) +from dipdup.test import run_in_tmp +from dipdup.test import tmp_project +from tests import TEST_CONFIGS async def assert_run_token() -> None: @@ -262,11 +198,13 @@ async def test_run_init( cmd: str, assert_fn: Callable[[], Awaitable[None]], ) -> None: - config_path = CONFIGS_PATH / config + config_path = TEST_CONFIGS / config + env_config_path = TEST_CONFIGS / 'test_sqlite.yaml' + async with AsyncExitStack() as stack: tmp_package_path, env = await stack.enter_async_context( tmp_project( - config_path, + [config_path, env_config_path], package, exists=cmd != 'init', ), @@ -280,47 +218,3 @@ async def test_run_init( ) await assert_fn() - - -async def _count_tables() -> int: - conn = get_connection() - _, res = await conn.execute_query('SELECT count(name) FROM sqlite_master WHERE type = "table";') - return int(res[0][0]) - - -async def test_schema() -> None: - package = 'demo_token' - config_path = CONFIGS_PATH / f'{package}.yml' - - async with AsyncExitStack() as stack: - tmp_package_path, env = await stack.enter_async_context( - tmp_project( - config_path, - package, - exists=True, - ), - ) - - def tortoise() -> AbstractAsyncContextManager[None]: - return tortoise_wrapper( - f'sqlite://{tmp_package_path}/db.sqlite3', - f'{package}.models', - ) - - async with tortoise(): - conn = get_connection() - assert (await _count_tables()) == 0 - - await run_in_tmp(tmp_package_path, env, 'schema', 'init') - - async with tortoise(): - conn = get_connection() - assert (await _count_tables()) == 10 - await conn.execute_script('CREATE TABLE test (id INTEGER PRIMARY KEY);') - assert (await _count_tables()) == 11 - - await run_in_tmp(tmp_package_path, env, 'schema', 'wipe', '--force') - - async with tortoise(): - conn = get_connection() - assert (await _count_tables()) == 0 diff --git a/tests/test_hasura.py b/tests/test_hasura.py index ea233c607..bd48ce3f7 100644 --- a/tests/test_hasura.py +++ b/tests/test_hasura.py @@ -1,5 +1,3 @@ -import asyncio -import atexit import os from contextlib import AsyncExitStack from pathlib import Path @@ -9,7 +7,6 @@ import pytest from aiohttp import web from aiohttp.pytest_plugin import AiohttpClient -from docker.client import DockerClient # type: ignore[import-untyped] from tortoise import Tortoise from dipdup.config import DipDupConfig @@ -20,76 +17,14 @@ from dipdup.hasura import HasuraGateway from dipdup.models import ReindexingAction from dipdup.models import ReindexingReason -from dipdup.project import get_default_answers from dipdup.test import create_dummy_dipdup +from dipdup.test import run_hasura_container +from dipdup.test import run_postgres_container if TYPE_CHECKING: from aiohttp.test_utils import TestClient -def get_docker_client() -> DockerClient: - docker_socks = ( - Path('/var/run/docker.sock'), - Path.home() / 'Library' / 'Containers' / 'com.docker.docker' / 'Data' / 'vms' / '0' / 'docker.sock', - Path.home() / 'Library' / 'Containers' / 'com.docker.docker' / 'Data' / 'docker.sock', - ) - for path in docker_socks: - if path.exists(): - return DockerClient(base_url=f'unix://{path}') - else: - pytest.skip('Docker socket not found', allow_module_level=True) - - -async def run_postgres_container() -> PostgresDatabaseConfig: - docker = get_docker_client() - postgres_container = docker.containers.run( - image=get_default_answers()['postgres_image'], - environment={ - 'POSTGRES_USER': 'test', - 'POSTGRES_PASSWORD': 'test', - 'POSTGRES_DB': 'test', - }, - detach=True, - remove=True, - ) - atexit.register(postgres_container.stop) - postgres_container.reload() - postgres_ip = postgres_container.attrs['NetworkSettings']['IPAddress'] - - while not postgres_container.exec_run('pg_isready').exit_code == 0: - await asyncio.sleep(0.1) - - return PostgresDatabaseConfig( - kind='postgres', - host=postgres_ip, - port=5432, - user='test', - database='test', - password='test', - ) - - -async def run_hasura_container(postgres_ip: str) -> HasuraConfig: - docker = get_docker_client() - hasura_container = docker.containers.run( - image=get_default_answers()['hasura_image'], - environment={ - 'HASURA_GRAPHQL_DATABASE_URL': f'postgres://test:test@{postgres_ip}:5432', - }, - detach=True, - remove=True, - ) - atexit.register(hasura_container.stop) - hasura_container.reload() - hasura_ip = hasura_container.attrs['NetworkSettings']['IPAddress'] - - return HasuraConfig( - url=f'http://{hasura_ip}:8080', - source='new_source', - create_source=True, - ) - - async def test_configure_hasura() -> None: if os.uname().sysname != 'Linux' or 'microsoft' in os.uname().release: # check for WSL, Windows, mac and else pytest.skip('Test is not supported for os archetecture', allow_module_level=True) diff --git a/tests/test_index/test_tzkt_operations.py b/tests/test_index/test_tzkt_operations.py index 3e4032e4a..dbb2a28a3 100644 --- a/tests/test_index/test_tzkt_operations.py +++ b/tests/test_index/test_tzkt_operations.py @@ -19,7 +19,7 @@ from dipdup.models.tezos_tzkt import TzktOperationType from dipdup.test import create_dummy_dipdup from dipdup.test import spawn_index -from tests import CONFIGS_PATH +from tests import TEST_CONFIGS from tests import tzkt_replay @@ -31,7 +31,7 @@ async def tzkt() -> AsyncIterator[TzktDatasource]: @pytest.fixture def index_config() -> TzktOperationsIndexConfig: - config = DipDupConfig.load([CONFIGS_PATH / 'operation_filters.yml'], True) + config = DipDupConfig.load([TEST_CONFIGS / 'operation_filters.yml'], True) config.initialize() return cast(TzktOperationsIndexConfig, config.indexes['test']) @@ -124,7 +124,7 @@ async def test_get_transaction_filters(tzkt: TzktDatasource, index_config: TzktO async def test_get_sync_level() -> None: - config = DipDupConfig.load([CONFIGS_PATH / 'demo_token.yml'], True) + config = DipDupConfig.load([TEST_CONFIGS / 'demo_token.yml'], True) async with AsyncExitStack() as stack: dipdup = await create_dummy_dipdup(config, stack) index = await spawn_index(dipdup, 'tzbtc_holders_mainnet') @@ -149,7 +149,7 @@ async def test_get_sync_level() -> None: async def test_realtime() -> None: from demo_token import models - config = DipDupConfig.load([CONFIGS_PATH / 'demo_token.yml'], True) + config = DipDupConfig.load([TEST_CONFIGS / 'demo_token.yml'], True) async with AsyncExitStack() as stack: dipdup = await create_dummy_dipdup(config, stack) await dipdup._set_up_datasources(stack) diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 000000000..da62ed023 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,183 @@ +from contextlib import AbstractAsyncContextManager +from contextlib import AsyncExitStack + +from dipdup.database import get_connection +from dipdup.database import get_tables +from dipdup.database import tortoise_wrapper +from dipdup.test import run_in_tmp +from dipdup.test import run_postgres_container +from dipdup.test import tmp_project +from tests import TEST_CONFIGS + +_dipdup_tables = { + 'dipdup_contract_metadata', + 'dipdup_model_update', + 'dipdup_schema', + 'dipdup_contract', + 'dipdup_token_metadata', + 'dipdup_head', + 'dipdup_index', + 'dipdup_meta', +} + + +async def test_schema_sqlite() -> None: + package = 'demo_domains' + config_path = TEST_CONFIGS / f'{package}.yml' + env_config_path = TEST_CONFIGS / 'test_sqlite.yaml' + + async with AsyncExitStack() as stack: + tmp_package_path, env = await stack.enter_async_context( + tmp_project( + [config_path, env_config_path], + package, + exists=True, + ), + ) + + def tortoise() -> AbstractAsyncContextManager[None]: + return tortoise_wrapper( + f'sqlite://{tmp_package_path}/db.sqlite3', + f'{package}.models', + ) + + async with tortoise(): + conn = get_connection() + assert await get_tables() == set() + + await run_in_tmp(tmp_package_path, env, 'schema', 'init') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'sqlite_sequence'} + await conn.execute_script('CREATE TABLE test (id INTEGER PRIMARY KEY);') + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'sqlite_sequence', 'test'} + + await run_in_tmp(tmp_package_path, env, 'schema', 'wipe', '--force') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == set() + + +async def test_schema_sqlite_immune() -> None: + package = 'demo_domains' + config_path = TEST_CONFIGS / f'{package}.yml' + env_config_path = TEST_CONFIGS / 'test_sqlite_immune.yaml' + + async with AsyncExitStack() as stack: + tmp_package_path, env = await stack.enter_async_context( + tmp_project( + [config_path, env_config_path], + package, + exists=True, + ), + ) + + def tortoise() -> AbstractAsyncContextManager[None]: + return tortoise_wrapper( + f'sqlite://{tmp_package_path}/db.sqlite3', + f'{package}.models', + ) + + async with tortoise(): + conn = get_connection() + assert await get_tables() == set() + + await run_in_tmp(tmp_package_path, env, 'schema', 'init') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'sqlite_sequence'} + await conn.execute_script('CREATE TABLE test (id INTEGER PRIMARY KEY);') + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'sqlite_sequence', 'test'} + + await run_in_tmp(tmp_package_path, env, 'schema', 'wipe', '--force') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == {'dipdup_meta', 'test', 'domain', 'tld'} + + +async def test_schema_postgres() -> None: + package = 'demo_domains' + config_path = TEST_CONFIGS / f'{package}.yml' + env_config_path = TEST_CONFIGS / 'test_postgres.yaml' + + async with AsyncExitStack() as stack: + tmp_package_path, env = await stack.enter_async_context( + tmp_project( + [config_path, env_config_path], + package, + exists=True, + ), + ) + + database_config = await run_postgres_container() + env['POSTGRES_HOST'] = database_config.host + + def tortoise() -> AbstractAsyncContextManager[None]: + return tortoise_wrapper( + database_config.connection_string, + f'{package}.models', + ) + + async with tortoise(): + conn = get_connection() + assert await get_tables() == set() + + await run_in_tmp(tmp_package_path, env, 'schema', 'init') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain'} + await conn.execute_script('CREATE TABLE test (id INTEGER PRIMARY KEY);') + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'test'} + + await run_in_tmp(tmp_package_path, env, 'schema', 'wipe', '--force') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == {'dipdup_meta'} + + +async def test_schema_postgres_immune() -> None: + package = 'demo_domains' + config_path = TEST_CONFIGS / f'{package}.yml' + env_config_path = TEST_CONFIGS / 'test_postgres_immune.yaml' + + async with AsyncExitStack() as stack: + tmp_package_path, env = await stack.enter_async_context( + tmp_project( + [config_path, env_config_path], + package, + exists=True, + ), + ) + + database_config = await run_postgres_container() + env['POSTGRES_HOST'] = database_config.host + + def tortoise() -> AbstractAsyncContextManager[None]: + return tortoise_wrapper( + database_config.connection_string, + f'{package}.models', + ) + + async with tortoise(): + conn = get_connection() + assert await get_tables() == set() + + await run_in_tmp(tmp_package_path, env, 'schema', 'init') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain'} + await conn.execute_script('CREATE TABLE test (id INTEGER PRIMARY KEY);') + assert await get_tables() == _dipdup_tables | {'tld', 'record', 'domain', 'test'} + + await run_in_tmp(tmp_package_path, env, 'schema', 'wipe', '--force') + + async with tortoise(): + conn = get_connection() + assert await get_tables() == {'dipdup_meta', 'test', 'domain', 'tld'}