use super::PostgresConnection;
use crate::nodes::{FutStream, RunParams, StreamOperators};
use crate::types::*;
use anyhow::Context;
use chrono::NaiveDateTime;
use futures::StreamExt;
use std::pin::Pin;
use std::rc::Rc;
use tokio_postgres::NoTls;
use tokio_postgres::types::ToSql;
pub trait PostgresSerialize {
fn to_params(&self) -> Vec<Box<dyn ToSql + Sync + Send>>;
}
#[must_use]
pub fn postgres_write<T>(
connection: impl Into<PostgresConnection>,
table_name: impl Into<String>,
upstream: &Rc<dyn Stream<Burst<T>>>,
) -> Rc<dyn Node>
where
T: Element + Send + PostgresSerialize + 'static,
{
let connection = connection.into();
let table_name = table_name.into();
let consumer = Box::new(
move |_ctx: RunParams, source: Pin<Box<dyn FutStream<Burst<T>>>>| {
postgres_write_consumer(connection, table_name, source)
},
);
upstream.consume_async(consumer)
}
async fn postgres_write_consumer<T>(
connection: PostgresConnection,
table_name: String,
mut source: Pin<Box<dyn FutStream<Burst<T>>>>,
) -> anyhow::Result<()>
where
T: Element + Send + PostgresSerialize + 'static,
{
let (client, conn) = tokio_postgres::connect(&connection.conn_str, NoTls)
.await
.with_context(|| {
format!(
"postgres_write: failed to connect: {}",
connection.redacted()
)
})?;
tokio::spawn(async move {
if let Err(e) = conn.await {
log::error!("postgres connection error: {e}");
}
});
let table_sql = super::quote_table(&table_name);
let mut prepared: Option<tokio_postgres::Statement> = None;
while let Some((time, batch)) = source.next().await {
if batch.is_empty() {
continue;
}
let ts: NaiveDateTime = time.into();
let rows: Vec<Vec<Box<dyn ToSql + Sync + Send>>> =
batch.iter().map(|record| record.to_params()).collect();
let n = rows[0].len() + 1;
let stmt = match &prepared {
Some(s) => s,
None => {
let placeholders = (1..=n)
.map(|i| format!("${i}"))
.collect::<Vec<_>>()
.join(", ");
let sql = format!("INSERT INTO {table_sql} VALUES ({placeholders})");
let s = client
.prepare(&sql)
.await
.with_context(|| format!("postgres_write: failed to prepare `{sql}`"))?;
prepared.insert(s)
}
};
let client = &client;
let inserts = rows.iter().map(|values| {
let mut params: Vec<&(dyn ToSql + Sync)> = Vec::with_capacity(n);
params.push(&ts);
for value in values {
params.push(value.as_ref());
}
async move { client.execute(stmt, ¶ms).await }
});
futures::future::try_join_all(inserts)
.await
.with_context(|| format!("postgres_write: insert into `{table_name}` failed"))?;
}
Ok(())
}
pub trait PostgresWriteOperators<T: Element> {
#[must_use]
fn postgres_write(
self: &Rc<Self>,
conn: impl Into<PostgresConnection>,
table: &str,
) -> Rc<dyn Node>;
}
impl<T: Element + Send + PostgresSerialize + 'static> PostgresWriteOperators<T>
for dyn Stream<Burst<T>>
{
fn postgres_write(
self: &Rc<Self>,
conn: impl Into<PostgresConnection>,
table: &str,
) -> Rc<dyn Node> {
postgres_write(conn, table, self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::burst;
use crate::nodes::constant;
#[derive(Debug, Clone, Default)]
struct TestTrade {
sym: String,
price: f64,
qty: i64,
}
impl PostgresSerialize for TestTrade {
fn to_params(&self) -> Vec<Box<dyn ToSql + Sync + Send>> {
vec![
Box::new(self.sym.clone()),
Box::new(self.price),
Box::new(self.qty),
]
}
}
#[test]
fn test_postgres_write_node_creation() {
let stream = constant(burst![TestTrade {
sym: "TEST".to_string(),
price: 100.0,
qty: 1,
}]);
let _node = postgres_write("host=localhost dbname=db", "trades", &stream);
}
#[test]
fn test_to_params_len() {
let trade = TestTrade {
sym: "AAPL".into(),
price: 1.0,
qty: 2,
};
assert_eq!(trade.to_params().len(), 3);
}
}