use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NotNanF64(f64);
impl NotNanF64 {
pub const fn new(v: f64) -> Result<Self, f64> {
if v.is_nan() { Err(v) } else { Ok(Self(v)) }
}
#[must_use]
pub const fn get(self) -> f64 {
self.0
}
}
impl PartialEq for NotNanF64 {
fn eq(&self, other: &Self) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for NotNanF64 {}
impl Hash for NotNanF64 {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
impl PartialOrd for NotNanF64 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for NotNanF64 {
fn cmp(&self, other: &Self) -> Ordering {
self.0.partial_cmp(&other.0).unwrap_or(Ordering::Equal)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct AutovacuumOptions {
pub enabled: Option<bool>,
pub vacuum_threshold: Option<u64>,
pub vacuum_scale_factor: Option<NotNanF64>,
pub vacuum_cost_delay: Option<u64>,
pub vacuum_cost_limit: Option<u64>,
pub analyze_threshold: Option<u64>,
pub analyze_scale_factor: Option<NotNanF64>,
pub freeze_max_age: Option<u64>,
pub freeze_min_age: Option<u64>,
pub freeze_table_age: Option<u64>,
pub multixact_freeze_max_age: Option<u64>,
pub multixact_freeze_min_age: Option<u64>,
pub multixact_freeze_table_age: Option<u64>,
pub vacuum_insert_threshold: Option<u64>,
pub vacuum_insert_scale_factor: Option<NotNanF64>,
pub log_min_duration: Option<i64>,
}
impl AutovacuumOptions {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.enabled.is_none()
&& self.vacuum_threshold.is_none()
&& self.vacuum_scale_factor.is_none()
&& self.vacuum_cost_delay.is_none()
&& self.vacuum_cost_limit.is_none()
&& self.analyze_threshold.is_none()
&& self.analyze_scale_factor.is_none()
&& self.freeze_max_age.is_none()
&& self.freeze_min_age.is_none()
&& self.freeze_table_age.is_none()
&& self.multixact_freeze_max_age.is_none()
&& self.multixact_freeze_min_age.is_none()
&& self.multixact_freeze_table_age.is_none()
&& self.vacuum_insert_threshold.is_none()
&& self.vacuum_insert_scale_factor.is_none()
&& self.log_min_duration.is_none()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct TableStorageOptions {
pub fillfactor: Option<u32>,
pub autovacuum: AutovacuumOptions,
pub parallel_workers: Option<u32>,
pub toast_tuple_target: Option<u32>,
pub user_catalog_table: Option<bool>,
pub vacuum_truncate: Option<bool>,
pub extra: BTreeMap<String, String>,
}
impl TableStorageOptions {
#[must_use]
pub fn is_empty(&self) -> bool {
self.fillfactor.is_none()
&& self.autovacuum.is_empty()
&& self.parallel_workers.is_none()
&& self.toast_tuple_target.is_none()
&& self.user_catalog_table.is_none()
&& self.vacuum_truncate.is_none()
&& self.extra.is_empty()
}
}
pub type MaterializedViewStorageOptions = TableStorageOptions;
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct IndexStorageOptions {
pub fillfactor: Option<u32>,
pub fastupdate: Option<bool>,
pub gin_pending_list_limit: Option<u64>,
pub buffering: Option<BufferingMode>,
pub deduplicate_items: Option<bool>,
pub pages_per_range: Option<u32>,
pub autosummarize: Option<bool>,
pub extra: BTreeMap<String, String>,
}
impl IndexStorageOptions {
#[must_use]
pub fn is_empty(&self) -> bool {
self.fillfactor.is_none()
&& self.fastupdate.is_none()
&& self.gin_pending_list_limit.is_none()
&& self.buffering.is_none()
&& self.deduplicate_items.is_none()
&& self.pages_per_range.is_none()
&& self.autosummarize.is_none()
&& self.extra.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BufferingMode {
On,
Off,
Auto,
}
impl BufferingMode {
#[must_use]
pub const fn sql_keyword(self) -> &'static str {
match self {
Self::On => "on",
Self::Off => "off",
Self::Auto => "auto",
}
}
}
impl FromStr for BufferingMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"on" => Ok(Self::On),
"off" => Ok(Self::Off),
"auto" => Ok(Self::Auto),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_nan_rejects_nan() {
assert!(NotNanF64::new(f64::NAN).is_err());
assert!(NotNanF64::new(1.5).is_ok());
assert!(NotNanF64::new(0.0).is_ok());
assert!(NotNanF64::new(f64::INFINITY).is_ok());
}
#[test]
fn not_nan_equality_is_bit_exact() {
let a = NotNanF64::new(0.1).unwrap();
let b = NotNanF64::new(0.1).unwrap();
assert_eq!(a, b);
}
#[test]
fn default_storage_is_empty() {
assert!(TableStorageOptions::default().is_empty());
assert!(IndexStorageOptions::default().is_empty());
assert!(AutovacuumOptions::default().is_empty());
}
#[test]
fn non_empty_storage_detected() {
let s = TableStorageOptions {
fillfactor: Some(80),
..Default::default()
};
assert!(!s.is_empty());
}
#[test]
fn buffering_mode_roundtrips() {
for m in [BufferingMode::On, BufferingMode::Off, BufferingMode::Auto] {
assert_eq!(m.sql_keyword().parse::<BufferingMode>(), Ok(m));
}
assert!("bogus".parse::<BufferingMode>().is_err());
}
#[test]
fn extra_is_sorted_via_btreemap() {
let mut s = TableStorageOptions::default();
s.extra.insert("zebra".into(), "1".into());
s.extra.insert("alpha".into(), "2".into());
let keys: Vec<_> = s.extra.keys().cloned().collect();
assert_eq!(keys, vec!["alpha", "zebra"]);
}
}