Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2491e15
feat: implement Radionuclides backfill job and BDD step definitions
kbighorse Feb 28, 2026
90e87b7
fix: harden chemistry backfill test cleanup
kbighorse Feb 28, 2026
03df1f1
fix: harden backfill robustness and remove misleading batch_size
kbighorse Feb 28, 2026
2cba0ae
fix: make migration idempotent, remove batch-size drift, scope test c…
kbighorse Feb 28, 2026
20cee26
fix: harden migration FK lookup, add row-error tracebacks, scope test…
kbighorse Feb 28, 2026
56fa0a6
Formatting changes
kbighorse Feb 28, 2026
c1d1b3f
fix: scope sample assertions, normalize existing_keys case, broaden r…
kbighorse Feb 28, 2026
848524a
Formatting changes
kbighorse Feb 28, 2026
4d867ee
Merge branch 'staging' into 558-radionuclides-backfill
kbighorse Mar 2, 2026
ac44f6b
fix(tests): scope analysis method cleanup to only backfill-created rows
kbighorse Mar 2, 2026
10f3e41
fix(tests): clear backfill tracking after scenario cleanup
kbighorse Mar 2, 2026
1f09e20
fix: deterministic volume handling and explicit CLI arg rejection
kbighorse Mar 2, 2026
ae60c1f
Formatting changes
kbighorse Mar 2, 2026
243f5cf
Merge remote-tracking branch 'origin/staging' into 558-radionuclides-…
kbighorse Mar 2, 2026
f7cef22
Merge branch '558-radionuclides-backfill' of https://github.com/DataI…
kbighorse Mar 2, 2026
09c31ff
fix: make pg_cron optional for local development
kbighorse Mar 2, 2026
72cdf83
fix: make pg_cron optional for local development
kbighorse Mar 2, 2026
0acbfb6
Merge branch '558-radionuclides-backfill' of https://github.com/DataI…
kbighorse Mar 2, 2026
c906134
Merge staging into 600-minor-trace-chemistry-backfill
kbighorse Apr 3, 2026
7b92d1f
feat: implement MinorTraceChemistry backfill job and BDD steps
kbighorse Apr 3, 2026
6f6f098
Formatting changes
kbighorse Apr 3, 2026
9bc0e08
fix: use ug/L default_unit for minor trace parameters, warn on NULL A…
kbighorse Apr 3, 2026
5b05b22
fix: address PR review comments from Copilot
kbighorse Apr 3, 2026
2d6cd6e
fix: add missing AnalysisDate to radionuclides BDD scenarios
kbighorse Apr 3, 2026
9fb6905
fix: address second round of Copilot review comments
kbighorse Apr 3, 2026
c265328
Formatting changes
kbighorse Apr 3, 2026
f96f5e7
chore: move datetime imports to module level, add logger.warning for …
kbighorse Apr 3, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""merge migration heads after staging merge

Revision ID: 1f1bdf75e0c0
Revises: 545a5b77e5e8, t6u7v8w9x0y1
Create Date: 2026-04-03 10:12:48.253856

"""

from typing import Sequence, Union

from alembic import op
import geoalchemy2
import sqlalchemy as sa
import sqlalchemy_utils

# revision identifiers, used by Alembic.
revision: str = "1f1bdf75e0c0"
down_revision: Union[str, Sequence[str], None] = ("545a5b77e5e8", "t6u7v8w9x0y1")
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
pass


def downgrade() -> None:
"""Downgrade schema."""
pass
204 changes: 204 additions & 0 deletions alembic/versions/545a5b77e5e8_add_chemistry_backfill_columns_to_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""add chemistry backfill columns to observation and sample

Revision ID: 545a5b77e5e8
Revises: d5e6f7a8b9c0
Create Date: 2026-02-27 11:30:45.380002

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "545a5b77e5e8"
down_revision: Union[str, Sequence[str], None] = "d5e6f7a8b9c0"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Add chemistry backfill columns to observation and sample tables."""
# Observation table: 4 new columns
op.add_column(
"observation",
sa.Column(
"nma_pk_chemistryresults",
sa.String(),
nullable=True,
comment="NM_Aquifer GlobalID for chemistry results — transfer audit and idempotent upsert key",
),
)
op.add_column(
"observation",
sa.Column(
"detect_flag",
sa.Boolean(),
nullable=True,
comment="True=detected, False=below detection limit (legacy Symbol '<'), None=no qualifier",
),
)
op.add_column(
"observation",
sa.Column(
"uncertainty",
sa.Float(),
nullable=True,
comment="Measurement uncertainty for the observation value",
),
)
op.add_column(
"observation",
sa.Column(
"analysis_agency",
sa.String(),
nullable=True,
comment="Agency or lab that performed the analysis",
),
)
op.create_unique_constraint(
"uq_observation_nma_pk_chemistryresults",
"observation",
["nma_pk_chemistryresults"],
)

