use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum QuantizationMode {
F32,
Int8,
Dual,
#[default]
Auto,
}
impl QuantizationMode {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"f32" | "full" | "exact" => Some(Self::F32),
"int8" | "sq8" | "quantized" => Some(Self::Int8),
"dual" | "hybrid" => Some(Self::Dual),
"auto" | "default" => Some(Self::Auto),
_ => None,
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::F32 => "f32",
Self::Int8 => "int8",
Self::Dual => "dual",
Self::Auto => "auto",
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct WithClause {
pub options: Vec<WithOption>,
}
impl WithClause {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_option(mut self, key: impl Into<String>, value: WithValue) -> Self {
self.options.push(WithOption {
key: key.into(),
value,
});
self
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&WithValue> {
self.options
.iter()
.find(|opt| opt.key.eq_ignore_ascii_case(key))
.map(|opt| &opt.value)
}
#[must_use]
pub fn get_mode(&self) -> Option<&str> {
self.mode_value().and_then(WithValue::as_str)
}
#[must_use]
pub fn mode_value(&self) -> Option<&WithValue> {
self.mode_values().next()
}
fn mode_values(&self) -> impl Iterator<Item = &WithValue> {
["mode", "quality"].into_iter().flat_map(move |key| {
self.options
.iter()
.filter(move |opt| opt.key.eq_ignore_ascii_case(key))
.map(|opt| &opt.value)
})
}
#[cfg(feature = "persistence")]
pub fn search_quality(&self) -> Result<Option<crate::SearchQuality>, String> {
let qualities = self
.mode_values()
.map(Self::parse_mode_value)
.collect::<Result<Vec<_>, _>>()?;
Ok(qualities.first().copied())
}
#[cfg(feature = "persistence")]
fn parse_mode_value(value: &WithValue) -> Result<crate::SearchQuality, String> {
let Some(mode) = value.as_str() else {
return Err(format!(
"Search mode must be a string. {}",
crate::api_types::SEARCH_MODE_FORMS
));
};
crate::api_types::parse_search_mode(mode)
}
#[must_use]
pub fn get_ef_search(&self) -> Option<usize> {
self.get("ef_search")
.and_then(WithValue::as_integer)
.and_then(|v| usize::try_from(v).ok())
}
#[must_use]
pub fn ef_search_value(&self) -> Option<&WithValue> {
self.ef_search_values().next()
}
fn ef_search_values(&self) -> impl Iterator<Item = &WithValue> {
self.options
.iter()
.filter(|opt| opt.key.eq_ignore_ascii_case("ef_search"))
.map(|opt| &opt.value)
}
pub fn ef_search(&self) -> Result<Option<usize>, String> {
let values = self
.ef_search_values()
.map(Self::parse_ef_search_value)
.collect::<Result<Vec<_>, _>>()?;
Ok(values.first().copied())
}
fn parse_ef_search_value(value: &WithValue) -> Result<usize, String> {
let Some(raw) = value.as_integer() else {
return Err(crate::api_types::ef_search_out_of_range(value));
};
crate::api_types::parse_with_ef_search(raw)
}
#[must_use]
#[allow(clippy::cast_sign_loss)]
pub fn get_timeout_ms(&self) -> Option<u64> {
self.get("timeout_ms")
.and_then(WithValue::as_integer)
.map(|v| v as u64)
}
#[must_use]
pub fn get_rerank(&self) -> Option<bool> {
self.get("rerank").and_then(WithValue::as_bool)
}
#[must_use]
pub fn get_quantization(&self) -> Option<QuantizationMode> {
self.get("quantization")
.and_then(WithValue::as_str)
.and_then(QuantizationMode::parse)
}
#[must_use]
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn get_oversampling(&self) -> Option<usize> {
self.get("oversampling")
.and_then(WithValue::as_integer)
.map(|v| v.max(1) as usize)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WithOption {
pub key: String,
pub value: WithValue,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WithValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Identifier(String),
}
impl std::fmt::Display for WithValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
Self::Integer(v) => write!(f, "{v}"),
Self::Float(v) => write_float_literal(f, *v),
Self::Boolean(v) => write!(f, "{v}"),
Self::Identifier(s) if crate::velesql::parser::reads_back_as_bare_identifier(s) => {
f.write_str(s)
}
Self::Identifier(s) => write!(f, "\"{}\"", s.replace('"', "\"\"")),
}
}
}
const NAN_DISPLAY: &str = "0.0/0.0";
fn write_float_literal(f: &mut std::fmt::Formatter<'_>, v: f64) -> std::fmt::Result {
if v.is_nan() {
return f.write_str(NAN_DISPLAY);
}
if v.is_infinite() {
let sign = if v.is_sign_negative() { "-" } else { "" };
return write!(f, "{sign}{}0.0", f64::MAX);
}
let digits = v.to_string();
if digits.contains('.') {
f.write_str(&digits)
} else {
write!(f, "{digits}.0")
}
}
impl WithValue {
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) | Self::Identifier(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(i) => Some(*i),
_ => None,
}
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
#[allow(clippy::cast_precision_loss)]
Self::Integer(i) => Some(*i as f64),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Boolean(b) => Some(*b),
_ => None,
}
}
}