Files
sqlalchemy-pgview/tests/test_alembic.py
2026-02-08 10:09:48 +01:00

1062 lines
36 KiB
Python

"""Tests for Alembic integration."""
from unittest.mock import MagicMock
from sqlalchemy import Column, Integer, MetaData, String, Table, func, select
from sqlalchemy.engine import Engine
from sqlalchemy_pgview import MaterializedView, View
from sqlalchemy_pgview.alembic import (
CreateMaterializedViewOp,
CreateViewOp,
DropMaterializedViewOp,
DropViewOp,
RefreshMaterializedViewOp,
)
from sqlalchemy_pgview.alembic.autogenerate import (
_get_db_views,
_normalize_definition,
compare_views,
render_create_materialized_view,
render_create_view,
render_drop_materialized_view,
render_drop_view,
)
from sqlalchemy_pgview.alembic.ops import (
_create_materialized_view_impl,
_create_view_impl,
_drop_materialized_view_impl,
_drop_view_impl,
_refresh_materialized_view_impl,
)
class TestAlembicOperations:
"""Tests for Alembic operation classes."""
def test_create_view_op(self) -> None:
"""Test CreateViewOp initialization."""
op = CreateViewOp(
"test_view",
"SELECT id, name FROM users",
schema="public",
or_replace=True,
)
assert op.view_name == "test_view"
assert op.select_query == "SELECT id, name FROM users"
assert op.schema == "public"
assert op.or_replace is True
def test_create_view_op_defaults(self) -> None:
"""Test CreateViewOp default values."""
op = CreateViewOp("test_view", "SELECT 1")
assert op.schema is None
assert op.or_replace is False
def test_create_view_op_reverse(self) -> None:
"""Test CreateViewOp reverse returns DropViewOp."""
op = CreateViewOp("test_view", "SELECT 1", schema="analytics")
reverse = op.reverse()
assert isinstance(reverse, DropViewOp)
assert reverse.view_name == "test_view"
assert reverse.schema == "analytics"
def test_drop_view_op(self) -> None:
"""Test DropViewOp initialization."""
op = DropViewOp(
"test_view",
schema="public",
if_exists=True,
cascade=True,
)
assert op.view_name == "test_view"
assert op.schema == "public"
assert op.if_exists is True
assert op.cascade is True
def test_drop_view_op_defaults(self) -> None:
"""Test DropViewOp default values."""
op = DropViewOp("test_view")
assert op.schema is None
assert op.if_exists is True
assert op.cascade is False
def test_create_materialized_view_op(self) -> None:
"""Test CreateMaterializedViewOp initialization."""
op = CreateMaterializedViewOp(
"test_mview",
"SELECT id FROM users",
schema="analytics",
with_data=True,
if_not_exists=True,
)
assert op.view_name == "test_mview"
assert op.schema == "analytics"
assert op.with_data is True
assert op.if_not_exists is True
def test_create_materialized_view_op_defaults(self) -> None:
"""Test CreateMaterializedViewOp default values."""
op = CreateMaterializedViewOp("test_mview", "SELECT 1")
assert op.schema is None
assert op.with_data is True
assert op.if_not_exists is False
def test_create_materialized_view_op_reverse(self) -> None:
"""Test CreateMaterializedViewOp reverse returns DropMaterializedViewOp."""
op = CreateMaterializedViewOp("test_mview", "SELECT 1", schema="public")
reverse = op.reverse()
assert isinstance(reverse, DropMaterializedViewOp)
assert reverse.view_name == "test_mview"
assert reverse.schema == "public"
def test_drop_materialized_view_op(self) -> None:
"""Test DropMaterializedViewOp initialization."""
op = DropMaterializedViewOp(
"test_mview",
schema="public",
if_exists=False,
cascade=True,
)
assert op.view_name == "test_mview"
assert op.if_exists is False
assert op.cascade is True
def test_drop_materialized_view_op_defaults(self) -> None:
"""Test DropMaterializedViewOp default values."""
op = DropMaterializedViewOp("test_mview")
assert op.schema is None
assert op.if_exists is True
assert op.cascade is False
def test_refresh_materialized_view_op(self) -> None:
"""Test RefreshMaterializedViewOp initialization."""
op = RefreshMaterializedViewOp(
"test_mview",
schema="analytics",
concurrently=True,
with_data=True,
)
assert op.view_name == "test_mview"
assert op.schema == "analytics"
assert op.concurrently is True
assert op.with_data is True
def test_refresh_materialized_view_op_defaults(self) -> None:
"""Test RefreshMaterializedViewOp default values."""
op = RefreshMaterializedViewOp("test_mview")
assert op.schema is None
assert op.concurrently is False
assert op.with_data is True
class TestAlembicImplementations:
"""Tests for Alembic operation implementations."""
def test_create_view_impl(self) -> None:
"""Test _create_view_impl generates correct SQL."""
mock_ops = MagicMock()
op = CreateViewOp("test_view", "SELECT id FROM users")
_create_view_impl(mock_ops, op)
mock_ops.execute.assert_called_once()
sql = mock_ops.execute.call_args[0][0]
assert "CREATE VIEW test_view AS SELECT id FROM users" in sql
def test_create_view_impl_or_replace(self) -> None:
"""Test _create_view_impl with OR REPLACE."""
mock_ops = MagicMock()
op = CreateViewOp("test_view", "SELECT 1", or_replace=True)
_create_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "CREATE OR REPLACE VIEW" in sql
def test_create_view_impl_with_schema(self) -> None:
"""Test _create_view_impl with schema."""
mock_ops = MagicMock()
op = CreateViewOp("test_view", "SELECT 1", schema="analytics")
_create_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "analytics.test_view" in sql
def test_drop_view_impl(self) -> None:
"""Test _drop_view_impl generates correct SQL."""
mock_ops = MagicMock()
op = DropViewOp("test_view", if_exists=False)
_drop_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert sql == "DROP VIEW test_view"
def test_drop_view_impl_if_exists(self) -> None:
"""Test _drop_view_impl with IF EXISTS."""
mock_ops = MagicMock()
op = DropViewOp("test_view", if_exists=True)
_drop_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "IF EXISTS" in sql
def test_drop_view_impl_cascade(self) -> None:
"""Test _drop_view_impl with CASCADE."""
mock_ops = MagicMock()
op = DropViewOp("test_view", cascade=True, if_exists=False)
_drop_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "CASCADE" in sql
def test_drop_view_impl_with_schema(self) -> None:
"""Test _drop_view_impl with schema."""
mock_ops = MagicMock()
op = DropViewOp("test_view", schema="analytics", if_exists=False)
_drop_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "analytics.test_view" in sql
def test_create_materialized_view_impl(self) -> None:
"""Test _create_materialized_view_impl generates correct SQL."""
mock_ops = MagicMock()
op = CreateMaterializedViewOp("test_mview", "SELECT id FROM users")
_create_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "CREATE MATERIALIZED VIEW test_mview" in sql
assert "WITH DATA" in sql
def test_create_materialized_view_impl_without_data(self) -> None:
"""Test _create_materialized_view_impl with WITH NO DATA."""
mock_ops = MagicMock()
op = CreateMaterializedViewOp("test_mview", "SELECT 1", with_data=False)
_create_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "WITH NO DATA" in sql
def test_create_materialized_view_impl_if_not_exists(self) -> None:
"""Test _create_materialized_view_impl with IF NOT EXISTS."""
mock_ops = MagicMock()
op = CreateMaterializedViewOp("test_mview", "SELECT 1", if_not_exists=True)
_create_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "IF NOT EXISTS" in sql
def test_create_materialized_view_impl_with_schema(self) -> None:
"""Test _create_materialized_view_impl with schema."""
mock_ops = MagicMock()
op = CreateMaterializedViewOp("test_mview", "SELECT 1", schema="analytics")
_create_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "analytics.test_mview" in sql
def test_drop_materialized_view_impl(self) -> None:
"""Test _drop_materialized_view_impl generates correct SQL."""
mock_ops = MagicMock()
op = DropMaterializedViewOp("test_mview", if_exists=False)
_drop_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert sql == "DROP MATERIALIZED VIEW test_mview"
def test_drop_materialized_view_impl_if_exists(self) -> None:
"""Test _drop_materialized_view_impl with IF EXISTS."""
mock_ops = MagicMock()
op = DropMaterializedViewOp("test_mview", if_exists=True)
_drop_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "IF EXISTS" in sql
def test_drop_materialized_view_impl_cascade(self) -> None:
"""Test _drop_materialized_view_impl with CASCADE."""
mock_ops = MagicMock()
op = DropMaterializedViewOp("test_mview", cascade=True, if_exists=False)
_drop_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "CASCADE" in sql
def test_drop_materialized_view_impl_with_schema(self) -> None:
"""Test _drop_materialized_view_impl with schema."""
mock_ops = MagicMock()
op = DropMaterializedViewOp("test_mview", schema="analytics", if_exists=False)
_drop_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "analytics.test_mview" in sql
def test_refresh_materialized_view_impl(self) -> None:
"""Test _refresh_materialized_view_impl generates correct SQL."""
mock_ops = MagicMock()
op = RefreshMaterializedViewOp("test_mview")
_refresh_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "REFRESH MATERIALIZED VIEW test_mview" in sql
assert "WITH DATA" in sql
def test_refresh_materialized_view_impl_concurrently(self) -> None:
"""Test _refresh_materialized_view_impl with CONCURRENTLY."""
mock_ops = MagicMock()
op = RefreshMaterializedViewOp("test_mview", concurrently=True)
_refresh_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "CONCURRENTLY" in sql
def test_refresh_materialized_view_impl_without_data(self) -> None:
"""Test _refresh_materialized_view_impl with WITH NO DATA."""
mock_ops = MagicMock()
op = RefreshMaterializedViewOp("test_mview", with_data=False)
_refresh_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "WITH NO DATA" in sql
def test_refresh_materialized_view_impl_with_schema(self) -> None:
"""Test _refresh_materialized_view_impl with schema."""
mock_ops = MagicMock()
op = RefreshMaterializedViewOp("test_mview", schema="analytics")
_refresh_materialized_view_impl(mock_ops, op)
sql = mock_ops.execute.call_args[0][0]
assert "analytics.test_mview" in sql
class TestAlembicClassMethods:
"""Tests for Alembic operation classmethods."""
def test_create_view_classmethod(self) -> None:
"""Test CreateViewOp.create_view classmethod."""
mock_ops = MagicMock()
CreateViewOp.create_view(
mock_ops, "test_view", "SELECT 1", schema="public", or_replace=True
)
mock_ops.invoke.assert_called_once()
invoked_op = mock_ops.invoke.call_args[0][0]
assert isinstance(invoked_op, CreateViewOp)
assert invoked_op.view_name == "test_view"
assert invoked_op.or_replace is True
def test_drop_view_classmethod(self) -> None:
"""Test DropViewOp.drop_view classmethod."""
mock_ops = MagicMock()
DropViewOp.drop_view(mock_ops, "test_view", schema="public", cascade=True)
mock_ops.invoke.assert_called_once()
invoked_op = mock_ops.invoke.call_args[0][0]
assert isinstance(invoked_op, DropViewOp)
assert invoked_op.cascade is True
def test_create_materialized_view_classmethod(self) -> None:
"""Test CreateMaterializedViewOp.create_materialized_view classmethod."""
mock_ops = MagicMock()
CreateMaterializedViewOp.create_materialized_view(
mock_ops, "test_mview", "SELECT 1", with_data=False, if_not_exists=True
)
mock_ops.invoke.assert_called_once()
invoked_op = mock_ops.invoke.call_args[0][0]
assert isinstance(invoked_op, CreateMaterializedViewOp)
assert invoked_op.with_data is False
assert invoked_op.if_not_exists is True
def test_drop_materialized_view_classmethod(self) -> None:
"""Test DropMaterializedViewOp.drop_materialized_view classmethod."""
mock_ops = MagicMock()
DropMaterializedViewOp.drop_materialized_view(
mock_ops, "test_mview", cascade=True
)
mock_ops.invoke.assert_called_once()
invoked_op = mock_ops.invoke.call_args[0][0]
assert isinstance(invoked_op, DropMaterializedViewOp)
assert invoked_op.cascade is True
def test_refresh_materialized_view_classmethod(self) -> None:
"""Test RefreshMaterializedViewOp.refresh_materialized_view classmethod."""
mock_ops = MagicMock()
RefreshMaterializedViewOp.refresh_materialized_view(
mock_ops, "test_mview", concurrently=True, with_data=False
)
mock_ops.invoke.assert_called_once()
invoked_op = mock_ops.invoke.call_args[0][0]
assert isinstance(invoked_op, RefreshMaterializedViewOp)
assert invoked_op.concurrently is True
assert invoked_op.with_data is False
class TestAutogenerateHelpers:
"""Tests for autogenerate helper functions."""
def test_normalize_definition_none(self) -> None:
"""Test _normalize_definition with None."""
assert _normalize_definition(None) == ""
def test_normalize_definition_empty(self) -> None:
"""Test _normalize_definition with empty string."""
assert _normalize_definition("") == ""
def test_normalize_definition_whitespace(self) -> None:
"""Test _normalize_definition normalizes whitespace."""
definition = "SELECT id,\n name FROM users"
normalized = _normalize_definition(definition)
assert normalized == "select id, name from users"
def test_normalize_definition_semicolon(self) -> None:
"""Test _normalize_definition removes trailing semicolon."""
definition = "SELECT id FROM users;"
normalized = _normalize_definition(definition)
assert not normalized.endswith(";")
def test_normalize_definition_case(self) -> None:
"""Test _normalize_definition converts to lowercase."""
definition = "SELECT ID FROM USERS"
normalized = _normalize_definition(definition)
assert normalized == "select id from users"
class TestAutogenerateGetDbViews:
"""Tests for _get_db_views function."""
def test_get_db_views(self, pg_engine: Engine) -> None:
"""Test _get_db_views retrieves views from database."""
metadata = MetaData()
users = Table(
"test_users_autogen",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String(100)),
)
view = View(
"test_view_autogen",
select(users.c.id, users.c.name),
)
from sqlalchemy_pgview import CreateView, DropView
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
conn.execute(CreateView(view, or_replace=True))
db_views = _get_db_views(conn)
assert "test_view_autogen" in db_views
assert db_views["test_view_autogen"]["is_materialized"] is False
conn.execute(DropView(view, if_exists=True))
finally:
metadata.drop_all(pg_engine)
def test_get_db_views_with_schema_filter(self, pg_engine: Engine) -> None:
"""Test _get_db_views with schema filter."""
metadata = MetaData()
users = Table(
"test_users_schema",
metadata,
Column("id", Integer, primary_key=True),
)
view = View("test_view_schema", select(users.c.id))
from sqlalchemy_pgview import CreateView, DropView
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
conn.execute(CreateView(view, or_replace=True))
# Filter by public schema
db_views = _get_db_views(conn, schema="public")
assert "test_view_schema" in db_views
# Filter by non-existent schema
db_views = _get_db_views(conn, schema="nonexistent")
assert "test_view_schema" not in db_views
conn.execute(DropView(view, if_exists=True))
finally:
metadata.drop_all(pg_engine)
def test_get_db_views_materialized(self, pg_engine: Engine) -> None:
"""Test _get_db_views identifies materialized views."""
metadata = MetaData()
users = Table(
"test_users_mv_autogen",
metadata,
Column("id", Integer, primary_key=True),
)
mview = MaterializedView(
"test_mview_autogen",
select(func.count(users.c.id).label("count")),
with_data=True,
)
from sqlalchemy_pgview import CreateMaterializedView, DropMaterializedView
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
conn.execute(CreateMaterializedView(mview, if_not_exists=True))
db_views = _get_db_views(conn)
assert "test_mview_autogen" in db_views
assert db_views["test_mview_autogen"]["is_materialized"] is True
conn.execute(DropMaterializedView(mview, if_exists=True))
finally:
metadata.drop_all(pg_engine)
class TestAutogenerateRenderers:
"""Tests for autogenerate renderer functions."""
def test_render_create_view(self) -> None:
"""Test render_create_view generates correct Python code."""
op = CreateViewOp("test_view", "SELECT id FROM users")
mock_context = MagicMock()
result = render_create_view(mock_context, op)
assert "op.create_view(" in result
assert "'test_view'" in result
assert "SELECT id FROM users" in result
def test_render_create_view_with_schema(self) -> None:
"""Test render_create_view with schema."""
op = CreateViewOp("test_view", "SELECT 1", schema="analytics")
mock_context = MagicMock()
result = render_create_view(mock_context, op)
assert "schema='analytics'" in result
def test_render_create_view_or_replace(self) -> None:
"""Test render_create_view with or_replace."""
op = CreateViewOp("test_view", "SELECT 1", or_replace=True)
mock_context = MagicMock()
result = render_create_view(mock_context, op)
assert "or_replace=True" in result
def test_render_drop_view(self) -> None:
"""Test render_drop_view generates correct Python code."""
op = DropViewOp("test_view")
mock_context = MagicMock()
result = render_drop_view(mock_context, op)
assert "op.drop_view(" in result
assert "'test_view'" in result
def test_render_drop_view_with_options(self) -> None:
"""Test render_drop_view with options."""
op = DropViewOp("test_view", schema="analytics", if_exists=False, cascade=True)
mock_context = MagicMock()
result = render_drop_view(mock_context, op)
assert "schema='analytics'" in result
assert "if_exists=False" in result
assert "cascade=True" in result
def test_render_create_materialized_view(self) -> None:
"""Test render_create_materialized_view generates correct Python code."""
op = CreateMaterializedViewOp("test_mview", "SELECT 1")
mock_context = MagicMock()
result = render_create_materialized_view(mock_context, op)
assert "op.create_materialized_view(" in result
assert "'test_mview'" in result
def test_render_create_materialized_view_without_data(self) -> None:
"""Test render_create_materialized_view with with_data=False."""
op = CreateMaterializedViewOp("test_mview", "SELECT 1", with_data=False)
mock_context = MagicMock()
result = render_create_materialized_view(mock_context, op)
assert "with_data=False" in result
def test_render_create_materialized_view_with_schema(self) -> None:
"""Test render_create_materialized_view with schema."""
op = CreateMaterializedViewOp("test_mview", "SELECT 1", schema="analytics")
mock_context = MagicMock()
result = render_create_materialized_view(mock_context, op)
assert "schema='analytics'" in result
def test_render_drop_materialized_view(self) -> None:
"""Test render_drop_materialized_view generates correct Python code."""
op = DropMaterializedViewOp("test_mview")
mock_context = MagicMock()
result = render_drop_materialized_view(mock_context, op)
assert "op.drop_materialized_view(" in result
assert "'test_mview'" in result
def test_render_drop_materialized_view_with_options(self) -> None:
"""Test render_drop_materialized_view with options."""
op = DropMaterializedViewOp(
"test_mview", schema="analytics", if_exists=False, cascade=True
)
mock_context = MagicMock()
result = render_drop_materialized_view(mock_context, op)
assert "schema='analytics'" in result
assert "if_exists=False" in result
assert "cascade=True" in result
class TestCompareViews:
"""Tests for compare_views autogenerate function."""
def test_compare_views_non_postgresql(self) -> None:
"""Test compare_views returns early for non-PostgreSQL."""
mock_context = MagicMock()
mock_context.connection.dialect.name = "sqlite"
mock_context.metadata = MetaData()
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should not add any ops for non-PostgreSQL
assert len(mock_upgrade_ops.ops) == 0
def test_compare_views_none_connection(self) -> None:
"""Test compare_views returns early for None connection."""
mock_context = MagicMock()
mock_context.connection = None
mock_context.metadata = MetaData()
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should not add any ops for None connection
assert len(mock_upgrade_ops.ops) == 0
def test_compare_views_creates_new_view(self, pg_engine: Engine) -> None:
"""Test compare_views detects view to create."""
# First create tables only (no views)
table_metadata = MetaData()
Table(
"test_users_compare",
table_metadata,
Column("id", Integer, primary_key=True),
Column("name", String(100)),
)
try:
table_metadata.create_all(pg_engine)
# Now create a separate metadata with a view registered
view_metadata = MetaData()
users_ref = Table(
"test_users_compare",
view_metadata,
Column("id", Integer, primary_key=True),
Column("name", String(100)),
)
# Register a view in the new metadata (not in database yet)
View(
"test_view_compare",
select(users_ref.c.id, users_ref.c.name),
metadata=view_metadata,
)
with pg_engine.connect() as conn:
mock_context = MagicMock()
mock_context.connection = conn
mock_context.metadata = view_metadata
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should detect view to create
create_ops = [
op for op in mock_upgrade_ops.ops if isinstance(op, CreateViewOp)
]
assert len(create_ops) == 1
assert create_ops[0].view_name == "test_view_compare"
finally:
table_metadata.drop_all(pg_engine)
def test_compare_views_creates_new_materialized_view(
self, pg_engine: Engine
) -> None:
"""Test compare_views detects materialized view to create."""
# First create tables only (no views)
table_metadata = MetaData()
Table(
"test_users_compare_mv",
table_metadata,
Column("id", Integer, primary_key=True),
)
try:
table_metadata.create_all(pg_engine)
# Now create a separate metadata with a materialized view registered
view_metadata = MetaData()
users_ref = Table(
"test_users_compare_mv",
view_metadata,
Column("id", Integer, primary_key=True),
)
# Register a materialized view in the new metadata (not in database yet)
MaterializedView(
"test_mview_compare",
select(func.count(users_ref.c.id).label("count")),
metadata=view_metadata,
with_data=True,
)
with pg_engine.connect() as conn:
mock_context = MagicMock()
mock_context.connection = conn
mock_context.metadata = view_metadata
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should detect materialized view to create
create_ops = [
op
for op in mock_upgrade_ops.ops
if isinstance(op, CreateMaterializedViewOp)
]
assert len(create_ops) == 1
assert create_ops[0].view_name == "test_mview_compare"
finally:
table_metadata.drop_all(pg_engine)
def test_compare_views_drops_orphan_view(self, pg_engine: Engine) -> None:
"""Test compare_views detects view to drop."""
metadata = MetaData()
users = Table(
"test_users_drop_compare",
metadata,
Column("id", Integer, primary_key=True),
)
# Create a view in the database but NOT in metadata
orphan_view = View(
"test_orphan_view",
select(users.c.id),
)
from sqlalchemy_pgview import CreateView, DropView
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
conn.execute(CreateView(orphan_view, or_replace=True))
# Now compare with empty metadata (no views registered)
empty_metadata = MetaData()
with pg_engine.connect() as conn:
mock_context = MagicMock()
mock_context.connection = conn
mock_context.metadata = empty_metadata
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should detect orphan view to drop
drop_ops = [
op for op in mock_upgrade_ops.ops if isinstance(op, DropViewOp)
]
orphan_drop = [
op for op in drop_ops if op.view_name == "test_orphan_view"
]
assert len(orphan_drop) == 1
# Cleanup
with pg_engine.begin() as conn:
conn.execute(DropView(orphan_view, if_exists=True))
finally:
metadata.drop_all(pg_engine)
def test_compare_views_drops_orphan_materialized_view(
self, pg_engine: Engine
) -> None:
"""Test compare_views detects materialized view to drop."""
metadata = MetaData()
users = Table(
"test_users_drop_mv_compare",
metadata,
Column("id", Integer, primary_key=True),
)
# Create a materialized view in the database but NOT in metadata
orphan_mview = MaterializedView(
"test_orphan_mview",
select(func.count(users.c.id).label("cnt")),
with_data=True,
)
from sqlalchemy_pgview import CreateMaterializedView, DropMaterializedView
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
conn.execute(CreateMaterializedView(orphan_mview, if_not_exists=True))
# Now compare with empty metadata (no views registered)
empty_metadata = MetaData()
with pg_engine.connect() as conn:
mock_context = MagicMock()
mock_context.connection = conn
mock_context.metadata = empty_metadata
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should detect orphan materialized view to drop
drop_ops = [
op
for op in mock_upgrade_ops.ops
if isinstance(op, DropMaterializedViewOp)
]
orphan_drop = [
op for op in drop_ops if op.view_name == "test_orphan_mview"
]
assert len(orphan_drop) == 1
# Cleanup
with pg_engine.begin() as conn:
conn.execute(DropMaterializedView(orphan_mview, if_exists=True))
finally:
metadata.drop_all(pg_engine)
def test_compare_views_no_changes(self, pg_engine: Engine) -> None:
"""Test compare_views when views are in sync."""
metadata = MetaData()
users = Table(
"test_users_sync",
metadata,
Column("id", Integer, primary_key=True),
)
# Register a view in metadata
view = View(
"test_view_sync",
select(users.c.id),
metadata=metadata,
)
from sqlalchemy_pgview import CreateView, DropView
try:
metadata.create_all(pg_engine)
# Create the same view in database
with pg_engine.begin() as conn:
conn.execute(CreateView(view, or_replace=True))
with pg_engine.connect() as conn:
mock_context = MagicMock()
mock_context.connection = conn
mock_context.metadata = metadata
mock_upgrade_ops = MagicMock()
mock_upgrade_ops.ops = []
compare_views(mock_context, mock_upgrade_ops, [None])
# Should not detect any changes for this specific view
# (there might be other system views to drop)
create_ops = [
op
for op in mock_upgrade_ops.ops
if isinstance(op, CreateViewOp)
and op.view_name == "test_view_sync"
]
assert len(create_ops) == 0
# Cleanup
with pg_engine.begin() as conn:
conn.execute(DropView(view, if_exists=True))
finally:
metadata.drop_all(pg_engine)
class TestAlembicIntegration:
"""Integration tests for Alembic operations with real database."""
def test_create_and_drop_view_integration(self, pg_engine: Engine) -> None:
"""Test creating and dropping a view using Alembic ops."""
metadata = MetaData()
Table(
"test_users_alembic_int",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String(100)),
)
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
# Create view using raw SQL (simulating Alembic implementation)
conn.exec_driver_sql(
"CREATE VIEW test_view_alembic_int AS SELECT id, name FROM test_users_alembic_int"
)
# Verify view exists
from sqlalchemy import text
result = conn.execute(
text(
"SELECT COUNT(*) FROM pg_class WHERE relname = 'test_view_alembic_int'"
)
).scalar()
assert result == 1
# Drop view
conn.exec_driver_sql("DROP VIEW IF EXISTS test_view_alembic_int")
# Verify view is gone
result = conn.execute(
text(
"SELECT COUNT(*) FROM pg_class WHERE relname = 'test_view_alembic_int'"
)
).scalar()
assert result == 0
finally:
metadata.drop_all(pg_engine)
def test_create_and_drop_materialized_view_integration(
self, pg_engine: Engine
) -> None:
"""Test creating and dropping a materialized view using Alembic ops."""
metadata = MetaData()
Table(
"test_users_mview_int",
metadata,
Column("id", Integer, primary_key=True),
)
try:
metadata.create_all(pg_engine)
with pg_engine.begin() as conn:
# Create materialized view
conn.exec_driver_sql(
"CREATE MATERIALIZED VIEW test_mview_alembic_int AS "
"SELECT COUNT(*) as cnt FROM test_users_mview_int WITH DATA"
)
# Verify materialized view exists
from sqlalchemy import text
result = conn.execute(
text(
"SELECT COUNT(*) FROM pg_class WHERE relname = 'test_mview_alembic_int' AND relkind = 'm'"
)
).scalar()
assert result == 1
# Refresh materialized view
conn.exec_driver_sql(
"REFRESH MATERIALIZED VIEW test_mview_alembic_int WITH DATA"
)
# Drop materialized view
conn.exec_driver_sql(
"DROP MATERIALIZED VIEW IF EXISTS test_mview_alembic_int"
)
# Verify materialized view is gone
result = conn.execute(
text(
"SELECT COUNT(*) FROM pg_class WHERE relname = 'test_mview_alembic_int'"
)
).scalar()
assert result == 0
finally:
metadata.drop_all(pg_engine)