Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""link surface water photos to surface water data

Revision ID: a1b2c3d4e5f6
Revises: f6e5d4c3b2a1
Create Date: 2026-02-05 11:10:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f6"
down_revision: Union[str, Sequence[str], None] = "f6e5d4c3b2a1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.create_unique_constraint(
"uq_surface_water_data_surface_id",
"NMA_SurfaceWaterData",
["SurfaceID"],
)
op.create_foreign_key(
"fk_surface_water_photos_surface_id",
"NMA_SurfaceWaterPhotos",
"NMA_SurfaceWaterData",
["SurfaceID"],
["SurfaceID"],
ondelete="CASCADE",
)
op.execute("""
DELETE FROM "NMA_SurfaceWaterPhotos" p
WHERE p."SurfaceID" IS NULL
OR NOT EXISTS (
SELECT 1
FROM "NMA_SurfaceWaterData" d
WHERE d."SurfaceID" = p."SurfaceID"
)
""")
op.alter_column(
"NMA_SurfaceWaterPhotos",
"SurfaceID",
existing_type=sa.UUID(),
nullable=False,
)


def downgrade() -> None:
"""Downgrade schema."""
op.alter_column(
"NMA_SurfaceWaterPhotos",
"SurfaceID",
existing_type=sa.UUID(),
nullable=True,
)
op.drop_constraint(
"fk_surface_water_photos_surface_id",
"NMA_SurfaceWaterPhotos",
type_="foreignkey",
)
op.drop_constraint(
"uq_surface_water_data_surface_id",
"NMA_SurfaceWaterData",
type_="unique",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""link weather photos to weather data

Revision ID: b7c8d9e0f1a2
Revises: a1b2c3d4e5f6
Create Date: 2026-02-05 11:20:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "b7c8d9e0f1a2"
down_revision: Union[str, Sequence[str], None] = "a1b2c3d4e5f6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.create_unique_constraint(
"uq_weather_data_weather_id",
"NMA_WeatherData",
["WeatherID"],
)
op.create_foreign_key(
"fk_weather_photos_weather_id",
"NMA_WeatherPhotos",
"NMA_WeatherData",
["WeatherID"],
["WeatherID"],
ondelete="CASCADE",
)
op.execute("""
DELETE FROM "NMA_WeatherPhotos" p
WHERE p."WeatherID" IS NULL
OR NOT EXISTS (
SELECT 1
FROM "NMA_WeatherData" d
WHERE d."WeatherID" = p."WeatherID"
)
""")
op.alter_column(
"NMA_WeatherPhotos",
"WeatherID",
existing_type=sa.UUID(),
nullable=False,
)


def downgrade() -> None:
"""Downgrade schema."""
op.alter_column(
"NMA_WeatherPhotos",
"WeatherID",
existing_type=sa.UUID(),
nullable=True,
)
op.drop_constraint(
"fk_weather_photos_weather_id",
"NMA_WeatherPhotos",
type_="foreignkey",
)
op.drop_constraint(
"uq_weather_data_weather_id",
"NMA_WeatherData",
type_="unique",
)
58 changes: 58 additions & 0 deletions alembic/versions/f6e5d4c3b2a1_add_thing_id_to_nma_weather_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""add thing_id to NMA_WeatherData

Revision ID: f6e5d4c3b2a1
Revises: c7f8a9b0c1d2
Create Date: 2026-02-05 10:40:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "f6e5d4c3b2a1"
down_revision: Union[str, Sequence[str], None] = "c7f8a9b0c1d2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
"NMA_WeatherData",
sa.Column("thing_id", sa.Integer(), nullable=True),
)
op.create_foreign_key(
"fk_weather_data_thing_id",
"NMA_WeatherData",
"thing",
["thing_id"],
["id"],
ondelete="CASCADE",
)
# Backfill thing_id based on LocationId -> Thing.nma_pk_location
op.execute("""
UPDATE "NMA_WeatherData" wd
SET thing_id = t.id
FROM thing t
WHERE t.nma_pk_location IS NOT NULL
AND wd."LocationId" IS NOT NULL
AND t.nma_pk_location = wd."LocationId"::text
""")
# Remove any rows that cannot be linked to a Thing, then enforce NOT NULL
op.execute('DELETE FROM "NMA_WeatherData" WHERE thing_id IS NULL')
op.alter_column(
"NMA_WeatherData", "thing_id", existing_type=sa.Integer(), nullable=False
)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_constraint(
"fk_weather_data_thing_id",
"NMA_WeatherData",
type_="foreignkey",
)
op.drop_column("NMA_WeatherData", "thing_id")
90 changes: 76 additions & 14 deletions db/nma_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ class NMA_SurfaceWaterData(Base):

# Legacy PK (for audit)
surface_id: Mapped[uuid.UUID] = mapped_column(
"SurfaceID", UUID(as_uuid=True), nullable=False
"SurfaceID", UUID(as_uuid=True), nullable=False, unique=True
)

# Legacy FK (for audit)
Expand Down Expand Up @@ -617,6 +617,12 @@ class NMA_SurfaceWaterData(Base):

# Relationships
thing: Mapped["Thing"] = relationship("Thing", back_populates="surface_water_data")
surface_water_photos: Mapped[list["NMA_SurfaceWaterPhotos"]] = relationship(
"NMA_SurfaceWaterPhotos",
back_populates="surface_water_data",
cascade="all, delete-orphan",
passive_deletes=True,
)

@validates("thing_id")
def validate_thing_id(self, key, value):
Expand All @@ -632,7 +638,7 @@ class NMA_SurfaceWaterPhotos(Base):
"""
Legacy SurfaceWaterPhotos table from NM_Aquifer.

Note: This table is OUT OF SCOPE for refactoring (not a Thing child).
Note: This table is a child of NMA_SurfaceWaterData via SurfaceID.
"""

