fix: схема dkb создаётся только при отсутствии — миграции работают без права CREATE на базу
PostgreSQL проверяет право CREATE на базу раньше IF NOT EXISTS: роль миграций, которой платформа выдала готовую схему dkb без этого права, падала на CREATE SCHEMA IF NOT EXISTS. env.py (онлайн) проверяет pg_namespace и создаёт схему только при отсутствии; офлайн-SQL и upgrade() ревизии 0001 — DO-блок с той же проверкой. Тест: схема заранее во владении отдельной роли, CREATE на базу отозван у PUBLIC — upgrade/check/downgrade/upgrade проходят. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3a057b2591
commit
78e4a95327
@ -6,7 +6,8 @@ URL БД (по убыванию приоритета):
|
||||
шаблонной заглушки `driver://...` из `alembic.ini`;
|
||||
3. `settings.OPENBAO.DATABASE.DATABASE_URL` (`.env` / `ENV_FILE`) — обычный `alembic upgrade head` / `make migrate`.
|
||||
|
||||
Схема `dkb` создаётся (`CREATE SCHEMA IF NOT EXISTS`) до таблицы версий `dkb.alembic_version` и до миграций.
|
||||
Схема `dkb` создаётся до таблицы версий `dkb.alembic_version` и до миграций — только если её нет: PostgreSQL проверяет
|
||||
право CREATE на базу раньше, чем `IF NOT EXISTS`, а платформа может выдать роли миграций готовую схему без этого права.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -22,6 +23,11 @@ from src.models import Base # модели регистрируются в src/
|
||||
SCHEMA = "dkb"
|
||||
#: Значение `sqlalchemy.url` в шаблонном `alembic.ini` — признак «URL не задан вызывающим кодом».
|
||||
INI_PLACEHOLDER_PREFIX = "driver://"
|
||||
#: Офлайн-вариант условного создания схемы: условие проверяется при применении SQL-скрипта, а не при его генерации.
|
||||
CREATE_SCHEMA_IF_MISSING_SQL = (
|
||||
f"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '{SCHEMA}') "
|
||||
f"THEN CREATE SCHEMA {SCHEMA}; END IF; END $$"
|
||||
)
|
||||
|
||||
config = context.config
|
||||
|
||||
@ -62,7 +68,7 @@ CONFIGURE_OPTS = {
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Офлайн-режим (`--sql`): DDL печатается в вывод; `CREATE SCHEMA` идёт первым, до таблицы версий."""
|
||||
"""Офлайн-режим (`--sql`): DDL печатается в вывод; условный `CREATE SCHEMA` идёт первым, до таблицы версий."""
|
||||
context.configure(
|
||||
url=database_url(),
|
||||
literal_binds=True,
|
||||
@ -71,14 +77,17 @@ def run_migrations_offline() -> None:
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
|
||||
context.execute(CREATE_SCHEMA_IF_MISSING_SQL)
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
# Схема нужна до того, как alembic создаст `dkb.alembic_version`; фиксируем её отдельной транзакцией —
|
||||
# иначе autobegin оставил бы соединение во «внешней» транзакции и alembic не стал бы коммитить миграции.
|
||||
connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}"))
|
||||
# Схема нужна до того, как alembic создаст `dkb.alembic_version`. CREATE — только если схемы нет (см. docstring
|
||||
# модуля). Транзакцию закрываем сразу: иначе autobegin оставил бы соединение во «внешней» транзакции и alembic
|
||||
# не стал бы коммитить миграции.
|
||||
exists = connection.execute(text("SELECT 1 FROM pg_namespace WHERE nspname = :name"), {"name": SCHEMA}).scalar()
|
||||
if exists is None:
|
||||
connection.execute(text(f"CREATE SCHEMA {SCHEMA}"))
|
||||
connection.commit()
|
||||
|
||||
context.configure(connection=connection, **CONFIGURE_OPTS)
|
||||
|
||||
@ -24,6 +24,12 @@ depends_on: str | Sequence[str] | None = None
|
||||
|
||||
SCHEMA = "dkb"
|
||||
FK_CYCLE = "fk_stored_file_current_recognition_job_id_recognition_job"
|
||||
#: Схема создаётся только при отсутствии (и без env.py, и в офлайн-SQL): PostgreSQL проверяет право CREATE на базу
|
||||
#: раньше, чем `IF NOT EXISTS`, а роли миграций платформа может выдать готовую схему без этого права.
|
||||
CREATE_SCHEMA_IF_MISSING_SQL = (
|
||||
f"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '{SCHEMA}') "
|
||||
f"THEN CREATE SCHEMA {SCHEMA}; END IF; END $$"
|
||||
)
|
||||
|
||||
|
||||
def _enum(name: str, *values: str) -> postgresql.ENUM:
|
||||
@ -171,11 +177,11 @@ TABLES: tuple[str, ...] = (
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Схема dkb (идемпотентно) -> 27 enum-типов -> 43 таблицы с их индексами в порядке зависимостей FK.
|
||||
"""Схема dkb (если её нет) -> 27 enum-типов -> 43 таблицы с их индексами в порядке зависимостей FK.
|
||||
|
||||
FK цикла `stored_file.current_recognition_job_id` добавляется сразу после создания `recognition_job`.
|
||||
"""
|
||||
op.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
|
||||
op.execute(CREATE_SCHEMA_IF_MISSING_SQL)
|
||||
bind = op.get_bind()
|
||||
for enum_type in ENUMS.values():
|
||||
enum_type.create(bind, checkfirst=False)
|
||||
|
||||
@ -16,12 +16,12 @@ from typing import Any
|
||||
import pytest
|
||||
from alembic.autogenerate import compare_metadata
|
||||
from alembic.runtime.migration import MigrationContext
|
||||
from sqlalchemy import Connection, text
|
||||
from sqlalchemy import Connection, make_url, text
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession, create_async_engine
|
||||
|
||||
from src.models import Base
|
||||
from src.models.enums import ENUMS
|
||||
from tests.pg_helpers import create_scratch_db, drop_scratch_db
|
||||
from tests.pg_helpers import base_url, create_scratch_db, drop_scratch_db
|
||||
from tests.test_models_part_01_03 import _check_related_group_member_uniqueness
|
||||
from tests.test_models_part_04_06 import _check_nulls_not_distinct, _make_file_with_job
|
||||
from tests.test_models_part_07_09 import (
|
||||
@ -275,10 +275,58 @@ async def test_migrated_schema_behaviour(scratch_db_url: str) -> None:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_upgrade_into_preprovisioned_schema_without_database_create(scratch_db_url: str) -> None:
|
||||
"""Схему dkb выдала платформа во владение роли миграций, права CREATE на БД у роли нет: цикл миграций проходит.
|
||||
|
||||
PostgreSQL проверяет право CREATE на БД раньше, чем IF NOT EXISTS, поэтому схема создаётся только при отсутствии.
|
||||
"""
|
||||
role = f"armcore_mig_{uuid.uuid4().hex[:12]}" # роли общие для кластера — имя уникальное, удаляется в finally
|
||||
password = uuid.uuid4().hex
|
||||
scratch_url = make_url(scratch_db_url)
|
||||
role_url = scratch_url.set(username=role, password=password).render_as_string(hide_password=False)
|
||||
server = create_async_engine(base_url(), isolation_level="AUTOCOMMIT")
|
||||
scratch = create_async_engine(scratch_db_url, isolation_level="AUTOCOMMIT")
|
||||
role_created = False
|
||||
try:
|
||||
async with server.connect() as conn:
|
||||
await conn.execute(text(f"CREATE ROLE \"{role}\" LOGIN PASSWORD '{password}'"))
|
||||
role_created = True
|
||||
async with scratch.connect() as conn:
|
||||
await conn.execute(text(f'REVOKE CREATE ON DATABASE "{scratch_url.database}" FROM PUBLIC'))
|
||||
await conn.execute(text(f'CREATE SCHEMA dkb AUTHORIZATION "{role}"'))
|
||||
can_create = await conn.scalar(
|
||||
text("SELECT has_database_privilege(:role, :db, 'CREATE')"),
|
||||
{"role": role, "db": scratch_url.database},
|
||||
)
|
||||
assert can_create is False
|
||||
|
||||
await alembic(role_url, "upgrade", "head")
|
||||
await alembic(role_url, "check")
|
||||
await alembic(role_url, "downgrade", "base")
|
||||
await alembic(role_url, "upgrade", "head")
|
||||
async with scratch.connect() as conn:
|
||||
await _assert_full_model(conn)
|
||||
owner = await conn.scalar(text("SELECT nspowner::regrole::text FROM pg_namespace WHERE nspname = 'dkb'"))
|
||||
assert owner == role
|
||||
finally:
|
||||
if role_created:
|
||||
async with scratch.connect() as conn:
|
||||
await conn.execute(text(f'DROP OWNED BY "{role}" CASCADE'))
|
||||
async with server.connect() as conn:
|
||||
await conn.execute(text(f'DROP ROLE "{role}"'))
|
||||
await scratch.dispose()
|
||||
await server.dispose()
|
||||
|
||||
|
||||
def test_offline_sql_creates_schema_before_version_table() -> None:
|
||||
"""Офлайн-режим (`upgrade head --sql`): CREATE SCHEMA dkb идёт до таблицы версий и до enum-типов."""
|
||||
"""Офлайн-режим (`upgrade head --sql`): условный CREATE SCHEMA dkb идёт до таблицы версий и до enum-типов."""
|
||||
output = _run_alembic("postgresql+asyncpg://user:pass@localhost/offline", "upgrade", "head", "--sql")
|
||||
schema_at = output.index("CREATE SCHEMA IF NOT EXISTS dkb")
|
||||
schema_at = output.index(
|
||||
"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'dkb') "
|
||||
"THEN CREATE SCHEMA dkb; END IF; END $$"
|
||||
)
|
||||
assert "CREATE SCHEMA IF NOT EXISTS" not in output # требует права CREATE на базу даже при существующей схеме
|
||||
assert schema_at < output.index("CREATE TABLE dkb.alembic_version")
|
||||
assert schema_at < output.index("CREATE TYPE dkb.entity_type AS ENUM ('ЮЛ', 'ИП')")
|
||||
assert f"INSERT INTO dkb.alembic_version (version_num) VALUES ('{HEAD}')" in output
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user