Skip to content

Commit 27e0695

Browse files
committed
• fix(well-inventory): make CSV import reruns idempotent
- Detect previously imported well inventory rows before inserting related records - Skip recreating field activity water-level samples and observations when the same row is reprocessed - Return serializable existing-row results so CLI reruns report cleanly instead of crashing
1 parent b2df9ab commit 27e0695

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

services/well_inventory_csv.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,44 @@ def _generate_autogen_well_id(session, prefix: str, offset: int = 0) -> tuple[st
438438
return f"{prefix}{new_number:04d}", new_number
439439

440440

441+
def _find_existing_imported_well(
442+
session: Session, model: WellInventoryRow
443+
) -> Thing | None:
444+
if model.measurement_date_time is not None:
445+
sample_name = (
446+
f"{model.well_name_point_id}-WL-"
447+
f"{model.measurement_date_time.strftime('%Y%m%d%H%M')}"
448+
)
449+
existing = session.scalars(
450+
select(Thing)
451+
.join(FieldEvent, FieldEvent.thing_id == Thing.id)
452+
.join(FieldActivity, FieldActivity.field_event_id == FieldEvent.id)
453+
.join(Sample, Sample.field_activity_id == FieldActivity.id)
454+
.where(
455+
Thing.name == model.well_name_point_id,
456+
Thing.thing_type == "water well",
457+
FieldActivity.activity_type == "well inventory",
458+
Sample.sample_name == sample_name,
459+
)
460+
.order_by(Thing.id.asc())
461+
).first()
462+
if existing is not None:
463+
return existing
464+
465+
return session.scalars(
466+
select(Thing)
467+
.join(FieldEvent, FieldEvent.thing_id == Thing.id)
468+
.join(FieldActivity, FieldActivity.field_event_id == FieldEvent.id)
469+
.where(
470+
Thing.name == model.well_name_point_id,
471+
Thing.thing_type == "water well",
472+
FieldEvent.event_date == model.date_time,
473+
FieldActivity.activity_type == "well inventory",
474+
)
475+
.order_by(Thing.id.asc())
476+
).first()
477+
478+
441479
def _make_row_models(rows, session):
442480
models = []
443481
validation_errors = []
@@ -542,6 +580,10 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user)
542580
name = model.well_name_point_id
543581
date_time = model.date_time
544582

583+
existing_well = _find_existing_imported_well(session, model)
584+
if existing_well is not None:
585+
return existing_well.name
586+
545587
# --------------------
546588
# Location and associated tables
547589
# --------------------

tests/test_well_inventory.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,38 @@ def test_blank_depth_to_water_still_creates_water_level_records(tmp_path):
494494
assert observations[0].measuring_point_height == 2.5
495495

496496

497+
def test_rerunning_same_well_inventory_csv_is_idempotent():
498+
"""Re-importing the same CSV should not create duplicate well inventory records."""
499+
file = Path("tests/features/data/well-inventory-valid.csv")
500+
assert file.exists(), "Test data file does not exist."
501+
502+
first = well_inventory_csv(file)
503+
assert first.exit_code == 0, first.stderr
504+
505+
with session_ctx() as session:
506+
counts_after_first = {
507+
"things": session.query(Thing).count(),
508+
"field_events": session.query(FieldEvent).count(),
509+
"field_activities": session.query(FieldActivity).count(),
510+
"samples": session.query(Sample).count(),
511+
"observations": session.query(Observation).count(),
512+
}
513+
514+
second = well_inventory_csv(file)
515+
assert second.exit_code == 0, second.stderr
516+
517+
with session_ctx() as session:
518+
counts_after_second = {
519+
"things": session.query(Thing).count(),
520+
"field_events": session.query(FieldEvent).count(),
521+
"field_activities": session.query(FieldActivity).count(),
522+
"samples": session.query(Sample).count(),
523+
"observations": session.query(Observation).count(),
524+
}
525+
526+
assert counts_after_second == counts_after_first
527+
528+
497529
# =============================================================================
498530
# Error Handling Tests - Cover API error paths
499531
# =============================================================================

0 commit comments

Comments
 (0)