use crate::{
backend::{WhereClause, WhereOperator},
error::{FraiseQLError, Result},
schema::PaginationOrder,
};
pub const AUTO_PARAM_NAMES: &[&str] = &[
"where", "limit", "offset", "orderBy", "first", "last", "after", "before", "nearest",
];
pub fn nearest_order_and_limit(
arguments: &std::collections::HashMap<String, serde_json::Value>,
schema: &crate::schema::CompiledSchema,
query_def: &crate::schema::QueryDefinition,
security_context: Option<&crate::security::SecurityContext>,
) -> Result<Option<(crate::backend::OrderByClause, u32)>> {
let Some(raw) = arguments.get("nearest") else {
return Ok(None);
};
if query_def.relay {
return Err(FraiseQLError::validation(
"`nearest` is not supported on relay (connection) queries",
));
}
if !query_def.returns_list {
return Err(FraiseQLError::validation("`nearest` requires a list-returning query"));
}
if arguments.contains_key("limit") {
return Err(FraiseQLError::validation(
"`nearest.k` sets the page size; do not combine `nearest` with `limit`",
));
}
if arguments.contains_key("orderBy") {
return Err(FraiseQLError::validation(
"`nearest` orders by vector distance; do not combine it with `orderBy`",
));
}
let type_def = schema.find_type(&query_def.return_type).ok_or_else(|| {
FraiseQLError::validation(format!(
"`nearest` target type '{}' is not defined",
query_def.return_type
))
})?;
let obj = raw.as_object().ok_or_else(nearest_shape_err)?;
for key in obj.keys() {
if !matches!(key.as_str(), "vector" | "k" | "metric" | "field") {
return Err(FraiseQLError::validation(format!(
"unknown `nearest` argument key '{key}' (expected vector, k, field, metric)"
)));
}
}
let field = select_vector_field(type_def, obj.get("field"))?;
super::super::support::security::refuse_unreadable_reference(
schema,
&query_def.return_type,
field,
security_context,
"order by similarity",
)?;
let k = obj
.get("k")
.and_then(serde_json::Value::as_u64)
.and_then(|k| u32::try_from(k).ok())
.filter(|k| *k > 0)
.ok_or_else(|| FraiseQLError::validation("`nearest.k` must be a positive integer"))?;
let (literal, kind) = query_vector_literal(
obj.get("vector").ok_or_else(nearest_shape_err)?,
field,
type_def.name.as_str(),
)?;
let metric = resolve_metric(obj.get("metric"), field, type_def.name.as_str())?;
let column = crate::backend::utils::vector_storage_column(field.name.as_str())?;
let mut clause = crate::backend::OrderByClause::new(
field.name.to_string(),
crate::backend::OrderDirection::Asc,
);
clause.native_column = Some(format!("\"{column}\""));
clause.vector = Some(crate::backend::VectorDistanceOrder {
operator: metric.operator().to_string(),
query_vector: literal,
kind,
});
Ok(Some((clause, k)))
}
fn nearest_shape_err() -> FraiseQLError {
FraiseQLError::validation(
"`nearest` takes {vector: [Float, ...] | \"1011…\", k: Int, field: String, metric: \
\"cosine\"|\"l2\"|\"inner_product\"|\"hamming\"|\"jaccard\"} (vector is an array of \
floats on a Vector field and a run of 0/1 bits on a BitVector one; field optional on \
a type with one vector field; metric optional — defaults to the field's declared \
distance metric)",
)
}
fn query_vector_literal(
operand: &serde_json::Value,
field: &crate::schema::FieldDefinition,
type_name: &str,
) -> Result<(String, crate::backend::VectorOperandKind)> {
let config = field.vector_config.as_ref().expect("caller selected a configured vector field");
let declared = config.dimensions as usize;
let dimension_err = |got: usize, unit: &str| {
FraiseQLError::validation(format!(
"`nearest.vector` has {got} {unit} but '{type_name}.{}' declares {declared} \
dimensions",
field.name
))
};
if field.is_bit_vector() {
let bits = operand.as_str().ok_or_else(|| {
FraiseQLError::validation(format!(
"'{type_name}.{}' is a BitVector field, so `nearest.vector` is a string of \
'0' and '1' characters, not an array",
field.name
))
})?;
if !bits.chars().all(|c| matches!(c, '0' | '1')) {
return Err(FraiseQLError::validation(
"`nearest.vector` on a BitVector field must contain only '0' and '1'",
));
}
if bits.len() != declared {
return Err(dimension_err(bits.len(), "bits"));
}
return Ok((bits.to_string(), crate::backend::VectorOperandKind::Bit));
}
if matches!(field.field_type, crate::schema::FieldType::SparseVector) {
let sparse = operand.as_str().ok_or_else(|| {
FraiseQLError::validation(format!(
"'{type_name}.{}' is a SparseVector field, so `nearest.vector` is a string in \
pgvector's sparse form — {{1:0.5,7:0.25}}/1000 — not an array. A dense array \
would defeat the reason the field is sparse",
field.name
))
})?;
let dimensions = parse_sparse_literal(sparse)?;
if dimensions != declared {
return Err(dimension_err(dimensions, "dimensions"));
}
return Ok((sparse.to_string(), crate::backend::VectorOperandKind::Sparse));
}
let vector = operand.as_array().ok_or_else(nearest_shape_err)?;
if vector.len() != declared {
return Err(dimension_err(vector.len(), "components"));
}
let mut literal = String::with_capacity(vector.len() * 8 + 2);
literal.push('[');
for (i, component) in vector.iter().enumerate() {
let n = component.as_f64().filter(|n| n.is_finite()).ok_or_else(|| {
FraiseQLError::validation("`nearest.vector` components must all be finite numbers")
})?;
if i > 0 {
literal.push(',');
}
std::fmt::Write::write_fmt(&mut literal, format_args!("{n}"))
.expect("write to String is infallible");
}
literal.push(']');
Ok((literal, crate::backend::VectorOperandKind::Float))
}
fn parse_sparse_literal(literal: &str) -> Result<usize> {
let shape_err = || {
FraiseQLError::validation(
"a sparse `nearest.vector` is pgvector's `{index:value,...}/dimensions` form, \
with 1-based indices — e.g. \"{1:0.5,7:0.25}/1000\"",
)
};
let body = literal.strip_prefix('{').ok_or_else(shape_err)?;
let (entries, dimensions) = body.split_once("}/").ok_or_else(shape_err)?;
let dimensions: usize = dimensions.parse().map_err(|_| shape_err())?;
if dimensions == 0 {
return Err(shape_err());
}
for entry in entries.split(',').filter(|e| !e.is_empty()) {
let (index, value) = entry.split_once(':').ok_or_else(shape_err)?;
let index: usize = index.trim().parse().map_err(|_| shape_err())?;
if index == 0 || index > dimensions {
return Err(FraiseQLError::validation(format!(
"sparse vector index {index} is outside 1..={dimensions}"
)));
}
let value: f64 = value.trim().parse().map_err(|_| shape_err())?;
if !value.is_finite() {
return Err(FraiseQLError::validation(
"sparse vector values must all be finite numbers",
));
}
}
Ok(dimensions)
}
fn resolve_metric(
requested: Option<&serde_json::Value>,
field: &crate::schema::FieldDefinition,
type_name: &str,
) -> Result<crate::schema::DistanceMetric> {
use crate::schema::DistanceMetric;
let config = field.vector_config.as_ref().expect("caller selected a configured vector field");
let Some(requested) = requested else {
return Ok(config.distance_metric);
};
let name = requested.as_str().ok_or_else(nearest_shape_err)?;
let metric = match name {
"cosine" => DistanceMetric::Cosine,
"l2" => DistanceMetric::L2,
"inner_product" => DistanceMetric::InnerProduct,
"hamming" => DistanceMetric::Hamming,
"jaccard" => DistanceMetric::Jaccard,
other => {
return Err(FraiseQLError::validation(format!(
"unknown `nearest.metric` '{other}' (expected cosine, l2, inner_product, \
hamming or jaccard)"
)));
},
};
if metric.is_binary() != field.is_bit_vector() {
let (operates_on, declared_as, alternatives) = if metric.is_binary() {
("binary (bit)", "Vector", "cosine, l2 or inner_product")
} else {
("float", "BitVector", "hamming or jaccard")
};
return Err(FraiseQLError::validation(format!(
"`nearest.metric` '{name}' operates on {operates_on} vectors, and \
'{type_name}.{}' is a {declared_as} field; use {alternatives}",
field.name
)));
}
Ok(metric)
}
fn select_vector_field<'a>(
type_def: &'a crate::schema::TypeDefinition,
requested: Option<&serde_json::Value>,
) -> Result<&'a crate::schema::FieldDefinition> {
let mut vector_fields = type_def.fields.iter().filter(|f| is_vector_field(f));
let Some(first) = vector_fields.next() else {
return Err(FraiseQLError::validation(format!(
"`nearest` requires a vector field on type '{}', and it declares none",
type_def.name
)));
};
let Some(requested) = requested else {
if vector_fields.next().is_some() {
return Err(FraiseQLError::validation(format!(
"type '{}' declares more than one vector field ({}); name the one to \
search with `nearest.field`",
type_def.name,
vector_field_names(type_def).join(", ")
)));
}
return Ok(first);
};
let name = requested.as_str().ok_or_else(|| {
FraiseQLError::validation("`nearest.field` must be the name of a vector field")
})?;
type_def
.fields
.iter()
.find(|f| f.name == name && is_vector_field(f))
.ok_or_else(|| {
let declared = vector_field_names(type_def);
FraiseQLError::validation(format!(
"`nearest.field` '{name}' is not a vector field on type '{}'; it declares {}",
type_def.name,
if declared.is_empty() {
"none".to_string()
} else {
declared.join(", ")
}
))
})
}
fn vector_field_names(type_def: &crate::schema::TypeDefinition) -> Vec<&str> {
type_def
.fields
.iter()
.filter(|f| is_vector_field(f))
.map(|f| f.name.as_str())
.collect()
}
const fn is_vector_field(field: &crate::schema::FieldDefinition) -> bool {
field.field_type.is_searchable_vector() && field.vector_config.is_some()
}
pub fn inject_param_where_clause(
col: &str,
value: serde_json::Value,
native_columns: &std::collections::HashMap<String, String>,
) -> WhereClause {
if let Some(pg_type) = native_columns.get(col) {
WhereClause::NativeField {
column: crate::utils::to_snake_case(col),
pg_cast: pg_type_to_cast(pg_type).to_string(),
operator: WhereOperator::Eq,
value,
}
} else {
WhereClause::Field {
path: vec![crate::utils::to_snake_case(col)],
operator: WhereOperator::Eq,
value,
}
}
}
pub fn pg_type_to_cast(data_type: &str) -> &'static str {
crate::runtime::native_columns::pg_type_to_cast(data_type)
}
pub fn compute_projection_reduction(projected_field_count: usize) -> u32 {
const BASELINE_FIELD_COUNT: usize = 20;
let requested = projected_field_count.min(BASELINE_FIELD_COUNT);
let saved = BASELINE_FIELD_COUNT.saturating_sub(requested);
#[allow(clippy::cast_possible_truncation)] let percent = ((saved * 100) / BASELINE_FIELD_COUNT) as u32;
percent.clamp(10, 90)
}
pub fn client_where_argument(
schema: &crate::schema::CompiledSchema,
query_def: &crate::schema::QueryDefinition,
arguments: &std::collections::HashMap<String, serde_json::Value>,
rls_policy: Option<&dyn crate::security::RLSPolicy>,
security_context: Option<&crate::security::SecurityContext>,
) -> Result<Option<WhereClause>> {
let Some(raw) = arguments.get("where") else {
return Ok(None);
};
if !query_def.auto_params.has_where {
return Err(FraiseQLError::Validation {
message: format!(
"Query '{}' does not accept a `where` argument (`where_clause = \
false`), so the filter in this request cannot be applied. Over REST \
that is a `?field=value`, `?field[op]=value`, `?filter=`, \
`?or=`/`?and=`/`?not=` or `?search=` parameter. The request is refused \
rather than answered with the whole unfiltered relation; enable \
`where_clause` on this query to filter it.",
query_def.name
),
path: Some("where".to_string()),
});
}
let types = super::query_projection::where_field_types(schema, &query_def.return_type);
let clause = WhereClause::from_graphql_json(raw, &types)?;
super::super::support::security::refuse_unreadable_where(
schema,
&query_def.return_type,
&clause,
security_context,
)?;
let clause = super::query_nested::guard_relation_filters(
schema,
rls_policy,
&query_def.return_type,
&clause,
security_context,
)?;
Ok(Some(clause))
}
pub fn combine_explicit_arg_where(
existing: Option<WhereClause>,
defined_args: &[crate::schema::ArgumentDefinition],
provided_args: &std::collections::HashMap<String, serde_json::Value>,
native_columns: &std::collections::HashMap<String, String>,
) -> Option<WhereClause> {
let explicit_conditions: Vec<WhereClause> = defined_args
.iter()
.filter(|arg| !AUTO_PARAM_NAMES.contains(&arg.name.as_str()))
.filter_map(|arg| {
provided_args.get(&arg.name).map(|value| {
if let Some(pg_type) = native_columns.get(&arg.name) {
WhereClause::NativeField {
column: crate::utils::to_snake_case(&arg.name),
pg_cast: pg_type_to_cast(pg_type).to_string(),
operator: WhereOperator::Eq,
value: value.clone(),
}
} else {
WhereClause::Field {
path: vec![crate::utils::to_snake_case(&arg.name)],
operator: WhereOperator::Eq,
value: value.clone(),
}
}
})
})
.collect();
if explicit_conditions.is_empty() {
return existing;
}
let mut all_conditions = Vec::new();
if let Some(prev) = existing {
all_conditions.push(prev);
}
all_conditions.extend(explicit_conditions);
match all_conditions.len() {
1 => Some(all_conditions.remove(0)),
_ => Some(WhereClause::And(all_conditions)),
}
}
pub fn coerce_pagination_arg(
arg_name: &str,
value: Option<&serde_json::Value>,
) -> Result<Option<u32>> {
let Some(value) = value else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
if let Some(n) = value.as_u64() {
if let Ok(v) = u32::try_from(n) {
return Ok(Some(v));
}
return Err(FraiseQLError::Validation {
message: format!(
"`{arg_name}` {n} is larger than the maximum row count ({})",
u32::MAX
),
path: Some(arg_name.to_string()),
});
}
let received = match value {
serde_json::Value::Number(n) if n.is_f64() => "a fractional number".to_string(),
serde_json::Value::Number(_) => "a negative number".to_string(),
serde_json::Value::String(_) => "a string".to_string(),
serde_json::Value::Bool(_) => "a boolean".to_string(),
serde_json::Value::Array(_) => "a list".to_string(),
serde_json::Value::Object(_) | serde_json::Value::Null => "an object".to_string(),
};
Err(FraiseQLError::Validation {
message: format!(
"`{arg_name}` must be a non-negative Int, but received {received}. Refusing rather \
than returning an unpaginated result."
),
path: Some(arg_name.to_string()),
})
}
pub fn enforce_max_page_size(
value: Option<u32>,
max: Option<u32>,
arg_name: &str,
) -> Result<Option<u32>> {
if let (Some(v), Some(m)) = (value, max) {
if v > m {
return Err(FraiseQLError::Validation {
message: format!("`{arg_name}` {v} exceeds the maximum page size of {m}"),
path: Some(arg_name.to_string()),
});
}
}
Ok(value)
}
#[must_use]
pub fn apply_pagination_order(
order_by: Option<Vec<crate::backend::OrderByClause>>,
query_def: &crate::schema::QueryDefinition,
limit: Option<u32>,
offset: Option<u32>,
) -> Option<Vec<crate::backend::OrderByClause>> {
let Some(order) = query_def.pagination_order.as_ref() else {
return order_by;
};
if limit.is_none() && offset.is_none() {
return order_by;
}
let mut clauses = order_by.unwrap_or_default();
if crate::backend::order_by::orders_by_identity(&clauses) {
return Some(clauses);
}
clauses.push(match order {
PaginationOrder::JsonIdentity => {
crate::backend::OrderByClause::identity("id".to_string(), None)
},
PaginationOrder::Column(col) => {
crate::backend::OrderByClause::identity(col.clone(), Some(col.clone()))
},
});
Some(clauses)
}
#[cfg(test)]
#[path = "query_params_tests.rs"]
mod query_params_tests;