Skip to content

Commit 0383897

Browse files
committed
feat(use-cases): add TaskPilot - SaaS playground for Self-Driving Postgres
TaskPilot is a full-featured Linear/Jira clone designed as a testing playground for postgres_ai monitoring tools. It provides: Architecture: - Python FastAPI backend with SQLAlchemy 2.0 + asyncpg - PostgreSQL 16 with pg_stat_statements, pg_trgm extensions - Alembic for database migrations - Docker Compose for easy deployment - Redis for caching and task queue Database Schema: - 15+ tables modeling a real issue tracker - Multi-tenant architecture with organizations - Issues, comments, activity logs, notifications - Designed to generate ~10 GiB initial data, growing 10 GiB/week Testing Tools: - k6 load testing scripts simulating real user behavior - Data seeding script for 10 GiB initial database - AI engineer simulator making 1-3 schema changes/day AI Engineer Roadmap (12 weeks): - Simulates 3 engineers with different experience levels - Mix of good, suboptimal, and problematic changes - Designed to trigger postgres_ai findings: - Missing indexes (H002) - Redundant indexes (H004) - Bloat detection (F004/F005) - Slow queries (K003) This enables testing of: - postgres_ai monitoring dashboards - Checkup reports (A001-N001) - pg_index_pilot recommendations - Schema migration analysis
1 parent 6d64dfc commit 0383897

29 files changed

Lines changed: 7434 additions & 0 deletions

