From f6cb2fd806d888d41083f1027ff29b20f0479301 Mon Sep 17 00:00:00 2001 From: Likitha Bommasani Date: Thu, 20 Aug 2026 11:40:19 -0700 Subject: [PATCH 1/6] fix(ogc): expand actively_monitored_wells to all groups --- ...expand_actively_monitored_wells_to_all_.py | 169 ++++++++++++++++++ tests/features/ogc-cleanup-sprint1.feature | 40 ++--- tests/test_ogc.py | 49 +++++ 3 files changed, 229 insertions(+), 29 deletions(-) create mode 100644 alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py diff --git a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py new file mode 100644 index 00000000..32c3d395 --- /dev/null +++ b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py @@ -0,0 +1,169 @@ +"""expand actively_monitored_wells to all groups + +Drops the "WHERE group name = 'water level network'" restriction so the view +covers currently-monitored wells in any group, not just one. Inner join to +group/group_thing_association is kept as-is (prod has no currently-monitored +well with zero group memberships); wells in multiple groups intentionally +appear once per group, no aggregation. + +Revision ID: 986e0eb85ab3 +Revises: c3d4e5f6a7b8 +Create Date: 2026-08-20 10:55:25.697907 + +""" +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import text + +# revision identifiers, used by Alembic. +revision: str = '986e0eb85ab3' +down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _drop_view_or_materialized_view(view_name: str) -> None: + # DROP VIEW IF EXISTS / DROP MATERIALIZED VIEW IF EXISTS only suppress + # "relation does not exist" -- Postgres still raises WrongObjectType if + # the relation exists as the other kind, so the relation's actual kind + # must be checked first rather than trying both blindly. + bind = op.get_bind() + relkind = bind.execute( + text("SELECT relkind FROM pg_class WHERE oid = to_regclass(:name)"), + {"name": view_name}, + ).scalar() + if relkind == "m": + op.execute(text(f"DROP MATERIALIZED VIEW IF EXISTS {view_name}")) + elif relkind == "v": + op.execute(text(f"DROP VIEW IF EXISTS {view_name}")) + + +def _create_actively_monitored_wells_view(all_groups: bool) -> str: + group_filter = ( + "" if all_groups + else "lower(trim(g.name)) = 'water level network'\n AND " + ) + return f""" + CREATE VIEW ogc_actively_monitored_wells AS + WITH latest_monitoring_status AS ( + SELECT DISTINCT ON (sh.target_id) + sh.target_id AS thing_id, + sh.status_value + FROM status_history AS sh + WHERE + sh.target_table = 'thing' + AND sh.status_type = 'Monitoring Status' + ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ) + SELECT + wws.id, + wws.name, + 'water well'::text AS thing_type, + wws.well_depth, + wws.elevation, + wws.elevation_method, + wws.formation_zone, + wws.total_water_levels, + wws.last_water_level, + wws.last_water_level_datetime, + wws.min_water_level, + wws.max_water_level, + wws.water_level_trend_ft_per_year, + g.id AS group_id, + g.name AS group_name, + g.group_type, + wws.point + FROM "group" AS g + JOIN group_thing_association AS gta ON gta.group_id = g.id + JOIN ogc_water_well_summary AS wws ON wws.id = gta.thing_id + JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id + WHERE {group_filter}lms.status_value = 'Currently monitored' + """ + + +def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: + group_filter = ( + "" if all_groups + else "lower(trim(g.name)) = 'water level network'\n AND " + ) + return f""" + CREATE VIEW ogc_internal_actively_monitored_wells AS + WITH latest_monitoring_status AS ( + SELECT DISTINCT ON (sh.target_id) + sh.target_id AS thing_id, + sh.status_value + FROM status_history AS sh + WHERE + sh.target_table = 'thing' + AND sh.status_type = 'Monitoring Status' + ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ) + SELECT + wws.id, + wws.name, + 'water well'::text AS thing_type, + wws.well_depth, + wws.elevation, + wws.elevation_method, + wws.formation_zone, + wws.total_water_levels, + wws.last_water_level, + wws.last_water_level_datetime, + wws.min_water_level, + wws.max_water_level, + wws.water_level_trend_ft_per_year, + g.id AS group_id, + g.name AS group_name, + g.group_type, + wws.point + FROM "group" AS g + JOIN group_thing_association AS gta ON gta.group_id = g.id + JOIN ogc_internal_water_well_summary AS wws ON wws.id = gta.thing_id + JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id + WHERE {group_filter}lms.status_value = 'Currently monitored' + """ + + +def upgrade() -> None: + """Upgrade schema.""" + _drop_view_or_materialized_view("ogc_actively_monitored_wells") + op.execute(text(_create_actively_monitored_wells_view(all_groups=True))) + op.execute( + text( + "COMMENT ON VIEW ogc_actively_monitored_wells IS " + "'Actively (currently) monitored wells across all groups for pygeoapi.'" + ) + ) + + _drop_view_or_materialized_view("ogc_internal_actively_monitored_wells") + op.execute(text(_create_internal_actively_monitored_wells_view(all_groups=True))) + op.execute( + text( + "COMMENT ON VIEW ogc_internal_actively_monitored_wells IS " + "'Actively (currently) monitored wells across all groups, " + "for the internal pygeoapi mount.'" + ) + ) + + +def downgrade() -> None: + """Downgrade schema.""" + _drop_view_or_materialized_view("ogc_actively_monitored_wells") + op.execute(text(_create_actively_monitored_wells_view(all_groups=False))) + op.execute( + text( + "COMMENT ON VIEW ogc_actively_monitored_wells IS " + "'Wells in the Water Level Network group for pygeoapi.'" + ) + ) + + _drop_view_or_materialized_view("ogc_internal_actively_monitored_wells") + op.execute(text(_create_internal_actively_monitored_wells_view(all_groups=False))) + op.execute( + text( + "COMMENT ON VIEW ogc_internal_actively_monitored_wells IS " + "'Unfiltered wells in the Water Level Network group, " + "for the internal pygeoapi mount.'" + ) + ) diff --git a/tests/features/ogc-cleanup-sprint1.feature b/tests/features/ogc-cleanup-sprint1.feature index d747c8be..bfab8bc9 100644 --- a/tests/features/ogc-cleanup-sprint1.feature +++ b/tests/features/ogc-cleanup-sprint1.feature @@ -135,47 +135,29 @@ Feature: OGC Feature Layer Cleanup — Sprint 1 | /ogcapi/collections/water_wells/items?datetime=2020-01-01/2024-01-01 | # --------------------------------------------------------------------------- - # A4 — Fix brittle SQL filter in actively_monitored_wells + # A4/A6 — actively_monitored_wells covers all groups; name unchanged # --------------------------------------------------------------------------- - # Note: these scenarios use actively_monitored_wells (the pre-A6 name). After A6 - # is applied, requests to this ID redirect via the 90-day deprecation route. + # A6's original rename to water_level_network_wells was withdrawn: the name + # was fine, the filter was too narrow. A4 (brittle group-name filter) and A6 + # (naming) are resolved together by the same SQL change. @backend @ogc-data-currency @sprint-1 @high-priority @A4 - Scenario: Layer result set is unchanged after replacing the string filter - Given the "Water Level Network" group exists in the database + Scenario: Layer includes a well from a group other than Water Level Network + Given a well is currently monitored under the "Test Other Group" group When a client requests features from the actively_monitored_wells layer - Then the feature count is 322 + Then the response includes that well @backend @ogc-data-currency @sprint-1 @high-priority @A4 Scenario: Layer is resilient to group display name changes Given the "Water Level Network" group display name is changed to "Water Level Monitoring Network" When a client requests features from the actively_monitored_wells layer - Then the feature count is 322 - - # --------------------------------------------------------------------------- - # A6 — Rename actively_monitored_wells to water_level_network_wells - # --------------------------------------------------------------------------- - - @backend @ogc-naming @sprint-1 @high-priority @A6 - Scenario: Layer is accessible under the new ID water_level_network_wells - When a client requests /ogcapi/collections/water_level_network_wells/items - Then the response HTTP status is 200 - And the response Content-Type is "application/geo+json" + Then wells in that group still appear in the response @backend @ogc-naming @sprint-1 @high-priority @A6 - Scenario: The renamed layer is discoverable in the collections catalog - Given the rename to water_level_network_wells has been applied across service configuration + Scenario: Layer keeps its existing ID and is discoverable in the collections catalog When a client requests /ogcapi/collections - Then the water_level_network_wells collection appears in the response - And the actively_monitored_wells collection does not appear in the response - - @backend @ogc-naming @sprint-1 @high-priority @A6 - Scenario: Old layer ID returns deprecation headers during the 90-day grace period - When a client requests /ogcapi/collections/actively_monitored_wells/items - Then the response HTTP status is 200 - And the response includes a Deprecation header - And the response includes a Sunset header containing a valid RFC 7231 date - And the response includes a Link header pointing to the water_level_network_wells collection + Then the actively_monitored_wells collection appears in the response + And the water_level_network_wells collection does not appear in the response # --------------------------------------------------------------------------- # A11 — Stand up authenticated internal OGC mount at /ogcapi-internal diff --git a/tests/test_ogc.py b/tests/test_ogc.py index 8b49ac0a..80f5902b 100644 --- a/tests/test_ogc.py +++ b/tests/test_ogc.py @@ -559,6 +559,55 @@ def test_ogc_actively_monitored_wells_excludes_latest_not_currently_monitored( session.commit() +def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( + water_well_thing, + groundwater_level_observation, +): + with session_ctx() as session: + session.execute(text("REFRESH MATERIALIZED VIEW ogc_water_well_summary")) + session.commit() + + group = Group( + name="Test Other Group", + group_type="Monitoring Plan", + release_status="public", + ) + session.add(group) + session.flush() + + group_assoc = GroupThingAssociation( + group_id=group.id, + thing_id=water_well_thing.id, + ) + session.add(group_assoc) + status_history = StatusHistory( + status_type="Monitoring Status", + status_value="Currently monitored", + start_date=date(2024, 1, 1), + target_id=water_well_thing.id, + target_table="thing", + ) + session.add(status_history) + session.commit() + + row = session.execute( + text( + "SELECT group_id, group_name, group_type " + "FROM ogc_actively_monitored_wells WHERE id = :thing_id" + ), + {"thing_id": water_well_thing.id}, + ).one() + + assert row.group_id == group.id + assert row.group_name == "Test Other Group" + assert row.group_type == "Monitoring Plan" + + session.delete(status_history) + session.delete(group_assoc) + session.delete(group) + session.commit() + + def test_ogc_collections(ogc_client): response = ogc_client.get("/ogcapi/collections") assert response.status_code == 200 From 355169fa7b9c138f2ada4075d7d51edd3f12e562 Mon Sep 17 00:00:00 2001 From: likithabommasani21 <275146718+likithabommasani21@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:52:43 +0000 Subject: [PATCH 2/6] Formatting changes --- ...eb85ab3_expand_actively_monitored_wells_to_all_.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py index 32c3d395..1ecfa93b 100644 --- a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py +++ b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py @@ -11,14 +11,15 @@ Create Date: 2026-08-20 10:55:25.697907 """ + from typing import Sequence, Union from alembic import op from sqlalchemy import text # revision identifiers, used by Alembic. -revision: str = '986e0eb85ab3' -down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8' +revision: str = "986e0eb85ab3" +down_revision: Union[str, Sequence[str], None] = "c3d4e5f6a7b8" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -41,7 +42,8 @@ def _drop_view_or_materialized_view(view_name: str) -> None: def _create_actively_monitored_wells_view(all_groups: bool) -> str: group_filter = ( - "" if all_groups + "" + if all_groups else "lower(trim(g.name)) = 'water level network'\n AND " ) return f""" @@ -84,7 +86,8 @@ def _create_actively_monitored_wells_view(all_groups: bool) -> str: def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: group_filter = ( - "" if all_groups + "" + if all_groups else "lower(trim(g.name)) = 'water level network'\n AND " ) return f""" From 3858ef38c390951cdb52478046c594d8019d1905 Mon Sep 17 00:00:00 2001 From: Likitha Bommasani Date: Sun, 23 Aug 2026 18:08:19 -0700 Subject: [PATCH 3/6] "fix(ogc): keep draft/private groups off the public actively_monitored_wells view and added tests for internal" --- ...expand_actively_monitored_wells_to_all_.py | 11 ++- tests/test_ogc.py | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py index 1ecfa93b..058d5839 100644 --- a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py +++ b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py @@ -1,7 +1,9 @@ """expand actively_monitored_wells to all groups Drops the "WHERE group name = 'water level network'" restriction so the view -covers currently-monitored wells in any group, not just one. Inner join to +covers currently-monitored wells in any group, not just one. Public view +adds a group release_status = 'public' check instead, so draft/private +groups don't leak through now that any group can show up. Inner join to group/group_thing_association is kept as-is (prod has no currently-monitored well with zero group memberships); wells in multiple groups intentionally appear once per group, no aggregation. @@ -41,8 +43,13 @@ def _drop_view_or_materialized_view(view_name: str) -> None: def _create_actively_monitored_wells_view(all_groups: bool) -> str: + # The all_groups branch drops the group-name predicate but still needs + # to keep draft/private groups off the public mount -- unlike the old + # single-group filter, any group can appear here now, so the group's own + # release_status has to be checked directly (mirrors + # _create_project_areas_view's public_only handling). group_filter = ( - "" + "g.release_status = 'public'\n AND " if all_groups else "lower(trim(g.name)) = 'water level network'\n AND " ) diff --git a/tests/test_ogc.py b/tests/test_ogc.py index 80f5902b..ce2bc151 100644 --- a/tests/test_ogc.py +++ b/tests/test_ogc.py @@ -565,6 +565,9 @@ def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( ): with session_ctx() as session: session.execute(text("REFRESH MATERIALIZED VIEW ogc_water_well_summary")) + session.execute( + text("REFRESH MATERIALIZED VIEW ogc_internal_water_well_summary") + ) session.commit() group = Group( @@ -602,6 +605,73 @@ def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( assert row.group_name == "Test Other Group" assert row.group_type == "Monitoring Plan" + internal_row = session.execute( + text( + "SELECT group_id, group_name, group_type " + "FROM ogc_internal_actively_monitored_wells WHERE id = :thing_id" + ), + {"thing_id": water_well_thing.id}, + ).one() + + assert internal_row.group_id == group.id + assert internal_row.group_name == "Test Other Group" + assert internal_row.group_type == "Monitoring Plan" + + session.delete(status_history) + session.delete(group_assoc) + session.delete(group) + session.commit() + + +def test_ogc_actively_monitored_wells_hides_draft_group_on_public_view( + water_well_thing, + groundwater_level_observation, +): + with session_ctx() as session: + session.execute(text("REFRESH MATERIALIZED VIEW ogc_water_well_summary")) + session.execute( + text("REFRESH MATERIALIZED VIEW ogc_internal_water_well_summary") + ) + session.commit() + + group = Group( + name="Test Draft Group", + group_type="Monitoring Plan", + release_status="draft", + ) + session.add(group) + session.flush() + + group_assoc = GroupThingAssociation( + group_id=group.id, + thing_id=water_well_thing.id, + ) + session.add(group_assoc) + status_history = StatusHistory( + status_type="Monitoring Status", + status_value="Currently monitored", + start_date=date(2024, 1, 1), + target_id=water_well_thing.id, + target_table="thing", + ) + session.add(status_history) + session.commit() + + public_row = session.execute( + text("SELECT id FROM ogc_actively_monitored_wells WHERE id = :thing_id"), + {"thing_id": water_well_thing.id}, + ).one_or_none() + assert public_row is None + + internal_row = session.execute( + text( + "SELECT group_id FROM ogc_internal_actively_monitored_wells " + "WHERE id = :thing_id" + ), + {"thing_id": water_well_thing.id}, + ).one() + assert internal_row.group_id == group.id + session.delete(status_history) session.delete(group_assoc) session.delete(group) From 1bdabccece438d0af4ddf0c150b89370519672c8 Mon Sep 17 00:00:00 2001 From: Likitha Bommasani Date: Mon, 24 Aug 2026 10:34:50 -0700 Subject: [PATCH 4/6] fix(ogc): aggregate group memberships on actively_monitored_wells to keep id unique and updated tests accordingly. --- ...expand_actively_monitored_wells_to_all_.py | 133 +++++++++++++++--- tests/test_ogc.py | 60 +++++--- 2 files changed, 147 insertions(+), 46 deletions(-) diff --git a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py index 058d5839..52768ef3 100644 --- a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py +++ b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py @@ -3,10 +3,10 @@ Drops the "WHERE group name = 'water level network'" restriction so the view covers currently-monitored wells in any group, not just one. Public view adds a group release_status = 'public' check instead, so draft/private -groups don't leak through now that any group can show up. Inner join to -group/group_thing_association is kept as-is (prod has no currently-monitored -well with zero group memberships); wells in multiple groups intentionally -appear once per group, no aggregation. +groups don't leak through now that any group can show up. A well in +multiple groups is aggregated into one row (group_ids/group_names/group_types +as arrays) rather than one row per group, so `id` stays unique -- pygeoapi's +id_field: id assumes exactly one row per id for /items/{id} lookups. Revision ID: 986e0eb85ab3 Revises: c3d4e5f6a7b8 @@ -43,17 +43,59 @@ def _drop_view_or_materialized_view(view_name: str) -> None: def _create_actively_monitored_wells_view(all_groups: bool) -> str: - # The all_groups branch drops the group-name predicate but still needs - # to keep draft/private groups off the public mount -- unlike the old - # single-group filter, any group can appear here now, so the group's own - # release_status has to be checked directly (mirrors - # _create_project_areas_view's public_only handling). - group_filter = ( - "g.release_status = 'public'\n AND " - if all_groups - else "lower(trim(g.name)) = 'water level network'\n AND " - ) - return f""" + if all_groups: + # Aggregated: one row per well, group_ids/group_names/group_types as + # arrays, so `id` stays unique even when a well belongs to several + # groups. release_status = 'public' is checked on the group row + # itself (mirrors _create_project_areas_view's public_only handling) + # since any group can appear here now, not just one hardcoded one. + return """ + CREATE VIEW ogc_actively_monitored_wells AS + WITH latest_monitoring_status AS ( + SELECT DISTINCT ON (sh.target_id) + sh.target_id AS thing_id, + sh.status_value + FROM status_history AS sh + WHERE + sh.target_table = 'thing' + AND sh.status_type = 'Monitoring Status' + ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ) + SELECT + wws.id, + wws.name, + 'water well'::text AS thing_type, + wws.well_depth, + wws.elevation, + wws.elevation_method, + wws.formation_zone, + wws.total_water_levels, + wws.last_water_level, + wws.last_water_level_datetime, + wws.min_water_level, + wws.max_water_level, + wws.water_level_trend_ft_per_year, + array_agg(g.id) AS group_ids, + array_agg(g.name) AS group_names, + array_agg(g.group_type) AS group_types, + wws.point + FROM ogc_water_well_summary AS wws + JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id + JOIN group_thing_association AS gta ON gta.thing_id = wws.id + JOIN "group" AS g ON g.id = gta.group_id + WHERE lms.status_value = 'Currently monitored' + AND g.release_status = 'public' + GROUP BY + wws.id, wws.name, wws.well_depth, wws.elevation, + wws.elevation_method, wws.formation_zone, + wws.total_water_levels, wws.last_water_level, + wws.last_water_level_datetime, wws.min_water_level, + wws.max_water_level, wws.water_level_trend_ft_per_year, + wws.point + """ + # Historical (downgrade target): byte-for-byte the pre-fix view, single + # group_id/group_name/group_type columns, scoped to one hardcoded group. + return """ CREATE VIEW ogc_actively_monitored_wells AS WITH latest_monitoring_status AS ( SELECT DISTINCT ON (sh.target_id) @@ -87,17 +129,61 @@ def _create_actively_monitored_wells_view(all_groups: bool) -> str: JOIN group_thing_association AS gta ON gta.group_id = g.id JOIN ogc_water_well_summary AS wws ON wws.id = gta.thing_id JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id - WHERE {group_filter}lms.status_value = 'Currently monitored' + WHERE lower(trim(g.name)) = 'water level network' + AND lms.status_value = 'Currently monitored' """ def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: - group_filter = ( - "" - if all_groups - else "lower(trim(g.name)) = 'water level network'\n AND " - ) - return f""" + if all_groups: + # Aggregated, same shape as the public view's all_groups branch, but + # no release_status filter -- the internal mount is unfiltered by + # design, same as its sibling views. + return """ + CREATE VIEW ogc_internal_actively_monitored_wells AS + WITH latest_monitoring_status AS ( + SELECT DISTINCT ON (sh.target_id) + sh.target_id AS thing_id, + sh.status_value + FROM status_history AS sh + WHERE + sh.target_table = 'thing' + AND sh.status_type = 'Monitoring Status' + ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ) + SELECT + wws.id, + wws.name, + 'water well'::text AS thing_type, + wws.well_depth, + wws.elevation, + wws.elevation_method, + wws.formation_zone, + wws.total_water_levels, + wws.last_water_level, + wws.last_water_level_datetime, + wws.min_water_level, + wws.max_water_level, + wws.water_level_trend_ft_per_year, + array_agg(g.id) AS group_ids, + array_agg(g.name) AS group_names, + array_agg(g.group_type) AS group_types, + wws.point + FROM ogc_internal_water_well_summary AS wws + JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id + JOIN group_thing_association AS gta ON gta.thing_id = wws.id + JOIN "group" AS g ON g.id = gta.group_id + WHERE lms.status_value = 'Currently monitored' + GROUP BY + wws.id, wws.name, wws.well_depth, wws.elevation, + wws.elevation_method, wws.formation_zone, + wws.total_water_levels, wws.last_water_level, + wws.last_water_level_datetime, wws.min_water_level, + wws.max_water_level, wws.water_level_trend_ft_per_year, + wws.point + """ + # Historical (downgrade target): byte-for-byte the pre-fix view. + return """ CREATE VIEW ogc_internal_actively_monitored_wells AS WITH latest_monitoring_status AS ( SELECT DISTINCT ON (sh.target_id) @@ -131,7 +217,8 @@ def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: JOIN group_thing_association AS gta ON gta.group_id = g.id JOIN ogc_internal_water_well_summary AS wws ON wws.id = gta.thing_id JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id - WHERE {group_filter}lms.status_value = 'Currently monitored' + WHERE lower(trim(g.name)) = 'water level network' + AND lms.status_value = 'Currently monitored' """ diff --git a/tests/test_ogc.py b/tests/test_ogc.py index ce2bc151..35ed69ac 100644 --- a/tests/test_ogc.py +++ b/tests/test_ogc.py @@ -491,15 +491,15 @@ def test_ogc_actively_monitored_wells_exposes_water_level_network_group_wells( row = session.execute( text( - "SELECT group_id, group_name, group_type " + "SELECT group_ids, group_names, group_types " "FROM ogc_actively_monitored_wells WHERE id = :thing_id" ), {"thing_id": water_well_thing.id}, ).one() - assert row.group_id == group.id - assert row.group_name == "Water Level Network" - assert row.group_type == "Monitoring Plan" + assert row.group_ids == [group.id] + assert row.group_names == ["Water Level Network"] + assert row.group_types == ["Monitoring Plan"] session.delete(status_history) session.delete(group_assoc) @@ -559,7 +559,7 @@ def test_ogc_actively_monitored_wells_excludes_latest_not_currently_monitored( session.commit() -def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( +def test_ogc_actively_monitored_wells_aggregates_multiple_groups( water_well_thing, groundwater_level_observation, ): @@ -570,19 +570,28 @@ def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( ) session.commit() - group = Group( - name="Test Other Group", + group_a = Group( + name="Test Other Group A", group_type="Monitoring Plan", release_status="public", ) - session.add(group) + group_b = Group( + name="Test Other Group B", + group_type="Monitoring Plan", + release_status="public", + ) + session.add_all([group_a, group_b]) session.flush() - group_assoc = GroupThingAssociation( - group_id=group.id, + group_assoc_a = GroupThingAssociation( + group_id=group_a.id, thing_id=water_well_thing.id, ) - session.add(group_assoc) + group_assoc_b = GroupThingAssociation( + group_id=group_b.id, + thing_id=water_well_thing.id, + ) + session.add_all([group_assoc_a, group_assoc_b]) status_history = StatusHistory( status_type="Monitoring Status", status_value="Currently monitored", @@ -595,31 +604,36 @@ def test_ogc_actively_monitored_wells_includes_wells_from_other_groups( row = session.execute( text( - "SELECT group_id, group_name, group_type " + "SELECT group_ids, group_names, group_types " "FROM ogc_actively_monitored_wells WHERE id = :thing_id" ), {"thing_id": water_well_thing.id}, ).one() - assert row.group_id == group.id - assert row.group_name == "Test Other Group" - assert row.group_type == "Monitoring Plan" + assert set(row.group_ids) == {group_a.id, group_b.id} + assert set(row.group_names) == {"Test Other Group A", "Test Other Group B"} + assert row.group_types == ["Monitoring Plan", "Monitoring Plan"] internal_row = session.execute( text( - "SELECT group_id, group_name, group_type " + "SELECT group_ids, group_names, group_types " "FROM ogc_internal_actively_monitored_wells WHERE id = :thing_id" ), {"thing_id": water_well_thing.id}, ).one() - assert internal_row.group_id == group.id - assert internal_row.group_name == "Test Other Group" - assert internal_row.group_type == "Monitoring Plan" + assert set(internal_row.group_ids) == {group_a.id, group_b.id} + assert set(internal_row.group_names) == { + "Test Other Group A", + "Test Other Group B", + } + assert internal_row.group_types == ["Monitoring Plan", "Monitoring Plan"] session.delete(status_history) - session.delete(group_assoc) - session.delete(group) + session.delete(group_assoc_a) + session.delete(group_assoc_b) + session.delete(group_a) + session.delete(group_b) session.commit() @@ -665,12 +679,12 @@ def test_ogc_actively_monitored_wells_hides_draft_group_on_public_view( internal_row = session.execute( text( - "SELECT group_id FROM ogc_internal_actively_monitored_wells " + "SELECT group_ids FROM ogc_internal_actively_monitored_wells " "WHERE id = :thing_id" ), {"thing_id": water_well_thing.id}, ).one() - assert internal_row.group_id == group.id + assert internal_row.group_ids == [group.id] session.delete(status_history) session.delete(group_assoc) From 96badc7efd10531533806e85f3b1dd3585126463 Mon Sep 17 00:00:00 2001 From: Likitha Bommasani Date: Mon, 24 Aug 2026 10:52:14 -0700 Subject: [PATCH 5/6] fix(ogc): keep actively_monitored_wells field descriptions in sync with the renamed group_ids/group_names/group_types columns --- core/ogc-field-descriptions.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/core/ogc-field-descriptions.yml b/core/ogc-field-descriptions.yml index 2c83681f..dd01ac95 100644 --- a/core/ogc-field-descriptions.yml +++ b/core/ogc-field-descriptions.yml @@ -315,17 +315,19 @@ actively_monitored_wells: is falling. x-ogc-unit: https://qudt.org/vocab/unit/FT-PER-YR x-ogc-unitLang: QUDT - group_id: - title: Network ID - description: Identifier of the monitoring network the well belongs to. - group_name: - title: Network name - description: >- - Name of the monitoring network the well belongs to. Always the Water - Level Network in this collection. - group_type: - title: Network type - description: Kind of grouping the network record represents. + group_ids: + title: Network IDs + description: Identifiers of every monitoring network the well belongs to. + group_names: + title: Network names + description: >- + Names of every monitoring network the well belongs to, in the same + order as group_ids. + group_types: + title: Network types + description: >- + Kind of grouping each network record represents, in the same order as + group_ids. depth_to_water_trend_wells: record_count: From 4e95758dbf964402bf31d229654ab089ac8b2f15 Mon Sep 17 00:00:00 2001 From: Likitha Bommasani Date: Mon, 24 Aug 2026 11:02:45 -0700 Subject: [PATCH 6/6] fix(ogc): dedupe group_thing_association rows and keep group arrays aligned by group_id --- ...expand_actively_monitored_wells_to_all_.py | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py index 52768ef3..0b71d362 100644 --- a/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py +++ b/alembic/versions/986e0eb85ab3_expand_actively_monitored_wells_to_all_.py @@ -49,6 +49,11 @@ def _create_actively_monitored_wells_view(all_groups: bool) -> str: # groups. release_status = 'public' is checked on the group row # itself (mirrors _create_project_areas_view's public_only handling) # since any group can appear here now, not just one hardcoded one. + # group_thing_association has no unique constraint on + # (group_id, thing_id), so distinct_memberships de-dupes before + # aggregating; all three arrays are ordered by the same group_id key + # so they stay index-aligned with each other (ordering each array by + # its own column, e.g. names alphabetically, would desync them). return """ CREATE VIEW ogc_actively_monitored_wells AS WITH latest_monitoring_status AS ( @@ -60,6 +65,16 @@ def _create_actively_monitored_wells_view(all_groups: bool) -> str: sh.target_table = 'thing' AND sh.status_type = 'Monitoring Status' ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ), + distinct_memberships AS ( + SELECT DISTINCT + gta.thing_id, + g.id AS group_id, + g.name AS group_name, + g.group_type + FROM group_thing_association AS gta + JOIN "group" AS g ON g.id = gta.group_id + WHERE g.release_status = 'public' ) SELECT wws.id, @@ -75,16 +90,14 @@ def _create_actively_monitored_wells_view(all_groups: bool) -> str: wws.min_water_level, wws.max_water_level, wws.water_level_trend_ft_per_year, - array_agg(g.id) AS group_ids, - array_agg(g.name) AS group_names, - array_agg(g.group_type) AS group_types, + array_agg(dm.group_id ORDER BY dm.group_id) AS group_ids, + array_agg(dm.group_name ORDER BY dm.group_id) AS group_names, + array_agg(dm.group_type ORDER BY dm.group_id) AS group_types, wws.point FROM ogc_water_well_summary AS wws JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id - JOIN group_thing_association AS gta ON gta.thing_id = wws.id - JOIN "group" AS g ON g.id = gta.group_id + JOIN distinct_memberships AS dm ON dm.thing_id = wws.id WHERE lms.status_value = 'Currently monitored' - AND g.release_status = 'public' GROUP BY wws.id, wws.name, wws.well_depth, wws.elevation, wws.elevation_method, wws.formation_zone, @@ -138,7 +151,8 @@ def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: if all_groups: # Aggregated, same shape as the public view's all_groups branch, but # no release_status filter -- the internal mount is unfiltered by - # design, same as its sibling views. + # design, same as its sibling views. See the public branch's comment + # for why distinct_memberships + a shared ORDER BY key is needed. return """ CREATE VIEW ogc_internal_actively_monitored_wells AS WITH latest_monitoring_status AS ( @@ -150,6 +164,15 @@ def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: sh.target_table = 'thing' AND sh.status_type = 'Monitoring Status' ORDER BY sh.target_id, sh.start_date DESC, sh.id DESC + ), + distinct_memberships AS ( + SELECT DISTINCT + gta.thing_id, + g.id AS group_id, + g.name AS group_name, + g.group_type + FROM group_thing_association AS gta + JOIN "group" AS g ON g.id = gta.group_id ) SELECT wws.id, @@ -165,14 +188,13 @@ def _create_internal_actively_monitored_wells_view(all_groups: bool) -> str: wws.min_water_level, wws.max_water_level, wws.water_level_trend_ft_per_year, - array_agg(g.id) AS group_ids, - array_agg(g.name) AS group_names, - array_agg(g.group_type) AS group_types, + array_agg(dm.group_id ORDER BY dm.group_id) AS group_ids, + array_agg(dm.group_name ORDER BY dm.group_id) AS group_names, + array_agg(dm.group_type ORDER BY dm.group_id) AS group_types, wws.point FROM ogc_internal_water_well_summary AS wws JOIN latest_monitoring_status AS lms ON lms.thing_id = wws.id - JOIN group_thing_association AS gta ON gta.thing_id = wws.id - JOIN "group" AS g ON g.id = gta.group_id + JOIN distinct_memberships AS dm ON dm.thing_id = wws.id WHERE lms.status_value = 'Currently monitored' GROUP BY wws.id, wws.name, wws.well_depth, wws.elevation,