__tablename__ = "NMA_SurfaceWaterPhotos"
Expand All @@ -643,27 +649,45 @@ class NMA_SurfaceWaterPhotos(Base):
)

# FK
# FK not assigned.
surface_id: Mapped[uuid.UUID] = mapped_column(
"SurfaceID",
UUID(as_uuid=True),
ForeignKey("NMA_SurfaceWaterData.SurfaceID", ondelete="CASCADE"),
nullable=False,
)

# Legacy PK (for audit)
# Current `global_id` is also the original PK in the legacy DB

# Legacy FK (for audit)
surface_id: Mapped[Optional[uuid.UUID]] = mapped_column(
"SurfaceID", UUID(as_uuid=True)
)
# surface_id is also the legacy FK in the source table.

# Additional columns
point_id: Mapped[str] = mapped_column("PointID", String(50), nullable=False)
ole_path: Mapped[Optional[str]] = mapped_column("OLEPath", String(50))
object_id: Mapped[Optional[int]] = mapped_column("OBJECTID", Integer, unique=True)

# Relationships
surface_water_data: Mapped["NMA_SurfaceWaterData"] = relationship(
"NMA_SurfaceWaterData", back_populates="surface_water_photos"
)

@validates("surface_id")
def validate_surface_id(self, key, value):
"""Prevent orphan NMA_SurfaceWaterPhotos - must have a parent NMA_SurfaceWaterData."""
if value is None:
raise ValueError(
"NMA_SurfaceWaterPhotos requires a parent NMA_SurfaceWaterData "
"(surface_id cannot be None)"
)
return value


class NMA_WeatherData(Base):
"""
Legacy WeatherData table from AMPAPI.

Note: This table is OUT OF SCOPE for refactoring (not a Thing child).
Note: This table is a Thing child and must link to a parent Thing.
"""

__tablename__ = "NMA_WeatherData"
Expand All @@ -672,11 +696,13 @@ class NMA_WeatherData(Base):
object_id: Mapped[int] = mapped_column("OBJECTID", Integer, primary_key=True)

# FK
# FK not assigned.
thing_id: Mapped[int] = mapped_column(
Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False
)

# Legacy PK (for audit)
weather_id: Mapped[Optional[uuid.UUID]] = mapped_column(
"WeatherID", UUID(as_uuid=True)
"WeatherID", UUID(as_uuid=True), unique=True
)

# Legacy FK (for audit)
Expand All @@ -687,12 +713,30 @@ class NMA_WeatherData(Base):
# Additional columns
point_id: Mapped[str] = mapped_column("PointID", String(10))

# Relationships
thing: Mapped["Thing"] = relationship("Thing", back_populates="weather_data")
weather_photos: Mapped[list["NMA_WeatherPhotos"]] = relationship(
"NMA_WeatherPhotos",
back_populates="weather_data",
cascade="all, delete-orphan",
passive_deletes=True,
)

@validates("thing_id")
def validate_thing_id(self, key, value):
"""Prevent orphan NMA_WeatherData - must have a parent Thing."""
if value is None:
raise ValueError(
"NMA_WeatherData requires a parent Thing (thing_id cannot be None)"
)
return value


class NMA_WeatherPhotos(Base):
"""
Legacy WeatherPhotos table from NM_Aquifer.

Note: This table is OUT OF SCOPE for refactoring (not a Thing child).
Note: This table is a child of NMA_WeatherData via WeatherID.
"""

__tablename__ = "NMA_WeatherPhotos"
Expand All @@ -703,21 +747,39 @@ class NMA_WeatherPhotos(Base):
)

# FK:
# FK not assigned.
weather_id: Mapped[uuid.UUID] = mapped_column(
"WeatherID",
UUID(as_uuid=True),
ForeignKey("NMA_WeatherData.WeatherID", ondelete="CASCADE"),
nullable=False,
)

# Legacy PK (for audit):
# Current `global_id` is also the original PK in the legacy DB

# Legacy FK (for audit):
weather_id: Mapped[Optional[uuid.UUID]] = mapped_column(
"WeatherID", UUID(as_uuid=True)
)
# weather_id is also the legacy FK in the source table.

# Additional columns
point_id: Mapped[str] = mapped_column("PointID", String(50), nullable=False)
ole_path: Mapped[Optional[str]] = mapped_column("OLEPath", String(50))
object_id: Mapped[Optional[int]] = mapped_column("OBJECTID", Integer, unique=True)

# Relationships
weather_data: Mapped["NMA_WeatherData"] = relationship(
"NMA_WeatherData", back_populates="weather_photos"
)

@validates("weather_id")
def validate_weather_id(self, key, value):
"""Prevent orphan NMA_WeatherPhotos - must have a parent NMA_WeatherData."""
if value is None:
raise ValueError(
"NMA_WeatherPhotos requires a parent NMA_WeatherData "
"(weather_id cannot be None)"
)
return value


class NMA_Soil_Rock_Results(Base):
"""
Expand Down
7 changes: 7 additions & 0 deletions db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
NMA_Stratigraphy,
NMA_SurfaceWaterData,
NMA_WaterLevelsContinuous_Pressure_Daily,
NMA_WeatherData,
)


Expand Down Expand Up @@ -368,6 +369,12 @@ class Thing(
cascade="all, delete-orphan",
passive_deletes=True,
)
weather_data: Mapped[List["NMA_WeatherData"]] = relationship(
"NMA_WeatherData",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
)

# --- Association Proxies ---
assets: AssociationProxy[list["Asset"]] = association_proxy(
Expand Down
Loading
Loading