From e87150ab59588cb3b892d5692f11b4aa59289306 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Tue, 11 Nov 2025 13:55:00 -0700 Subject: [PATCH 1/4] feat: create new `measuring_point_history` model. The current schema lacks a way to track the authoritative measuring point height over time. Created a new model, `measuring_point_history`, to store the official measuring point and description for a Thing. This table serves as a specialized historical log that tracks the measuring point over time. --- db/measuring_point_history.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 db/measuring_point_history.py diff --git a/db/measuring_point_history.py b/db/measuring_point_history.py new file mode 100644 index 000000000..e69de29bb From 545c2e852d33ed4fc42013570018bd64eabf4671 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Tue, 11 Nov 2025 14:26:28 -0700 Subject: [PATCH 2/4] feat: add new relationship to `Thing` model. A Thing may have multiple measuring points over time. As such, the `Thing` model requires a relationship to the new `measuring_point_history` model. A new One-To-Many relationship named `measuring_points` was added to the `Thing` model. --- db/measuring_point_history.py | 61 +++++++++++++++++++++++++++++++++++ db/thing.py | 9 ++++++ 2 files changed, 70 insertions(+) diff --git a/db/measuring_point_history.py b/db/measuring_point_history.py index e69de29bb..1039b9096 100644 --- a/db/measuring_point_history.py +++ b/db/measuring_point_history.py @@ -0,0 +1,61 @@ +""" +SQLAlchemy model for the MeasuringPointHistory table. + +This table stores the authoritative MP height of a Thing from +construction or modification events. It provides a complete, auditable +history of the official, surveyed measuring point (MP) descriptions +and heights for a Thing. + +This table is not for storing routine field checks of the +MP height (which are stored on the `Observation` table). This table should +only be updated when a well is first installed, physically modified +(e.g., a new wellhead is installed), or officially re-surveyed. +""" + +from typing import TYPE_CHECKING + +from sqlalchemy import Integer, ForeignKey, Date, Text, Numeric +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from db.base import Base, AutoBaseMixin, ReleaseMixin + +if TYPE_CHECKING: + from db.thing import Thing + + +class MeasuringPointHistory(Base, AutoBaseMixin, ReleaseMixin): + """ + Represents a single, authoritative, time-stamped record of a + Thing's measuring point description and height. + """ + + # --- Foreign Keys --- + thing_id: Mapped[int] = mapped_column( + Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False + ) + + # --- Columns --- + measuring_point_height: Mapped[float] = mapped_column( + Numeric, + nullable=False, + comment="The official, surveyed height of the measuring point relative to ground surface (in feet).", + ) + mp_description: Mapped[str] = mapped_column( + Text, + nullable=True, + comment="A clear description of the measuring point (e.g., 'North side of casing, top of PVC', 'Top of new steel collar').", + ) + start_date: Mapped[Date] = mapped_column( + Date, + nullable=False, + comment="The date this measuring point configuration became effective.", + ) + end_date: Mapped[Date] = mapped_column( + Date, + nullable=True, + comment="The date this measuring point configuration was superseded. A `NULL` value indicates this is the current, active, and authoritative record for the `Thing`.", + ) + + # --- Relationships --- + # Many-To-One: A description history record belongs to one Thing. Many history records may belong to a single Thing. + thing: Mapped["Thing"] = relationship("Thing", back_populates="mp_history") diff --git a/db/thing.py b/db/thing.py index bedc4430d..468d22584 100644 --- a/db/thing.py +++ b/db/thing.py @@ -29,6 +29,7 @@ StatusHistoryMixin, PermissionMixin, ) +from db.measuring_point_history import MeasuringPointHistory if TYPE_CHECKING: from db.location import Location @@ -223,6 +224,14 @@ class Thing(Base, AutoBaseMixin, ReleaseMixin, StatusHistoryMixin, PermissionMix lazy="joined", ) + # One-To-Many: A Thing (well) can have multiple measuring points over time. + measuring_points: Mapped[List["MeasuringPointHistory"]] = relationship( + "MeasuringPointHistory", + back_populates="thing", + cascade="all, delete-orphan", + passive_deletes=True, + ) + # --- Association Proxies --- assets: AssociationProxy[list["Asset"]] = association_proxy( "asset_associations", "asset" From 15c770f555ee1c413e9cac09d08a2431c1c74bb8 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Tue, 11 Nov 2025 14:36:40 -0700 Subject: [PATCH 3/4] refactor: add new field to `measuring_point_history` model. It would be useful to track the reason for updating the measuring point. Create new `reason` field in the `measuring_point_history` model. --- db/measuring_point_history.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/db/measuring_point_history.py b/db/measuring_point_history.py index 1039b9096..fc6d0f9c7 100644 --- a/db/measuring_point_history.py +++ b/db/measuring_point_history.py @@ -56,6 +56,12 @@ class MeasuringPointHistory(Base, AutoBaseMixin, ReleaseMixin): comment="The date this measuring point configuration was superseded. A `NULL` value indicates this is the current, active, and authoritative record for the `Thing`.", ) + reason: Mapped[str] = mapped_column( + Text, + nullable=True, + comment="Describes the reason for the new or updated measuring point (e.g., 'A new wellhead was installed').", + ) + # --- Relationships --- # Many-To-One: A description history record belongs to one Thing. Many history records may belong to a single Thing. thing: Mapped["Thing"] = relationship("Thing", back_populates="mp_history") From 8d1d8fa01823407c221a57298aeb1bc971374229 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 12 Nov 2025 12:04:45 -0700 Subject: [PATCH 4/4] refactor: update 'measuring_point_history' model. The `back_populates` parameter in the relationship section was updated to match the relationship name in the `THing` model. The `mp_description` field was renamed `measuring_point_description` for clarity. --- db/measuring_point_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/measuring_point_history.py b/db/measuring_point_history.py index fc6d0f9c7..7d23518a1 100644 --- a/db/measuring_point_history.py +++ b/db/measuring_point_history.py @@ -40,7 +40,7 @@ class MeasuringPointHistory(Base, AutoBaseMixin, ReleaseMixin): nullable=False, comment="The official, surveyed height of the measuring point relative to ground surface (in feet).", ) - mp_description: Mapped[str] = mapped_column( + measuring_point_description: Mapped[str] = mapped_column( Text, nullable=True, comment="A clear description of the measuring point (e.g., 'North side of casing, top of PVC', 'Top of new steel collar').", @@ -64,4 +64,4 @@ class MeasuringPointHistory(Base, AutoBaseMixin, ReleaseMixin): # --- Relationships --- # Many-To-One: A description history record belongs to one Thing. Many history records may belong to a single Thing. - thing: Mapped["Thing"] = relationship("Thing", back_populates="mp_history") + thing: Mapped["Thing"] = relationship("Thing", back_populates="measuring_points")