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
122 changes: 122 additions & 0 deletions alembic/versions/n7a8b9c0d1e2_fix_water_elevation_units_to_feet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""fix water elevation units to feet

Revision ID: n7a8b9c0d1e2
Revises: m6f7a8b9c0d1
Create Date: 2026-03-10 11:10:00.000000
"""

from typing import Sequence, Union

from alembic import op
from sqlalchemy import inspect, text

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

METERS_TO_FEET = 3.28084

LATEST_LOCATION_CTE = """
SELECT DISTINCT ON (lta.thing_id)
lta.thing_id,
lta.location_id,
lta.effective_start
FROM location_thing_association AS lta
WHERE lta.effective_end IS NULL
ORDER BY lta.thing_id, lta.effective_start DESC
""".strip()


def _create_water_elevation_view() -> str:
return f"""
CREATE MATERIALIZED VIEW ogc_water_elevation_wells AS
WITH latest_location AS (
{LATEST_LOCATION_CTE}
),
ranked_obs AS (
SELECT
fe.thing_id,
o.id AS observation_id,
o.observation_datetime,
(o.value - COALESCE(o.measuring_point_height, 0))
AS depth_to_water_below_ground_surface
FROM observation AS o
JOIN sample AS s ON s.id = o.sample_id
JOIN field_activity AS fa ON fa.id = s.field_activity_id
JOIN field_event AS fe ON fe.id = fa.field_event_id
JOIN thing AS t ON t.id = fe.thing_id
WHERE
t.thing_type = 'water well'
AND fa.activity_type = 'groundwater level'
AND o.value IS NOT NULL
AND o.observation_datetime IS NOT NULL
),
latest_obs AS (
SELECT
ro.*,
ROW_NUMBER() OVER (
PARTITION BY ro.thing_id
ORDER BY ro.observation_datetime DESC, ro.observation_id DESC
) AS rn
FROM ranked_obs AS ro
)
SELECT
t.id AS id,
t.name,
t.thing_type,
lo.observation_id,
lo.observation_datetime,
l.elevation,
lo.depth_to_water_below_ground_surface,
((l.elevation * {METERS_TO_FEET}) - lo.depth_to_water_below_ground_surface)
AS water_elevation,
Comment on lines +73 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize depth units before subtracting from feet elevation

In _create_water_elevation_view, water_elevation now converts l.elevation to feet but subtracts depth_to_water_below_ground_surface without unit normalization, so any groundwater observation stored in meters (o.unit = 'm') will produce a mixed-unit result (feet minus meters) and an incorrect elevation. This regression is user-impacting wherever groundwater-level records are not already in feet, so the expression should convert depth based on o.unit (or explicitly constrain to feet-only data).

Useful? React with 👍 / 👎.

l.point
FROM latest_obs AS lo
JOIN thing AS t ON t.id = lo.thing_id
JOIN latest_location AS ll ON ll.thing_id = t.id
JOIN location AS l ON l.id = ll.location_id
WHERE lo.rn = 1
"""


def upgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
existing_tables = set(inspector.get_table_names(schema="public"))
required_tables = {
"thing",
"location",
"location_thing_association",
"observation",
"sample",
"field_activity",
"field_event",
}

if not required_tables.issubset(existing_tables):
missing = sorted(t for t in required_tables if t not in existing_tables)
raise RuntimeError(
"Cannot create ogc_water_elevation_wells. Missing required tables: "
+ ", ".join(missing)
)

op.execute(text("DROP MATERIALIZED VIEW IF EXISTS ogc_water_elevation_wells"))
op.execute(text(_create_water_elevation_view()))
op.execute(
text(
"COMMENT ON MATERIALIZED VIEW ogc_water_elevation_wells IS "
"'Latest water elevation per well in feet; computed as (elevation_m * 3.28084) - depth_to_water_below_ground_surface_ft.'"
)
)
op.execute(
text(
"CREATE UNIQUE INDEX ux_ogc_water_elevation_wells_id "
"ON ogc_water_elevation_wells (id)"
)
)


def downgrade() -> None:
op.execute(text("DROP MATERIALIZED VIEW IF EXISTS ogc_water_elevation_wells"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore previous view definition in downgrade

The migration's downgrade() only drops ogc_water_elevation_wells, so rolling back from this revision leaves the database at revision m6f7a8b9c0d1 without the view that revision expects, breaking rollback consistency and any features/commands that query or refresh that materialized view after a downgrade. The downgrade path should recreate the prior view definition instead of deleting it outright.

Useful? React with 👍 / 👎.

Comment on lines +121 to +122
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The downgrade() only drops the materialized view, but since this migration modifies an existing view (created by m6f7a8b9c0d1), the downgrade should recreate the previous version of the view (the one without the meters-to-feet conversion). Simply dropping the view leaves the database without ogc_water_elevation_wells entirely after a downgrade, which would break any code that depends on it (e.g., cli/cli.py refreshes, pygeoapi collection endpoints, etc.).

The downgrade should drop the current view and then recreate the original view definition from m6f7a8b9c0d1 (using l.elevation - ro.depth_to_water_below_ground_surface without the conversion factor), along with the original comment and unique index.

Copilot uses AI. Check for mistakes.
3 changes: 2 additions & 1 deletion tests/test_ogc.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,8 @@ def test_ogc_water_elevation_wells_computes_elevation_minus_depth_to_water(

assert float(row.depth_to_water_below_ground_surface) == 5.0
assert float(row.elevation) == 2464.9
assert abs(float(row.water_elevation) - 2459.9) < 1e-9
expected_water_elevation_ft = (2464.9 * 3.28084) - 5.0
assert abs(float(row.water_elevation) - expected_water_elevation_ft) < 1e-9


def test_ogc_collections():
Expand Down