use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool};
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 INTEGER PRIMARY KEY AUTOINCREMENT,
claim_token TEXT,
name TEXT NOT NULL,
message_id TEXT,
kind TEXT NOT NULL,
payload BLOB NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/json',
metadata TEXT NOT NULL DEFAULT '[]',
available_at REAL NOT NULL DEFAULT (unixepoch('now','subsec')),
locked_until REAL,
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 INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
message_id TEXT,
kind TEXT NOT NULL,
payload BLOB NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/json',
metadata TEXT NOT NULL DEFAULT '[]',
appended_at REAL NOT NULL DEFAULT (unixepoch('now','subsec')),
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 INTEGER NOT NULL DEFAULT 0,
CHECK (consumer <> ''),
CHECK (last_seq >= 0)
)";
fn db_err(context: &str, err: sqlx::Error) -> TransportError {
sql_db_err(SqliteBusDialect::BACKEND, context, err)
}
fn push_name_filter(query: &mut QueryBuilder<Sqlite>, names: &[String]) {
query.push("(name IN (");
{
let mut separated = query.separated(", ");
for name in names {
separated.push_bind(name.as_str());
}
}
query.push(") OR name IS NULL)");
}
pub type SqliteBus = SqlBus<SqliteBusDialect>;
pub type SqliteQueueReceived = SqlQueueReceived<SqliteBusDialect>;
pub type SqliteLogReceived = SqlLogReceived<SqliteBusDialect>;
impl SqliteBus {
pub fn new(pool: SqlitePool) -> Self {
SqlBus::from_dialect(SqliteBusDialect { pool })
}
pub fn new_with_group(pool: SqlitePool, group: impl Into<String>) -> Self {
Self::new(pool).group(group)
}
}
#[derive(Clone)]
pub struct SqliteBusDialect {
pool: SqlitePool,
}
impl SqliteBusDialect {
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 SqliteBusDialect {
const BACKEND: &'static str = "sqlite";
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 (?, ?, ?, ?, ?, ?)",
"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 (?, ?, ?, ?, ?, ?)",
"append",
message,
)
.await
}
async fn claim(
&self,
names: &[String],
lease_secs: f64,
limit: i64,
) -> Result<Vec<ClaimedRow>, TransportError> {
let mut query = QueryBuilder::<Sqlite>::new(
"UPDATE bus_queue \
SET locked_until = unixepoch('now','subsec') + ",
);
query.push_bind(lease_secs);
query.push(
", claim_token = lower(hex(randomblob(16))), \
attempts = attempts + 1 \
WHERE seq IN ( \
SELECT seq FROM bus_queue \
WHERE ",
);
push_name_filter(&mut query, names);
query.push(
" AND available_at <= unixepoch('now','subsec') \
AND (locked_until IS NULL OR locked_until <= unixepoch('now','subsec')) \
ORDER BY seq LIMIT ",
);
query.push_bind(limit);
query.push(
") \
RETURNING seq, claim_token, name, message_id, kind, payload, content_type, metadata",
);
let rows = query
.build()
.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 mut query = QueryBuilder::<Sqlite>::new(
"SELECT seq, name, message_id, kind, payload, content_type, metadata \
FROM bus_log \
WHERE ",
);
push_name_filter(&mut query, names);
query.push(" AND seq > COALESCE((SELECT last_seq FROM bus_offset WHERE consumer = ");
query.push_bind(consumer);
query.push("), 0) ORDER BY seq LIMIT ");
query.push_bind(limit);
let rows = query
.build()
.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 = ? AND claim_token = ?")
.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 = ? AND claim_token = ?",
)
.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 (?, ?) \
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(())
}
}