# Observation version table (sqlalchemy-continuum)
op.add_column(
"observation_version",
sa.Column(
"nma_pk_chemistryresults",
sa.String(),
autoincrement=False,
nullable=True,
comment="NM_Aquifer GlobalID for chemistry results — transfer audit and idempotent upsert key",
),
)
op.add_column(
"observation_version",
sa.Column(
"detect_flag",
sa.Boolean(),
autoincrement=False,
nullable=True,
comment="True=detected, False=below detection limit (legacy Symbol '<'), None=no qualifier",
),
)
op.add_column(
"observation_version",
sa.Column(
"uncertainty",
sa.Float(),
autoincrement=False,
nullable=True,
comment="Measurement uncertainty for the observation value",
),
)
op.add_column(
"observation_version",
sa.Column(
"analysis_agency",
sa.String(),
autoincrement=False,
nullable=True,
comment="Agency or lab that performed the analysis",
),
)

# Sample table: 3 new columns
op.add_column(
"sample",
sa.Column(
"nma_pk_chemistrysample",
sa.String(),
nullable=True,
comment="NM_Aquifer SamplePtID for chemistry samples — transfer audit key",
),
)
op.add_column(
"sample",
sa.Column(
"volume",
sa.Float(),
nullable=True,
comment="Volume of the sample collected",
),
)
op.add_column(
"sample",
sa.Column(
"volume_unit",
sa.String(),
nullable=True,
comment="Unit for the sample volume (e.g. mL, L)",
),
)
op.create_unique_constraint(
"uq_sample_nma_pk_chemistrysample",
"sample",
["nma_pk_chemistrysample"],
)

# Drop stale thing_id column from NMA_Radionuclides (model no longer defines it;
# relationships go through NMA_Chemistry_SampleInfo.thing_id instead).
# Guard against environments where the column was already removed.
conn = op.get_bind()
has_thing_id = conn.execute(
sa.text(
"SELECT 1 FROM information_schema.columns "
"WHERE table_name = 'NMA_Radionuclides' AND column_name = 'thing_id'"
)
).scalar()
if has_thing_id:
# FK name may differ across environments; look it up dynamically.
fks = sa.inspect(conn).get_foreign_keys("NMA_Radionuclides")
for fk in fks:
if "thing_id" in fk["constrained_columns"] and fk.get("name"):
op.drop_constraint(fk["name"], "NMA_Radionuclides", type_="foreignkey")
op.drop_column("NMA_Radionuclides", "thing_id")


def downgrade() -> None:
"""Remove chemistry backfill columns."""
# Restore NMA_Radionuclides.thing_id (add nullable, backfill, then enforce)
op.add_column(
"NMA_Radionuclides",
sa.Column(
"thing_id",
sa.Integer(),
nullable=True,
),
)
op.execute(
'UPDATE "NMA_Radionuclides" r '
"SET thing_id = csi.thing_id "
'FROM "NMA_Chemistry_SampleInfo" csi '
"WHERE r.chemistry_sample_info_id = csi.id"
)
op.alter_column("NMA_Radionuclides", "thing_id", nullable=False)
op.create_foreign_key(
"NMA_Radionuclides_thing_id_fkey",
"NMA_Radionuclides",
"thing",
["thing_id"],
["id"],
ondelete="CASCADE",
)

op.drop_constraint("uq_sample_nma_pk_chemistrysample", "sample", type_="unique")
op.drop_column("sample", "volume_unit")
op.drop_column("sample", "volume")
op.drop_column("sample", "nma_pk_chemistrysample")

op.drop_column("observation_version", "analysis_agency")
op.drop_column("observation_version", "uncertainty")
op.drop_column("observation_version", "detect_flag")
op.drop_column("observation_version", "nma_pk_chemistryresults")

