phase 2 ready for qa review

This commit is contained in:
2026-08-16 23:53:30 +01:00
parent bd63b7e788
commit 7a64f00014
+33 -34
View File
@@ -119,33 +119,30 @@ pub fn update_lookups(conn: &Connection, lookup_maps: &LookupMap) -> Result<()>
/// Upserts landing records using delete-then-insert per month.
///
/// Uses `unchecked_transaction` (takes `&self`) per DuckDB docs, and the
/// Appender API for bulk inserts as recommended by the official
/// DuckDB Rust documentation.
///
/// All rows belonging to the same month(s) are deleted first, then
/// re-inserted within a single transaction. This ensures that revised
/// data from the API replaces stale records atomically.
pub fn upsert_landings(conn: &mut Connection, rows: &[Landing]) -> Result<usize> {
pub fn upsert_landings(conn: &Connection, rows: &[Landing]) -> Result<usize> {
if rows.is_empty() {
return Ok(0);
}
let months: HashSet<&str> = rows.iter().map(|r| r.month.as_str()).collect();
let tx = conn.transaction()?;
let tx = conn.unchecked_transaction()?;
for month in &months {
tx.execute("DELETE FROM landings WHERE month = ?", params![month])?;
}
{
let mut stmt = tx.prepare(
"INSERT INTO landings (
month, species_code, species_label, gear_code, zone_code,
processing_code, preservation_code, shipsize_code,
measure_code, value
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)?;
let mut app = tx.appender("landings")?;
for row in rows {
stmt.execute(params![
app.append_row(params![
row.month,
row.species_code,
row.species_label,
@@ -158,6 +155,8 @@ pub fn upsert_landings(conn: &mut Connection, rows: &[Landing]) -> Result<usize>
row.value,
])?;
}
// Flush explicitly — Drop discards errors per DuckDB docs.
app.flush()?;
}
tx.commit()?;
@@ -288,10 +287,10 @@ mod tests {
#[test]
fn test_upsert_insert_and_idempotency() {
let mut conn = test_conn();
let conn = test_conn();
let rows = sample_landings();
let inserted = upsert_landings(&mut conn, &rows).unwrap();
let inserted = upsert_landings(&conn, &rows).unwrap();
assert_eq!(inserted, 3);
let count: i64 = conn
@@ -299,7 +298,7 @@ mod tests {
.unwrap();
assert_eq!(count, 3);
let inserted = upsert_landings(&mut conn, &rows).unwrap();
let inserted = upsert_landings(&conn, &rows).unwrap();
assert_eq!(inserted, 3);
let count: i64 = conn
@@ -310,8 +309,8 @@ mod tests {
#[test]
fn test_upsert_replaces_month_data() {
let mut conn = test_conn();
upsert_landings(&mut conn, &sample_landings()).unwrap();
let conn = test_conn();
upsert_landings(&conn, &sample_landings()).unwrap();
let modified = vec![Landing {
month: "2024M01".to_string(),
@@ -325,7 +324,7 @@ mod tests {
measure_code: "MASS".to_string(),
value: Some(9999.9),
}];
upsert_landings(&mut conn, &modified).unwrap();
upsert_landings(&conn, &modified).unwrap();
// 2024M01 had 2 rows, now has 1. 2024M02 untouched.
let count: i64 = conn
@@ -346,16 +345,16 @@ mod tests {
#[test]
fn test_upsert_empty_input() {
let mut conn = test_conn();
let result = upsert_landings(&mut conn, &[]);
let conn = test_conn();
let result = upsert_landings(&conn, &[]);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_upsert_multi_month_atomic() {
let mut conn = test_conn();
upsert_landings(&mut conn, &sample_landings()).unwrap();
let conn = test_conn();
upsert_landings(&conn, &sample_landings()).unwrap();
// Upsert both months in one call — both should be replaced.
let rows = vec![
@@ -384,7 +383,7 @@ mod tests {
value: Some(222.0),
},
];
upsert_landings(&mut conn, &rows).unwrap();
upsert_landings(&conn, &rows).unwrap();
let jan_val: f64 = conn
.query_row(
@@ -415,7 +414,7 @@ mod tests {
#[test]
fn test_null_preserved_on_insert() {
let mut conn = test_conn();
let conn = test_conn();
let rows = vec![Landing {
month: "2024M01".to_string(),
species_code: "COD".to_string(),
@@ -428,7 +427,7 @@ mod tests {
measure_code: "VALUE".to_string(),
value: None,
}];
upsert_landings(&mut conn, &rows).unwrap();
upsert_landings(&conn, &rows).unwrap();
let val: Option<f64> = conn
.query_row(
@@ -442,8 +441,8 @@ mod tests {
#[test]
fn test_null_preserved_after_upsert() {
let mut conn = test_conn();
upsert_landings(&mut conn, &sample_landings()).unwrap();
let conn = test_conn();
upsert_landings(&conn, &sample_landings()).unwrap();
let val: Option<f64> = conn
.query_row(
@@ -467,8 +466,8 @@ mod tests {
#[test]
fn test_get_last_month_populated() {
let mut conn = test_conn();
upsert_landings(&mut conn, &sample_landings()).unwrap();
let conn = test_conn();
upsert_landings(&conn, &sample_landings()).unwrap();
let last = get_last_month(&conn).unwrap();
assert_eq!(last.as_deref(), Some("2024M02"));
@@ -476,7 +475,7 @@ mod tests {
#[test]
fn test_get_last_month_single_month() {
let mut conn = test_conn();
let conn = test_conn();
let rows = vec![Landing {
month: "2015M03".to_string(),
species_code: "COD".to_string(),
@@ -489,7 +488,7 @@ mod tests {
measure_code: "MASS".to_string(),
value: Some(100.0),
}];
upsert_landings(&mut conn, &rows).unwrap();
upsert_landings(&conn, &rows).unwrap();
let last = get_last_month(&conn).unwrap();
assert_eq!(last.as_deref(), Some("2015M03"));
@@ -587,8 +586,8 @@ mod tests {
#[test]
fn test_export_parquet() {
let mut conn = test_conn();
upsert_landings(&mut conn, &sample_landings()).unwrap();
let conn = test_conn();
upsert_landings(&conn, &sample_landings()).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap().to_string() + ".parquet";
@@ -610,7 +609,7 @@ mod tests {
#[test]
fn test_faroese_labels_round_trip() {
let mut conn = test_conn();
let conn = test_conn();
let rows = vec![Landing {
month: "2024M01".to_string(),
@@ -624,7 +623,7 @@ mod tests {
measure_code: "MASS".to_string(),
value: Some(100.0),
}];
upsert_landings(&mut conn, &rows).unwrap();
upsert_landings(&conn, &rows).unwrap();
let label: String = conn
.query_row("SELECT species_label FROM landings LIMIT 1", [], |row| {