La convention de docs/architecture/40-data.md impose des noms de tables au singulier, que les quatre tables d'authentification respectent déjà. Les six tables data passent donc au singulier, avec leurs contraintes et leurs index. La révision n'étant appliquée que sur des bases locales, elle est modifiée sur place plutôt que doublée d'une migration de renommage. alert_id désignait deux colonnes différentes : la clé métier text de l'API Mock et la clé étrangère bigint de recommendation. La première devient source_alert_id, la seconde pointe désormais vers alert.alert_id.
219 lines
9.5 KiB
Python
219 lines
9.5 KiB
Python
"""Création des six tables Data et de l'hypertable reading.
|
|
|
|
Revision ID: e6d2026091501
|
|
Revises: 821f71be74c0
|
|
"""
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision = "e6d2026091501"
|
|
down_revision = "821f71be74c0"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table(
|
|
"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),
|
|
sa.Column("storage_uri", sa.Text(), nullable=False),
|
|
sa.Column("source_timezone", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"metadata", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
|
),
|
|
sa.CheckConstraint("dataset_id > 0", name="ck_dataset_positive_id"),
|
|
sa.PrimaryKeyConstraint("dataset_id"),
|
|
sa.UniqueConstraint("archive_sha256", name="uq_dataset_archive_sha256"),
|
|
)
|
|
op.create_table(
|
|
"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),
|
|
sa.Column("location", sa.Text(), nullable=True),
|
|
sa.Column("capacity_kw", sa.Double(), nullable=True),
|
|
sa.Column("status", sa.Text(), nullable=True),
|
|
sa.PrimaryKeyConstraint("site_id"),
|
|
)
|
|
op.create_table(
|
|
"prediction",
|
|
sa.Column("prediction_id", sa.BigInteger(), autoincrement=True, nullable=False),
|
|
sa.Column("site_id", sa.Text(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("target_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("target_metric", sa.Text(), nullable=False),
|
|
sa.Column("period_minutes", sa.Integer(), nullable=True),
|
|
sa.Column("predicted_value", sa.Double(), nullable=True),
|
|
sa.Column("model_reference", sa.Text(), nullable=False),
|
|
sa.Column("status", sa.Text(), nullable=False),
|
|
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_prediction_status",
|
|
),
|
|
sa.CheckConstraint(
|
|
"target_metric <> 'consumption_kwh' OR period_minutes IS NOT NULL",
|
|
name="ck_prediction_energy_period",
|
|
),
|
|
sa.CheckConstraint(
|
|
"target_metric IN ('consumption_kwh', 'consumption_kw')", name="ck_prediction_metric"
|
|
),
|
|
sa.CheckConstraint(
|
|
"period_minutes IS NULL OR period_minutes > 0", name="ck_prediction_period"
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["site_id"], ["site.site_id"], name="fk_prediction_site", ondelete="RESTRICT"
|
|
),
|
|
sa.PrimaryKeyConstraint("prediction_id"),
|
|
sa.UniqueConstraint("prediction_id", "site_id", name="uq_prediction_id_site"),
|
|
)
|
|
op.create_index(
|
|
"ix_prediction_site_target", "prediction", ["site_id", "target_at"], unique=False
|
|
)
|
|
op.create_table(
|
|
"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),
|
|
sa.Column("source", sa.Text(), nullable=False),
|
|
sa.Column("dataset_id", sa.BigInteger(), nullable=True),
|
|
sa.Column("consumption_kw", sa.Double(), nullable=True),
|
|
sa.Column("consumption_kwh", sa.Double(), nullable=True),
|
|
sa.Column("consumption_euros", sa.Numeric(precision=14, scale=2), nullable=True),
|
|
sa.Column("voltage_v", sa.Double(), nullable=True),
|
|
sa.Column("current_a", sa.Double(), nullable=True),
|
|
sa.Column("power_factor", sa.Double(), nullable=True),
|
|
sa.Column("temperature_celsius", sa.Double(), nullable=True),
|
|
sa.Column("humidity_percent", sa.Double(), nullable=True),
|
|
sa.Column("solar_irradiance_wm2", sa.Double(), nullable=True),
|
|
sa.Column("is_working_hours", sa.Boolean(), nullable=True),
|
|
sa.Column("data_quality", sa.Text(), nullable=True),
|
|
sa.Column("null_reasons", postgresql.ARRAY(sa.Text()), nullable=True),
|
|
sa.Column(
|
|
"imputed_values", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=True
|
|
),
|
|
sa.Column("imputation_method", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"ingested_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"raw_data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
|
),
|
|
sa.CheckConstraint(
|
|
"(source = 'csv' AND dataset_id IS NOT NULL) OR (source IN ('api_current', 'api_history') AND dataset_id IS NULL)",
|
|
name="ck_reading_dataset_source",
|
|
),
|
|
sa.CheckConstraint(
|
|
"data_quality IS NULL OR data_quality IN ('good', 'partial', 'degraded', 'critical')",
|
|
name="ck_reading_quality",
|
|
),
|
|
sa.CheckConstraint(
|
|
"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_reading_imputation",
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["dataset_id"], ["dataset.dataset_id"], name="fk_reading_dataset", ondelete="RESTRICT"
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["site_id"], ["site.site_id"], name="fk_reading_site", ondelete="RESTRICT"
|
|
),
|
|
sa.PrimaryKeyConstraint("reading_id", "timestamp"),
|
|
)
|
|
op.create_index("ix_reading_dataset_id", "reading", ["dataset_id"], unique=False)
|
|
op.create_index(
|
|
"ix_reading_site_timestamp", "reading", ["site_id", "timestamp"], unique=False
|
|
)
|
|
op.create_index(
|
|
"uq_reading_source",
|
|
"reading",
|
|
["site_id", "timestamp", "source", sa.literal_column("coalesce(dataset_id, 0)")],
|
|
unique=True,
|
|
)
|
|
op.execute(
|
|
"SELECT create_hypertable('reading', by_range('timestamp'), create_default_indexes => FALSE)"
|
|
)
|
|
op.create_table(
|
|
"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),
|
|
sa.Column("type", sa.Text(), nullable=False),
|
|
sa.Column("severity", sa.Text(), nullable=False),
|
|
sa.Column("message", sa.Text(), nullable=False),
|
|
sa.Column("value", sa.Double(), nullable=True),
|
|
sa.Column("threshold", sa.Double(), nullable=True),
|
|
sa.Column("metric", sa.Text(), nullable=True),
|
|
sa.Column("prediction_id", sa.BigInteger(), nullable=True),
|
|
sa.Column(
|
|
"raw_data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
|
),
|
|
sa.CheckConstraint(
|
|
"severity IN ('low', 'medium', 'high', 'critical')", name="ck_alert_severity"
|
|
),
|
|
sa.CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alert_source"),
|
|
sa.CheckConstraint(
|
|
"type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alert_type"
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["prediction_id", "site_id"],
|
|
["prediction.prediction_id", "prediction.site_id"],
|
|
name="fk_alert_prediction_site",
|
|
ondelete="RESTRICT",
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["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"
|
|
),
|
|
)
|
|
op.create_index("ix_alert_site_timestamp", "alert", ["site_id", "timestamp"], unique=False)
|
|
op.create_table(
|
|
"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),
|
|
sa.Column("explanation", sa.Text(), nullable=False),
|
|
sa.Column("rule_reference", sa.Text(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["alert_id"], ["alert.alert_id"], name="fk_recommendation_alert", ondelete="RESTRICT"
|
|
),
|
|
sa.PrimaryKeyConstraint("recommendation_id"),
|
|
sa.UniqueConstraint("alert_id", "rule_reference", name="uq_recommendation_alert_rule"),
|
|
)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("recommendation")
|
|
op.drop_table("alert")
|
|
op.drop_table("reading")
|
|
op.drop_table("prediction")
|
|
op.drop_table("site")
|
|
op.drop_table("dataset")
|