use std::{collections::HashSet, sync::Arc};
use super::{query::QueryRunner, root_read::RootRead};
use crate::{
backend::{RawRows, types::JsonbValue},
error::{FraiseQLError, Result},
graphql::FieldSelection,
runtime::QueryMatch,
schema::{CompiledSchema, FieldType},
};
pub(super) enum RawRead {
Body(Vec<u8>),
Rows(Arc<Vec<JsonbValue>>),
}
impl QueryRunner {
pub(super) async fn read_raw(
&self,
read: &RootRead,
query_match: &QueryMatch,
session_pairs: &[(&str, &str)],
) -> Result<Option<RawRead>> {
let Some(shape) = RawShape::of(read, query_match, &self.ctx.schema) else {
return Ok(None);
};
let query_def = &query_match.query_def;
let Some(rows) = self
.ctx
.adapter
.execute_with_projection_raw_with_session(
&read.projection_request(),
session_pairs,
query_def.read_routing,
)
.await?
else {
return Ok(None);
};
let selections = query_match
.selections
.first()
.map_or(&[][..], |root| root.nested_fields.as_slice());
let completes_unchanged =
!shape.may_hold_null_at_non_null(
&rows,
selections,
&self.ctx.schema,
&query_def.return_type,
) && (query_def.returns_list || query_def.nullable || !rows.rows().is_empty());
if !rows.exact() || !completes_unchanged {
let parsed = rows.parse().map_err(|e| FraiseQLError::Internal {
message: format!("a row the database serialized is not valid JSON: {e}"),
source: None,
})?;
return Ok(Some(RawRead::Rows(Arc::new(
parsed.into_iter().map(JsonbValue::new).collect(),
))));
}
if let Some(budget) = read.budget() {
budget.charge_raw_rows(&rows)?;
}
Ok(Some(RawRead::Body(envelope(
&rows,
query_match.response_key(),
query_def.returns_list,
))))
}
}
fn envelope(rows: &RawRows, response_key: &str, is_list: bool) -> Vec<u8> {
let mut body = Vec::with_capacity(rows.bytes().len() + response_key.len() + 16);
body.extend_from_slice(b"{\"data\":{");
serde_json::to_writer(&mut body, response_key).unwrap_or_default();
body.push(b':');
if is_list {
body.push(b'[');
body.extend_from_slice(rows.bytes());
body.push(b']');
} else {
body.extend_from_slice(rows.row(0).unwrap_or(b"null"));
}
body.extend_from_slice(b"}}");
body
}
struct RawShape {
non_null: bool,
}
impl RawShape {
fn may_hold_null_at_non_null(
&self,
rows: &RawRows,
selections: &[FieldSelection],
schema: &CompiledSchema,
type_name: &str,
) -> bool {
self.non_null
&& rows.has_null_where(|key| {
key.is_none_or(|key| selects_non_null_under(selections, schema, type_name, key))
})
}
fn of(read: &RootRead, query_match: &QueryMatch, schema: &CompiledSchema) -> Option<Self> {
if !Self::flat(read) {
return None;
}
let root = query_match.selections.first()?;
let mut shape = Self { non_null: false };
shape
.admits(&root.nested_fields, schema, &query_match.query_def.return_type)
.then_some(shape)
}
fn flat(read: &RootRead) -> bool {
read.projection.is_some()
&& read.nested.is_empty()
&& read.source_args.is_none()
&& !read.gated_present
&& !read.abstract_read
&& read.nearest_k.is_none()
&& read.nearest_count.is_none()
&& (read.built_in_sql || read.selection.masks_nothing())
}
fn admits(
&mut self,
selections: &[FieldSelection],
schema: &CompiledSchema,
type_name: &str,
) -> bool {
let Some(type_def) = schema.find_type(type_name) else {
return false;
};
let mut keys = HashSet::with_capacity(selections.len());
selections.iter().all(|sel| {
if !sel.arguments.is_empty() || !keys.insert(sel.response_key()) {
return false;
}
if sel.name == "__typename" {
return sel.nested_fields.is_empty();
}
let Some(field) = type_def.fields.iter().find(|f| f.name == sel.name.as_str()) else {
return false;
};
if field.localized
|| field.authorize
|| field.encryption.is_some()
|| field.vector_distance.is_some()
|| field.alias.is_some()
|| field.hierarchy.is_some()
{
return false;
}
self.non_null |= !field.nullable;
match &field.field_type {
FieldType::String
| FieldType::Id
| FieldType::Uuid
| FieldType::Int
| FieldType::Float
| FieldType::Decimal
| FieldType::Boolean => sel.nested_fields.is_empty(),
ty => object_type(ty).is_some_and(|child| {
!sel.nested_fields.is_empty() && self.admits(&sel.nested_fields, schema, child)
}),
}
})
}
}
fn selects_non_null_under(
selections: &[FieldSelection],
schema: &CompiledSchema,
type_name: &str,
key: &[u8],
) -> bool {
let Some(type_def) = schema.find_type(type_name) else {
return true;
};
selections.iter().any(|sel| {
let Some(field) = type_def.fields.iter().find(|f| f.name == sel.name.as_str()) else {
return false;
};
(!field.nullable && sel.response_key().as_bytes() == key)
|| object_type(&field.field_type)
.is_some_and(|child| selects_non_null_under(&sel.nested_fields, schema, child, key))
})
}
fn object_type(field_type: &FieldType) -> Option<&str> {
match field_type {
FieldType::Object(child) => Some(child),
list => list.inner_type().filter(|item| item.is_object()).and_then(FieldType::type_name),
}
}
#[cfg(test)]
#[path = "query_raw_tests.rs"]
mod tests;