op.drop_constraint(
"uq_observation_nma_pk_chemistryresults", "observation", type_="unique"
)
op.drop_column("observation", "analysis_agency")
op.drop_column("observation", "uncertainty")
op.drop_column("observation", "detect_flag")
op.drop_column("observation", "nma_pk_chemistryresults")
14 changes: 14 additions & 0 deletions core/lexicon.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,13 @@
"term": "mg/L",
"definition": "Milligrams per Liter"
},
{
"categories": [
"unit"
],
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ug/L is used as the default unit for MinorTraceChemistry Parameters/Observations, but it is not present in core/lexicon.json (and Parameter.default_unit / Observation.unit are lexicon-backed). This will cause FK/validation failures when running the backfill against a freshly initialized database (outside the BDD test seeding, which inserts ug/L manually). Add a ug/L unit term to the lexicon (or switch to an existing unit term already seeded).

Suggested change
],
],
"term": "ug/L",
"definition": "Micrograms per Liter"
},
{
"categories": [
"unit"
],

Copilot uses AI. Check for mistakes.
"term": "pCi/L",
"definition": "Picocuries per Liter"
},
{
"categories": [
"unit"
Expand Down Expand Up @@ -8325,6 +8332,13 @@
"term": "Site Notes (legacy)",
"definition": "Legacy site notes field from WaterLevels"
},
{
"categories": [
"note_type"
],
"term": "Chemistry Observation",
"definition": "Notes from chemistry observation results"
},
{
"categories": [
"well_pump_type"
Expand Down
19 changes: 19 additions & 0 deletions db/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin):

# NM_Aquifer fields for audits
nma_pk_waterlevels: Mapped[str] = mapped_column(nullable=True)
nma_pk_chemistryresults: Mapped[str | None] = mapped_column(
nullable=True,
unique=True,
comment="NM_Aquifer GlobalID for chemistry results — transfer audit and idempotent upsert key",
)

# Chemistry-specific columns
detect_flag: Mapped[bool | None] = mapped_column(
nullable=True,
comment="True=detected, False=below detection limit (legacy Symbol '<'), None=no qualifier",
)
uncertainty: Mapped[float | None] = mapped_column(
nullable=True,
comment="Measurement uncertainty for the observation value",
)
analysis_agency: Mapped[str | None] = mapped_column(
nullable=True,
comment="Agency or lab that performed the analysis",
)

# --- Foreign Keys ---
sample_id: Mapped[int] = mapped_column(
Expand Down
15 changes: 15 additions & 0 deletions db/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin):
nullable=True,
comment="NM_Aquifer primary key for waterlevels - to be used for transfer audits",
)
nma_pk_chemistrysample: Mapped[str | None] = mapped_column(
nullable=True,
unique=True,
comment="NM_Aquifer SamplePtID for chemistry samples — transfer audit key",
)

# Chemistry sample attributes
volume: Mapped[float | None] = mapped_column(
nullable=True,
comment="Volume of the sample collected",
)
volume_unit: Mapped[str | None] = mapped_column(
nullable=True,
comment="Unit for the sample volume (e.g. mL, L)",
)

# --- Relationship Definitions ---
field_activity: Mapped["FieldActivity"] = relationship(back_populates="samples")
Expand Down
3 changes: 1 addition & 2 deletions run_backfill.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# e.g. state, county, quad_name,etc. It will also be used to handle data refactors/corrections in the future.

# Load environment variables from .env and run the staging backfill.
# Usage: ./run_backfill.sh [--batch-size N]
# Usage: ./run_backfill.sh

# github workflow equivalent: for reference only
#- name: Run backfill script on staging database
Expand Down Expand Up @@ -38,5 +38,4 @@ set +a

uv run alembic upgrade head

# Forward any args (e.g., --batch-size 500)
python -m transfers.backfill.backfill "$@"
4 changes: 4 additions & 0 deletions schemas/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ class BaseObservationResponse(BaseResponseModel):
value: float | None
unit: Unit
nma_data_quality: str | None = None
nma_pk_chemistryresults: str | None = None
detect_flag: bool | None = None
uncertainty: float | None = None
analysis_agency: str | None = None


class GroundwaterLevelObservationResponse(BaseObservationResponse):
Expand Down
3 changes: 3 additions & 0 deletions schemas/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ class SampleResponse(BaseResponseModel):
notes: str | None
depth_top: float | None
depth_bottom: float | None
nma_pk_chemistrysample: str | None = None
volume: float | None = None
volume_unit: str | None = None


# ============= EOF =============================================
Loading
Loading