mirror of
https://github.com/d3vyce/sqlalchemy-pgview.git
synced 2026-03-01 19:50:46 +01:00
Initial commit
This commit is contained in:
193
README.md
Normal file
193
README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# SQLAlchemy-PGView
|
||||
|
||||
A SQLAlchemy 2.0+ extension that provides first-class support for PostgreSQL views and materialized views.
|
||||
|
||||
[](https://github.com/astral-sh/ty)
|
||||
[](https://github.com/astral-sh/uv)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://www.sqlalchemy.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Features
|
||||
|
||||
- **Declarative Views** - Class-based view definitions using multiple inheritance with SQLAlchemy ORM
|
||||
- **View & MaterializedView Classes** - Define PostgreSQL views as Python objects with full DDL support
|
||||
- **Alembic Integration** - Database migration operations (`op.create_view()`, `op.drop_view()`, etc.)
|
||||
- **Auto-Refresh** - Automatically refresh materialized views on data changes
|
||||
- **Async Support** - Works with asyncpg and SQLAlchemy's async engines
|
||||
- **Dependency Tracking** - Query PostgreSQL system catalogs for view dependencies
|
||||
- **Type Safety** - Full type annotations for modern Python development
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- SQLAlchemy 2.0+
|
||||
- PostgreSQL database
|
||||
- Alembic 1.10+ (optional, for migrations)
|
||||
|
||||
## Installation
|
||||
|
||||
Base package
|
||||
```bash
|
||||
uv pip install "sqlalchemy-pgview"
|
||||
```
|
||||
|
||||
With alembic support
|
||||
```bash
|
||||
uv pip install "sqlalchemy-pgview[alembic]"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import create_engine, select, func, String, Numeric, Integer
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
|
||||
from sqlalchemy_pgview import ViewBase, MaterializedViewBase
|
||||
|
||||
# Define your base and models
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
is_active: Mapped[bool] = mapped_column(default=True)
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = "orders"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer)
|
||||
total: Mapped[Decimal] = mapped_column(Numeric(10, 2))
|
||||
|
||||
# Define a regular view (computed on every query)
|
||||
class ActiveUsers(ViewBase, Base):
|
||||
__tablename__ = "active_users"
|
||||
__select__ = select(User.id, User.name).where(User.is_active == True)
|
||||
|
||||
# Define a materialized view (cached results, needs refresh)
|
||||
class UserStats(MaterializedViewBase, Base):
|
||||
__tablename__ = "user_stats"
|
||||
__select__ = select(
|
||||
User.id.label("user_id"),
|
||||
User.name,
|
||||
func.count(Order.id).label("order_count"),
|
||||
func.coalesce(func.sum(Order.total), 0).label("total_spent"),
|
||||
).select_from(User.__table__.outerjoin(Order.__table__, User.id == Order.user_id)
|
||||
).group_by(User.id, User.name)
|
||||
|
||||
# Create everything (tables + views)
|
||||
engine = create_engine("postgresql://user:pass@localhost/mydb")
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Query the view
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(select(ActiveUsers.as_table())).fetchall()
|
||||
for row in result:
|
||||
print(f"{row.name}")
|
||||
|
||||
# Refresh materialized view
|
||||
with engine.begin() as conn:
|
||||
UserStats.refresh(conn)
|
||||
```
|
||||
|
||||
Automatically refresh materialized views when underlying data changes:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Enable auto-refresh when Order table changes
|
||||
UserStats.auto_refresh_on(Session, Order.__table__)
|
||||
|
||||
# Now commits automatically refresh the materialized view
|
||||
with Session(engine) as session:
|
||||
session.add(Order(user_id=1, total=Decimal("100.00")))
|
||||
session.commit() # UserStats is automatically refreshed
|
||||
```
|
||||
|
||||
## Alembic Integration
|
||||
|
||||
SQLAlchemy-PGView integrates with Alembic for automatic view detection and migration generation.
|
||||
|
||||
Import the alembic module in your `env.py` to enable autogenerate:
|
||||
|
||||
```python
|
||||
# env.py
|
||||
import sqlalchemy_pgview.alembic # Registers autogenerate support
|
||||
```
|
||||
|
||||
Then generate migrations automatically:
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "add user views"
|
||||
```
|
||||
|
||||
Alembic will detect:
|
||||
- **New views**: Views in metadata but not in database
|
||||
- **Removed views**: Views in database but not in metadata
|
||||
|
||||
You also can manually add refresh in existing migration:
|
||||
|
||||
```python
|
||||
def upgrade():
|
||||
# After data changes, refresh materialized views
|
||||
op.refresh_materialized_view("user_stats", concurrently=True)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### ViewBase (Declarative)
|
||||
|
||||
```python
|
||||
class MyView(ViewBase, Base):
|
||||
__tablename__ = "my_view" # Required: view name
|
||||
__select__ = select(...) # Required: SELECT statement
|
||||
__schema__ = "public" # Optional: schema name
|
||||
```
|
||||
|
||||
### MaterializedViewBase (Declarative)
|
||||
|
||||
```python
|
||||
class MyMaterializedView(MaterializedViewBase, Base):
|
||||
__tablename__ = "my_mview" # Required: view name
|
||||
__select__ = select(...) # Required: SELECT statement
|
||||
__schema__ = "public" # Optional: schema name
|
||||
__with_data__ = True # Optional: populate on creation (default: True)
|
||||
```
|
||||
|
||||
### View (Imperative)
|
||||
|
||||
```python
|
||||
View(
|
||||
name: str, # View name
|
||||
selectable: Select, # SQLAlchemy SELECT statement
|
||||
schema: str | None = None, # Schema name (default: public)
|
||||
metadata: MetaData | None = None, # MetaData for auto-registration
|
||||
)
|
||||
```
|
||||
|
||||
### MaterializedView (Imperative)
|
||||
|
||||
```python
|
||||
MaterializedView(
|
||||
name: str, # View name
|
||||
selectable: Select, # SQLAlchemy SELECT statement
|
||||
schema: str | None = None, # Schema name (default: public)
|
||||
metadata: MetaData | None = None, # MetaData for auto-registration
|
||||
with_data: bool = True, # Populate on creation
|
||||
)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues and pull requests.
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit the [docs](docs/) directory.
|
||||
Reference in New Issue
Block a user