from typing import Dict, List, Optional
from sqlalchemy import (
Column,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
event,
select,
)
from sqlalchemy.engine import Engine
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, Session, declarative_base, relationship
# from sqlalchemy.dialects.postgresql import JSON
from buildingmotif.database.utils import JSONType
Base = declarative_base()
# https://docs.sqlalchemy.org/en/14/dialects/sqlite.html#foreign-key-support
[docs]@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
[docs]class DBModel(Base):
"""A Model is a metadata model of all or part of a building."""
__tablename__ = "models"
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column(String())
description: Mapped[str] = Column(Text(), default="", nullable=False)
graph_id: Mapped[str] = Column(String())
manifest_id: Mapped[int] = Column(
Integer, ForeignKey("shape_collection.id", ondelete="CASCADE"), nullable=False
)
manifest: "DBShapeCollection" = relationship(
"DBShapeCollection",
uselist=False,
cascade="all",
passive_deletes=True,
)
[docs]class DBShapeCollection(Base):
"""A ShapeCollection is a collection of shapes, which are used to validate
parts of a model.
"""
__tablename__ = "shape_collection"
id: Mapped[int] = Column(Integer, primary_key=True)
graph_id: Mapped[str] = Column(String())
[docs]class DBLibrary(Base):
"""A Library is a distributable collection of Templates and Shapes."""
__tablename__ = "library"
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column(String(), nullable=False, unique=True)
# do not use passive_deletes here because we want to handle the deletion of templates
templates: Mapped[List["DBTemplate"]] = relationship(
"DBTemplate", back_populates="library", cascade="all"
)
shape_collection_id = Column(
Integer, ForeignKey("shape_collection.id", ondelete="CASCADE"), nullable=False
)
shape_collection: DBShapeCollection = relationship(
"DBShapeCollection",
uselist=False,
cascade="all",
passive_deletes=True,
)
[docs]class DBTemplate(Base):
"""A Template is used to generate content for a model."""
# TODO: doc table properties better.
__tablename__ = "template"
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column(String(), nullable=False)
body_id: Mapped[str] = Column(String())
optional_args: Mapped[List[str]] = Column(JSONType) # type: ignore
library_id: Mapped[int] = Column(
Integer, ForeignKey("library.id", ondelete="CASCADE"), nullable=False
)
library: Mapped[DBLibrary] = relationship("DBLibrary", back_populates="templates")
dependencies: Mapped["DBTemplateDependency"] = relationship(
"DBTemplateDependency", back_populates="template", cascade="all,delete-orphan"
)
__table_args__ = (
UniqueConstraint(
"name",
"library_id",
name="name_library_unique_constraint",
),
)
[docs]class DBTemplateDependency(Base):
__tablename__ = "template_dependency"
id: Mapped[int] = Column(Integer, primary_key=True)
template_id: Mapped[int] = Column(
Integer, ForeignKey("template.id", ondelete="CASCADE"), nullable=False
)
template: Mapped[DBTemplate] = relationship(
DBTemplate, back_populates="dependencies"
)
dependency_library_name: Mapped[str] = Column(String, nullable=False)
dependency_template_name: Mapped[str] = Column(String, nullable=False)
# args are a mapping of dependee args to dependant args
args: Mapped[Dict[str, str]] = Column(JSONType) # type: ignore
@hybrid_property
def dependency_template(self) -> Optional[DBTemplate]:
session = Session.object_session(self)
statement = (
select(DBTemplate)
.join(DBTemplate.library)
.where(
DBTemplate.name == self.dependency_template_name,
DBLibrary.name == self.dependency_library_name,
)
)
try:
return session.scalars(statement).one()
except NoResultFound:
return None
@dependency_template.expression
def _dependency_tempalate(self):
return (
select(DBTemplate)
.join(DBTemplate.library)
.where(
DBTemplate.name == self.dependency_template_name,
DBLibrary.name == self.dependency_library_name,
)
)
__table_args__ = (
UniqueConstraint(
"template_id",
"dependency_library_name",
"dependency_template_name",
"args",
name="template_dependency_unique_constraint",
),
)