use fraiseql_error::{FraiseQLError, Result};
use serde::{Deserialize, Serialize};
use crate::{types::db_types::DatabaseType, utils::to_snake_case};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ScalarFieldType {
#[default]
Text,
Integer,
Numeric,
Boolean,
DateTime,
Date,
Time,
Uuid,
Vector,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OrderByClause {
pub field: String,
pub direction: OrderDirection,
#[serde(default)]
pub field_type: ScalarFieldType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_column: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vector: Option<VectorDistanceOrder>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relevance: Option<RelevanceOrder>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub identity: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelevanceOrder {
pub fields: Vec<String>,
pub query: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VectorDistanceOrder {
pub operator: String,
pub query_vector: String,
#[serde(default)]
pub kind: VectorOperandKind,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum VectorOperandKind {
#[default]
Float,
Bit,
Sparse,
}
impl VectorOperandKind {
#[must_use]
pub const fn cast(self) -> &'static str {
match self {
Self::Bit => "varbit",
Self::Sparse => "sparsevec",
_ => "vector",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum OrderDirection {
Asc,
Desc,
}
impl OrderDirection {
#[must_use]
pub const fn as_sql(self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC",
}
}
}
impl OrderByClause {
#[must_use]
pub fn new(field: String, direction: OrderDirection) -> Self {
Self {
field,
direction,
field_type: ScalarFieldType::default(),
native_column: None,
vector: None,
relevance: None,
identity: false,
}
}
#[must_use]
pub const fn identity(field: String, native_column: Option<String>) -> Self {
Self {
field,
direction: OrderDirection::Asc,
field_type: ScalarFieldType::Text,
native_column,
vector: None,
relevance: None,
identity: true,
}
}
#[must_use]
pub fn by_relevance(relevance: RelevanceOrder) -> Self {
Self {
field: String::new(),
direction: OrderDirection::Desc,
field_type: ScalarFieldType::default(),
native_column: None,
vector: None,
relevance: Some(relevance),
identity: false,
}
}
#[must_use]
pub const fn binds_parameter(&self) -> bool {
self.relevance.is_some()
}
#[must_use]
pub fn storage_key(&self) -> String {
to_snake_case(&self.field)
}
pub fn validate_field_name(field: &str) -> Result<()> {
crate::utils::validate_graphql_identifier(field, "orderBy")
}
pub fn from_graphql_json(value: &serde_json::Value) -> Result<Vec<Self>> {
if let Some(obj) = value.as_object() {
obj.iter()
.map(|(field, dir_val)| {
let dir_str = dir_val.as_str().ok_or_else(|| FraiseQLError::Validation {
message: format!("orderBy direction for '{field}' must be a string"),
path: None,
})?;
let direction = match dir_str.to_ascii_uppercase().as_str() {
"ASC" => OrderDirection::Asc,
"DESC" => OrderDirection::Desc,
_ => {
return Err(FraiseQLError::Validation {
message: format!(
"orderBy direction '{dir_str}' must be ASC or DESC"
),
path: None,
});
},
};
Self::validate_field_name(field)?;
Ok(Self::new(field.clone(), direction))
})
.collect()
} else if let Some(arr) = value.as_array() {
arr.iter()
.map(|item| {
let obj = item.as_object().ok_or_else(|| FraiseQLError::Validation {
message: "orderBy array items must be objects".to_string(),
path: None,
})?;
let field = obj
.get("field")
.and_then(|v| v.as_str())
.ok_or_else(|| FraiseQLError::Validation {
message: "orderBy item missing 'field' string".to_string(),
path: None,
})?
.to_string();
let dir_str = obj.get("direction").and_then(|v| v.as_str()).unwrap_or("ASC");
let direction = match dir_str.to_ascii_uppercase().as_str() {
"ASC" => OrderDirection::Asc,
"DESC" => OrderDirection::Desc,
_ => {
return Err(FraiseQLError::Validation {
message: format!(
"orderBy direction '{dir_str}' must be ASC or DESC"
),
path: None,
});
},
};
Self::validate_field_name(&field)?;
Ok(Self::new(field, direction))
})
.collect()
} else {
Err(FraiseQLError::Validation {
message: "orderBy must be an object or array".to_string(),
path: None,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SqlProjectionHint {
pub database: DatabaseType,
pub projection_template: String,
pub estimated_reduction_percent: u32,
}
impl SqlProjectionHint {
#[must_use]
pub const fn new(
database: DatabaseType,
projection_template: String,
estimated_reduction_percent: u32,
) -> Self {
Self {
database,
projection_template,
estimated_reduction_percent,
}
}
}
#[cfg(test)]
mod tests;