-
First Check
Commit to Help
Example Codefrom datetime import date, datetime, time
from enum import Enum, unique
from typing import Optional, Tuple
from uuid import uuid4
from pydantic import UUID4, condecimal
from sqlalchemy import Column, DateTime
from sqlalchemy.dialects.postgresql import DATERANGE, ENUM, INT4RANGE, UUID
from sqlalchemy.schema import Index, PrimaryKeyConstraint
from sqlalchemy.sql import func
from sqlmodel import Field, SQLModel, create_engine, UniqueConstraint, select, Session, Relationship
from ..utils.db_view import create_materialized_view # NOTE: Same/Similar to https://sqlalchemy-utils.readthedocs.io/en/latest/_modules/sqlalchemy_utils/view.html#create_materialized_view
from .user import User
@unique
class Season(str, Enum):
"""Season enum"""
summer = "summer"
winter = "winter"
autumn = "autumn"
spring = "spring"
@unique
class PeakType(str, Enum):
"""Peak usage? enum"""
offpeak = "offpeak"
partialpeak = "partialpeak"
peak = "peak"
class PlanBase(SQLModel):
"""Base rate plan model"""
id: Optional[int]
uid: UUID4 = Field(
default_factory=uuid4, nullable=False, sa_column=Column(UUID(as_uuid=True))
)
name: str
is_deprecated: Optional[bool] = Field(default=False)
class Plan(PlanBase, table=True):
"""db plan model"""
__table_args__ = (UniqueConstraint("uid"),)
id: Optional[int] = Field(default=None, primary_key=True)
added_on: datetime = Field(default_factory=datetime.utcnow)
deprecated_on: Optional[datetime]
rates: list["Rate"] = Relationship(back_populates="plan")
users: list["User"] = Relationship(back_populates="plan")
class RateBase(SQLModel):
"""Plan rate model"""
id: Optional[int]
plan_id: Optional[int] = Field(foreign_key="plan.id")
plan: Optional[Plan] = Relationship(back_populates="rates")
rate: condecimal(max_digits=6, decimal_places=5) = Field(default=1.0)
season: Season = Field(
sa_column=Column(
ENUM(Season, metadata=SQLModel.metadata),
default=Season.summer,
)
)
peak_type: PeakType = Field(
sa_column=Column(
ENUM(PeakType, metadata=SQLModel.metadata),
default=PeakType.offpeak,
)
)
date_range: Tuple[date, date] = Field(nullable=False, sa_column=Column(DATERANGE))
time_range: Tuple[time, time] = Field(nullable=False, sa_column=Column(INT4RANGE))
class Rate(RateBase, table=True):
"""db rate model"""
__table_args__ = (
Index(
"idx_plan_ts",
"plan_id",
"date_range",
"time_range",
postgresql_using="gist",
),
UniqueConstraint("plan_id", "date_range", "time_range"),
)
id: Optional[int] = Field(default=None, primary_key=True)
units: list["ConsumptionBase"] = Relationship(back_populates="rate")
class ConsumptionBase(SQLModel):
"""User consumption model"""
user_id: int = Field(foreign_key="user.id")
user: Optional["User"] = Relationship(back_populates="consumption")
rate_id: Optional[int] = Field(foreign_key="rate.id")
rate: Optional[Rate] = Relationship(back_populates="units")
ts: datetime = Field(sa_column=Column(DateTime(timezone=True)))
units: condecimal(max_digits=8, decimal_places=5) = Field(default=0.0)
class Consumption(ConsumptionBase, table=True):
"""db user consumption model"""
__table_args__ = (
PrimaryKeyConstraint("user_id", "ts"),
UniqueConstraint("user_id", "ts"),
{"timescaledb_hypertable": {"time_column_name": "ts"}},
)
class Usage(SQLModel):
"""Materialized View combining user, rate and consumption data
NOTE: Only for creation of view, **DO NOT** use this model for querying
"""
# NOTE: Same/Similar to https://sqlalchemy-utils.readthedocs.io/en/latest/_modules/sqlalchemy_utils/view.html#create_materialized_view
# from sqlalchemy_utils import create_materialized_view
__table__ = create_materialized_view(
name="usage",
selectable=select(
[
Consumption.user_id,
Consumption.ts,
Rate.plan_id,
Consumption.units,
func.round(Rate.rate, 4).label("rate"),
func.round(Consumption.units * Rate.rate, 2).label("cost"),
Rate.season,
Rate.peak_type,
]
)
.join(Rate, Rate.id == Consumption.rate_id)
.order_by(Consumption.ts.desc()),
metadata=SQLModel.metadata,
indexes=[
Index(
"idx_usage_user_ts",
"user_id",
"ts",
postgresql_using="gist",
# postgresql_ops={ "ts": "DESC NULLS LAST"}, # NOTE: `gist` doesn't support `DESC NULLS LAST`
),
Index(
"idx_usage_user_plan_ts",
"user_id",
"plan_id",
"ts",
unique=True,
),
],
)
engine = create_engine("timescaledb+postgresql://test:pass@localhost:5432/test")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
query = select(Usage)
usage = session.execute(query).scalars().all() # TypeError: 'SQLModelMetaclass' object is not iterable Description
File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 5166, in create_legacy_select
self._raw_columns = [
^
TypeError: 'SQLModelMetaclass' object is not iterable Operating SystemLinux, macOS, Other Operating System Details
SQLModel Version0.0.6 Python VersionPython 3.11.4 Additional ContextI tried a few different approaches:
class UsageRead(SQLModel, table=True):
__tablename__ = "usage"
user_id: int
ts: datetime
plan_id: int
units: condecimal(max_digits=8, decimal_places=5)
rate: condecimal(max_digits=6, decimal_places=5)
cost: condecimal(max_digits=8, decimal_places=2)
season: Season
peak_type: PeakType This lead to a primary key error: sqlalchemy.exc.ArgumentError: Mapper mapped class UsageRead->usage could not assemble any primary key columns for mapped table 'usage' Trying any combination of (PS - you can find the same question posted on SO in case you want more information) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Solved it by using a custom class mapper! (ref.) from datetime import datetime
from pydantic import condecimal
from sqlalchemy.orm import registry
from sqlalchemy.schema import Index
from sqlalchemy.sql import func
from sqlmodel import SQLModel, select
from ..utils.db_view import create_materialized_view
mapper_registry = registry()
class Usage(SQLModel):
"""View combining user, rate and consumption data
NOTE: any changes to the `_Usage` schema must be reflected here
"""
__tablename__ = "usage"
user_id: int
ts: datetime
plan_id: int
units: condecimal(max_digits=8, decimal_places=5)
rate: condecimal(max_digits=6, decimal_places=5)
cost: condecimal(max_digits=10, decimal_places=2)
season: Season
peak_type: PeakType
class _Usage(SQLModel):
"""View combining user, rate and consumption data
NOTE: This is an internal model used **ONLY** for creating the materialized view
"""
__table__ = create_materialized_view(
name="usage",
selectable=select(
[
Consumption.user_id,
Consumption.ts,
Rate.plan_id,
Consumption.units,
func.round(Rate.rate, 4).label("rate"),
func.round(Consumption.units * Rate.rate, 2).label("cost"),
Rate.season,
Rate.peak_type,
]
)
.join(Rate, Rate.id == Consumption.rate_id)
.order_by(Consumption.ts.desc()),
metadata=SQLModel.metadata,
indexes=[
Index(
"idx_usage_user_ts",
"user_id",
"ts",
postgresql_using="gist",
# postgresql_ops={ "ts": "DESC NULLS LAST"}, # NOTE: `gist` doesn't support `DESC NULLS LAST`
),
Index(
"idx_usage_user_plan_ts",
"user_id",
"ts",
"plan_id",
unique=True,
),
],
)
mapper_registry.map_imperatively(Usage, _Usage.__table__) Now I can use the |
Beta Was this translation helpful? Give feedback.
Solved it by using a custom class mapper! (ref.)