diff --git a/CHANGELOG.md b/CHANGELOG.md index f34247b..b480bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ for a version is copied into its GitHub release notes, so write it for the peopl are no longer offered. What is stored is never changed. ### Changed +- **Adding a later water reading no longer loses the water level recorded with the borewell.** + That level is now kept as the borewell's first reading, dated with the borewell's date, so the + change in water level over time can be seen. Borewells already saved are updated the first time + this version opens the database. - **A title such as "STRETA CHART OF TUBE WELL" above the chart no longer stops a log from being read.** Such logs needed their columns chosen by hand before. diff --git a/Cargo.lock b/Cargo.lock index 32d646e..eb192cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3180,9 +3180,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "once_cell", "ring", diff --git a/core/db/src/repo/layers.rs b/core/db/src/repo/layers.rs index a532439..6a3769e 100644 --- a/core/db/src/repo/layers.rs +++ b/core/db/src/repo/layers.rs @@ -231,6 +231,9 @@ pub fn water_for(conn: &Connection, borewell_id: &str) -> Result>>()?) } +/// Source of a reading made from the water level written on the borewell itself. +const DRILLING_READING_SOURCE: &str = "Water level recorded with the borewell"; + /// Adds a dated water reading. The borewell's current water level follows its newest reading. pub fn add_water_reading( conn: &Connection, @@ -253,6 +256,15 @@ pub fn add_water_reading( { return Err(DbError::Invalid("Water level cannot be negative.".into())); } + // The level written on the borewell itself is its first reading; keep it before a later + // reading takes its place as the current level. Migration 5 did this for older borewells. + conn.execute( + "INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source) + SELECT ?2, id, date, water_level, dynamic_water_level, ?3 FROM borewells + WHERE id = ?1 AND water_level IS NOT NULL AND trim(date) <> '' AND trim(date) <> ?4 + AND NOT EXISTS (SELECT 1 FROM water_readings WHERE borewell_id = ?1)", + params![borewell_id, new_id(), DRILLING_READING_SOURCE, reading.measured_on.trim()], + )?; let id = new_id(); conn.execute( "INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source, remarks) diff --git a/core/db/src/schema.rs b/core/db/src/schema.rs index fa53327..a6c7acc 100644 --- a/core/db/src/schema.rs +++ b/core/db/src/schema.rs @@ -199,6 +199,15 @@ pub const MIGRATIONS: &[&str] = &[ ALTER TABLE borewells ADD COLUMN column_pipe_dia REAL; ALTER TABLE borewells ADD COLUMN column_pipe_material TEXT NOT NULL DEFAULT ''; "#, + // 5: the water level written on a borewell becomes its first reading, so a later reading + // no longer replaces it. Only for borewells with no readings yet (see add_water_reading). + r#" + INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source) + SELECT lower(hex(randomblob(16))), id, date, water_level, dynamic_water_level, 'Water level recorded with the borewell' + FROM borewells b + WHERE water_level IS NOT NULL AND trim(date) <> '' + AND NOT EXISTS (SELECT 1 FROM water_readings w WHERE w.borewell_id = b.id); + "#, ]; /// The schema version this build understands. diff --git a/core/db/tests/database.rs b/core/db/tests/database.rs index 9573f5e..a11e4dc 100644 --- a/core/db/tests/database.rs +++ b/core/db/tests/database.rs @@ -488,12 +488,12 @@ fn pipes_and_water_readings() { static_level: Some(s), ..Default::default() }; - db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-03-01", 104.0))) + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-09-01", 104.0))) .unwrap(); let newest = db - .with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-06-01", 118.0))) + .with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-10-01", 118.0))) .unwrap(); - db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2025-11-01", 99.0))) + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-08-20", 99.0))) .unwrap(); assert_eq!( db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, @@ -507,7 +507,115 @@ fn pipes_and_water_readings() { db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, Some(104.0) ); - assert_eq!(db.with(|c| layers::water_for(c, &b.id)).unwrap().len(), 2); + assert_eq!( + db.with(|c| layers::water_for(c, &b.id)).unwrap().len(), + 3, + "the level recorded with the borewell is kept as a reading" + ); +} + +fn levels(db: &strata_db::Database, id: &str) -> Vec<(String, Option)> { + let mut v: Vec<_> = db + .with(|c| layers::water_for(c, id)) + .unwrap() + .into_iter() + .map(|r| (r.measured_on, r.static_level)) + .collect(); + v.sort_by(|a, b| a.0.cmp(&b.0)); + v +} + +#[test] +fn the_first_later_reading_keeps_the_level_recorded_with_the_borewell() { + let (_dir, db) = open_temp(); + let b = db + .with_tx(|tx| borewells::create(tx, &input("BW-W1"))) + .unwrap(); + let later = WaterReading { + measured_on: "2027-08-01".into(), + static_level: Some(125.0), + ..Default::default() + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &later)) + .unwrap(); + assert_eq!( + levels(&db, &b.id), + vec![ + ("2026-08-08".into(), Some(110.0)), + ("2027-08-01".into(), Some(125.0)) + ] + ); + assert_eq!( + db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, + Some(125.0) + ); + + // A second reading does not copy the borewell's (now current) level again. + let next = WaterReading { + measured_on: "2028-08-01".into(), + ..later + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &next)) + .unwrap(); + assert_eq!(levels(&db, &b.id).len(), 3); +} + +#[test] +fn a_reading_on_the_borewells_own_date_is_not_doubled() { + let (_dir, db) = open_temp(); + let b = db + .with_tx(|tx| borewells::create(tx, &input("BW-W2"))) + .unwrap(); + let same_day = WaterReading { + measured_on: "2026-08-08".into(), + static_level: Some(112.0), + ..Default::default() + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &same_day)) + .unwrap(); + assert_eq!(levels(&db, &b.id), vec![("2026-08-08".into(), Some(112.0))]); +} + +#[test] +fn upgrading_turns_a_borewells_own_water_level_into_its_first_reading() { + let dir = tempfile::tempdir().unwrap(); + let (plain, with_reading, no_level) = { + let (db, _) = Database::open(dir.path()).unwrap(); + let plain = db + .with_tx(|tx| borewells::create(tx, &input("BW-M1"))) + .unwrap(); + let with_reading = db + .with_tx(|tx| borewells::create(tx, &input("BW-M2"))) + .unwrap(); + let mut dry = input("BW-M3"); + dry.water_level = None; + let no_level = db.with_tx(|tx| borewells::create(tx, &dry)).unwrap(); + db.with(|c| { + c.execute( + "INSERT INTO water_readings (id, borewell_id, measured_on, static_level) VALUES ('w1', ?1, '2027-01-01', 120)", + [&with_reading.id], + )?; + Ok(c.execute_batch("PRAGMA user_version = 4")?) + }) + .unwrap(); + (plain.id, with_reading.id, no_level.id) + }; + let (db, report) = Database::open(dir.path()).unwrap(); + assert_eq!(report.previous_version, 4); + assert_eq!( + levels(&db, &plain), + vec![("2026-08-08".into(), Some(110.0))] + ); + assert_eq!( + levels(&db, &with_reading), + vec![("2027-01-01".into(), Some(120.0))] + ); + assert!(levels(&db, &no_level).is_empty()); + + // Running the same step again adds nothing. + db.with(|c| Ok(c.execute_batch(schema::MIGRATIONS[4])?)) + .unwrap(); + assert_eq!(levels(&db, &plain).len(), 1); } #[test] diff --git a/e2e/run.mjs b/e2e/run.mjs index ad3a78f..394a98a 100644 --- a/e2e/run.mjs +++ b/e2e/run.mjs @@ -357,9 +357,12 @@ try { check("Files attach", attached.files.includes("site-notes.pdf"), attached.files.join(", ")); const water = await page(` - await __t.invoke('water_reading_add', { borewellId: '${manual.bwid}', reading: { measuredOn: '2026-09-29', staticLevel: 47 } }); - return (await __t.invoke('borewell_get', { id: '${manual.bwid}' })).borewell.waterLevel;`); - check("Water readings over time: the latest sets the water level", water === 47, String(water)); + // The day after the borewell's own date (today), so it is the newest reading. + const later = new Date(Date.now() + 86400000).toISOString().slice(0, 10); + await __t.invoke('water_reading_add', { borewellId: '${manual.bwid}', reading: { measuredOn: later, staticLevel: 47 } }); + const r = await __t.invoke('borewell_get', { id: '${manual.bwid}' }); + return { level: r.borewell.waterLevel, readings: r.waterReadings.map(w => w.staticLevel).sort((a, b) => a - b).join(',') };`); + check("Water readings over time: the latest sets the water level, the first level is kept", water.level === 47 && water.readings === "45,47", JSON.stringify(water)); // ── Finding borewells ────────────────────────────────────────────────── const search = await page(` @@ -405,7 +408,7 @@ try { __t.btn('SPOT-2016', popup).click(); const card = await __t.until(() => document.querySelector('[role="dialog"][aria-label="SPOT-2016 details"]'), 5000).catch(() => null); // Which readings count: the last 3 years by default (the 2016 one is left out), or all of them. - const since = () => /going by readings since 2023/.test(__t.text()); + const since = () => /going by readings since \\d{4}/.test(__t.text()); const byDefault = since(); await __t.choose(document.querySelector('[aria-label="Which water readings to use"]'), 'All readings'); const all = !since();