Skip to content

Commit 60ffcf4

Browse files
authored
Merge pull request #868 from DataIntegrationGroup/feat/backfill-acoustic-data-maturity
feat(transducer): backfill data_maturity on acoustic (Wellntel) observations
2 parents e425fa1 + 7a3915f commit 60ffcf4

2 files changed

Lines changed: 183 additions & 1 deletion

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ===============================================================================
2+
# Copyright 2026 ross
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ===============================================================================
16+
"""
17+
Set `data_maturity` on the acoustic (Wellntel) transducer observations that
18+
alembic revision `b2c3d4e5f6a7` left NULL.
19+
20+
That revision backfilled maturity from `nma_waterlevelscontinuous_pressure_qced`,
21+
the AMPAPI flag recording whether a reading was quality controlled. Acoustic
22+
readings have no such flag -- AMPAPI's `WaterLevelsContinuous_Acoustic` table has
23+
no `QCed` column at all -- so all 394,086 of them were skipped, which is the
24+
entire acoustic record (BDMS-1169).
25+
26+
`MATURITY` is a deliberate choice, not a derivation. There is no QC field in the
27+
acoustic legacy schema to read, so nothing here computes the answer; the value
28+
below is the one recorded for these readings, applied uniformly. The transfer's
29+
`review_status='approved'` blocks are *not* evidence for it -- those come from
30+
`PublicRelease`, which every acoustic source row carries and which describes
31+
visibility rather than review.
32+
33+
Rows are matched on `nma_waterlevelscontinuous_acoustic_global_id`, the AMPAPI
34+
row identity. It is written by `WaterLevelsContinuousAcousticTransferer` on every
35+
acoustic row and never by the pressure transferer, so it is the provenance
36+
marker: 394,086 rows carry it, and they are exactly the rows with no
37+
`pressure_qced`.
38+
39+
Only rows where `data_maturity` is already NULL are touched. Re-running is
40+
therefore a no-op, and a maturity set deliberately since -- by the hydrograph
41+
corrector, or by a later migration once the acoustic QC history is known -- is
42+
left alone rather than reset to the blanket value.
43+
"""
44+
45+
from sqlalchemy import update
46+
from sqlalchemy.orm import Session
47+
48+
from data_migrations.base import DataMigration
49+
from db.transducer import TransducerObservation
50+
51+
MATURITY = "approved"
52+
53+
54+
def run(session: Session) -> None:
55+
"""Set the maturity on acoustic observations that have none."""
56+
result = session.execute(
57+
update(TransducerObservation)
58+
.where(
59+
TransducerObservation.nma_waterlevelscontinuous_acoustic_global_id.isnot(
60+
None
61+
),
62+
TransducerObservation.data_maturity.is_(None),
63+
)
64+
.values(data_maturity=MATURITY)
65+
.execution_options(synchronize_session=False)
66+
)
67+
print(
68+
f" set data_maturity={MATURITY!r} on {result.rowcount} acoustic observations"
69+
)
70+
return None
71+
72+
73+
MIGRATION = DataMigration(
74+
id="20260820_0001_backfill_acoustic_data_maturity",
75+
alembic_revision="b2c3d4e5f6a7",
76+
name="Backfill data_maturity on acoustic (Wellntel) observations",
77+
description=(
78+
"Revision b2c3d4e5f6a7 backfilled data_maturity from the pressure QC "
79+
"flag, which acoustic readings do not have, leaving the entire 394,086 "
80+
f"row Wellntel record NULL (BDMS-1169). Sets it to {MATURITY!r}. Only "
81+
"touches rows whose maturity is still NULL."
82+
),
83+
run=run,
84+
is_repeatable=False,
85+
)
86+
87+
88+
# ============= EOF =============================================

tests/test_data_migrations.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@
1414
# limitations under the License.
1515
# ===============================================================================
1616
import importlib
17+
from datetime import datetime, timedelta, timezone
1718

18-
from sqlalchemy import select
19+
from sqlalchemy import delete, select
1920

2021
move_notes = importlib.import_module(
2122
"data_migrations.migrations.20260205_0001_move_nma_location_notes"
2223
)
2324
publish_project_areas = importlib.import_module(
2425
"data_migrations.migrations.20260714_0001_publish_project_areas"
2526
)
27+
backfill_acoustic_maturity = importlib.import_module(
28+
"data_migrations.migrations.20260820_0001_backfill_acoustic_data_maturity"
29+
)
2630
from db.location import Location
2731
from db.notes import Notes
2832
from db.group import Group
2933
from db.engine import session_ctx
34+
from db.transducer import TransducerObservation
35+
from tests import get_parameter_id
3036

