Initial commit

This commit is contained in:
2026-02-08 10:09:48 +01:00
commit d165506add
58 changed files with 9879 additions and 0 deletions

448
tests/test_relationships.py Normal file
View File

@@ -0,0 +1,448 @@
"""Tests for views with one-to-many and many-to-many relationships."""
from decimal import Decimal
from sqlalchemy import Table, func, select
from sqlalchemy.engine import Engine
from sqlalchemy_pgview import (
CreateMaterializedView,
CreateView,
DropMaterializedView,
DropView,
MaterializedView,
View,
)
class TestOneToManyViews:
"""Tests for views based on one-to-many relationships."""
def test_author_book_count_view(
self, pg_engine: Engine, pg_one_to_many_tables: dict[str, Table]
) -> None:
"""Test view that counts books per author (1:N aggregation)."""
authors = pg_one_to_many_tables["authors"]
books = pg_one_to_many_tables["books"]
# Create view: author with book count
author_stats = View(
"author_book_stats",
select(
authors.c.id,
authors.c.name,
authors.c.country,
func.count(books.c.id).label("book_count"),
func.coalesce(func.sum(books.c.price), 0).label("total_price"),
)
.select_from(authors.outerjoin(books, authors.c.id == books.c.author_id))
.group_by(authors.c.id, authors.c.name, authors.c.country),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(author_stats, or_replace=True))
# Query the view
result = conn.execute(
select(author_stats.as_table()).order_by(author_stats.as_table().c.name)
).fetchall()
assert len(result) == 3
# Gabriel Garcia Marquez - 1 book
assert result[0].name == "Gabriel Garcia Marquez"
assert result[0].book_count == 1
# George Orwell - 2 books
assert result[1].name == "George Orwell"
assert result[1].book_count == 2
# Haruki Murakami - 2 books
assert result[2].name == "Haruki Murakami"
assert result[2].book_count == 2
conn.execute(DropView(author_stats, if_exists=True))
def test_book_review_stats_view(
self, pg_engine: Engine, pg_one_to_many_tables: dict[str, Table]
) -> None:
"""Test view with nested 1:N (books -> reviews) aggregation."""
books = pg_one_to_many_tables["books"]
reviews = pg_one_to_many_tables["reviews"]
# Create view: book with review stats
book_review_stats = View(
"book_review_stats",
select(
books.c.id,
books.c.title,
books.c.price,
func.count(reviews.c.id).label("review_count"),
func.coalesce(func.avg(reviews.c.rating), 0).label("avg_rating"),
)
.select_from(books.outerjoin(reviews, books.c.id == reviews.c.book_id))
.group_by(books.c.id, books.c.title, books.c.price),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(book_review_stats, or_replace=True))
result = conn.execute(
select(book_review_stats.as_table()).order_by(
book_review_stats.as_table().c.review_count.desc()
)
).fetchall()
assert len(result) == 5
# 1984 has 3 reviews
assert result[0].title == "1984"
assert result[0].review_count == 3
assert float(result[0].avg_rating) > 4.0
conn.execute(DropView(book_review_stats, if_exists=True))
def test_chained_one_to_many_view(
self, pg_engine: Engine, pg_one_to_many_tables: dict[str, Table]
) -> None:
"""Test view spanning multiple 1:N relationships (author -> books -> reviews)."""
authors = pg_one_to_many_tables["authors"]
books = pg_one_to_many_tables["books"]
reviews = pg_one_to_many_tables["reviews"]
# Create view: author with aggregated review stats across all their books
author_review_summary = View(
"author_review_summary",
select(
authors.c.id,
authors.c.name,
func.count(reviews.c.id).label("total_reviews"),
func.coalesce(func.avg(reviews.c.rating), 0).label("avg_rating"),
)
.select_from(
authors.outerjoin(books, authors.c.id == books.c.author_id).outerjoin(
reviews, books.c.id == reviews.c.book_id
)
)
.group_by(authors.c.id, authors.c.name),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(author_review_summary, or_replace=True))
result = conn.execute(
select(author_review_summary.as_table()).order_by(
author_review_summary.as_table().c.total_reviews.desc()
)
).fetchall()
assert len(result) == 3
# George Orwell has most reviews (1984: 3 + Animal Farm: 1 = 4)
assert result[0].name == "George Orwell"
assert result[0].total_reviews == 4
conn.execute(DropView(author_review_summary, if_exists=True))
def test_one_to_many_materialized_view(
self, pg_engine: Engine, pg_one_to_many_tables: dict[str, Table]
) -> None:
"""Test materialized view with 1:N relationship."""
authors = pg_one_to_many_tables["authors"]
books = pg_one_to_many_tables["books"]
# Create materialized view for author statistics
author_stats_mv = MaterializedView(
"author_stats_mv",
select(
authors.c.id,
authors.c.name,
func.count(books.c.id).label("book_count"),
)
.select_from(authors.outerjoin(books, authors.c.id == books.c.author_id))
.group_by(authors.c.id, authors.c.name),
with_data=True,
)
with pg_engine.begin() as conn:
conn.execute(CreateMaterializedView(author_stats_mv))
result = conn.execute(
select(author_stats_mv.as_table()).order_by(
author_stats_mv.as_table().c.book_count.desc()
)
).fetchall()
assert len(result) == 3
assert result[0].book_count == 2 # Orwell or Murakami
conn.execute(DropMaterializedView(author_stats_mv, if_exists=True))
class TestManyToManyViews:
"""Tests for views based on many-to-many relationships."""
def test_student_course_count_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test view counting courses per student (M:N aggregation)."""
students = pg_many_to_many_tables["students"]
student_courses = pg_many_to_many_tables["student_courses"]
# Create view: student with course count and GPA
student_stats = View(
"student_stats",
select(
students.c.id,
students.c.name,
students.c.email,
func.count(student_courses.c.course_id).label("course_count"),
func.coalesce(func.avg(student_courses.c.grade), 0).label("gpa"),
)
.select_from(
students.outerjoin(
student_courses, students.c.id == student_courses.c.student_id
)
)
.group_by(students.c.id, students.c.name, students.c.email),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(student_stats, or_replace=True))
result = conn.execute(
select(student_stats.as_table()).order_by(
student_stats.as_table().c.course_count.desc()
)
).fetchall()
assert len(result) == 3
# Alice is enrolled in 3 courses
assert result[0].name == "Alice"
assert result[0].course_count == 3
# Bob is enrolled in 2 courses
assert result[1].name == "Bob"
assert result[1].course_count == 2
# Charlie is enrolled in 1 course
assert result[2].name == "Charlie"
assert result[2].course_count == 1
conn.execute(DropView(student_stats, if_exists=True))
def test_course_enrollment_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test view counting students per course (reverse M:N)."""
courses = pg_many_to_many_tables["courses"]
student_courses = pg_many_to_many_tables["student_courses"]
# Create view: course with enrollment count
course_enrollment = View(
"course_enrollment",
select(
courses.c.id,
courses.c.name,
courses.c.credits,
func.count(student_courses.c.student_id).label("student_count"),
func.coalesce(func.avg(student_courses.c.grade), 0).label("avg_grade"),
)
.select_from(
courses.outerjoin(
student_courses, courses.c.id == student_courses.c.course_id
)
)
.group_by(courses.c.id, courses.c.name, courses.c.credits),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(course_enrollment, or_replace=True))
result = conn.execute(
select(course_enrollment.as_table()).order_by(
course_enrollment.as_table().c.name
)
).fetchall()
assert len(result) == 3
# Database Systems - 2 students (Alice, Bob)
assert result[0].name == "Database Systems"
assert result[0].student_count == 2
# Machine Learning - 2 students (Alice, Bob)
assert result[1].name == "Machine Learning"
assert result[1].student_count == 2
# Web Development - 2 students (Alice, Charlie)
assert result[2].name == "Web Development"
assert result[2].student_count == 2
conn.execute(DropView(course_enrollment, if_exists=True))
def test_course_tags_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test view joining through M:N to aggregate tags."""
courses = pg_many_to_many_tables["courses"]
course_tags = pg_many_to_many_tables["course_tags"]
tags = pg_many_to_many_tables["tags"]
# Create view: course with concatenated tag names
course_with_tags = View(
"course_with_tags",
select(
courses.c.id,
courses.c.name,
func.count(tags.c.id).label("tag_count"),
func.string_agg(tags.c.name, ", ").label("tag_names"),
)
.select_from(
courses.outerjoin(course_tags, courses.c.id == course_tags.c.course_id).outerjoin(
tags, course_tags.c.tag_id == tags.c.id
)
)
.group_by(courses.c.id, courses.c.name),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(course_with_tags, or_replace=True))
result = conn.execute(
select(course_with_tags.as_table()).order_by(
course_with_tags.as_table().c.tag_count.desc()
)
).fetchall()
assert len(result) == 3
# Machine Learning has 2 tags (data, ai)
assert result[0].name == "Machine Learning"
assert result[0].tag_count == 2
conn.execute(DropView(course_with_tags, if_exists=True))
def test_student_courses_detailed_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test flattened view of M:N relationship (no aggregation)."""
students = pg_many_to_many_tables["students"]
courses = pg_many_to_many_tables["courses"]
student_courses = pg_many_to_many_tables["student_courses"]
# Create view: detailed enrollment records
enrollment_details = View(
"enrollment_details",
select(
students.c.name.label("student_name"),
students.c.email,
courses.c.name.label("course_name"),
courses.c.credits,
student_courses.c.grade,
).select_from(
student_courses.join(students, student_courses.c.student_id == students.c.id).join(
courses, student_courses.c.course_id == courses.c.id
)
),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(enrollment_details, or_replace=True))
result = conn.execute(select(enrollment_details.as_table())).fetchall()
# Total enrollments: 6
assert len(result) == 6
# Check Alice's Web Development grade
alice_web = [
r for r in result if r.student_name == "Alice" and r.course_name == "Web Development"
]
assert len(alice_web) == 1
assert alice_web[0].grade == Decimal("4.00")
conn.execute(DropView(enrollment_details, if_exists=True))
def test_many_to_many_materialized_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test materialized view with M:N relationship."""
students = pg_many_to_many_tables["students"]
student_courses = pg_many_to_many_tables["student_courses"]
# Create materialized view for student GPA
student_gpa_mv = MaterializedView(
"student_gpa_mv",
select(
students.c.id,
students.c.name,
func.round(func.avg(student_courses.c.grade), 2).label("gpa"),
)
.select_from(
students.join(student_courses, students.c.id == student_courses.c.student_id)
)
.group_by(students.c.id, students.c.name),
with_data=True,
)
with pg_engine.begin() as conn:
conn.execute(CreateMaterializedView(student_gpa_mv))
result = conn.execute(
select(student_gpa_mv.as_table()).order_by(
student_gpa_mv.as_table().c.gpa.desc()
)
).fetchall()
assert len(result) == 3
# Alice has highest GPA (3.8 + 4.0 + 3.5) / 3 = 3.77
assert result[0].name == "Alice"
conn.execute(DropMaterializedView(student_gpa_mv, if_exists=True))
def test_double_many_to_many_view(
self, pg_engine: Engine, pg_many_to_many_tables: dict[str, Table]
) -> None:
"""Test view joining two M:N relationships."""
students = pg_many_to_many_tables["students"]
courses = pg_many_to_many_tables["courses"]
student_courses = pg_many_to_many_tables["student_courses"]
course_tags = pg_many_to_many_tables["course_tags"]
tags = pg_many_to_many_tables["tags"]
# Create view: students with their course tags
student_tags = View(
"student_tags",
select(
students.c.id.label("student_id"),
students.c.name.label("student_name"),
func.count(func.distinct(tags.c.id)).label("unique_tags"),
)
.select_from(
students.join(student_courses, students.c.id == student_courses.c.student_id)
.join(courses, student_courses.c.course_id == courses.c.id)
.join(course_tags, courses.c.id == course_tags.c.course_id)
.join(tags, course_tags.c.tag_id == tags.c.id)
)
.group_by(students.c.id, students.c.name),
)
with pg_engine.begin() as conn:
conn.execute(CreateView(student_tags, or_replace=True))
result = conn.execute(
select(student_tags.as_table()).order_by(
student_tags.as_table().c.unique_tags.desc()
)
).fetchall()
assert len(result) == 3
# Alice takes all 3 courses, which have tags: data, programming, ai
assert result[0].student_name == "Alice"
assert result[0].unique_tags == 3
conn.execute(DropView(student_tags, if_exists=True))