use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerPolicy {
Suspended,
Live,
}
impl TriggerPolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Suspended => "suspended",
Self::Live => "live",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CostDirective {
pub rows: u64,
pub measured: Duration,
pub triggers: TriggerPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CostDirectiveError {
#[error("`@cost` must name {0}")]
Missing(&'static str),
#[error("`@cost` field `{0}` is not `key=value`")]
NotAPair(String),
#[error("`@cost` does not take `{0}`; it takes rows, measured and triggers")]
UnknownField(String),
#[error("`@cost` names `{0}` twice")]
Duplicate(&'static str),
#[error("`@cost` rows must be a count, got `{0}`")]
Rows(String),
#[error("`@cost` measured must be a duration like `250ms`, `2.0s` or `3m`, got `{0}`")]
Measured(String),
#[error("`@cost` triggers must be `suspended` or `live`, got `{0}`")]
Triggers(String),
#[error("`@cost` is declared {0} times; one migration states its cost once")]
Repeated(usize),
}
const DIRECTIVE: &str = "-- @cost:";
pub fn parse(sql: &str) -> Result<Option<CostDirective>, CostDirectiveError> {
let lines: Vec<&str> = sql
.lines()
.map(str::trim)
.take_while(|line| line.is_empty() || line.starts_with("--"))
.filter_map(|line| line.strip_prefix(DIRECTIVE))
.collect();
match lines.as_slice() {
[] => Ok(None),
[single] => parse_fields(single).map(Some),
many => Err(CostDirectiveError::Repeated(many.len())),
}
}
fn parse_fields(rest: &str) -> Result<CostDirective, CostDirectiveError> {
let mut rows: Option<u64> = None;
let mut measured: Option<Duration> = None;
let mut triggers: Option<TriggerPolicy> = None;
for field in rest.split_whitespace() {
let (key, value) = field
.split_once('=')
.ok_or_else(|| CostDirectiveError::NotAPair(field.to_owned()))?;
match key {
"rows" => {
if rows.is_some() {
return Err(CostDirectiveError::Duplicate("rows"));
}
rows = Some(
value
.parse()
.ok()
.ok_or_else(|| CostDirectiveError::Rows(value.to_owned()))?,
);
},
"measured" => {
if measured.is_some() {
return Err(CostDirectiveError::Duplicate("measured"));
}
measured = Some(parse_duration(value)?);
},
"triggers" => {
if triggers.is_some() {
return Err(CostDirectiveError::Duplicate("triggers"));
}
triggers = Some(match value {
"suspended" => TriggerPolicy::Suspended,
"live" => TriggerPolicy::Live,
other => return Err(CostDirectiveError::Triggers(other.to_owned())),
});
},
other => return Err(CostDirectiveError::UnknownField(other.to_owned())),
}
}
Ok(CostDirective {
rows: rows.ok_or(CostDirectiveError::Missing("rows"))?,
measured: measured.ok_or(CostDirectiveError::Missing("measured"))?,
triggers: triggers.ok_or(CostDirectiveError::Missing("triggers"))?,
})
}
fn parse_duration(value: &str) -> Result<Duration, CostDirectiveError> {
let malformed = || CostDirectiveError::Measured(value.to_owned());
let (number, multiplier) = if let Some(rest) = value.strip_suffix("ms") {
(rest, 1.0)
} else if let Some(rest) = value.strip_suffix('s') {
(rest, 1_000.0)
} else if let Some(rest) = value.strip_suffix('m') {
(rest, 60_000.0)
} else {
return Err(malformed());
};
let parsed: f64 = number.parse().ok().ok_or_else(malformed)?;
if !parsed.is_finite() || parsed < 0.0 {
return Err(malformed());
}
let millis = parsed * multiplier;
if millis > u64::MAX as f64 {
return Err(malformed());
}
Ok(Duration::from_millis(millis.round() as u64))
}