3137

3238
def test_move_nma_location_notes_creates_notes_and_clears_field():
@@ -139,3 +145,91 @@ def test_publish_project_areas_marks_project_area_groups_public():
139145
session.delete(draft_with_area)
140146
session.delete(draft_without_area)
141147
session.commit()
148+
149+
150+
def test_backfill_acoustic_data_maturity_only_touches_null_acoustic_rows(
151+
sensor_to_water_well_thing_deployment,
152+
):
153+
deployment_id = sensor_to_water_well_thing_deployment.id
154+
parameter_id = get_parameter_id("groundwater level", "Field Parameter")
155+
observed = datetime(2019, 7, 23, 12, 0, tzinfo=timezone.utc)
156+
157+
with session_ctx() as session:
158+
# An acoustic row with no maturity -- the case this migration exists for.
159+
acoustic = TransducerObservation(
160+
parameter_id=parameter_id,
161+
deployment_id=deployment_id,
162+
observation_datetime=observed,
163+
value=42.0,
164+
nma_waterlevelscontinuous_acoustic_global_id="ACOUSTIC-NULL",
165+
)
166+
# An acoustic row whose maturity was already set deliberately. The
167+
# blanket value must not overwrite a decision someone made.
168+
acoustic_already_set = TransducerObservation(
169+
parameter_id=parameter_id,
170+
deployment_id=deployment_id,
171+
observation_datetime=observed + timedelta(hours=1),
172+
value=43.0,
173+
nma_waterlevelscontinuous_acoustic_global_id="ACOUSTIC-SET",
174+
data_maturity="provisional",
175+
)
176+
# A pressure row with no maturity. NULL here means the pressure QC flag
177+
# was NULL, which is a different question -- leave it alone.
178+
pressure = TransducerObservation(
179+
parameter_id=parameter_id,
180+
deployment_id=deployment_id,
181+
observation_datetime=observed + timedelta(hours=2),
182+
value=44.0,
183+
nma_waterlevelscontinuous_pressure_global_id="PRESSURE-NULL",
184+
)
185+
session.add_all([acoustic, acoustic_already_set, pressure])
186+
session.commit()
187+
ids = (acoustic.id, acoustic_already_set.id, pressure.id)
188+
189+
try:
190+
backfill_acoustic_maturity.run(session)
191+
192+
session.refresh(acoustic)
193+
session.refresh(acoustic_already_set)
194+
session.refresh(pressure)
195+
assert acoustic.data_maturity == backfill_acoustic_maturity.MATURITY
196+
assert acoustic_already_set.data_maturity == "provisional"
197+
assert pressure.data_maturity is None
198+
finally:
199+
session.execute(
200+
delete(TransducerObservation).where(TransducerObservation.id.in_(ids))
201+
)
202+
session.commit()
203+
204+
205+
def test_backfill_acoustic_data_maturity_is_idempotent(
206+
sensor_to_water_well_thing_deployment,
207+
):
208+
deployment_id = sensor_to_water_well_thing_deployment.id
209+
parameter_id = get_parameter_id("groundwater level", "Field Parameter")
210+
211+
with session_ctx() as session:
212+
observation = TransducerObservation(
213+
parameter_id=parameter_id,
214+
deployment_id=deployment_id,
215+
observation_datetime=datetime(2020, 1, 1, tzinfo=timezone.utc),
216+
value=45.0,
217+
nma_waterlevelscontinuous_acoustic_global_id="ACOUSTIC-REPEAT",
218+
)
219+
session.add(observation)
220+
session.commit()
221+
observation_id = observation.id
222+
223+
try:
224+
backfill_acoustic_maturity.run(session)
225+
backfill_acoustic_maturity.run(session)
226+
227+
session.refresh(observation)
228+
assert observation.data_maturity == backfill_acoustic_maturity.MATURITY
229+
finally:
230+
session.execute(
231+
delete(TransducerObservation).where(
232+
TransducerObservation.id == observation_id
233+
)
234+
)
235+
session.commit()

0 commit comments

Comments
 (0)