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
89 changes: 83 additions & 6 deletions alembic/versions/n7a8b9c0d1e2_fix_water_elevation_units_to_feet.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ def _create_water_elevation_view() -> str:
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
CASE
WHEN lower(trim(o.unit)) IN ('m', 'meter', 'meters', 'metre', 'metres') THEN
(o.value * {METERS_TO_FEET}) - COALESCE(o.measuring_point_height, 0)
WHEN lower(trim(o.unit)) IN ('ft', 'foot', 'feet') THEN
o.value - COALESCE(o.measuring_point_height, 0)
ELSE
NULL
END 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
Expand All @@ -52,6 +58,16 @@ def _create_water_elevation_view() -> str:
AND fa.activity_type = 'groundwater level'
AND o.value IS NOT NULL
AND o.observation_datetime IS NOT NULL
AND lower(trim(o.unit)) IN (
'm',
'meter',
'meters',
'metre',
'metres',
'ft',
'foot',
'feet'
Comment on lines +61 to +69
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 Include ftbgs units when selecting groundwater observations

The new unit whitelist in _create_water_elevation_view() excludes observations whose unit is ftbgs, even though ftbgs is a valid lexicon unit (core/lexicon.json, around line 843). For wells whose latest (or only) groundwater-level observation uses ftbgs, this CTE now drops the record entirely, so ogc_water_elevation_wells can lose rows after this migration. Please add handling for ftbgs (and its depth semantics) instead of filtering it out.

Useful? React with 👍 / 👎.

)
),
latest_obs AS (
SELECT
Expand All @@ -68,10 +84,10 @@ def _create_water_elevation_view() -> str:
t.thing_type,
lo.observation_id,
lo.observation_datetime,
l.elevation,
lo.depth_to_water_below_ground_surface,
l.elevation AS elevation_m,
lo.depth_to_water_below_ground_surface AS depth_to_water_below_ground_surface_ft,
((l.elevation * {METERS_TO_FEET}) - lo.depth_to_water_below_ground_surface)
AS water_elevation,
AS water_elevation_ft,
l.point
FROM latest_obs AS lo
JOIN thing AS t ON t.id = lo.thing_id
Expand All @@ -81,6 +97,54 @@ def _create_water_elevation_view() -> str:
"""


def _create_water_elevation_view_m6() -> 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,
ROW_NUMBER() OVER (
PARTITION BY fe.thing_id
ORDER BY o.observation_datetime DESC, o.id DESC
) AS rn
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
)
SELECT
t.id AS id,
t.name,
t.thing_type,
ro.observation_id,
ro.observation_datetime,
l.elevation,
ro.depth_to_water_below_ground_surface,
(
l.elevation - ro.depth_to_water_below_ground_surface
) AS water_elevation,
l.point
FROM ranked_obs AS ro
JOIN thing AS t ON t.id = ro.thing_id
JOIN latest_location AS ll ON ll.thing_id = t.id
JOIN location AS l ON l.id = ll.location_id
WHERE ro.rn = 1
"""


def upgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
Expand All @@ -107,7 +171,7 @@ def upgrade() -> None:
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.'"
"'Latest water elevation per well with explicit units: elevation_m, depth_to_water_below_ground_surface_ft, water_elevation_ft.'"
)
)
op.execute(
Expand All @@ -120,3 +184,16 @@ def upgrade() -> None:

def downgrade() -> None:
op.execute(text("DROP MATERIALIZED VIEW IF EXISTS ogc_water_elevation_wells"))
op.execute(text(_create_water_elevation_view_m6()))
op.execute(
text(
"COMMENT ON MATERIALIZED VIEW ogc_water_elevation_wells IS "
"'Latest water elevation per well (elevation minus depth to water below ground surface).'"
)
)
op.execute(
text(
"CREATE UNIQUE INDEX ux_ogc_water_elevation_wells_id "
"ON ogc_water_elevation_wells (id)"
)
)
52 changes: 48 additions & 4 deletions tests/test_ogc.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,16 +336,60 @@ def test_ogc_water_elevation_wells_computes_elevation_minus_depth_to_water(

row = session.execute(
text(
"SELECT elevation, depth_to_water_below_ground_surface, water_elevation "
"SELECT elevation_m, depth_to_water_below_ground_surface_ft, water_elevation_ft "
"FROM ogc_water_elevation_wells WHERE id = :thing_id"
),
{"thing_id": water_well_thing.id},
).one()

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


def test_ogc_water_elevation_wells_normalizes_meter_observations_to_feet(
water_well_thing, groundwater_level_observation
):
with session_ctx() as session:
meter_observation = groundwater_level_observation.__class__(
observation_datetime=datetime(2025, 1, 2, 0, 4, 0),
sample_id=groundwater_level_observation.sample_id,
Comment on lines +355 to +357
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.

observation_datetime is passed a naive datetime(...) while other observation fixtures use UTC timestamps (e.g., ISO strings with Z). For a timestamp with time zone column, naive datetimes are interpreted in the DB/session timezone, which can make the “latest” observation selection in the view depend on environment settings. Use a timezone-aware UTC datetime (e.g., datetime(..., tzinfo=timezone.utc)) or an explicit ...Z timestamp string for consistency.

Copilot uses AI. Check for mistakes.
sensor_id=groundwater_level_observation.sensor_id,
parameter_id=groundwater_level_observation.parameter_id,
release_status="draft",
value=3.0,
unit="m",
measuring_point_height=2.0,
groundwater_level_reason="Water level not affected",
)
session.add(meter_observation)
session.commit()

session.execute(text("REFRESH MATERIALIZED VIEW ogc_water_elevation_wells"))
session.commit()

row = session.execute(
text(
"SELECT depth_to_water_below_ground_surface_ft, water_elevation_ft "
"FROM ogc_water_elevation_wells WHERE id = :thing_id"
),
{"thing_id": water_well_thing.id},
).one()

expected_depth_ft = (3.0 * 3.28084) - 2.0
expected_water_elevation_ft = (2464.9 * 3.28084) - expected_depth_ft

assert (
abs(float(row.depth_to_water_below_ground_surface_ft) - expected_depth_ft)
< 1e-9
)
assert abs(float(row.water_elevation_ft) - expected_water_elevation_ft) < 1e-9

session.delete(meter_observation)
session.commit()
session.execute(text("REFRESH MATERIALIZED VIEW ogc_water_elevation_wells"))
session.commit()


def test_ogc_collections():
Expand Down