refactor(backend): passe les tables data au singulier et clarifie alert_id
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.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user