use rupg_common::{Error, Result, SqlState};
use rupg_sql::nodes::Node;
use super::view::{def_string, parse_bool};
#[derive(Clone, Copy)]
pub(super) enum Kind {
Bool,
Int(i32, i32),
Real(f64, f64),
Enum(&'static [&'static str], &'static str),
}
pub(super) struct Opt {
pub(super) name: &'static str,
pub(super) kind: Kind,
pub(super) toast: bool,
}
pub(super) const ATTRIBUTE: &[Opt] = &[
Opt { name: "n_distinct", kind: Kind::Real(-1.0, f64::MAX), toast: false },
Opt { name: "n_distinct_inherited", kind: Kind::Real(-1.0, f64::MAX), toast: false },
];
const INDEX_CLEANUP: Kind = Kind::Enum(
&["auto", "on", "off", "true", "false", "yes", "no", "1", "0"],
"Valid values are \"on\", \"off\", and \"auto\".",
);
pub(super) const HEAP: &[Opt] = &[
Opt { name: "fillfactor", kind: Kind::Int(10, 100), toast: false },
Opt { name: "autovacuum_enabled", kind: Kind::Bool, toast: true },
Opt { name: "autovacuum_vacuum_threshold", kind: Kind::Int(0, i32::MAX), toast: true },
Opt { name: "autovacuum_vacuum_max_threshold", kind: Kind::Int(-1, i32::MAX), toast: true },
Opt { name: "autovacuum_vacuum_insert_threshold", kind: Kind::Int(-1, i32::MAX), toast: true },
Opt { name: "autovacuum_analyze_threshold", kind: Kind::Int(0, i32::MAX), toast: false },
Opt { name: "autovacuum_vacuum_cost_limit", kind: Kind::Int(1, 10000), toast: true },
Opt { name: "autovacuum_freeze_min_age", kind: Kind::Int(0, 1_000_000_000), toast: true },
Opt { name: "autovacuum_freeze_max_age", kind: Kind::Int(100_000, 2_000_000_000), toast: true },
Opt { name: "autovacuum_freeze_table_age", kind: Kind::Int(0, 2_000_000_000), toast: true },
Opt {
name: "autovacuum_multixact_freeze_min_age",
kind: Kind::Int(0, 1_000_000_000),
toast: true,
},
Opt {
name: "autovacuum_multixact_freeze_max_age",
kind: Kind::Int(10000, 2_000_000_000),
toast: true,
},
Opt {
name: "autovacuum_multixact_freeze_table_age",
kind: Kind::Int(0, 2_000_000_000),
toast: true,
},
Opt { name: "log_autovacuum_min_duration", kind: Kind::Int(-1, i32::MAX), toast: true },
Opt { name: "log_autoanalyze_min_duration", kind: Kind::Int(-1, i32::MAX), toast: false },
Opt { name: "toast_tuple_target", kind: Kind::Int(128, 8160), toast: false },
Opt { name: "autovacuum_vacuum_cost_delay", kind: Kind::Real(0.0, 100.0), toast: true },
Opt { name: "autovacuum_vacuum_scale_factor", kind: Kind::Real(0.0, 100.0), toast: true },
Opt {
name: "autovacuum_vacuum_insert_scale_factor",
kind: Kind::Real(0.0, 100.0),
toast: true,
},
Opt { name: "autovacuum_analyze_scale_factor", kind: Kind::Real(0.0, 100.0), toast: false },
Opt { name: "user_catalog_table", kind: Kind::Bool, toast: false },
Opt { name: "parallel_workers", kind: Kind::Int(0, 1024), toast: false },
Opt { name: "vacuum_index_cleanup", kind: INDEX_CLEANUP, toast: true },
Opt { name: "vacuum_truncate", kind: Kind::Bool, toast: true },
Opt { name: "vacuum_max_eager_freeze_failure_rate", kind: Kind::Real(0.0, 1.0), toast: true },
];
pub(super) const VIEW: &[Opt] = &[
Opt { name: "security_barrier", kind: Kind::Bool, toast: false },
Opt { name: "security_invoker", kind: Kind::Bool, toast: false },
Opt {
name: "check_option",
kind: Kind::Enum(&["local", "cascaded"], "Valid values are \"local\" and \"cascaded\"."),
toast: false,
},
];
pub(super) const BTREE: &[Opt] = &[
Opt { name: "fillfactor", kind: Kind::Int(10, 100), toast: false },
Opt { name: "deduplicate_items", kind: Kind::Bool, toast: false },
];
pub(super) const HASH: &[Opt] =
&[Opt { name: "fillfactor", kind: Kind::Int(10, 100), toast: false }];
const TABLESPACE: &[Opt] = &[
Opt { name: "random_page_cost", kind: Kind::Real(0.0, f64::MAX), toast: false },
Opt { name: "seq_page_cost", kind: Kind::Real(0.0, f64::MAX), toast: false },
Opt { name: "effective_io_concurrency", kind: Kind::Int(0, 1000), toast: false },
Opt { name: "maintenance_io_concurrency", kind: Kind::Int(0, 1000), toast: false },
];
pub fn tablespace_options(defs: &[Option<Node>]) -> Result<Vec<String>> {
let options = transform(&[], defs, None, &[], false)?;
validate(&options, TABLESPACE, false)?;
Ok(options)
}
fn invalid(message: String) -> Error {
Error::new(SqlState::INVALID_PARAMETER_VALUE, message)
}
pub(super) fn transform(
old: &[String],
defs: &[Option<Node>],
namespace: Option<&str>,
valid: &[&str],
reset: bool,
) -> Result<Vec<String>> {
transform_with(old, defs, namespace, valid, reset, false)
}
pub(super) fn transform_create(
defs: &[Option<Node>],
namespace: Option<&str>,
valid: &[&str],
) -> Result<Vec<String>> {
transform_with(&[], defs, namespace, valid, false, true)
}
fn transform_with(
old: &[String],
defs: &[Option<Node>],
namespace: Option<&str>,
valid: &[&str],
reset: bool,
accept_oids_off: bool,
) -> Result<Vec<String>> {
let defs: Vec<_> = defs
.iter()
.flatten()
.filter_map(|n| match n {
Node::DefElem(def) => Some(def),
_ => None,
})
.collect();
let mut out: Vec<String> = old
.iter()
.filter(|text| {
let name = text.split_once('=').map_or(text.as_str(), |(name, _)| name);
!defs.iter().any(|def| {
def.defnamespace.as_deref() == namespace && def.defname.as_deref() == Some(name)
})
})
.cloned()
.collect();
for def in defs {
if reset {
if def.arg.is_some() {
return Err(Error::new(
SqlState::SYNTAX_ERROR,
"RESET must not include values for parameters",
));
}
continue;
}
if let Some(space) = def.defnamespace.as_deref()
&& !valid.contains(&space)
{
return Err(invalid(format!("unrecognized parameter namespace \"{space}\"")));
}
if def.defnamespace.as_deref() != namespace {
continue;
}
let name = def.defname.as_deref().unwrap_or_default();
if name.contains('=') {
return Err(invalid(format!("invalid option name \"{name}\": must not contain \"=\"")));
}
if accept_oids_off && def.defnamespace.is_none() && name == "oids" {
if def_boolean(name, def.arg.as_ref())? {
return Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
"tables declared WITH OIDS are not supported",
));
}
continue;
}
out.push(format!("{name}={}", def_string(def.arg.as_ref())));
}
Ok(out)
}
pub(super) fn def_boolean(name: &str, arg: Option<&Node>) -> Result<bool> {
let value = match arg {
None => return Ok(true),
Some(Node::Integer(0)) => Some(false),
Some(Node::Integer(1)) => Some(true),
Some(Node::Integer(_)) => None,
Some(other) => match def_string(Some(other)).to_ascii_lowercase().as_str() {
"true" | "on" => Some(true),
"false" | "off" => Some(false),
_ => None,
},
};
value.ok_or_else(|| {
Error::new(SqlState::SYNTAX_ERROR, format!("{name} requires a Boolean value"))
})
}
pub(super) fn validate(options: &[String], known: &[Opt], toast: bool) -> Result<()> {
let mut seen: Vec<&str> = Vec::new();
for text in options {
let (name, value) = text.split_once('=').unwrap_or((text, ""));
let Some(opt) = known.iter().find(|o| o.name == name && (o.toast || !toast)) else {
return Err(invalid(format!("unrecognized parameter \"{name}\"")));
};
if seen.contains(&name) {
return Err(invalid(format!("parameter \"{name}\" specified more than once")));
}
seen.push(name);
let out_of_bounds = |detail: String| {
invalid(format!("value {value} out of bounds for option \"{name}\""))
.with_detail(detail)
};
match opt.kind {
Kind::Bool => {
if parse_bool(value).is_none() {
return Err(invalid(format!(
"invalid value for boolean option \"{name}\": {value}"
)));
}
}
Kind::Int(min, max) => {
let Some(parsed) = parse_int(value) else {
return Err(invalid(format!(
"invalid value for integer option \"{name}\": {value}"
)));
};
if parsed < min || parsed > max {
return Err(out_of_bounds(format!(
"Valid values are between \"{min}\" and \"{max}\"."
)));
}
}
Kind::Real(min, max) => {
let Some(parsed) = parse_real(value) else {
return Err(invalid(format!(
"invalid value for floating point option \"{name}\": {value}"
)));
};
if parsed < min || parsed > max {
return Err(out_of_bounds(format!(
"Valid values are between \"{min:.6}\" and \"{max:.6}\"."
)));
}
}
Kind::Enum(words, detail) => {
if !words.iter().any(|w| w.eq_ignore_ascii_case(value)) {
return Err(invalid(format!(
"invalid value for enum option \"{name}\": {value}"
))
.with_detail(detail));
}
}
}
}
Ok(())
}
pub(super) fn int_option(options: &[String], name: &str) -> Option<i32> {
options.iter().find_map(|text| {
let (found, value) = text.split_once('=')?;
if found == name { parse_int(value) } else { None }
})
}
fn is_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c')
}
fn strtol(text: &str) -> Option<(i64, &str)> {
let s = text.trim_start_matches(is_space);
let (negative, s) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"));
let (radix, digits) = match hex {
Some(rest) if rest.starts_with(|c: char| c.is_ascii_hexdigit()) => (16, rest),
_ if s.starts_with('0') => (8, s),
_ => (10, s),
};
let end = digits.find(|c: char| !c.is_digit(radix)).unwrap_or(digits.len());
if end == 0 {
return None;
}
let magnitude = i128::from_str_radix(&digits[..end], radix).ok()?;
let value = i64::try_from(if negative { -magnitude } else { magnitude }).ok()?;
Some((value, &digits[end..]))
}
fn strtod(text: &str) -> Option<(f64, &str)> {
let s = text.trim_start_matches(is_space);
let body = s.strip_prefix(['-', '+']).unwrap_or(s);
let sign = s.len() - body.len();
for word in ["infinity", "inf", "nan"] {
if body.get(..word.len()).is_some_and(|w| w.eq_ignore_ascii_case(word)) {
let end = sign + word.len();
return Some((s[..end].parse().ok()?, &s[end..]));
}
}
let digits = |from: usize| {
body[from..].find(|c: char| !c.is_ascii_digit()).map_or(body.len(), |n| from + n)
};
let mut end = digits(0);
let whole = end;
if body[end..].starts_with('.') {
end = digits(end + 1);
}
if whole == 0 && end <= 1 {
return None;
}
let mantissa = &body[..end];
if body[end..].starts_with(['e', 'E']) {
let after_sign = end + 1 + usize::from(body[end + 1..].starts_with(['-', '+']));
let exponent_end = digits(after_sign);
if exponent_end > after_sign {
end = exponent_end;
}
}
let value: f64 = s[..sign + end].parse().ok()?;
let underflow = (value == 0.0 && mantissa.bytes().any(|b| matches!(b, b'1'..=b'9')))
|| (value != 0.0 && value.abs() < f64::MIN_POSITIVE);
if value.is_infinite() || underflow {
return None;
}
Some((value, &s[sign + end..]))
}
fn parse_int(text: &str) -> Option<i32> {
let (value, rest) = match strtol(text) {
Some((value, rest)) if !rest.starts_with(['.', 'e', 'E']) =>
{
#[allow(clippy::cast_precision_loss)]
(value as f64, rest)
}
_ => strtod(text)?,
};
if value.is_nan() || !rest.trim_start_matches(is_space).is_empty() {
return None;
}
let value = value.round_ties_even();
if value > f64::from(i32::MAX) || value < f64::from(i32::MIN) {
return None;
}
#[allow(clippy::cast_possible_truncation)]
Some(value as i32)
}
fn parse_real(text: &str) -> Option<f64> {
let (value, rest) = strtod(text)?;
(!value.is_nan() && rest.trim_start_matches(is_space).is_empty()).then_some(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integers() {
assert_eq!(parse_int(" 70 "), Some(70));
assert_eq!(parse_int("0x10"), Some(16));
assert_eq!(parse_int("-0x10"), Some(-16));
assert_eq!(parse_int("010"), Some(8));
assert_eq!(parse_int("7e1"), Some(70));
assert_eq!(parse_int("10.6e1"), Some(106));
assert_eq!(parse_int("70.5"), Some(70));
assert_eq!(parse_int("1.5"), Some(2));
assert_eq!(parse_int(".5"), Some(0));
assert_eq!(parse_int("2147483648"), None);
assert_eq!(parse_int("1e10"), None);
assert_eq!(parse_int("70abc"), None);
assert_eq!(parse_int("08"), None);
assert_eq!(parse_int(""), None);
assert_eq!(parse_int("true"), None);
assert_eq!(parse_int("inf"), None);
}
#[test]
fn reals() {
assert_eq!(parse_real(" 0.5 "), Some(0.5));
assert_eq!(parse_real("1e2"), Some(100.0));
assert_eq!(parse_real("-inf"), Some(f64::NEG_INFINITY));
assert_eq!(parse_real("1e400"), None);
assert_eq!(parse_real("1e-400"), None);
assert_eq!(parse_real("nan"), None);
assert_eq!(parse_real("1e"), None);
assert_eq!(parse_real("."), None);
}
}