mod job;
pub use job::PartitionMaintainerConfig;
pub(crate) use job::{PartitionMaintainerJobData, PartitionMaintainerJobInitializer};
use std::marker::PhantomData;
use crate::config::DEFAULT_PARTITION_WIDTH;
use crate::tables::MailboxTables;
const PARTITION_STORAGE_PARAMS: &str = "autovacuum_vacuum_insert_scale_factor = 0.0, \
autovacuum_vacuum_insert_threshold = 50000, \
autovacuum_freeze_min_age = 0, \
fillfactor = 100";
pub struct Partitions<Tables = crate::tables::DefaultMailboxTables> {
pool: sqlx::PgPool,
premake: u64,
_phantom: PhantomData<Tables>,
}
impl<Tables> Clone for Partitions<Tables> {
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
premake: self.premake,
_phantom: PhantomData,
}
}
}
impl<Tables> Partitions<Tables>
where
Tables: MailboxTables,
{
pub fn new(pool: &sqlx::PgPool, premake: u64) -> Self {
Self {
pool: pool.clone(),
premake,
_phantom: PhantomData,
}
}
async fn ddl_lock(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::Error> {
let table = Tables::persistent_outbox_events_table();
sqlx::query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))")
.bind(format!("obix:partition-ddl:{table}"))
.execute(&mut **tx)
.await?;
Ok(())
}
pub async fn ensure(&self) -> Result<(), sqlx::Error> {
let table = Tables::persistent_outbox_events_table();
let head = u64::from(Tables::highest_known_persistent_sequence(&self.pool).await?);
let first = head / DEFAULT_PARTITION_WIDTH;
let mut tx = self.pool.begin().await?;
self.ddl_lock(&mut tx).await?;
for k in first..=first + self.premake {
let lo = k * DEFAULT_PARTITION_WIDTH;
let hi = (k + 1) * DEFAULT_PARTITION_WIDTH;
let ddl = format!(
"CREATE TABLE IF NOT EXISTS {table}_p{k} PARTITION OF {table} \
FOR VALUES FROM ({lo}) TO ({hi}) WITH ({PARTITION_STORAGE_PARAMS})",
);
sqlx::query(&ddl).execute(&mut *tx).await?;
}
tx.commit().await
}
pub async fn recover_default(&self) -> Result<(), sqlx::Error> {
let table = Tables::persistent_outbox_events_table();
let default_child = format!("{table}_default");
let default_old = format!("{table}_default_old");
let bounds = sqlx::query(&format!(
"SELECT MIN(sequence) AS lo, MAX(sequence) AS hi FROM {default_child}"
))
.fetch_one(&self.pool)
.await?;
use sqlx::Row;
let (Some(min_seq), Some(max_seq)) = (
bounds.try_get::<Option<i64>, _>("lo")?,
bounds.try_get::<Option<i64>, _>("hi")?,
) else {
return Ok(());
};
let min_k = (min_seq as u64) / DEFAULT_PARTITION_WIDTH;
let max_k = (max_seq as u64) / DEFAULT_PARTITION_WIDTH + self.premake;
let mut tx = self.pool.begin().await?;
self.ddl_lock(&mut tx).await?;
sqlx::query(&format!(
"ALTER TABLE {table} DETACH PARTITION {default_child}"
))
.execute(&mut *tx)
.await?;
sqlx::query(&format!(
"ALTER TABLE {default_child} RENAME TO {table}_default_old"
))
.execute(&mut *tx)
.await?;
for k in min_k..=max_k {
let lo = k * DEFAULT_PARTITION_WIDTH;
let hi = (k + 1) * DEFAULT_PARTITION_WIDTH;
sqlx::query(&format!(
"CREATE TABLE IF NOT EXISTS {table}_p{k} PARTITION OF {table} \
FOR VALUES FROM ({lo}) TO ({hi}) WITH ({PARTITION_STORAGE_PARAMS})",
))
.execute(&mut *tx)
.await?;
}
sqlx::query(&format!(
"CREATE TABLE {table}_default PARTITION OF {table} DEFAULT"
))
.execute(&mut *tx)
.await?;
sqlx::query(&format!(
"WITH moved AS (DELETE FROM {default_old} RETURNING *) \
INSERT INTO {table} SELECT * FROM moved"
))
.execute(&mut *tx)
.await?;
sqlx::query(&format!("DROP TABLE {default_old}"))
.execute(&mut *tx)
.await?;
tx.commit().await
}
}