use sqlx::{PgPool, Row};
use super::sql_bus_common::{
db_err as sql_db_err, metadata_json, ClaimedRow, ReceivedRow, SqlBus, SqlBusDialect,
SqlLogReceived, SqlQueueReceived,
};
use super::{Message, TransportError};
const SCHEMA: &str = "\
CREATE TABLE IF NOT EXISTS bus_queue (
seq BIGSERIAL PRIMARY KEY,
claim_token TEXT,
name TEXT NOT NULL,
message_id TEXT,
kind TEXT NOT NULL,
payload BYTEA NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/json',
metadata TEXT NOT NULL DEFAULT '[]',
available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
locked_until TIMESTAMPTZ,
attempts INTEGER NOT NULL DEFAULT 0,
CHECK (claim_token IS NULL OR claim_token <> ''),
CHECK (name <> ''),
CHECK (kind IN ('command', 'event')),
CHECK (content_type <> ''),
CHECK (attempts >= 0)
);
CREATE INDEX IF NOT EXISTS bus_queue_claim_idx
ON bus_queue (name, available_at, locked_until, seq);
CREATE TABLE IF NOT EXISTS bus_log (
seq BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
message_id TEXT,
kind TEXT NOT NULL,
payload BYTEA NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/json',
metadata TEXT NOT NULL DEFAULT '[]',
appended_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (name <> ''),
CHECK (kind IN ('command', 'event')),
CHECK (content_type <> '')
);
CREATE INDEX IF NOT EXISTS bus_log_name_seq_idx ON bus_log (name, seq);
CREATE TABLE IF NOT EXISTS bus_offset (
consumer TEXT PRIMARY KEY,
last_seq BIGINT NOT NULL DEFAULT 0,
CHECK (consumer <> ''),
CHECK (last_seq >= 0)
)";
fn db_err(context: &str, err: sqlx::Error) -> TransportError {
sql_db_err(PostgresBusDialect::BACKEND, context, err)
}
pub type PostgresBus = SqlBus<PostgresBusDialect>;
pub type QueueReceived = SqlQueueReceived<PostgresBusDialect>;
pub type LogReceived = SqlLogReceived<PostgresBusDialect>;
impl PostgresBus {
pub fn new(pool: PgPool) -> Self {
SqlBus::from_dialect(PostgresBusDialect { pool })
}
pub fn new_with_group(pool: PgPool, group: impl Into<String>) -> Self {
Self::new(pool).group(group)
}
}
#[derive(Clone)]
pub struct PostgresBusDialect {
pool: PgPool,
}
impl PostgresBusDialect {
async fn insert(
&self,
sql: &'static str,
context: &'static str,
message: &Message,
) -> Result<(), TransportError> {
let metadata = metadata_json(message);
sqlx::query(sql)
.bind(&message.name)
.bind(&message.id)
.bind(message.kind.as_str())
.bind(&message.payload)
.bind(&message.content_type)
.bind(metadata)
.execute(&self.pool)
.await
.map_err(|err| db_err(context, err))?;
Ok(())
}
}
impl SqlBusDialect for PostgresBusDialect {
const BACKEND: &'static str = "postgres";
const SCHEMA: &'static str = SCHEMA;
async fn execute_ddl(&self, statement: &'static str) -> Result<(), TransportError> {
sqlx::query(statement)
.execute(&self.pool)
.await
.map_err(|err| db_err("ensure_tables", err))?;
Ok(())
}
async fn insert_queue(&self, message: &Message) -> Result<(), TransportError> {
self.insert(
"INSERT INTO bus_queue (name, message_id, kind, payload, content_type, metadata) \
VALUES ($1, $2, $3, $4, $5, $6)",
"enqueue",
message,
)
.await
}
async fn insert_log(&self, message: &Message) -> Result<(), TransportError> {
self.insert(
"INSERT INTO bus_log (name, message_id, kind, payload, content_type, metadata) \
VALUES ($1, $2, $3, $4, $5, $6)",
"append",
message,
)
.await
}
async fn claim(
&self,
names: &[String],
lease_secs: f64,
limit: i64,
) -> Result<Vec<ClaimedRow>, TransportError> {
let rows = sqlx::query(
"UPDATE bus_queue SET locked_until = now() + ($1 * interval '1 second'), \
claim_token = gen_random_uuid()::text, \
attempts = attempts + 1 \
WHERE seq IN ( \
SELECT seq FROM bus_queue \
WHERE (name = ANY($2) OR name IS NULL) AND available_at <= now() \
AND (locked_until IS NULL OR locked_until <= now()) \
ORDER BY seq FOR UPDATE SKIP LOCKED LIMIT $3 \
) \
RETURNING seq, claim_token, name, message_id, kind, payload, content_type, metadata",
)
.bind(lease_secs)
.bind(names)
.bind(limit)
.fetch_all(&self.pool)
.await
.map_err(|err| db_err("claim", err))?;
rows.into_iter()
.map(|row| {
let claim_token = row
.try_get("claim_token")
.map_err(|err| db_err("claim token", err))?;
Ok(ClaimedRow {
row: ReceivedRow::from_row(Self::BACKEND, &row),
claim_token,
})
})
.collect()
}
async fn log_read(
&self,
names: &[String],
consumer: &str,
limit: i64,
) -> Result<Vec<ReceivedRow>, TransportError> {
let rows = sqlx::query(
"SELECT seq, name, message_id, kind, payload, content_type, metadata FROM bus_log \
WHERE (name = ANY($1) OR name IS NULL) \
AND seq > COALESCE((SELECT last_seq FROM bus_offset WHERE consumer = $2), 0) \
ORDER BY seq LIMIT $3",
)
.bind(names)
.bind(consumer)
.bind(limit)
.fetch_all(&self.pool)
.await
.map_err(|err| db_err("log read", err))?;
Ok(rows
.iter()
.map(|row| ReceivedRow::from_row(Self::BACKEND, row))
.collect())
}
async fn delete_claimed(&self, seq: i64, claim_token: &str) -> Result<(), TransportError> {
sqlx::query("DELETE FROM bus_queue WHERE seq = $1 AND claim_token = $2")
.bind(seq)
.bind(claim_token)
.execute(&self.pool)
.await
.map_err(|err| db_err("delete", err))?;
Ok(())
}
async fn release_claim(&self, seq: i64, claim_token: &str) -> Result<(), TransportError> {
sqlx::query(
"UPDATE bus_queue \
SET locked_until = NULL, claim_token = NULL \
WHERE seq = $1 AND claim_token = $2",
)
.bind(seq)
.bind(claim_token)
.execute(&self.pool)
.await
.map_err(|err| db_err("nack", err))?;
Ok(())
}
async fn advance_offset(&self, consumer: &str, seq: i64) -> Result<(), TransportError> {
sqlx::query(
"INSERT INTO bus_offset (consumer, last_seq) VALUES ($1, $2) \
ON CONFLICT (consumer) DO UPDATE SET last_seq = EXCLUDED.last_seq \
WHERE bus_offset.last_seq < EXCLUDED.last_seq",
)
.bind(consumer)
.bind(seq)
.execute(&self.pool)
.await
.map_err(|err| db_err("advance offset", err))?;
Ok(())
}
}