Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,14 @@ def upgrade() -> None:
ondelete="CASCADE",
)
# Backfill thing_id based on LocationId -> Thing.nma_pk_location
op.execute(
"""
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(
Expand Down
72 changes: 60 additions & 12 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,21 +649,39 @@ 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):
"""
Expand All @@ -678,7 +702,7 @@ class NMA_WeatherData(Base):

# 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 @@ -691,6 +715,12 @@ class NMA_WeatherData(Base):

# 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):
Expand All @@ -706,7 +736,7 @@ 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 @@ -717,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
43 changes: 38 additions & 5 deletions tests/test_surface_water_photos_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,43 @@
from uuid import uuid4

from db.engine import session_ctx
from db.nma_legacy import NMA_SurfaceWaterPhotos
from db.nma_legacy import NMA_SurfaceWaterData, NMA_SurfaceWaterPhotos
from db.thing import Thing


def test_create_surface_water_photos_all_fields():
def _next_object_id() -> int:
# Use a negative value to avoid collisions with existing legacy OBJECTIDs.
return -(uuid4().int % 2_000_000_000)


def _attach_thing_with_location(session, water_well_thing):
location_id = uuid4()
thing = session.get(Thing, water_well_thing.id)
thing.nma_pk_location = str(location_id)
session.commit()
return thing, location_id


def _create_surface_water_data(session, water_well_thing):
thing, location_id = _attach_thing_with_location(session, water_well_thing)
record = NMA_SurfaceWaterData(
location_id=location_id,
thing_id=thing.id,
surface_id=uuid4(),
point_id="SW-1000",
object_id=_next_object_id(),
)
session.add(record)
session.commit()
return record


def test_create_surface_water_photos_all_fields(water_well_thing):
"""Test creating a surface water photos record with all fields."""
with session_ctx() as session:
parent = _create_surface_water_data(session, water_well_thing)
record = NMA_SurfaceWaterPhotos(
surface_id=uuid4(),
surface_id=parent.surface_id,
point_id="SW-0001",
ole_path="photo.jpg",
object_id=123,
Expand All @@ -52,14 +81,17 @@ def test_create_surface_water_photos_all_fields():
assert record.object_id == 123

session.delete(record)
session.delete(parent)
session.commit()


def test_create_surface_water_photos_minimal():
def test_create_surface_water_photos_minimal(water_well_thing):
"""Test creating a surface water photos record with required fields only."""
with session_ctx() as session:
parent = _create_surface_water_data(session, water_well_thing)
record = NMA_SurfaceWaterPhotos(
point_id="SW-0002",
surface_id=parent.surface_id,
global_id=uuid4(),
)
session.add(record)
Expand All @@ -68,11 +100,12 @@ def test_create_surface_water_photos_minimal():

assert record.global_id is not None
assert record.point_id == "SW-0002"
assert record.surface_id is None
assert record.surface_id is not None
assert record.ole_path is None
assert record.object_id is None

session.delete(record)
session.delete(parent)
session.commit()


Expand Down
Loading
Loading