fix(grid-rs): decode run_time / valid_time as NaiveDateTime

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<Utc> 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<Utc> signature. Test bind goes the other
direction — bind .naive_utc() against the TIMESTAMP column.
This commit is contained in:
Graham McIntire 2026-04-19 16:26:35 -05:00
parent a40e2d1630
commit 2d67cf7611
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -7,7 +7,7 @@
//! * On success: status=done, NOTIFY propagation_ready '<iso valid_time>'.
//! * On failure: status=failed, error=<string>. 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<Option<ClaimedTask>, DbError> {
};
let id: Uuid = row.try_get("id")?;
let run_time: DateTime<Utc> = 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<Utc> signature.
let run_time_naive: NaiveDateTime = row.try_get("run_time")?;
let run_time = DateTime::<Utc>::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<Utc> = row.try_get("valid_time")?;
let valid_time_naive: NaiveDateTime = row.try_get("valid_time")?;
let valid_time = DateTime::<Utc>::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();