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
14 changes: 12 additions & 2 deletions db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,10 +495,15 @@ def measuring_point_height(self) -> int | None:
Since measuring_point_history is eagerly loaded, this should not introduce N+1 query issues.
"""
if self.thing_type == "water well":
if not self.measuring_points:
return None
sorted_measuring_point_history = sorted(
self.measuring_points, key=lambda x: x.start_date, reverse=True
)
return sorted_measuring_point_history[0].measuring_point_height
for record in sorted_measuring_point_history:
if record.measuring_point_height is not None:
return record.measuring_point_height
return None
else:
return None

Expand All @@ -511,10 +516,15 @@ def measuring_point_description(self) -> str | None:
Since measuring_point_history is eagerly loaded, this should not introduce N+1 query issues.
"""
if self.thing_type == "water well":
if not self.measuring_points:
return None
sorted_measuring_point_history = sorted(
self.measuring_points, key=lambda x: x.start_date, reverse=True
)
return sorted_measuring_point_history[0].measuring_point_description
for record in sorted_measuring_point_history:
if record.measuring_point_description is not None:
return record.measuring_point_description
return None
else:
return None

Expand Down
45 changes: 43 additions & 2 deletions tests/test_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from datetime import timezone
from datetime import date, timezone

import pytest

Expand All @@ -25,7 +25,8 @@
viewer_function,
amp_viewer_function,
)
from db import Thing, WellScreen, ThingIdLink
from db import MeasuringPointHistory, Thing, ThingIdLink, WellScreen
from db.engine import session_ctx
from main import app
from schemas import DT_FMT
from schemas.location import LocationResponse
Expand Down Expand Up @@ -85,6 +86,46 @@ def test_update_well_allows_nma_formation_zone():
assert payload.nma_formation_zone == "FZ-001"


def test_measuring_point_properties_skip_null_history():
with session_ctx() as session:
well = Thing(
name="Null MP Height Well",
thing_type="water well",
release_status="draft",
)
session.add(well)
session.commit()
session.refresh(well)

old_history = MeasuringPointHistory(
thing_id=well.id,
measuring_point_height=2.5,
measuring_point_description="old mp",
start_date=date(2020, 1, 1),
end_date=None,
release_status="draft",
)
new_history = MeasuringPointHistory(
thing_id=well.id,
measuring_point_height=None,
measuring_point_description=None,
start_date=date(2021, 1, 1),
end_date=None,
release_status="draft",
)
session.add_all([old_history, new_history])
session.commit()
session.refresh(well)

assert well.measuring_point_height == 2.5
assert well.measuring_point_description == "old mp"

session.delete(new_history)
session.delete(old_history)
session.delete(well)
session.commit()


# this is not a valid test because measuring_point_height is not related to hole_depth
# def test_validate_mp_height_hole_depth():
# with pytest.raises(
Expand Down
60 changes: 34 additions & 26 deletions transfers/well_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,16 +569,9 @@ def _build_well_payload(self, row) -> CreateWell | None:

mpheight = row.MPHeight
mpheight_description = row.MeasuringPoint
if mpheight is None:
mphs = self._measuring_point_estimator.estimate_measuring_point_height(
row
)
if mphs:
try:
mpheight = mphs[0][0]
mpheight_description = mphs[1][0]
except IndexError:
pass
if mpheight is None or isna(mpheight):
# Treat missing/NaN MPHeight as unknown during migration.
mpheight = None
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When MPHeight is None or NaN, the measuring point description should also be checked for NaN and converted to None before passing to CreateWell. If row.MeasuringPoint contains a pandas NaN value (float), it could fail Pydantic validation since measuring_point_description expects str | None, not a float. Consider adding: mpheight_description = None if isna(row.MeasuringPoint) else row.MeasuringPoint inside the if block for consistency with the handling in _add_histories (lines 735-736).

Suggested change
mpheight = None
mpheight = None
mpheight_description = (
None if isna(row.MeasuringPoint) else row.MeasuringPoint
)

Copilot uses AI. Check for mistakes.

completion_date, completion_date_parse_failed = _normalize_completion_date(
row.CompletionDate
Expand Down Expand Up @@ -736,22 +729,9 @@ def _add_notes_and_provenance(
)

def _add_histories(self, session: Session, row, well: Thing) -> None:
mphs = self._measuring_point_estimator.estimate_measuring_point_height(row)
added_measuring_point = False
for mph, mph_desc, start_date, end_date in zip(*mphs):
session.add(
MeasuringPointHistory(
thing_id=well.id,
measuring_point_height=mph,
measuring_point_description=mph_desc,
start_date=start_date,
end_date=end_date,
)
)
added_measuring_point = True

# Preserve transfer intent even when no MP height can be measured/estimated.
if not added_measuring_point:
raw_mpheight = getattr(row, "MPHeight", None)
if raw_mpheight is None or isna(raw_mpheight):
# No estimator for NaN/missing MPHeight; persist NULL history row.
raw_desc = getattr(row, "MeasuringPoint", None)
mp_desc = None if isna(raw_desc) else raw_desc
session.add(
Expand All @@ -763,6 +743,34 @@ def _add_histories(self, session: Session, row, well: Thing) -> None:
end_date=None,
)
)
else:
mphs = self._measuring_point_estimator.estimate_measuring_point_height(row)
added_measuring_point = False
for mph, mph_desc, start_date, end_date in zip(*mphs):
session.add(
MeasuringPointHistory(
thing_id=well.id,
measuring_point_height=mph,
measuring_point_description=mph_desc,
start_date=start_date,
end_date=end_date,
)
)
added_measuring_point = True

# Preserve transfer intent even when no MP height can be measured/estimated.
if not added_measuring_point:
raw_desc = getattr(row, "MeasuringPoint", None)
mp_desc = None if isna(raw_desc) else raw_desc
session.add(
MeasuringPointHistory(
thing_id=well.id,
measuring_point_height=None,
measuring_point_description=mp_desc,
start_date=datetime.now(tz=UTC).date(),
end_date=None,
)
)
Comment on lines +762 to +773
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block that handles the fallback case when the estimator returns no results is duplicated from lines 735-745. Consider refactoring to eliminate this duplication. You could extract the logic for creating a NULL measuring point history record into a helper method or move this fallback outside the if-else block since both branches need it.

Copilot uses AI. Check for mistakes.

target_id = well.id
target_table = "thing"
Expand Down