mirror of
https://github.com/d3vyce/sqlalchemy-pgview.git
synced 2026-03-01 18:00:47 +01:00
1062 lines
36 KiB
Python
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)
|