Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions core/db/src/repo/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ pub fn water_for(conn: &Connection, borewell_id: &str) -> Result<Vec<WaterReadin
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}

/// 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,
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions core/db/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
116 changes: 112 additions & 4 deletions core/db/tests/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<f64>)> {
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]
Expand Down
11 changes: 7 additions & 4 deletions e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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();
Expand Down
Loading