|
| 1 | +use std::collections::BTreeSet; |
| 2 | + |
| 3 | +use arrow_schema::DataType; |
| 4 | +use serde::Serialize; |
| 5 | + |
| 6 | +use crate::catalog::Catalog; |
| 7 | +use crate::error::{CompilerError, Result}; |
| 8 | + |
| 9 | +use super::ast::{Clause, Mutation, QueryDecl}; |
| 10 | +use super::typecheck::{ |
| 11 | + CheckedQuery, MutationTarget, infer_query_result_schema, typecheck_query_decl, |
| 12 | +}; |
| 13 | + |
| 14 | +/// A compiled query's conservative graph access set and result shape. |
| 15 | +/// |
| 16 | +/// `reads` is conservative: every table the supported execution path may |
| 17 | +/// inspect is present. `writes` is the exact set of tables the mutation may |
| 18 | +/// change. Both sets are sorted and deduplicated for deterministic consumers. |
| 19 | +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] |
| 20 | +pub struct QueryOperationDescriptor { |
| 21 | + pub result: Vec<QueryResultFieldDescriptor>, |
| 22 | + pub reads: Vec<QueryGraphFact>, |
| 23 | + pub writes: Vec<QueryGraphFact>, |
| 24 | +} |
| 25 | + |
| 26 | +/// One field in a compiled query result. |
| 27 | +/// |
| 28 | +/// Kinds are decomposed so clients never have to parse Arrow display strings |
| 29 | +/// such as `Vector(3)` or `[Date]`. |
| 30 | +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] |
| 31 | +pub struct QueryResultFieldDescriptor { |
| 32 | + pub name: String, |
| 33 | + pub kind: QueryValueKind, |
| 34 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 35 | + pub item_kind: Option<QueryValueKind>, |
| 36 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 37 | + pub vector_dim: Option<u32>, |
| 38 | + pub nullable: bool, |
| 39 | +} |
| 40 | + |
| 41 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] |
| 42 | +#[serde(rename_all = "snake_case")] |
| 43 | +pub enum QueryValueKind { |
| 44 | + String, |
| 45 | + Bool, |
| 46 | + Int, |
| 47 | + #[serde(rename = "bigint")] |
| 48 | + BigInt, |
| 49 | + Float, |
| 50 | + Date, |
| 51 | + #[serde(rename = "datetime")] |
| 52 | + DateTime, |
| 53 | + Blob, |
| 54 | + Vector, |
| 55 | + List, |
| 56 | + Object, |
| 57 | +} |
| 58 | + |
| 59 | +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] |
| 60 | +pub struct QueryGraphFact { |
| 61 | + pub kind: QueryGraphFactKind, |
| 62 | + pub type_name: String, |
| 63 | +} |
| 64 | + |
| 65 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] |
| 66 | +#[serde(rename_all = "lowercase")] |
| 67 | +pub enum QueryGraphFactKind { |
| 68 | + Node, |
| 69 | + Edge, |
| 70 | +} |
| 71 | + |
| 72 | +/// Compile one parsed query into the shared operation descriptor. |
| 73 | +/// |
| 74 | +/// This is the single compiler owner used by lint today and by future server |
| 75 | +/// and SDK projections. An invalid query returns an error rather than a |
| 76 | +/// partial descriptor. |
| 77 | +pub fn describe_query_operation( |
| 78 | + catalog: &Catalog, |
| 79 | + query: &QueryDecl, |
| 80 | +) -> Result<QueryOperationDescriptor> { |
| 81 | + let checked = typecheck_query_decl(catalog, query)?; |
| 82 | + describe_checked_query_operation(catalog, query, &checked) |
| 83 | +} |
| 84 | + |
| 85 | +fn describe_checked_query_operation( |
| 86 | + catalog: &Catalog, |
| 87 | + query: &QueryDecl, |
| 88 | + checked: &CheckedQuery, |
| 89 | +) -> Result<QueryOperationDescriptor> { |
| 90 | + let mut reads = BTreeSet::new(); |
| 91 | + let mut writes = BTreeSet::new(); |
| 92 | + |
| 93 | + let result = match checked { |
| 94 | + CheckedQuery::Read(ctx) => { |
| 95 | + collect_clause_reads(catalog, &query.match_clause, &mut reads)?; |
| 96 | + infer_query_result_schema(catalog, query, ctx)? |
| 97 | + .fields() |
| 98 | + .iter() |
| 99 | + .map(result_field_descriptor) |
| 100 | + .collect::<Result<Vec<_>>>()? |
| 101 | + } |
| 102 | + CheckedQuery::Mutation(ctx) => { |
| 103 | + if ctx.targets.len() != query.mutations.len() { |
| 104 | + return Err(CompilerError::Type( |
| 105 | + "typechecked mutation target count does not match the query".to_string(), |
| 106 | + )); |
| 107 | + } |
| 108 | + for (mutation, target) in query.mutations.iter().zip(&ctx.targets) { |
| 109 | + let fact = target_fact(target); |
| 110 | + reads.insert(fact.clone()); |
| 111 | + writes.insert(fact); |
| 112 | + |
| 113 | + match (mutation, target) { |
| 114 | + (Mutation::Insert(_), MutationTarget::Edge { type_name }) => { |
| 115 | + let edge = catalog.edge_types.get(type_name).ok_or_else(|| { |
| 116 | + CompilerError::Type(format!( |
| 117 | + "typechecked edge type `{type_name}` is absent from the catalog" |
| 118 | + )) |
| 119 | + })?; |
| 120 | + reads.insert(node_fact(&edge.from_type)); |
| 121 | + reads.insert(node_fact(&edge.to_type)); |
| 122 | + } |
| 123 | + (Mutation::Delete(_), MutationTarget::Node { type_name }) => { |
| 124 | + for edge in catalog.edge_types.values().filter(|edge| { |
| 125 | + edge.from_type == *type_name || edge.to_type == *type_name |
| 126 | + }) { |
| 127 | + let edge_fact = QueryGraphFact { |
| 128 | + kind: QueryGraphFactKind::Edge, |
| 129 | + type_name: edge.name.clone(), |
| 130 | + }; |
| 131 | + reads.insert(edge_fact.clone()); |
| 132 | + writes.insert(edge_fact); |
| 133 | + } |
| 134 | + } |
| 135 | + _ => {} |
| 136 | + } |
| 137 | + } |
| 138 | + Vec::new() |
| 139 | + } |
| 140 | + }; |
| 141 | + |
| 142 | + Ok(QueryOperationDescriptor { |
| 143 | + result, |
| 144 | + reads: reads.into_iter().collect(), |
| 145 | + writes: writes.into_iter().collect(), |
| 146 | + }) |
| 147 | +} |
| 148 | + |
| 149 | +fn collect_clause_reads( |
| 150 | + catalog: &Catalog, |
| 151 | + clauses: &[Clause], |
| 152 | + reads: &mut BTreeSet<QueryGraphFact>, |
| 153 | +) -> Result<()> { |
| 154 | + for clause in clauses { |
| 155 | + match clause { |
| 156 | + Clause::Binding(binding) => { |
| 157 | + reads.insert(node_fact(&binding.type_name)); |
| 158 | + } |
| 159 | + Clause::Traversal(traversal) => { |
| 160 | + let edge = catalog |
| 161 | + .lookup_edge_by_name(&traversal.edge_name) |
| 162 | + .ok_or_else(|| { |
| 163 | + CompilerError::Type(format!( |
| 164 | + "typechecked edge type `{}` is absent from the catalog", |
| 165 | + traversal.edge_name |
| 166 | + )) |
| 167 | + })?; |
| 168 | + reads.insert(QueryGraphFact { |
| 169 | + kind: QueryGraphFactKind::Edge, |
| 170 | + type_name: edge.name.clone(), |
| 171 | + }); |
| 172 | + reads.insert(node_fact(&edge.from_type)); |
| 173 | + reads.insert(node_fact(&edge.to_type)); |
| 174 | + } |
| 175 | + Clause::Negation(inner) => collect_clause_reads(catalog, inner, reads)?, |
| 176 | + Clause::Filter(_) => {} |
| 177 | + } |
| 178 | + } |
| 179 | + Ok(()) |
| 180 | +} |
| 181 | + |
| 182 | +fn target_fact(target: &MutationTarget) -> QueryGraphFact { |
| 183 | + match target { |
| 184 | + MutationTarget::Node { type_name } => node_fact(type_name), |
| 185 | + MutationTarget::Edge { type_name } => QueryGraphFact { |
| 186 | + kind: QueryGraphFactKind::Edge, |
| 187 | + type_name: type_name.clone(), |
| 188 | + }, |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +fn node_fact(type_name: &str) -> QueryGraphFact { |
| 193 | + QueryGraphFact { |
| 194 | + kind: QueryGraphFactKind::Node, |
| 195 | + type_name: type_name.to_string(), |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +fn result_field_descriptor(field: &arrow_schema::FieldRef) -> Result<QueryResultFieldDescriptor> { |
| 200 | + let (kind, item_kind, vector_dim) = result_value_shape(field.data_type())?; |
| 201 | + Ok(QueryResultFieldDescriptor { |
| 202 | + name: field.name().clone(), |
| 203 | + kind, |
| 204 | + item_kind, |
| 205 | + vector_dim, |
| 206 | + nullable: field.is_nullable(), |
| 207 | + }) |
| 208 | +} |
| 209 | + |
| 210 | +fn result_value_shape( |
| 211 | + data_type: &DataType, |
| 212 | +) -> Result<(QueryValueKind, Option<QueryValueKind>, Option<u32>)> { |
| 213 | + match data_type { |
| 214 | + DataType::FixedSizeList(field, dim) if field.data_type() == &DataType::Float32 => { |
| 215 | + let dim = u32::try_from(*dim).map_err(|_| { |
| 216 | + CompilerError::Type(format!( |
| 217 | + "query result vector dimension `{dim}` cannot be represented" |
| 218 | + )) |
| 219 | + })?; |
| 220 | + Ok((QueryValueKind::Vector, None, Some(dim))) |
| 221 | + } |
| 222 | + DataType::List(field) => { |
| 223 | + let (item_kind, nested_item, vector_dim) = result_value_shape(field.data_type())?; |
| 224 | + if nested_item.is_some() || item_kind == QueryValueKind::List { |
| 225 | + return Err(CompilerError::Type( |
| 226 | + "nested query result lists are not representable".to_string(), |
| 227 | + )); |
| 228 | + } |
| 229 | + Ok((QueryValueKind::List, Some(item_kind), vector_dim)) |
| 230 | + } |
| 231 | + DataType::Struct(_) => Ok((QueryValueKind::Object, None, None)), |
| 232 | + scalar => Ok((scalar_result_kind(scalar)?, None, None)), |
| 233 | + } |
| 234 | +} |
| 235 | + |
| 236 | +fn scalar_result_kind(data_type: &DataType) -> Result<QueryValueKind> { |
| 237 | + match data_type { |
| 238 | + DataType::Utf8 => Ok(QueryValueKind::String), |
| 239 | + DataType::Boolean => Ok(QueryValueKind::Bool), |
| 240 | + DataType::Int32 | DataType::UInt32 => Ok(QueryValueKind::Int), |
| 241 | + DataType::Int64 | DataType::UInt64 => Ok(QueryValueKind::BigInt), |
| 242 | + DataType::Float32 | DataType::Float64 => Ok(QueryValueKind::Float), |
| 243 | + DataType::Date32 => Ok(QueryValueKind::Date), |
| 244 | + DataType::Date64 => Ok(QueryValueKind::DateTime), |
| 245 | + DataType::LargeBinary => Ok(QueryValueKind::Blob), |
| 246 | + other => Err(CompilerError::Type(format!( |
| 247 | + "query result data type `{other}` is not representable" |
| 248 | + ))), |
| 249 | + } |
| 250 | +} |
0 commit comments