diff --git a/apps/backend/alembic/versions/e6d2026091501_create_data_schema.py b/apps/backend/alembic/versions/e6d2026091501_create_data_schema.py index 50cc41f..8fb3694 100644 --- a/apps/backend/alembic/versions/e6d2026091501_create_data_schema.py +++ b/apps/backend/alembic/versions/e6d2026091501_create_data_schema.py @@ -1,4 +1,4 @@ -"""Création des six tables Data et de l'hypertable readings. +"""Création des six tables Data et de l'hypertable reading. Revision ID: e6d2026091501 Revises: 821f71be74c0 @@ -17,7 +17,7 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - "datasets", + "dataset", sa.Column("dataset_id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("dataset_name", sa.Text(), nullable=False), sa.Column("archive_sha256", sa.String(length=64), nullable=False), @@ -26,12 +26,12 @@ def upgrade() -> None: sa.Column( "metadata", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False ), - sa.CheckConstraint("dataset_id > 0", name="ck_datasets_positive_id"), + sa.CheckConstraint("dataset_id > 0", name="ck_dataset_positive_id"), sa.PrimaryKeyConstraint("dataset_id"), - sa.UniqueConstraint("archive_sha256", name="uq_datasets_archive_sha256"), + sa.UniqueConstraint("archive_sha256", name="uq_dataset_archive_sha256"), ) op.create_table( - "sites", + "site", sa.Column("site_id", sa.Text(), nullable=False), sa.Column("site_name", sa.Text(), nullable=False), sa.Column("site_type", sa.Text(), nullable=False), @@ -41,7 +41,7 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("site_id"), ) op.create_table( - "predictions", + "prediction", sa.Column("prediction_id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("site_id", sa.Text(), nullable=False), sa.Column( @@ -59,29 +59,29 @@ def upgrade() -> None: sa.Column("failure_reason", sa.Text(), nullable=True), sa.CheckConstraint( "(status = 'available' AND predicted_value IS NOT NULL AND failure_reason IS NULL) OR (status IN ('insufficient_data', 'error') AND predicted_value IS NULL AND failure_reason IS NOT NULL)", - name="ck_predictions_status", + name="ck_prediction_status", ), sa.CheckConstraint( "target_metric <> 'consumption_kwh' OR period_minutes IS NOT NULL", - name="ck_predictions_energy_period", + name="ck_prediction_energy_period", ), sa.CheckConstraint( - "target_metric IN ('consumption_kwh', 'consumption_kw')", name="ck_predictions_metric" + "target_metric IN ('consumption_kwh', 'consumption_kw')", name="ck_prediction_metric" ), sa.CheckConstraint( - "period_minutes IS NULL OR period_minutes > 0", name="ck_predictions_period" + "period_minutes IS NULL OR period_minutes > 0", name="ck_prediction_period" ), sa.ForeignKeyConstraint( - ["site_id"], ["sites.site_id"], name="fk_predictions_site", ondelete="RESTRICT" + ["site_id"], ["site.site_id"], name="fk_prediction_site", ondelete="RESTRICT" ), sa.PrimaryKeyConstraint("prediction_id"), - sa.UniqueConstraint("prediction_id", "site_id", name="uq_predictions_id_site"), + sa.UniqueConstraint("prediction_id", "site_id", name="uq_prediction_id_site"), ) op.create_index( - "ix_predictions_site_target", "predictions", ["site_id", "target_at"], unique=False + "ix_prediction_site_target", "prediction", ["site_id", "target_at"], unique=False ) op.create_table( - "readings", + "reading", sa.Column("reading_id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("site_id", sa.Text(), nullable=False), sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), @@ -114,44 +114,44 @@ def upgrade() -> None: ), sa.CheckConstraint( "(source = 'csv' AND dataset_id IS NOT NULL) OR (source IN ('api_current', 'api_history') AND dataset_id IS NULL)", - name="ck_readings_dataset_source", + name="ck_reading_dataset_source", ), sa.CheckConstraint( "data_quality IS NULL OR data_quality IN ('good', 'partial', 'degraded', 'critical')", - name="ck_readings_quality", + name="ck_reading_quality", ), sa.CheckConstraint( - "source IN ('csv', 'api_current', 'api_history')", name="ck_readings_source" + "source IN ('csv', 'api_current', 'api_history')", name="ck_reading_source" ), sa.CheckConstraint( "(imputed_values IS NULL AND imputation_method IS NULL) OR (imputed_values IS NOT NULL AND imputation_method IS NOT NULL)", - name="ck_readings_imputation", + name="ck_reading_imputation", ), sa.ForeignKeyConstraint( - ["dataset_id"], ["datasets.dataset_id"], name="fk_readings_dataset", ondelete="RESTRICT" + ["dataset_id"], ["dataset.dataset_id"], name="fk_reading_dataset", ondelete="RESTRICT" ), sa.ForeignKeyConstraint( - ["site_id"], ["sites.site_id"], name="fk_readings_site", ondelete="RESTRICT" + ["site_id"], ["site.site_id"], name="fk_reading_site", ondelete="RESTRICT" ), sa.PrimaryKeyConstraint("reading_id", "timestamp"), ) - op.create_index("ix_readings_dataset_id", "readings", ["dataset_id"], unique=False) + op.create_index("ix_reading_dataset_id", "reading", ["dataset_id"], unique=False) op.create_index( - "ix_readings_site_timestamp", "readings", ["site_id", "timestamp"], unique=False + "ix_reading_site_timestamp", "reading", ["site_id", "timestamp"], unique=False ) op.create_index( - "uq_readings_source", - "readings", + "uq_reading_source", + "reading", ["site_id", "timestamp", "source", sa.literal_column("coalesce(dataset_id, 0)")], unique=True, ) op.execute( - "SELECT create_hypertable('readings', by_range('timestamp'), create_default_indexes => FALSE)" + "SELECT create_hypertable('reading', by_range('timestamp'), create_default_indexes => FALSE)" ) op.create_table( - "alerts", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("alert_id", sa.Text(), nullable=False), + "alert", + sa.Column("alert_id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("source_alert_id", sa.Text(), nullable=False), sa.Column("site_id", sa.Text(), nullable=False), sa.Column("source", sa.Text(), nullable=False), sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), @@ -166,27 +166,29 @@ def upgrade() -> None: "raw_data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False ), sa.CheckConstraint( - "severity IN ('low', 'medium', 'high', 'critical')", name="ck_alerts_severity" + "severity IN ('low', 'medium', 'high', 'critical')", name="ck_alert_severity" ), - sa.CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alerts_source"), + sa.CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alert_source"), sa.CheckConstraint( - "type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alerts_type" + "type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alert_type" ), sa.ForeignKeyConstraint( ["prediction_id", "site_id"], - ["predictions.prediction_id", "predictions.site_id"], - name="fk_alerts_prediction_site", + ["prediction.prediction_id", "prediction.site_id"], + name="fk_alert_prediction_site", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( - ["site_id"], ["sites.site_id"], name="fk_alerts_site", ondelete="RESTRICT" + ["site_id"], ["site.site_id"], name="fk_alert_site", ondelete="RESTRICT" + ), + sa.PrimaryKeyConstraint("alert_id"), + sa.UniqueConstraint( + "source", "site_id", "source_alert_id", name="uq_alert_source_reference" ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("source", "site_id", "alert_id", name="uq_alerts_source_site_id"), ) - op.create_index("ix_alerts_site_timestamp", "alerts", ["site_id", "timestamp"], unique=False) + op.create_index("ix_alert_site_timestamp", "alert", ["site_id", "timestamp"], unique=False) op.create_table( - "recommendations", + "recommendation", sa.Column("recommendation_id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("alert_id", sa.BigInteger(), nullable=False), sa.Column("action", sa.Text(), nullable=False), @@ -199,18 +201,18 @@ def upgrade() -> None: nullable=False, ), sa.ForeignKeyConstraint( - ["alert_id"], ["alerts.id"], name="fk_recommendations_alert", ondelete="RESTRICT" + ["alert_id"], ["alert.alert_id"], name="fk_recommendation_alert", ondelete="RESTRICT" ), sa.PrimaryKeyConstraint("recommendation_id"), - sa.UniqueConstraint("alert_id", "rule_reference", name="uq_recommendations_alert_rule"), + sa.UniqueConstraint("alert_id", "rule_reference", name="uq_recommendation_alert_rule"), ) # ### end Alembic commands ### def downgrade() -> None: - op.drop_table("recommendations") - op.drop_table("alerts") - op.drop_table("readings") - op.drop_table("predictions") - op.drop_table("sites") - op.drop_table("datasets") + op.drop_table("recommendation") + op.drop_table("alert") + op.drop_table("reading") + op.drop_table("prediction") + op.drop_table("site") + op.drop_table("dataset") diff --git a/apps/backend/app/models/energy.py b/apps/backend/app/models/energy.py index de27c7c..578ca50 100644 --- a/apps/backend/app/models/energy.py +++ b/apps/backend/app/models/energy.py @@ -28,10 +28,10 @@ from app.db.base import Base class Dataset(Base): - __tablename__ = "datasets" + __tablename__ = "dataset" __table_args__ = ( - CheckConstraint("dataset_id > 0", name="ck_datasets_positive_id"), - UniqueConstraint("archive_sha256", name="uq_datasets_archive_sha256"), + CheckConstraint("dataset_id > 0", name="ck_dataset_positive_id"), + UniqueConstraint("archive_sha256", name="uq_dataset_archive_sha256"), ) dataset_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) @@ -44,7 +44,7 @@ class Dataset(Base): class Site(Base): - __tablename__ = "sites" + __tablename__ = "site" site_id: Mapped[str] = mapped_column(Text, primary_key=True) site_name: Mapped[str] = mapped_column(Text) @@ -55,38 +55,38 @@ class Site(Base): class Reading(Base): - __tablename__ = "readings" + __tablename__ = "reading" __table_args__ = ( CheckConstraint( - "source IN ('csv', 'api_current', 'api_history')", name="ck_readings_source" + "source IN ('csv', 'api_current', 'api_history')", name="ck_reading_source" ), CheckConstraint( "(source = 'csv' AND dataset_id IS NOT NULL) OR " "(source IN ('api_current', 'api_history') AND dataset_id IS NULL)", - name="ck_readings_dataset_source", + name="ck_reading_dataset_source", ), CheckConstraint( "data_quality IS NULL OR data_quality IN ('good', 'partial', 'degraded', 'critical')", - name="ck_readings_quality", + name="ck_reading_quality", ), CheckConstraint( "(imputed_values IS NULL AND imputation_method IS NULL) OR " "(imputed_values IS NOT NULL AND imputation_method IS NOT NULL)", - name="ck_readings_imputation", + name="ck_reading_imputation", ), - Index("ix_readings_site_timestamp", "site_id", "timestamp"), - Index("ix_readings_dataset_id", "dataset_id"), + Index("ix_reading_site_timestamp", "site_id", "timestamp"), + Index("ix_reading_dataset_id", "dataset_id"), ) reading_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) site_id: Mapped[str] = mapped_column( - Text, ForeignKey("sites.site_id", name="fk_readings_site", ondelete="RESTRICT") + Text, ForeignKey("site.site_id", name="fk_reading_site", ondelete="RESTRICT") ) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True) source: Mapped[str] = mapped_column(Text) dataset_id: Mapped[int | None] = mapped_column( BigInteger, - ForeignKey("datasets.dataset_id", name="fk_readings_dataset", ondelete="RESTRICT"), + ForeignKey("dataset.dataset_id", name="fk_reading_dataset", ondelete="RESTRICT"), ) consumption_kw: Mapped[float | None] = mapped_column(Double) consumption_kwh: Mapped[float | None] = mapped_column(Double) @@ -109,7 +109,7 @@ class Reading(Base): Index( - "uq_readings_source", + "uq_reading_source", Reading.site_id, Reading.timestamp, Reading.source, @@ -119,32 +119,32 @@ Index( class Prediction(Base): - __tablename__ = "predictions" + __tablename__ = "prediction" __table_args__ = ( - UniqueConstraint("prediction_id", "site_id", name="uq_predictions_id_site"), - Index("ix_predictions_site_target", "site_id", "target_at"), + UniqueConstraint("prediction_id", "site_id", name="uq_prediction_id_site"), + Index("ix_prediction_site_target", "site_id", "target_at"), CheckConstraint( "target_metric IN ('consumption_kwh', 'consumption_kw')", - name="ck_predictions_metric", + name="ck_prediction_metric", ), CheckConstraint( - "period_minutes IS NULL OR period_minutes > 0", name="ck_predictions_period" + "period_minutes IS NULL OR period_minutes > 0", name="ck_prediction_period" ), CheckConstraint( "target_metric <> 'consumption_kwh' OR period_minutes IS NOT NULL", - name="ck_predictions_energy_period", + name="ck_prediction_energy_period", ), CheckConstraint( "(status = 'available' AND predicted_value IS NOT NULL AND failure_reason IS NULL) OR " "(status IN ('insufficient_data', 'error') AND predicted_value IS NULL " "AND failure_reason IS NOT NULL)", - name="ck_predictions_status", + name="ck_prediction_status", ), ) prediction_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) site_id: Mapped[str] = mapped_column( - Text, ForeignKey("sites.site_id", name="fk_predictions_site", ondelete="RESTRICT") + Text, ForeignKey("site.site_id", name="fk_prediction_site", ondelete="RESTRICT") ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) target_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) @@ -157,29 +157,31 @@ class Prediction(Base): class Alert(Base): - __tablename__ = "alerts" + __tablename__ = "alert" __table_args__ = ( - UniqueConstraint("source", "site_id", "alert_id", name="uq_alerts_source_site_id"), - Index("ix_alerts_site_timestamp", "site_id", "timestamp"), + UniqueConstraint( + "source", "site_id", "source_alert_id", name="uq_alert_source_reference" + ), + Index("ix_alert_site_timestamp", "site_id", "timestamp"), ForeignKeyConstraint( ["prediction_id", "site_id"], - ["predictions.prediction_id", "predictions.site_id"], - name="fk_alerts_prediction_site", + ["prediction.prediction_id", "prediction.site_id"], + name="fk_alert_prediction_site", ondelete="RESTRICT", ), - CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alerts_source"), + CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alert_source"), CheckConstraint( - "type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alerts_type" + "type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alert_type" ), CheckConstraint( - "severity IN ('low', 'medium', 'high', 'critical')", name="ck_alerts_severity" + "severity IN ('low', 'medium', 'high', 'critical')", name="ck_alert_severity" ), ) - id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - alert_id: Mapped[str] = mapped_column(Text) + alert_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + source_alert_id: Mapped[str] = mapped_column(Text) site_id: Mapped[str] = mapped_column( - Text, ForeignKey("sites.site_id", name="fk_alerts_site", ondelete="RESTRICT") + Text, ForeignKey("site.site_id", name="fk_alert_site", ondelete="RESTRICT") ) source: Mapped[str] = mapped_column(Text) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True)) @@ -194,14 +196,15 @@ class Alert(Base): class Recommendation(Base): - __tablename__ = "recommendations" + __tablename__ = "recommendation" __table_args__ = ( - UniqueConstraint("alert_id", "rule_reference", name="uq_recommendations_alert_rule"), + UniqueConstraint("alert_id", "rule_reference", name="uq_recommendation_alert_rule"), ) recommendation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) alert_id: Mapped[int] = mapped_column( - BigInteger, ForeignKey("alerts.id", name="fk_recommendations_alert", ondelete="RESTRICT") + BigInteger, + ForeignKey("alert.alert_id", name="fk_recommendation_alert", ondelete="RESTRICT"), ) action: Mapped[str] = mapped_column(Text) explanation: Mapped[str] = mapped_column(Text) diff --git a/apps/backend/tests/db/test_data_schema.py b/apps/backend/tests/db/test_data_schema.py index aefc9fa..c564042 100644 --- a/apps/backend/tests/db/test_data_schema.py +++ b/apps/backend/tests/db/test_data_schema.py @@ -41,12 +41,12 @@ async def data_site(data_connection: AsyncConnection) -> str: return site_id -async def test_readings_is_a_time_hypertable_when_migrated( +async def test_reading_is_a_time_hypertable_when_migrated( data_connection: AsyncConnection, ) -> None: query = text( "SELECT column_name FROM timescaledb_information.dimensions " - "WHERE hypertable_schema = 'public' AND hypertable_name = 'readings'" + "WHERE hypertable_schema = 'public' AND hypertable_name = 'reading'" ) result = await data_connection.execute(query) @@ -216,7 +216,7 @@ async def test_alert_rejects_prediction_when_site_differs( async with data_connection.begin_nested(): await data_connection.execute( insert(Alert).values( - alert_id=str(uuid4()), + source_alert_id=str(uuid4()), site_id=other_site, source="enervision", timestamp=MOMENT, @@ -236,7 +236,7 @@ async def test_recommendation_is_unique_when_alert_and_rule_match( await data_connection.execute( insert(Alert) .values( - alert_id=str(uuid4()), + source_alert_id=str(uuid4()), site_id=data_site, source="api_mock", timestamp=MOMENT, @@ -245,7 +245,7 @@ async def test_recommendation_is_unique_when_alert_and_rule_match( message="Test", raw_data={}, ) - .returning(Alert.id) + .returning(Alert.alert_id) ) ).scalar_one() statement = insert(Recommendation).values(