use super::PostgresConnection;
use crate::adapters::common::{TimeWindow, WindowFilter, compute_validated_time_slices};
use crate::nodes::produce_async;
use crate::types::*;
use anyhow::Context;
use chrono::NaiveDateTime;
use log::info;
use std::rc::Rc;
use tokio_postgres::{NoTls, Row};
pub trait PostgresDeserialize: Sized {
fn from_row(row: &Row) -> anyhow::Result<(NanoTime, Self)>;
}
pub trait PostgresRowExt {
fn get_nanotime(&self, idx: usize) -> anyhow::Result<NanoTime>;
fn get_nanotime_named(&self, name: &str) -> anyhow::Result<NanoTime>;
}
impl PostgresRowExt for Row {
fn get_nanotime(&self, idx: usize) -> anyhow::Result<NanoTime> {
if let Ok(dt) = self.try_get::<_, NaiveDateTime>(idx) {
return Ok(dt.into());
}
let dt: chrono::DateTime<chrono::Utc> = self
.try_get(idx)
.with_context(|| format!("column {idx} is not a `timestamp`/`timestamptz`"))?;
Ok(dt.naive_utc().into())
}
fn get_nanotime_named(&self, name: &str) -> anyhow::Result<NanoTime> {
if let Ok(dt) = self.try_get::<_, NaiveDateTime>(name) {
return Ok(dt.into());
}
let dt: chrono::DateTime<chrono::Utc> = self
.try_get(name)
.with_context(|| format!("column `{name}` is not a `timestamp`/`timestamptz`"))?;
Ok(dt.naive_utc().into())
}
}
#[must_use]
pub fn postgres_timestamp(time: NanoTime) -> String {
let dt: NaiveDateTime = time.into();
dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string()
}
#[must_use]
pub fn postgres_read<T>(
connection: impl Into<PostgresConnection>,
period: std::time::Duration,
query_fn: impl FnMut((NanoTime, NanoTime), i32, usize) -> String + Send + 'static,
) -> Rc<dyn Stream<Burst<T>>>
where
T: Element + Send + PostgresDeserialize + 'static,
{
let connection = connection.into();
produce_async(move |ctx| {
let start_time = ctx.start_time;
let end_time_result = ctx.end_time();
let connection = connection;
let mut query_fn = query_fn;
async move {
let end_time_bound = end_time_result.as_ref().ok().copied();
let slices = compute_validated_time_slices(
"postgres_read",
start_time,
end_time_result,
period,
)?;
let end_time =
end_time_bound.expect("compute_validated_time_slices accepted a bounded end_time");
let (client, conn) = tokio_postgres::connect(&connection.conn_str, NoTls)
.await
.with_context(|| {
format!("postgres_read: failed to connect: {}", connection.conn_str)
})?;
tokio::spawn(async move {
if let Err(e) = conn.await {
log::error!("postgres connection error: {e}");
}
});
Ok(async_stream::stream! {
let mut prev_time: Option<NanoTime> = None;
'outer: for ((t0, t1), date, iteration) in slices {
let query = query_fn((t0, t1), date, iteration);
info!("postgres query: {query}");
let fetch_start = std::time::Instant::now();
let rows = match client.query(&query, &[]).await {
Ok(rows) => rows,
Err(e) => {
yield Err(anyhow::Error::new(e).context("postgres query failed"));
break;
}
};
info!("postgres query: {} rows in {:?}", rows.len(), fetch_start.elapsed());
let mut filter = WindowFilter::new(
"postgres_read",
TimeWindow::clamp(t0, t1, start_time, end_time),
);
for row in &rows {
let (time, record) = match T::from_row(row) {
Ok(r) => r,
Err(e) => { yield Err(e); break 'outer; }
};
if !filter.keep(time) {
continue;
}
if let Some(prev) = prev_time
&& time < prev
{
yield Err(anyhow::anyhow!(
"postgres data is not sorted by time: got {time:?} after {prev:?}. \
Add `ORDER BY time` to your query."
));
break 'outer;
}
prev_time = Some(time);
yield Ok((time, record));
}
filter.finish();
}
})
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_postgres_timestamp_format() {
let t = NanoTime::from_kdb_timestamp(0);
assert_eq!(postgres_timestamp(t), "2000-01-01 00:00:00.000000");
let t = NanoTime::from_kdb_timestamp(1_000_000_500_000);
assert_eq!(postgres_timestamp(t), "2000-01-01 00:16:40.000500");
}
}