From f63af237371105f167bc4d7fb4694da50d108e37 Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 12 Jun 2026 13:59:21 -0600 Subject: [PATCH 1/2] fix: drop child-row release filters from NGWMN construction/lithology views The release_status predicates on well_screen, well_casing_material, and thing_geologic_formation_association emptied the NGWMN exports on staging: transfers never set release_status on those child tables, so every row is 'draft' (3005/3005 screens, 63/63 casing materials, 993/993 formation associations). Construction and lithology rows are attributes of the well, so the thing-level public filter remains the gate; NGWMN_WaterLevels is unchanged because its field-data chain does carry real release values. Co-Authored-By: Claude Fable 5 --- ..._child_release_filters_from_ngwmn_views.py | 133 ++++++++++++++++++ tests/test_ngwmn_endpoints.py | 11 +- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 alembic/versions/w1x2y3z4a5b6_drop_child_release_filters_from_ngwmn_views.py diff --git a/alembic/versions/w1x2y3z4a5b6_drop_child_release_filters_from_ngwmn_views.py b/alembic/versions/w1x2y3z4a5b6_drop_child_release_filters_from_ngwmn_views.py new file mode 100644 index 00000000..5fe815a7 --- /dev/null +++ b/alembic/versions/w1x2y3z4a5b6_drop_child_release_filters_from_ngwmn_views.py @@ -0,0 +1,133 @@ +"""drop child-row release filters from NGWMN construction/lithology views + +The release_status filters added to NGWMN_WellConstruction and +NGWMN_Lithology for well_screen, well_casing_material, and +thing_geologic_formation_association rows emptied those exports in +practice: the transfers never set release_status on those child tables, +so every row defaults to 'draft' (verified 3005/3005 well_screen, +63/63 well_casing_material, 993/993 associations on transferred data). + +Construction and lithology rows are attributes of the well, and the +well's own release_status (genuinely managed: public/private) remains +the gate. The thing-level filters are kept; only the child-row +predicates are removed. NGWMN_WaterLevels is unchanged, since the whole +field-data chain it filters on does carry real release values. + +Revision ID: w1x2y3z4a5b6 +Revises: v0w1x2y3z4a5 +Create Date: 2026-06-12 00:00:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import inspect, text + +# revision identifiers, used by Alembic. +revision: str = "w1x2y3z4a5b6" +down_revision: Union[str, Sequence[str], None] = "v0w1x2y3z4a5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +REQUIRED_TABLES = { + "thing", + "well_screen", + "well_casing_material", + "thing_geologic_formation_association", + "geologic_formation", +} + +DROP_WELLCONSTRUCTION_SQL = 'DROP VIEW IF EXISTS "NGWMN_WellConstruction"' +DROP_LITHOLOGY_SQL = 'DROP VIEW IF EXISTS "NGWMN_Lithology"' + + +def _create_wellconstruction_view(with_child_filters: bool) -> str: + screen_filter = " AND ws.release_status = 'public'" if with_child_filters else "" + material_filter = " AND wcm.release_status = 'public'" if with_child_filters else "" + return f""" + CREATE VIEW "NGWMN_WellConstruction" AS + SELECT + t.name AS "PointID", + CASE WHEN t.well_casing_depth IS NOT NULL THEN 0::double precision END AS "CasingTop", + t.well_casing_depth AS "CasingBottom", + CASE WHEN t.well_casing_depth IS NOT NULL THEN 'ft bgs' END AS "CasingDepthUnits", + ws.screen_depth_top AS "ScreenTop", + ws.screen_depth_bottom AS "ScreenBottom", + CASE WHEN ws.screen_depth_bottom IS NOT NULL THEN 'ft bgs' END AS "ScreenBottomUnit", + ws.screen_description AS "ScreenDescription", + cm.materials AS "CasingDescription" + FROM thing AS t + LEFT JOIN well_screen AS ws + ON ws.thing_id = t.id{screen_filter} + LEFT JOIN LATERAL ( + SELECT string_agg(wcm.material, ', ' ORDER BY wcm.material) AS materials + FROM well_casing_material AS wcm + WHERE wcm.thing_id = t.id{material_filter} + ) AS cm ON TRUE + WHERE t.thing_type = 'water well' + AND t.release_status = 'public' + """ + + +def _create_lithology_view(with_child_filters: bool) -> str: + association_filter = ( + " AND tgfa.release_status = 'public'\n" if with_child_filters else "" + ) + return f""" + CREATE VIEW "NGWMN_Lithology" AS + SELECT + tgfa.id AS "OBJECTID", + t.name AS "PointID", + gf.lithology AS "Lithology", + gf.lithology AS "TERM", + NULL::character varying AS "StratSource", + tgfa.top_depth AS "StratTop", + CASE WHEN tgfa.top_depth IS NOT NULL THEN 'ft bgs' END AS "StratTopUnit", + tgfa.bottom_depth AS "StratBottom", + CASE WHEN tgfa.bottom_depth IS NOT NULL THEN 'ft bgs' END AS "StratBottomUnit" + FROM thing_geologic_formation_association AS tgfa + JOIN thing AS t ON t.id = tgfa.thing_id + JOIN geologic_formation AS gf ON gf.id = tgfa.geologic_formation_id + WHERE gf.lithology IS NOT NULL +{association_filter} AND t.release_status = 'public' + """ + + +def _recreate_views(with_child_filters: bool) -> None: + bind = op.get_bind() + inspector = inspect(bind) + existing_tables = set(inspector.get_table_names(schema="public")) + missing = REQUIRED_TABLES - existing_tables + if missing: + raise RuntimeError( + "Cannot recreate NGWMN views. Missing required tables: " + f"{', '.join(sorted(missing))}" + ) + + op.execute(text(DROP_WELLCONSTRUCTION_SQL)) + op.execute(text(_create_wellconstruction_view(with_child_filters))) + op.execute( + text( + 'COMMENT ON VIEW "NGWMN_WellConstruction" IS ' + "'Well casing and screen intervals in the NGWMN exchange format, " + "sourced from the Ocotillo thing/well_screen model.'" + ) + ) + + op.execute(text(DROP_LITHOLOGY_SQL)) + op.execute(text(_create_lithology_view(with_child_filters))) + op.execute( + text( + 'COMMENT ON VIEW "NGWMN_Lithology" IS ' + "'Lithology intervals in the NGWMN exchange format, sourced from " + "the Ocotillo geologic formation associations.'" + ) + ) + + +def upgrade() -> None: + _recreate_views(with_child_filters=False) + + +def downgrade() -> None: + _recreate_views(with_child_filters=True) diff --git a/tests/test_ngwmn_endpoints.py b/tests/test_ngwmn_endpoints.py index cf1a6b86..b36a8992 100644 --- a/tests/test_ngwmn_endpoints.py +++ b/tests/test_ngwmn_endpoints.py @@ -61,25 +61,28 @@ def ngwmn_well(): session.add(thing) session.flush() + # Screen, casing material, and lithology rows are deliberately left at + # the 'draft' default: transfers never set release_status on these + # child tables, and the export is gated on the thing's status alone. session.add( WellScreen( thing_id=thing.id, screen_depth_top=80.0, screen_depth_bottom=120.0, screen_description="4in slotted", - release_status="public", + release_status="draft", ) ) session.add( WellCasingMaterial( - thing_id=thing.id, material="Steel", release_status="public" + thing_id=thing.id, material="Steel", release_status="draft" ) ) formation = GeologicFormation( formation_code=None, lithology="Sandstone", - release_status="public", + release_status="draft", ) session.add(formation) session.flush() @@ -89,7 +92,7 @@ def ngwmn_well(): geologic_formation_id=formation.id, top_depth=0.0, bottom_depth=60.0, - release_status="public", + release_status="draft", ) ) From e2cd8fc20a43e0dfc23793109ff2c900c2cc395b Mon Sep 17 00:00:00 2001 From: jross Date: Fri, 12 Jun 2026 14:37:54 -0600 Subject: [PATCH 2/2] docs: clarify draft release_status comment in NGWMN test fixture Co-Authored-By: Claude Fable 5 --- tests/test_ngwmn_endpoints.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_ngwmn_endpoints.py b/tests/test_ngwmn_endpoints.py index b36a8992..b78d84fc 100644 --- a/tests/test_ngwmn_endpoints.py +++ b/tests/test_ngwmn_endpoints.py @@ -61,9 +61,10 @@ def ngwmn_well(): session.add(thing) session.flush() - # Screen, casing material, and lithology rows are deliberately left at - # the 'draft' default: transfers never set release_status on these - # child tables, and the export is gated on the thing's status alone. + # Screen, casing material, and lithology rows are explicitly set to + # 'draft' to mirror transferred data, where these child tables never + # get a release_status. The export is gated on the thing's status + # alone, so these rows must still appear in the NGWMN responses. session.add( WellScreen( thing_id=thing.id,