use-cases/taskpilot/Dockerfile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# TaskPilot API Dockerfile
2+
FROM python:3.12-slim
3+
4+
# Set environment variables
5+
ENV PYTHONDONTWRITEBYTECODE=1
6+
ENV PYTHONUNBUFFERED=1
7+
ENV PIP_NO_CACHE_DIR=1
8+
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
9+
10+
# Set work directory
11+
WORKDIR /app
12+
13+
# Install system dependencies
14+
RUN apt-get update && apt-get install -y --no-install-recommends \
15+
gcc \
16+
libpq-dev \
17+
curl \
18+
&& rm -rf /var/lib/apt/lists/*
19+
20+
# Install Python dependencies
21+
COPY requirements.txt .
22+
RUN pip install --no-cache-dir -r requirements.txt
23+
24+
# Copy application code
25+
COPY . .
26+
27+
# Create non-root user
28+
RUN useradd --create-home --shell /bin/bash appuser
29+
RUN chown -R appuser:appuser /app
30+
USER appuser
31+
32+
# Expose port
33+
EXPOSE 8000
34+
35+
# Health check
36+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
37+
CMD curl -f http://localhost:8000/health || exit 1
38+
39+
# Run the application
40+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

use-cases/taskpilot/README.md

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

use-cases/taskpilot/alembic.ini

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Alembic Configuration for TaskPilot
2+
3+
[alembic]
4+
script_location = migrations
5+
prepend_sys_path = .
6+
version_path_separator = os
7+
8+
# Template for new migration files
9+
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
10+
11+
# Timezone for revision timestamps
12+
timezone = UTC
13+
14+
# Truncate revision hashes
15+
truncate_slug_length = 40
16+
17+
# Generate revision numbers (sequence)
18+
revision_environment = false
19+
20+
# Keep empty migration files
21+
sourceless = false
22+
23+
# Output encoding
24+
output_encoding = utf-8
25+
26+
# Database URL (can be overridden by env var)
27+
sqlalchemy.url = postgresql://taskpilot:taskpilot@localhost:5433/taskpilot
28+
29+
[post_write_hooks]
30+
# hooks = black
31+
# black.type = console_scripts
32+
# black.entrypoint = black
33+
# black.options = -q
34+
35+
[loggers]
36+
keys = root,sqlalchemy,alembic
37+
38+
[handlers]
39+
keys = console
40+
41+
[formatters]
42+
keys = generic
43+
44+
[logger_root]
45+
level = WARN
46+
handlers = console
47+
qualname =
48+
49+
[logger_sqlalchemy]
50+
level = WARN
51+
handlers =
52+
qualname = sqlalchemy.engine
53+
54+
[logger_alembic]
55+
level = INFO
56+
handlers =
57+
qualname = alembic
58+
59+
[handler_console]
60+
class = StreamHandler
61+
args = (sys.stderr,)
62+
level = NOTSET
63+
formatter = generic
64+
65+
[formatter_generic]
66+
format = %(levelname)-5.5s [%(name)s] %(message)s
67+
datefmt = %H:%M:%S
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""TaskPilot - A Linear clone for Self-Driving Postgres testing."""
2+
__version__ = "0.1.0"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
TaskPilot API Routes
3+
4+
All API endpoints are organized by resource type.
5+
"""
6+
7+
from fastapi import APIRouter
8+
9+
from app.api import auth, issues, projects, comments, users, analytics
10+
11+
router = APIRouter()
12+
13+
# Include all route modules
14+
router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
15+
router.include_router(users.router, prefix="/users", tags=["Users"])
16+
router.include_router(projects.router, prefix="/projects", tags=["Projects"])
17+
router.include_router(issues.router, prefix="/issues", tags=["Issues"])
18+
router.include_router(comments.router, prefix="/issues/{issue_id}/comments", tags=["Comments"])
19+
router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"])
20+
21+
22+
@router.get("/")
23+
async def api_root():
24+
"""API root endpoint."""
25+
return {
26+
"message": "TaskPilot API v1",
27+
"endpoints": {
28+
"auth": "/api/v1/auth",
29+
"users": "/api/v1/users",
30+
"projects": "/api/v1/projects",
31+
"issues": "/api/v1/issues",
32+
"analytics": "/api/v1/analytics",
33+
}
34+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Analytics API endpoints.
3+
4+
These endpoints run heavy aggregate queries that benefit from
5+
materialized views and proper indexing.
6+
"""
7+
8+
from datetime import datetime, timedelta
9+
from typing import Annotated
10+
from uuid import UUID
11+
12+
from fastapi import APIRouter, Depends, Query
13+
from pydantic import BaseModel
14+
from sqlalchemy.ext.asyncio import AsyncSession
15+
16+
from app.models.database import get_db
17+
from app.api.auth import get_current_user
18+
19+
router = APIRouter()
20+
21+
22+
class OrganizationMetrics(BaseModel):
23+
"""Organization-level metrics."""
24+
total_issues: int
25+
open_issues: int
26+
completed_this_week: int
27+
overdue_issues: int
28+
avg_resolution_hours: float
29+
total_comments: int
30+
active_users: int
31+
32+
33+
class ProjectVelocity(BaseModel):
34+
"""Project velocity data."""
35+
week: str
36+
issues_completed: int
37+
points_completed: float
38+
moving_avg: float
39+
40+
41+
class WorkloadItem(BaseModel):
42+
"""Team workload item."""
43+
user_id: str
44+
user_name: str
45+
assigned_issues: int
46+
in_progress: int
47+
completed_this_week: int
48+
49+
50+
@router.get("/organization", response_model=OrganizationMetrics)
51+
async def get_organization_metrics(
52+
current_user: Annotated[dict, Depends(get_current_user)],
53+
db: Annotated[AsyncSession, Depends(get_db)],
54+
) -> OrganizationMetrics:
55+
"""
56+
Get organization-wide metrics.
57+
58+
This is a heavy query that aggregates across multiple tables.
59+
Benefits from the mv_organization_stats materialized view.
60+
"""
61+
# In production, query materialized view or aggregate
62+
return OrganizationMetrics(
63+
total_issues=50000,
64+
open_issues=15000,
65+
completed_this_week=500,
66+
overdue_issues=200,
67+
avg_resolution_hours=48.5,
68+
total_comments=200000,
69+
active_users=150,
70+
)
71+
72+
73+
@router.get("/projects/{project_id}/velocity")
74+
async def get_project_velocity(
75+
project_id: UUID,
76+
current_user: Annotated[dict, Depends(get_current_user)],
77+
db: Annotated[AsyncSession, Depends(get_db)],
78+
weeks: int = Query(12, ge=1, le=52),
79+
) -> list[ProjectVelocity]:
80+
"""
81+
Get project velocity over time.
82+
83+
Uses the mv_project_weekly_velocity materialized view.
84+
"""
85+
return [
86+
ProjectVelocity(
87+
week=(datetime.now() - timedelta(weeks=i)).strftime("%Y-%m-%d"),
88+
issues_completed=10 + i,
89+
points_completed=25.0 + i * 2,
90+
moving_avg=12.0 + i,
91+
)
92+
for i in range(weeks)
93+
]
94+
95+
96+
@router.get("/workload")
97+
async def get_team_workload(
98+
current_user: Annotated[dict, Depends(get_current_user)],
99+
db: Annotated[AsyncSession, Depends(get_db)],
100+
) -> list[WorkloadItem]:
101+
"""
102+
Get current team workload distribution.
103+
"""
104+
return [
105+
WorkloadItem(
106+
user_id=f"user-{i}",
107+
user_name=f"Team Member {i}",
108+
assigned_issues=10 + i * 2,
109+
in_progress=3 + i,
110+
completed_this_week=5 + i,
111+
)
112+
for i in range(1, 6)
113+
]
114+
115+
116+
@router.get("/search")
117+
async def search_issues(
118+
q: str,
119+
current_user: Annotated[dict, Depends(get_current_user)],
120+
db: Annotated[AsyncSession, Depends(get_db)],
121+
project_id: UUID = None,
122+
limit: int = Query(20, ge=1, le=100),
123+
):
124+
"""
125+
Full-text search across issues.
126+
127+
Uses the search_vector column with GIN index.
128+
"""
129+
return {
130+
"query": q,
131+
"results": [
132+
{
133+
"id": f"issue-{i}",
134+
"title": f"Issue matching '{q}' #{i}",
135+
"project_key": "PRJ",
136+
"number": i,
137+
"rank": 1.0 - (i * 0.1),
138+
}
139+
for i in range(1, min(limit + 1, 11))
140+
],
141+
"total": 10,
142+
}

0 commit comments

Comments
 (0)