use std::sync::Arc;
use crate::{
backend::{
OrderByClause, ProjectionField, ScalarFieldType,
projection_generator::FieldKind,
where_clause::{SharedFieldTypes, WhereFieldSchema},
},
graphql::FieldSelection,
schema::CompiledSchema,
};
pub fn build_typed_projection_fields(
selections: &[FieldSelection],
schema: &CompiledSchema,
parent_type_name: &str,
) -> Vec<ProjectionField> {
let type_def = schema.find_type(parent_type_name);
selections
.iter()
.filter(|sel| sel.name != "__typename")
.map(|sel| {
let field_def =
type_def.and_then(|td| td.fields.iter().find(|f| f.name == sel.name.as_str()));
let is_composite = field_def.is_some_and(|fd| !fd.field_type.is_scalar());
let is_text = field_def.is_some_and(|fd| {
matches!(
fd.field_type,
crate::schema::FieldType::String | crate::schema::FieldType::Id
)
});
let kind = if is_composite {
FieldKind::Composite
} else if is_text {
FieldKind::Text
} else {
FieldKind::Native
};
let sub_fields = field_def
.filter(|fd| {
matches!(
fd.field_type,
crate::schema::FieldType::Object(_)
| crate::schema::FieldType::Interface(_)
| crate::schema::FieldType::Union(_)
)
})
.and_then(|fd| fd.field_type.type_name())
.map(|child_type| {
build_typed_projection_fields(&sel.nested_fields, schema, child_type)
});
ProjectionField {
name: sel.response_key().to_string(),
source: sel.name.clone(),
kind,
sub_fields,
computed: None,
}
})
.collect()
}
#[must_use]
pub fn where_field_types(schema: &CompiledSchema, return_type: &str) -> WhereFieldSchema {
let Some(type_def) = schema.find_type(return_type) else {
return WhereFieldSchema::default();
};
if type_def.fields.is_empty() {
return WhereFieldSchema::default();
}
let casts: SharedFieldTypes = Arc::new(
type_def
.fields
.iter()
.map(|f| {
(
crate::utils::to_snake_case(f.name.as_str()),
field_type_to_where_type(&f.field_type),
)
})
.collect(),
);
let known = crate::schema::derived_inputs::where_keys_of(schema, type_def);
WhereFieldSchema::with_relations(casts, known, Arc::clone(&schema.where_relation_fields))
}
const fn field_type_to_order_by_type(ft: &crate::schema::FieldType) -> ScalarFieldType {
scalar_cast_hint(ft)
}
pub const fn field_type_to_where_type(ft: &crate::schema::FieldType) -> ScalarFieldType {
use crate::schema::FieldType as FT;
match ft {
FT::Id | FT::Uuid => ScalarFieldType::Uuid,
FT::Vector | FT::BitVector | FT::HalfVector | FT::SparseVector => ScalarFieldType::Vector,
_ => scalar_cast_hint(ft),
}
}
const fn scalar_cast_hint(ft: &crate::schema::FieldType) -> ScalarFieldType {
use crate::schema::FieldType as FT;
match ft {
FT::Int => ScalarFieldType::Integer,
FT::Float | FT::Decimal => ScalarFieldType::Numeric,
FT::Boolean => ScalarFieldType::Boolean,
FT::DateTime => ScalarFieldType::DateTime,
FT::Date => ScalarFieldType::Date,
FT::Time => ScalarFieldType::Time,
_ => ScalarFieldType::Text,
}
}
pub fn enrich_order_by_clauses(
mut clauses: Vec<OrderByClause>,
schema: &CompiledSchema,
return_type: &str,
native_columns: &std::collections::HashMap<String, String>,
security_context: Option<&crate::security::SecurityContext>,
) -> crate::error::Result<Vec<OrderByClause>> {
let type_def = schema.find_type(return_type);
let can_adjudicate = type_def.is_some_and(|td| !td.fields.is_empty());
for clause in &mut clauses {
if can_adjudicate {
let declared = type_def.is_some_and(|td| td.find_field(&clause.field).is_some());
let native = native_columns.contains_key(&clause.storage_key());
if !declared && !native {
return Err(unknown_sort_field(&clause.field, return_type, type_def));
}
}
if let Some(td) = type_def {
if let Some(field_def) = td.find_field(&clause.field) {
super::super::support::security::refuse_unreadable_reference(
schema,
return_type,
field_def,
security_context,
"order by",
)?;
clause.field_type = field_type_to_order_by_type(&field_def.field_type);
}
}
let storage_key = clause.storage_key();
if native_columns.contains_key(&storage_key) {
clause.native_column = Some(storage_key);
}
}
Ok(clauses)
}
fn unknown_sort_field(
field: &str,
return_type: &str,
type_def: Option<&crate::schema::TypeDefinition>,
) -> crate::error::FraiseQLError {
let candidates: Vec<&str> =
type_def.map_or_else(Vec::new, |td| td.fields.iter().map(|f| f.name.as_str()).collect());
let subject = format!("Cannot sort by '{field}' on type '{return_type}'.");
let message = match super::super::super::suggest_similar(field, &candidates).as_slice() {
[s] => format!("{subject} Did you mean '{s}'?"),
[a, b] => format!("{subject} Did you mean '{a}' or '{b}'?"),
[a, b, c, ..] => format!("{subject} Did you mean '{a}', '{b}', or '{c}'?"),
_ => subject,
};
crate::error::FraiseQLError::Validation {
message,
path: Some(format!("orderBy.{field}")),
}
}
pub fn selections_contain_field(selections: &[FieldSelection], field_name: &str) -> bool {
for sel in selections {
if sel.name == field_name {
return true;
}
if sel.name.starts_with("...") && selections_contain_field(&sel.nested_fields, field_name) {
return true;
}
}
false
}
#[cfg(test)]
#[path = "query_projection_tests.rs"]
mod query_projection_tests;
pub fn vector_distance_projection_fields(
selections: &[FieldSelection],
schema: &CompiledSchema,
parent_type_name: &str,
nearest: Option<&OrderByClause>,
) -> crate::error::Result<Vec<ProjectionField>> {
let Some(type_def) = schema.find_type(parent_type_name) else {
return Ok(Vec::new());
};
let mut fields = Vec::new();
for sel in selections {
let Some(field_def) = type_def.fields.iter().find(|f| f.name == sel.name.as_str()) else {
continue;
};
let Some(measures) = field_def.vector_distance.as_deref() else {
continue;
};
let Some(clause) = nearest else {
return Err(crate::error::FraiseQLError::validation(format!(
"'{parent_type_name}.{}' is the distance to '{measures}' and is only defined \
on a query that searches it; add a `nearest` argument or drop the field",
field_def.name
)));
};
if clause.field != measures {
return Err(crate::error::FraiseQLError::validation(format!(
"'{parent_type_name}.{}' is the distance to '{measures}', but this query \
searched '{}'",
field_def.name, clause.field
)));
}
let expr = crate::backend::order_by::vector_distance_expr(
clause,
crate::backend::DatabaseType::PostgreSQL,
)?
.ok_or_else(|| {
crate::error::FraiseQLError::internal("a nearest clause carried no vector operand")
})?;
fields.push(ProjectionField::computed(sel.response_key(), expr));
}
Ok(fields)
}
pub fn merge_computed_fields(typed: &mut Vec<ProjectionField>, computed: Vec<ProjectionField>) {
for field in computed {
if let Some(slot) = typed.iter_mut().find(|f| f.name == field.name) {
*slot = field;
} else {
typed.push(field);
}
}
}
#[cfg(test)]
mod cast_hint_characterisation {
use super::*;
use crate::schema::FieldType as FT;
fn representative_field_types() -> Vec<FT> {
vec![
FT::String,
FT::Int,
FT::Float,
FT::Boolean,
FT::Id,
FT::DateTime,
FT::Date,
FT::Time,
FT::Json,
FT::Uuid,
FT::Decimal,
]
}
#[test]
fn the_where_and_order_by_cast_hints_diverge_only_for_identity_fields() {
for ft in representative_field_types() {
let where_ty = field_type_to_where_type(&ft);
let order_ty = field_type_to_order_by_type(&ft);
if matches!(ft, FT::Id | FT::Uuid) {
assert_eq!(where_ty, ScalarFieldType::Uuid, "identity filters compare as identity");
assert_eq!(order_ty, ScalarFieldType::Text, "identity sorts stay text ordering");
} else {
assert_eq!(
where_ty, order_ty,
"only identities may diverge; {ft:?} did — intended? then update this"
);
}
}
}
#[test]
fn identity_fields_compare_as_identities_not_as_text() {
assert_eq!(field_type_to_where_type(&FT::Id), ScalarFieldType::Uuid);
assert_eq!(field_type_to_where_type(&FT::Uuid), ScalarFieldType::Uuid);
assert_ne!(
field_type_to_where_type(&FT::String),
ScalarFieldType::Uuid,
"a plain string that happens to hold a UUID must keep text semantics"
);
}
#[test]
fn identity_fields_sort_as_text_and_that_is_correct() {
assert_eq!(field_type_to_order_by_type(&FT::Id), ScalarFieldType::Text);
assert_eq!(field_type_to_order_by_type(&FT::Uuid), ScalarFieldType::Text);
}
#[test]
fn strongly_typed_scalars_keep_their_casts_on_both_sides() {
assert_eq!(field_type_to_where_type(&FT::Int), ScalarFieldType::Integer);
assert_eq!(field_type_to_where_type(&FT::Decimal), ScalarFieldType::Numeric);
assert_eq!(field_type_to_where_type(&FT::DateTime), ScalarFieldType::DateTime);
assert_eq!(field_type_to_order_by_type(&FT::Int), ScalarFieldType::Integer);
assert_eq!(field_type_to_order_by_type(&FT::Decimal), ScalarFieldType::Numeric);
assert_eq!(field_type_to_order_by_type(&FT::DateTime), ScalarFieldType::DateTime);
}
}
#[cfg(test)]
mod order_by_validation {
use std::collections::HashMap;
use super::*;
use crate::{
backend::OrderByClause,
schema::{FieldDefinition, FieldType, TypeDefinition},
};
fn schema_with_order() -> CompiledSchema {
let mut schema = CompiledSchema::default();
let mut order = TypeDefinition::new("Order", "v_order");
order.fields.push(FieldDefinition::new("reference", FieldType::String));
order.fields.push(FieldDefinition::new("total", FieldType::Decimal));
schema.types.push(order);
schema
}
fn clause(field: &str) -> Vec<OrderByClause> {
vec![OrderByClause::new(
field.to_string(),
crate::backend::OrderDirection::Asc,
)]
}
#[test]
fn an_unknown_sort_field_is_refused() {
let err = enrich_order_by_clauses(
clause("totallyBogusField"),
&schema_with_order(),
"Order",
&HashMap::new(),
None,
)
.expect_err("an unknown sort key silently ordered nothing");
let msg = err.to_string();
assert!(msg.contains("totallyBogusField"), "must name the field: {msg}");
assert!(msg.contains("Order"), "must name the type: {msg}");
}
#[test]
fn a_near_miss_sort_field_gets_a_did_you_mean_hint() {
let err = enrich_order_by_clauses(
clause("referense"),
&schema_with_order(),
"Order",
&HashMap::new(),
None,
)
.expect_err("a typo must be refused");
assert!(err.to_string().contains("Did you mean 'reference'?"), "got: {err}");
}
#[test]
fn a_declared_sort_field_passes_and_keeps_its_cast() {
let enriched = enrich_order_by_clauses(
clause("total"),
&schema_with_order(),
"Order",
&HashMap::new(),
None,
)
.expect("a declared field is a legitimate sort key");
assert_eq!(
enriched[0].field_type,
ScalarFieldType::Numeric,
"the declared type must still drive the cast"
);
}
#[test]
fn a_native_column_that_is_not_a_type_field_passes() {
let mut native = HashMap::new();
native.insert("pk_order".to_string(), "int4".to_string());
let enriched = enrich_order_by_clauses(
clause("pk_order"),
&schema_with_order(),
"Order",
&native,
None,
)
.expect("a native column is a legitimate sort key");
assert_eq!(
enriched[0].native_column.as_deref(),
Some("pk_order"),
"the native mapping must still be applied"
);
}
#[test]
fn an_unknown_type_cannot_adjudicate_and_passes() {
enrich_order_by_clauses(
clause("anything"),
&schema_with_order(),
"NoSuchType",
&HashMap::new(),
None,
)
.expect("no type information — a rejection cannot be justified");
}
#[test]
fn a_type_with_no_fields_cannot_adjudicate_and_passes() {
let mut schema = CompiledSchema::default();
schema.types.push(TypeDefinition::new("Order", "v_order"));
enrich_order_by_clauses(clause("anything"), &schema, "Order", &HashMap::new(), None)
.expect("an empty field list is absence of evidence, not evidence of absence");
}
}