From 2d67cf76113afbaec941027da81ab284c058f509 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 19 Apr 2026 16:26:35 -0500 Subject: [PATCH] fix(grid-rs): decode run_time / valid_time as NaiveDateTime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elixir's Ecto :utc_datetime maps to Postgres TIMESTAMP WITHOUT TIME ZONE — Ecto's contract is that the value is UTC, but the column itself is naive. sqlx's DateTime binds/reads as TIMESTAMPTZ, so the read failed with "mismatched types ... TIMESTAMPTZ is not compatible with SQL type TIMESTAMP". Read as NaiveDateTime and reattach UTC at the decode boundary, keeping the downstream DateTime signature. Test bind goes the other direction — bind .naive_utc() against the TIMESTAMP column. --- rust/prop_grid_rs/src/db.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rust/prop_grid_rs/src/db.rs b/rust/prop_grid_rs/src/db.rs index 633441fd..c55bc8fc 100644 --- a/rust/prop_grid_rs/src/db.rs +++ b/rust/prop_grid_rs/src/db.rs @@ -7,7 +7,7 @@ //! * On success: status=done, NOTIFY propagation_ready ''. //! * On failure: status=failed, error=. An hourly cron reseeds. -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use sqlx::postgres::{PgNotification, PgPool, PgPoolOptions}; use sqlx::Row; use std::time::Duration; @@ -63,10 +63,16 @@ pub async fn claim_next(pool: &PgPool) -> Result, DbError> { }; let id: Uuid = row.try_get("id")?; - let run_time: DateTime = row.try_get("run_time")?; + // Elixir writes run_time/valid_time via Ecto `:utc_datetime`, which + // lands in Postgres as TIMESTAMP WITHOUT TIME ZONE (values are UTC + // by Ecto contract). Read as NaiveDateTime and reattach UTC so the + // rest of the worker can keep its DateTime signature. + let run_time_naive: NaiveDateTime = row.try_get("run_time")?; + let run_time = DateTime::::from_naive_utc_and_offset(run_time_naive, Utc); let forecast_hour: i32 = row.try_get("forecast_hour")?; let forecast_hour = forecast_hour as i16; - let valid_time: DateTime = row.try_get("valid_time")?; + let valid_time_naive: NaiveDateTime = row.try_get("valid_time")?; + let valid_time = DateTime::::from_naive_utc_and_offset(valid_time_naive, Utc); let attempt: i32 = row.try_get("attempt")?; sqlx::query( @@ -177,7 +183,8 @@ mod tests { }; let pool = connect(&url, 4).await.unwrap(); - // Seed a unique fh>0 row. + // Seed a unique fh>0 row. run_time/valid_time bind as + // NaiveDateTime to match Ecto's :utc_datetime column type. let run_time = Utc::now(); let fh: i16 = 7; let valid_time = run_time + chrono::Duration::hours(fh as i64); @@ -187,9 +194,9 @@ mod tests { VALUES ($1, $2, $3, $4, 'queued', 0, NOW(), NOW())"#, ) .bind(id) - .bind(run_time) + .bind(run_time.naive_utc()) .bind(fh as i32) - .bind(valid_time) + .bind(valid_time.naive_utc()) .execute(&pool) .await .unwrap();