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