From a22f99a42f3f5efea20bef8aebaee8f2feb147b4 Mon Sep 17 00:00:00 2001 From: jakeross Date: Thu, 19 Mar 2026 18:27:16 -0600 Subject: [PATCH 1/2] Add actively monitored wells OGC collection --- ..._add_thing_id_to_nma_surface_water_data.py | 11 ++- ...a8b9c0_create_pygeoapi_supporting_views.py | 53 +++++++--- ...itored_wells_pygeoapi_materialized_view.py | 96 ++++++++++++++++++ ...6v7w8x9_drop_unused_well_type_ogc_views.py | 99 +++++++++++++++++++ core/pygeoapi-config.yml | 23 +++++ core/pygeoapi.py | 77 --------------- tests/test_ogc.py | 91 +++++++++++++---- 7 files changed, 337 insertions(+), 113 deletions(-) create mode 100644 alembic/versions/r2s3t4u5v6w7_add_actively_monitored_wells_pygeoapi_materialized_view.py create mode 100644 alembic/versions/s4t5u6v7w8x9_drop_unused_well_type_ogc_views.py diff --git a/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py b/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py index 8a3597688..d791e7a68 100644 --- a/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py +++ b/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py @@ -33,18 +33,23 @@ def upgrade() -> None: ondelete="CASCADE", ) # Backfill thing_id based on LocationId -> Thing.nma_pk_location - op.execute(""" + op.execute( + """ UPDATE "NMA_SurfaceWaterData" sw SET thing_id = t.id FROM thing t WHERE t.nma_pk_location IS NOT NULL AND sw."LocationId" IS NOT NULL AND t.nma_pk_location = sw."LocationId"::text - """) + """ + ) # Remove any rows that cannot be linked to a Thing, then enforce NOT NULL op.execute('DELETE FROM "NMA_SurfaceWaterData" WHERE thing_id IS NULL') op.alter_column( - "NMA_SurfaceWaterData", "thing_id", existing_type=sa.Integer(), nullable=False + "NMA_SurfaceWaterData", + "thing_id", + existing_type=sa.Integer(), + nullable=False, ) diff --git a/alembic/versions/d5e6f7a8b9c0_create_pygeoapi_supporting_views.py b/alembic/versions/d5e6f7a8b9c0_create_pygeoapi_supporting_views.py index e11bf2403..60d03fc04 100644 --- a/alembic/versions/d5e6f7a8b9c0_create_pygeoapi_supporting_views.py +++ b/alembic/versions/d5e6f7a8b9c0_create_pygeoapi_supporting_views.py @@ -34,7 +34,10 @@ ("monitoring_wells", "monitoring well"), ("observation_wells", "observation well"), ("other_things", "other"), - ("outfalls_wastewater_return_flow", "outfall of wastewater or return flow"), + ( + "outfalls_wastewater_return_flow", + "outfall of wastewater or return flow", + ), ("perennial_streams", "perennial stream"), ("piezometers", "piezometer"), ("production_wells", "production well"), @@ -107,8 +110,11 @@ def _create_latest_depth_view() -> str: o.observation_datetime, o.value, o.measuring_point_height, - -- Treat NULL measuring_point_height as 0 when computing depth_to_water_bgs - (o.value - COALESCE(o.measuring_point_height, 0)) AS depth_to_water_bgs, + -- Treat NULL measuring_point_height as 0 when computing + -- depth_to_water_bgs. + ( + o.value - COALESCE(o.measuring_point_height, 0) + ) AS depth_to_water_bgs, ROW_NUMBER() OVER ( PARTITION BY fe.thing_id ORDER BY o.observation_datetime DESC, o.id DESC @@ -151,7 +157,10 @@ def _create_avg_tds_view() -> str: SELECT csi.thing_id, mc.id AS major_chemistry_id, - COALESCE(mc."AnalysisDate", csi."CollectionDate")::date AS observation_date, + COALESCE( + mc."AnalysisDate", + csi."CollectionDate" + )::date AS observation_date, mc."SampleValue" AS sample_value, mc."Units" AS units FROM "NMA_MajorChemistry" AS mc @@ -193,15 +202,16 @@ def _drop_view_or_materialized_view(view_name: str) -> None: def _create_matview_indexes() -> None: # Required so REFRESH MATERIALIZED VIEW CONCURRENTLY can run. + avg_tds_index_sql = ( + "CREATE UNIQUE INDEX ux_ogc_avg_tds_wells_id " "ON ogc_avg_tds_wells (id)" + ) op.execute( text( "CREATE UNIQUE INDEX ux_ogc_latest_depth_to_water_wells_id " "ON ogc_latest_depth_to_water_wells (id)" ) ) - op.execute( - text("CREATE UNIQUE INDEX ux_ogc_avg_tds_wells_id " "ON ogc_avg_tds_wells (id)") - ) + op.execute(text(avg_tds_index_sql)) def _create_refresh_function() -> str: @@ -220,7 +230,11 @@ def _create_refresh_function() -> str: WHERE schemaname = 'public' AND matviewname LIKE 'ogc_%' LOOP - matview_fqname := format('%I.%I', matview_record.schemaname, matview_record.matviewname); + matview_fqname := format( + '%I.%I', + matview_record.schemaname, + matview_record.matviewname + ); EXECUTE format('REFRESH MATERIALIZED VIEW %s', matview_fqname); END LOOP; END; @@ -235,10 +249,15 @@ def upgrade() -> None: required_core = {"thing", "location", "location_thing_association"} existing_tables = set(inspector.get_table_names(schema="public")) if not required_core.issubset(existing_tables): - missing_tables = sorted(t for t in required_core if t not in existing_tables) + missing_tables = sorted( + table_name + for table_name in required_core + if table_name not in existing_tables + ) missing_tables_str = ", ".join(missing_tables) raise RuntimeError( - "Cannot create pygeoapi supporting views. The following required core " + "Cannot create pygeoapi supporting views. " + "The following required core " f"tables are missing: {missing_tables_str}" ) @@ -255,7 +274,8 @@ def upgrade() -> None: ) missing_depth_tables_str = ", ".join(missing_depth_tables) raise RuntimeError( - "Cannot create ogc_latest_depth_to_water_wells. The following required " + "Cannot create ogc_latest_depth_to_water_wells. " + "The following required " f"tables are missing: {missing_depth_tables_str}" ) op.execute(text(_create_latest_depth_view())) @@ -269,7 +289,11 @@ def upgrade() -> None: _drop_view_or_materialized_view("ogc_avg_tds_wells") required_tds = {"NMA_MajorChemistry", "NMA_Chemistry_SampleInfo"} if not required_tds.issubset(existing_tables): - missing_tds_tables = sorted(t for t in required_tds if t not in existing_tables) + missing_tds_tables = sorted( + table_name + for table_name in required_tds + if table_name not in existing_tables + ) missing_tds_tables_str = ", ".join(missing_tds_tables) raise RuntimeError( "Cannot create ogc_avg_tds_wells. The following required " @@ -288,7 +312,10 @@ def upgrade() -> None: def downgrade() -> None: - op.execute(text(f"DROP FUNCTION IF EXISTS public.{REFRESH_FUNCTION_NAME}()")) + drop_refresh_function_sql = ( + f"DROP FUNCTION IF EXISTS public.{REFRESH_FUNCTION_NAME}()" + ) + op.execute(text(drop_refresh_function_sql)) _drop_view_or_materialized_view("ogc_avg_tds_wells") _drop_view_or_materialized_view("ogc_latest_depth_to_water_wells") for view_id, _ in THING_COLLECTIONS: diff --git a/alembic/versions/r2s3t4u5v6w7_add_actively_monitored_wells_pygeoapi_materialized_view.py b/alembic/versions/r2s3t4u5v6w7_add_actively_monitored_wells_pygeoapi_materialized_view.py new file mode 100644 index 000000000..8c674c968 --- /dev/null +++ b/alembic/versions/r2s3t4u5v6w7_add_actively_monitored_wells_pygeoapi_materialized_view.py @@ -0,0 +1,96 @@ +"""add actively monitored wells pygeoapi view + +Revision ID: r2s3t4u5v6w7 +Revises: p9c0d1e2f3a4 +Create Date: 2026-03-19 10:10:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import inspect, text + +# revision identifiers, used by Alembic. +revision: str = "r2s3t4u5v6w7" +down_revision: Union[str, Sequence[str], None] = "p9c0d1e2f3a4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None +DROP_VIEW_SQL = "DROP VIEW IF EXISTS ogc_actively_monitored_wells" +DROP_MATVIEW_SQL = "DROP MATERIALIZED VIEW IF EXISTS " "ogc_actively_monitored_wells" + + +def _create_actively_monitored_wells_view() -> str: + return """ + CREATE VIEW ogc_actively_monitored_wells AS + 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 + WHERE lower(trim(g.name)) = 'water level network' + """ + + +def upgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + existing_tables = set(inspector.get_table_names(schema="public")) + required_tables = { + "group", + "group_thing_association", + } + + if not required_tables.issubset(existing_tables): + missing = sorted( + table_name + for table_name in required_tables + if table_name not in existing_tables + ) + raise RuntimeError( + "Cannot create ogc_actively_monitored_wells. " + f"Missing required tables: {', '.join(missing)}" + ) + + has_summary = bind.execute( + text( + "SELECT 1 FROM pg_matviews " + "WHERE schemaname = 'public' " + "AND matviewname = 'ogc_water_well_summary'" + ) + ).scalar() + if has_summary != 1: + raise RuntimeError( + "Cannot create ogc_actively_monitored_wells. " + "Missing required materialized view: ogc_water_well_summary" + ) + + op.execute(text(DROP_VIEW_SQL)) + op.execute(text(DROP_MATVIEW_SQL)) + op.execute(text(_create_actively_monitored_wells_view())) + op.execute( + text( + "COMMENT ON VIEW ogc_actively_monitored_wells IS " + "'Wells in the Water Level Network group for pygeoapi.'" + ) + ) + + +def downgrade() -> None: + op.execute(text(DROP_VIEW_SQL)) + op.execute(text(DROP_MATVIEW_SQL)) diff --git a/alembic/versions/s4t5u6v7w8x9_drop_unused_well_type_ogc_views.py b/alembic/versions/s4t5u6v7w8x9_drop_unused_well_type_ogc_views.py new file mode 100644 index 000000000..8800ed4d3 --- /dev/null +++ b/alembic/versions/s4t5u6v7w8x9_drop_unused_well_type_ogc_views.py @@ -0,0 +1,99 @@ +"""drop unused well-type OGC views + +Revision ID: s4t5u6v7w8x9 +Revises: r2s3t4u5v6w7 +Create Date: 2026-03-19 14:30:00.000000 +""" + +import re +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import text + +# revision identifiers, used by Alembic. +revision: str = "s4t5u6v7w8x9" +down_revision: Union[str, Sequence[str], None] = "r2s3t4u5v6w7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +REMOVED_THING_COLLECTIONS = [ + ("abandoned_wells", "abandoned well"), + ("artesian_wells", "artesian well"), + ("dry_holes", "dry hole"), + ("dug_wells", "dug well"), + ("exploration_wells", "exploration well"), + ("injection_wells", "injection well"), + ("monitoring_wells", "monitoring well"), + ("observation_wells", "observation well"), + ("piezometers", "piezometer"), + ("production_wells", "production well"), + ("test_wells", "test well"), +] + +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 _safe_view_id(view_id: str) -> str: + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", view_id): + raise ValueError(f"Unsafe view id: {view_id!r}") + return view_id + + +def _drop_view_or_materialized_view(view_name: str) -> None: + op.execute(text(f"DROP VIEW IF EXISTS {view_name}")) + op.execute(text(f"DROP MATERIALIZED VIEW IF EXISTS {view_name}")) + + +def _create_thing_view(view_id: str, thing_type: str) -> str: + safe_view_id = _safe_view_id(view_id) + escaped_thing_type = thing_type.replace("'", "''") + return f""" + CREATE VIEW ogc_{safe_view_id} AS + WITH latest_location AS ( +{LATEST_LOCATION_CTE} + ) + SELECT + t.id, + t.name, + t.first_visit_date, + t.nma_pk_welldata, + t.well_depth, + t.hole_depth, + t.well_casing_diameter, + t.well_casing_depth, + t.well_completion_date, + t.well_driller_name, + t.well_construction_method, + t.well_pump_type, + t.well_pump_depth, + t.formation_completion_code, + t.nma_formation_zone, + t.release_status, + l.elevation, + l.point + FROM thing AS t + JOIN latest_location AS ll ON ll.thing_id = t.id + JOIN location AS l ON l.id = ll.location_id + WHERE t.thing_type = '{escaped_thing_type}' + """ + + +def upgrade() -> None: + for view_id, _ in REMOVED_THING_COLLECTIONS: + _drop_view_or_materialized_view(f"ogc_{_safe_view_id(view_id)}") + + +def downgrade() -> None: + for view_id, thing_type in REMOVED_THING_COLLECTIONS: + safe_view_id = _safe_view_id(view_id) + _drop_view_or_materialized_view(f"ogc_{safe_view_id}") + op.execute(text(_create_thing_view(view_id, thing_type))) diff --git a/core/pygeoapi-config.yml b/core/pygeoapi-config.yml index 981a40cfb..f060e4999 100644 --- a/core/pygeoapi-config.yml +++ b/core/pygeoapi-config.yml @@ -241,4 +241,27 @@ resources: table: ogc_minor_chemistry_wells geom_field: point + actively_monitored_wells: + type: collection + title: Actively Monitored Wells + description: Wells in the collaborative network currently flagged as actively monitored. + keywords: [water-wells, monitoring, collaborative-network, actively-monitored] + extents: + spatial: + bbox: [-109.05, 31.33, -103.00, 37.00] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + providers: + - type: feature + name: PostgreSQL + data: + host: {postgres_host} + port: {postgres_port} + dbname: {postgres_db} + user: {postgres_user} + password: {postgres_password_env} + search_path: [public] + id_field: id + table: ogc_actively_monitored_wells + geom_field: point + {thing_collections_block} diff --git a/core/pygeoapi.py b/core/pygeoapi.py index 6c679a21f..0ac68b1b7 100644 --- a/core/pygeoapi.py +++ b/core/pygeoapi.py @@ -85,83 +85,6 @@ "description": "Locations where soil gas measurements or samples were collected.", "keywords": ["soil-gas", "sample-location"], }, - { - "id": "abandoned_wells", - "title": "Abandoned Wells", - "thing_type": "abandoned well", - "description": "Wells that are no longer active and are classified as abandoned.", - "keywords": ["abandoned-well", "well"], - }, - { - "id": "artesian_wells", - "title": "Artesian Wells", - "thing_type": "artesian well", - "description": "Wells that tap confined aquifers with artesian pressure conditions.", - "keywords": ["artesian", "well"], - }, - { - "id": "dry_holes", - "title": "Dry Holes", - "thing_type": "dry hole", - "description": "Drilled holes that did not produce usable groundwater.", - "keywords": ["dry-hole", "well"], - }, - { - "id": "dug_wells", - "title": "Dug Wells", - "thing_type": "dug well", - "description": "Large-diameter wells excavated by digging.", - "keywords": ["dug-well", "well"], - }, - { - "id": "exploration_wells", - "title": "Exploration Wells", - "thing_type": "exploration well", - "description": "Wells drilled to characterize geologic and groundwater conditions.", - "keywords": ["exploration-well", "well"], - }, - { - "id": "injection_wells", - "title": "Injection Wells", - "thing_type": "injection well", - "description": "Wells used to inject fluids into subsurface formations.", - "keywords": ["injection-well", "well"], - }, - { - "id": "monitoring_wells", - "title": "Monitoring Wells", - "thing_type": "monitoring well", - "description": "Wells primarily used for long-term groundwater monitoring.", - "keywords": ["monitoring-well", "groundwater", "well"], - }, - { - "id": "observation_wells", - "title": "Observation Wells", - "thing_type": "observation well", - "description": "Observation wells used for periodic water-level measurements.", - "keywords": ["observation-well", "groundwater", "well"], - }, - { - "id": "piezometers", - "title": "Piezometers", - "thing_type": "piezometer", - "description": "Piezometers used to measure hydraulic head at depth.", - "keywords": ["piezometer", "groundwater", "well"], - }, - { - "id": "production_wells", - "title": "Production Wells", - "thing_type": "production well", - "description": "Wells used for groundwater supply and extraction.", - "keywords": ["production-well", "groundwater", "well"], - }, - { - "id": "test_wells", - "title": "Test Wells", - "thing_type": "test well", - "description": "Temporary or investigative test wells.", - "keywords": ["test-well", "well"], - }, ] diff --git a/tests/test_ogc.py b/tests/test_ogc.py index f318339dd..7abcc9816 100644 --- a/tests/test_ogc.py +++ b/tests/test_ogc.py @@ -17,8 +17,10 @@ from importlib.util import find_spec import pytest +from fastapi.testclient import TestClient from sqlalchemy import text +from core.factory import create_api_app from core.dependencies import ( admin_function, editor_function, @@ -27,10 +29,15 @@ viewer_function, amp_viewer_function, ) -from db import NMA_Chemistry_SampleInfo, NMA_MajorChemistry, NMA_MinorTraceChemistry +from db import ( + Group, + GroupThingAssociation, + NMA_Chemistry_SampleInfo, + NMA_MajorChemistry, + NMA_MinorTraceChemistry, +) from db.engine import session_ctx -from main import app -from tests import client, override_authentication +from tests import override_authentication pytestmark = pytest.mark.skipif( find_spec("pygeoapi") is None, @@ -39,7 +46,8 @@ @pytest.fixture(scope="module", autouse=True) -def override_authentication_dependency_fixture(): +def ogc_client(): + app = create_api_app() app.dependency_overrides[admin_function] = override_authentication( default={"name": "foobar", "sub": "1234567890"} ) @@ -55,29 +63,30 @@ def override_authentication_dependency_fixture(): ) app.dependency_overrides[amp_viewer_function] = override_authentication() - yield + with TestClient(app) as client: + yield client app.dependency_overrides = {} -def test_ogc_landing(): - response = client.get("/ogcapi") +def test_ogc_landing(ogc_client): + response = ogc_client.get("/ogcapi") assert response.status_code == 200 payload = response.json() assert payload["title"] assert any(link["rel"] == "self" for link in payload["links"]) -def test_ogc_conformance(): - response = client.get("/ogcapi/conformance") +def test_ogc_conformance(ogc_client): + response = ogc_client.get("/ogcapi/conformance") assert response.status_code == 200 payload = response.json() assert "conformsTo" in payload assert any("ogcapi-features" in item for item in payload["conformsTo"]) -def test_ogc_openapi_has_paths(): - response = client.get("/ogcapi/openapi?f=json") +def test_ogc_openapi_has_paths(ogc_client): + response = ogc_client.get("/ogcapi/openapi?f=json") assert response.status_code == 200 payload = response.json() assert payload["openapi"].startswith("3.") @@ -392,8 +401,48 @@ def test_ogc_water_elevation_wells_normalizes_meter_observations_to_feet( session.commit() -def test_ogc_collections(): - response = client.get("/ogcapi/collections") +def test_ogc_actively_monitored_wells_exposes_water_level_network_group_wells( + 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="Water Level Network", + 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) + 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 == "Water Level Network" + assert row.group_type == "Monitoring Plan" + + 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 payload = response.json() ids = {collection["id"] for collection in payload["collections"]} @@ -407,10 +456,11 @@ def test_ogc_collections(): "water_well_summary", "major_chemistry_results", "minor_chemistry_wells", + "actively_monitored_wells", }.issubset(ids) -def test_ogc_new_collection_items_endpoints(): +def test_ogc_new_collection_items_endpoints(ogc_client): for collection_id in ( "latest_tds_wells", "depth_to_water_trend_wells", @@ -418,8 +468,9 @@ def test_ogc_new_collection_items_endpoints(): "water_well_summary", "major_chemistry_results", "minor_chemistry_wells", + "actively_monitored_wells", ): - response = client.get(f"/ogcapi/collections/{collection_id}/items?limit=10") + response = ogc_client.get(f"/ogcapi/collections/{collection_id}/items?limit=10") assert response.status_code == 200 payload = response.json() assert payload["type"] == "FeatureCollection" @@ -428,22 +479,22 @@ def test_ogc_new_collection_items_endpoints(): @pytest.mark.skip("PostGIS spatial operators not available in CI - see issue #449") def test_ogc_locations_items_bbox(location): bbox = "-107.95,33.80,-107.94,33.81" - response = client.get(f"/ogcapi/collections/locations/items?bbox={bbox}") + response = ogc_client.get(f"/ogcapi/collections/locations/items?bbox={bbox}") assert response.status_code == 200 payload = response.json() assert payload["type"] == "FeatureCollection" assert payload["numberReturned"] >= 1 -def test_ogc_wells_items_and_item(water_well_thing): - response = client.get("/ogcapi/collections/water_wells/items?limit=20") +def test_ogc_wells_items_and_item(ogc_client, water_well_thing): + response = ogc_client.get("/ogcapi/collections/water_wells/items?limit=20") assert response.status_code == 200 payload = response.json() assert payload["numberReturned"] >= 1 ids = {str(feature["id"]) for feature in payload["features"]} assert str(water_well_thing.id) in ids - response = client.get( + response = ogc_client.get( f"/ogcapi/collections/water_wells/items/{water_well_thing.id}" ) assert response.status_code == 200 @@ -454,7 +505,7 @@ def test_ogc_wells_items_and_item(water_well_thing): @pytest.mark.skip("PostGIS spatial operators not available in CI - see issue #449") def test_ogc_polygon_within_filter(location): polygon = "POLYGON((-107.95 33.80,-107.94 33.80,-107.94 33.81,-107.95 33.81,-107.95 33.80))" - response = client.get( + response = ogc_client.get( "/ogcapi/collections/locations/items", params={ "filter": f"WITHIN(geometry,{polygon})", From e341f5f19fd892a786909be9e95cac38856f4213 Mon Sep 17 00:00:00 2001 From: jirhiker <2035568+jirhiker@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:27:49 +0000 Subject: [PATCH 2/2] Formatting changes --- .../c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py b/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py index d791e7a68..021615490 100644 --- a/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py +++ b/alembic/versions/c7f8a9b0c1d2_add_thing_id_to_nma_surface_water_data.py @@ -33,16 +33,14 @@ def upgrade() -> None: ondelete="CASCADE", ) # Backfill thing_id based on LocationId -> Thing.nma_pk_location - op.execute( - """ + op.execute(""" UPDATE "NMA_SurfaceWaterData" sw SET thing_id = t.id FROM thing t WHERE t.nma_pk_location IS NOT NULL AND sw."LocationId" IS NOT NULL AND t.nma_pk_location = sw."LocationId"::text - """ - ) + """) # Remove any rows that cannot be linked to a Thing, then enforce NOT NULL op.execute('DELETE FROM "NMA_SurfaceWaterData" WHERE thing_id IS NULL') op.alter_column(