from __future__ import annotations import uuid from datetime import datetime from geoalchemy2 import Geometry from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Float, Index, JSON, String, Text, UniqueConstraint, func, text from sqlalchemy.sql.sqltypes import Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base class Project(Base): __tablename__ = "projects" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) region: Mapped[str] = mapped_column(String(120), default="Kempen") status: Mapped[str] = mapped_column(String(32), default="active") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) areas: Mapped[list["Area"]] = relationship("Area", back_populates="project", cascade="all, delete-orphan") datasets: Mapped[list["Dataset"]] = relationship("Dataset", back_populates="project", cascade="all, delete-orphan") class Area(Base): __tablename__ = "areas" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) geometry: Mapped[str] = mapped_column(Geometry("MultiPolygon", srid=4326), nullable=False) original_crs: Mapped[str | None] = mapped_column(String(64), nullable=True) area_m2: Mapped[float | None] = mapped_column(Float, nullable=True) bbox: Mapped[str | None] = mapped_column(Geometry("Polygon", srid=4326), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) project: Mapped[Project] = relationship("Project", back_populates="areas") class Dataset(Base): __tablename__ = "datasets" __table_args__ = ( CheckConstraint( "valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from", name="ck_datasets_temporal_valid_range", ), Index( "ix_datasets_project_temporal_series_observed", "project_id", "temporal_series_key", "observed_at", ), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) area_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("areas.id", ondelete="SET NULL"), nullable=True) name: Mapped[str] = mapped_column(String(255), nullable=False) dataset_type: Mapped[str] = mapped_column(String(64), nullable=False) source: Mapped[str] = mapped_column(String(120), nullable=False) storage_path: Mapped[str | None] = mapped_column(String(500), nullable=True) original_filename: Mapped[str | None] = mapped_column(String(255), nullable=True) stored_filename: Mapped[str | None] = mapped_column(String(255), nullable=True) content_type: Mapped[str | None] = mapped_column(String(120), nullable=True) size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True) checksum_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True) derived_from_dataset_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True, ) crs: Mapped[str | None] = mapped_column(String(64), nullable=True) bounds_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) resolution_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) bands_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) dataset_role: Mapped[str] = mapped_column(String(32), nullable=False, default="source", server_default="source") source_name: Mapped[str | None] = mapped_column(String(120), nullable=True) reference_layer_name: Mapped[str | None] = mapped_column(String(120), nullable=True) source_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True) provenance_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True) imported_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) temporal_series_key: Mapped[str | None] = mapped_column(String(255), nullable=True) observed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) temporal_granularity: Mapped[str | None] = mapped_column(String(32), nullable=True) source_version: Mapped[str | None] = mapped_column(String(120), nullable=True) status: Mapped[str] = mapped_column(String(32), default="uploaded") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) project: Mapped[Project] = relationship("Project", back_populates="datasets") versions: Mapped[list["DatasetVersion"]] = relationship( "DatasetVersion", back_populates="dataset", cascade="all, delete-orphan", ) vector_features: Mapped[list["VectorFeature"]] = relationship( "VectorFeature", back_populates="dataset", cascade="all, delete-orphan", ) class DatasetVersion(Base): __tablename__ = "dataset_versions" __table_args__ = ( CheckConstraint( "valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from", name="ck_dataset_versions_temporal_valid_range", ), Index("ix_dataset_versions_dataset_version", "dataset_id", "version", unique=True), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) dataset_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="CASCADE"), nullable=False) version: Mapped[int] = mapped_column(Integer, default=1) storage_path: Mapped[str | None] = mapped_column(String(500), nullable=True) source_version: Mapped[str | None] = mapped_column(String(120), nullable=True) observed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) checksum_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True) source_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True) provenance_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) dataset: Mapped[Dataset] = relationship("Dataset", back_populates="versions") class VectorFeature(Base): __tablename__ = "vector_features" __table_args__ = ( Index("ix_vector_features_dataset_id", "dataset_id"), Index("ix_vector_features_geometry", "geometry", postgresql_using="gist"), Index("ix_vector_features_dataset_source_feature", "dataset_id", "source_feature_id"), Index( "ix_vector_features_dataset_municipality", "dataset_id", text("(properties_json ->> 'municipality')"), ), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) dataset_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="CASCADE"), nullable=False) feature_class: Mapped[str | None] = mapped_column(String(120), nullable=True) source_feature_id: Mapped[str | None] = mapped_column(String(255), nullable=True) properties_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) geometry: Mapped[str] = mapped_column(Geometry("Geometry", srid=4326, spatial_index=False), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) dataset: Mapped[Dataset] = relationship("Dataset", back_populates="vector_features") class AnalysisRun(Base): __tablename__ = "analysis_runs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) area_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("areas.id", ondelete="SET NULL"), nullable=True) dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) job_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True) analysis_type: Mapped[str] = mapped_column(String(64), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False) model_name: Mapped[str | None] = mapped_column(String(255), nullable=True) model_version: Mapped[str | None] = mapped_column(String(120), nullable=True) parameters_json: Mapped[dict] = mapped_column(JSON, nullable=False) result_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) class Detection(Base): __tablename__ = "detections" __table_args__ = ( Index("ix_detections_project_id", "project_id"), Index("ix_detections_dataset_id", "dataset_id"), Index("ix_detections_analysis_run_id", "analysis_run_id"), Index("ix_detections_class_name", "class_name"), Index("ix_detections_geometry", "geometry", postgresql_using="gist"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True) job_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True) model_name: Mapped[str] = mapped_column(String(255), nullable=False) model_version: Mapped[str | None] = mapped_column(String(120), nullable=True) class_name: Mapped[str] = mapped_column(String(120), nullable=False) confidence: Mapped[float] = mapped_column(Float, nullable=False) geometry: Mapped[str] = mapped_column(Geometry("Geometry", srid=4326, spatial_index=False), nullable=False) bbox_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) source_tile_path: Mapped[str | None] = mapped_column(String(500), nullable=True) properties_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Segmentation(Base): __tablename__ = "segmentations" __table_args__ = ( Index("ix_segmentations_project_id", "project_id"), Index("ix_segmentations_dataset_id", "dataset_id"), Index("ix_segmentations_analysis_run_id", "analysis_run_id"), Index("ix_segmentations_job_id", "job_id"), Index("ix_segmentations_class_name", "class_name"), Index("ix_segmentations_geometry", "geometry", postgresql_using="gist"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) job_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True) model_name: Mapped[str] = mapped_column(String(255), nullable=False) model_version: Mapped[str | None] = mapped_column(String(120), nullable=True) class_name: Mapped[str] = mapped_column(String(120), nullable=False) confidence: Mapped[float | None] = mapped_column(Float, nullable=True) geometry: Mapped[str] = mapped_column(Geometry("MultiPolygon", srid=4326, spatial_index=False), nullable=False) bbox_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) area_m2: Mapped[float | None] = mapped_column(Float, nullable=True) mask_path: Mapped[str | None] = mapped_column(Text, nullable=True) source_tile_path: Mapped[str | None] = mapped_column(String(500), nullable=True) tile_index: Mapped[int | None] = mapped_column(Integer, nullable=True) properties_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) provenance_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class QualityCheck(Base): __tablename__ = "quality_checks" __table_args__ = ( Index("ix_quality_checks_project_id", "project_id"), Index("ix_quality_checks_reference_dataset_id", "reference_dataset_id"), Index("ix_quality_checks_candidate_dataset_id", "candidate_dataset_id"), Index("ix_quality_checks_analysis_run_id", "analysis_run_id"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) job_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True) candidate_dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) reference_dataset_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="CASCADE"), nullable=False) check_type: Mapped[str] = mapped_column(String(120), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False) score: Mapped[float | None] = mapped_column(Float, nullable=True) parameters_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) findings_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class Metric(Base): __tablename__ = "metrics" __table_args__ = ( Index("ix_metrics_quality_check_id", "quality_check_id"), Index("ix_metrics_analysis_run_id", "analysis_run_id"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) quality_check_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("quality_checks.id", ondelete="CASCADE"), nullable=True) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True) metric_key: Mapped[str] = mapped_column(String(120), nullable=False) metric_value: Mapped[float | None] = mapped_column(Float, nullable=True) metric_unit: Mapped[str | None] = mapped_column(String(64), nullable=True) label: Mapped[str | None] = mapped_column(String(120), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class DetectionReview(Base): __tablename__ = "detection_reviews" __table_args__ = ( CheckConstraint( "evidence_role IN ('false_positive', 'false_negative')", name="ck_detection_reviews_evidence_role", ), CheckConstraint( "decision IN ('confirmed_model_false_positive', 'confirmed_model_false_negative', " "'reference_gap_or_change', 'qa_alignment_mismatch', " "'imagery_obscured_or_uncertain', 'uncertain', 'unreviewed')", name="ck_detection_reviews_decision", ), UniqueConstraint( "quality_check_id", "evidence_role", "evidence_feature_id", name="uq_detection_reviews_evidence", ), Index("ix_detection_reviews_project_id", "project_id"), Index("ix_detection_reviews_quality_check_id", "quality_check_id"), Index("ix_detection_reviews_analysis_run_id", "analysis_run_id"), Index("ix_detection_reviews_decision", "decision"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) quality_check_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("quality_checks.id", ondelete="CASCADE"), nullable=False, ) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True, ) evidence_role: Mapped[str] = mapped_column(String(32), nullable=False) evidence_feature_id: Mapped[str] = mapped_column(String(255), nullable=False) detection_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("detections.id", ondelete="SET NULL"), nullable=True, ) reference_feature_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("vector_features.id", ondelete="SET NULL"), nullable=True, ) decision: Mapped[str] = mapped_column(String(64), nullable=False, default="unreviewed", server_default="unreviewed") notes: Mapped[str | None] = mapped_column(Text, nullable=True) reviewed_by: Mapped[str] = mapped_column(String(120), nullable=False, default="operator", server_default="operator") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) class Export(Base): __tablename__ = "exports" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) analysis_run_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("analysis_runs.id", ondelete="SET NULL"), nullable=True) export_type: Mapped[str] = mapped_column(String(64), nullable=False) storage_path: Mapped[str] = mapped_column(String(500), nullable=False) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Job(Base): __tablename__ = "jobs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) job_type: Mapped[str] = mapped_column(String(128), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued") project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) input_dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) output_dataset_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("datasets.id", ondelete="SET NULL"), nullable=True) parameters_json: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict) result_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)