use super::{PostgresConnection, PostgresDeserialize, quote_ident, quote_table};
use crate::RunMode;
use crate::nodes::produce_async;
use crate::types::*;
use anyhow::Context;
use futures::StreamExt;
use futures::channel::mpsc;
use log::info;
use std::rc::Rc;
use tokio_postgres::{AsyncMessage, NoTls};
#[must_use]
pub fn postgres_notify_trigger_sql(table: &str, channel: &str) -> String {
let table_sql = quote_table(table);
let fn_ident = quote_ident(&format!("{channel}_notify_fn"));
let trg_ident = quote_ident(&format!("{channel}_notify_trg"));
let chan_literal = channel.replace('\'', "''");
format!(
"CREATE OR REPLACE FUNCTION {fn_ident}() RETURNS trigger LANGUAGE plpgsql AS $$\n\
BEGIN\n\
\x20 PERFORM pg_notify('{chan_literal}', '');\n\
\x20 RETURN NULL;\n\
END $$;\n\
DROP TRIGGER IF EXISTS {trg_ident} ON {table_sql};\n\
CREATE TRIGGER {trg_ident} AFTER INSERT ON {table_sql}\n\
FOR EACH STATEMENT EXECUTE FUNCTION {fn_ident}();"
)
}
#[must_use]
pub fn postgres_sub<T, F>(
connection: impl Into<PostgresConnection>,
channel: impl Into<String>,
start_from: NanoTime,
query_fn: F,
) -> Rc<dyn Stream<Burst<T>>>
where
T: Element + Send + PostgresDeserialize + 'static,
F: FnMut(NanoTime) -> String + Send + 'static,
{
let connection = connection.into();
let channel = channel.into();
produce_async(move |ctx| {
let run_mode = ctx.run_mode;
let connection = connection;
let channel = channel;
let mut query_fn = query_fn;
async move {
if !matches!(run_mode, RunMode::RealTime) {
anyhow::bail!(
"postgres_sub requires RunMode::RealTime; \
use postgres_read for historical replay"
);
}
let (client, mut conn) = tokio_postgres::connect(&connection.conn_str, NoTls)
.await
.with_context(|| {
format!("postgres_sub: failed to connect: {}", connection.conn_str)
})?;
let (tx, mut rx) = mpsc::unbounded::<()>();
tokio::spawn(async move {
let mut messages = futures::stream::poll_fn(move |cx| conn.poll_message(cx));
while let Some(message) = messages.next().await {
match message {
Ok(AsyncMessage::Notification(_)) => {
if tx.unbounded_send(()).is_err() {
break; }
}
Ok(_) => {}
Err(e) => {
log::error!("postgres_sub connection error: {e}");
break;
}
}
}
});
client
.batch_execute(&format!("LISTEN {}", quote_ident(&channel)))
.await
.with_context(|| format!("postgres_sub: LISTEN on channel `{channel}` failed"))?;
Ok(async_stream::stream! {
let mut cursor = start_from;
loop {
let query = query_fn(cursor);
info!("postgres_sub query: {query}");
let rows = match client.query(&query, &[]).await {
Ok(rows) => rows,
Err(e) => {
yield Err(anyhow::Error::new(e).context("postgres_sub query failed"));
break;
}
};
if !rows.is_empty() {
info!("postgres_sub: {} new rows", rows.len());
}
for row in &rows {
let (time, record) = match T::from_row(row) {
Ok(r) => r,
Err(e) => { yield Err(e); return; }
};
if time > cursor {
cursor = time;
}
yield Ok((time, record));
}
match rx.next().await {
Some(()) => {
while rx.try_recv().is_ok() {}
}
None => {
yield Err(anyhow::anyhow!(
"postgres_sub: connection to postgres closed"
));
break;
}
}
}
})
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nodes::{NodeOperators, StreamOperators};
use crate::{RunFor, RunMode};
#[derive(Debug, Clone, Default)]
struct TestRow;
impl PostgresDeserialize for TestRow {
fn from_row(_row: &tokio_postgres::Row) -> anyhow::Result<(NanoTime, Self)> {
Ok((NanoTime::ZERO, TestRow))
}
}
#[test]
fn test_sub_rejects_historical_mode() {
let result = postgres_sub::<TestRow, _>(
"host=127.0.0.1 port=1 user=postgres dbname=postgres connect_timeout=1",
"chan",
NanoTime::ZERO,
|_| String::new(),
)
.collapse()
.collect()
.run(
RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)),
RunFor::Cycles(1),
);
let err = result.expect_err("historical mode must be rejected");
assert!(
format!("{err:#}").contains("requires RunMode::RealTime"),
"unexpected error: {err:#}"
);
}
#[test]
fn test_notify_trigger_sql_shape() {
let sql = postgres_notify_trigger_sql("public.trades", "my_chan");
assert!(sql.contains("pg_notify('my_chan', '')"));
assert!(sql.contains("ON \"public\".\"trades\""));
assert!(sql.contains("CREATE TRIGGER \"my_chan_notify_trg\""));
assert!(sql.contains("FOR EACH STATEMENT"));
let sql = postgres_notify_trigger_sql("t", "we'ird");
assert!(sql.contains("pg_notify('we''ird', '')"));
}
}