1mod parser;
4mod registry;
5
6use std::collections::BTreeMap;
7
8use crate::configuration::ParseConfiguration;
9use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
10use crate::query_plan::{
11 QueryAggregate, QueryAggregateFunction, QueryComparisonOperator, QueryFilter, QueryOperation,
12 QueryOrder, QueryPlan, QuerySortDirection, QuerySourceEvidence, QueryValue,
13};
14use crate::source::{ByteRange, Point, SourceSpan};
15
16use parser::{ByteSpan, Field, ValueKind, ValueNode};
17pub use registry::{
18 GraphQlAdapterError, GraphQlArgumentRole, GraphQlOperationType, GraphQlRootMapping,
19 GraphQlSchemaRegistry,
20};
21
22#[derive(Clone, Debug, PartialEq)]
24pub struct LoweredQueryPlan {
25 plan: QueryPlan,
26 network: LinkNetwork,
27 root_link: LinkId,
28}
29
30impl LoweredQueryPlan {
31 #[must_use]
33 pub const fn plan(&self) -> &QueryPlan {
34 &self.plan
35 }
36
37 #[must_use]
39 pub const fn network(&self) -> &LinkNetwork {
40 &self.network
41 }
42
43 #[must_use]
45 pub const fn root_link(&self) -> LinkId {
46 self.root_link
47 }
48
49 #[must_use]
51 pub fn into_parts(self) -> (QueryPlan, LinkNetwork, LinkId) {
52 (self.plan, self.network, self.root_link)
53 }
54}
55
56pub fn lower_graphql(
58 source: &str,
59 registry: &GraphQlSchemaRegistry,
60) -> Result<LoweredQueryPlan, GraphQlAdapterError> {
61 let mut network = LinkNetwork::parse(source, "GraphQL", ParseConfiguration::default());
62 if !network.verify_full_match(None).is_clean() {
63 return Err(GraphQlAdapterError::new(
64 "GraphQL CST validation failed; semantic lowering was not attempted",
65 ));
66 }
67 let document = parser::parse(source)?;
68 let [root] = document.root_fields.as_slice() else {
69 return Err(GraphQlAdapterError::new(
70 "exactly one GraphQL root field is required",
71 ));
72 };
73 if root.alias.is_some() {
74 return Err(GraphQlAdapterError::new(
75 "GraphQL root aliases are not represented by the query plan",
76 ));
77 }
78 let mapping = registry.root(document.operation, &root.name)?;
79 let mut plan = QueryPlan::new(mapping.operation, &mapping.resource);
80 plan.source_evidence
81 .push(evidence(source, "root", root.span));
82 lower_arguments(source, root, mapping, &mut plan)?;
83 lower_selection(source, root, mapping, &mut plan)?;
84 validate_plan(&plan)?;
85 let root_link = attach_plan_links(&mut network, &plan);
86 Ok(LoweredQueryPlan {
87 plan,
88 network,
89 root_link,
90 })
91}
92
93fn lower_arguments(
94 source: &str,
95 root: &Field,
96 mapping: &GraphQlRootMapping,
97 plan: &mut QueryPlan,
98) -> Result<(), GraphQlAdapterError> {
99 for argument in &root.arguments {
100 let role = mapping.arguments.get(&argument.name).ok_or_else(|| {
101 GraphQlAdapterError::new(format!("unmapped GraphQL argument {:?}", argument.name))
102 })?;
103 match role {
104 GraphQlArgumentRole::Filter => {
105 plan.filter = Some(lower_filter(&argument.value, mapping)?);
106 }
107 GraphQlArgumentRole::Order => {
108 plan.order = lower_order(&argument.value, mapping)?;
109 }
110 GraphQlArgumentRole::Limit => {
111 plan.limit = Some(non_negative_integer(&argument.value, "limit")?);
112 }
113 GraphQlArgumentRole::Offset => {
114 plan.offset = Some(non_negative_integer(&argument.value, "offset")?);
115 }
116 GraphQlArgumentRole::Group => {
117 plan.group_by = lower_group(&argument.value, mapping)?;
118 }
119 GraphQlArgumentRole::MutationInput => {
120 plan.mutation = lower_mutation(&argument.value, mapping)?;
121 }
122 }
123 plan.source_evidence.push(evidence(
124 source,
125 format!("argument:{}", role.as_str()),
126 argument.value.span,
127 ));
128 }
129 Ok(())
130}
131
132fn lower_selection(
133 source: &str,
134 root: &Field,
135 mapping: &GraphQlRootMapping,
136 plan: &mut QueryPlan,
137) -> Result<(), GraphQlAdapterError> {
138 if root.selection.is_empty() {
139 return Err(GraphQlAdapterError::new(
140 "a mapped GraphQL root must have a projection selection",
141 ));
142 }
143 for field in &root.selection {
144 if !field.selection.is_empty() {
145 return Err(GraphQlAdapterError::new(format!(
146 "nested projection field {:?} has no explicit mapping",
147 field.name
148 )));
149 }
150 if let Some(function) = mapping.aggregates.get(&field.name) {
151 let aggregate_field = aggregate_field(field, mapping)?;
152 if *function != QueryAggregateFunction::Count && aggregate_field.is_none() {
153 return Err(GraphQlAdapterError::new(format!(
154 "aggregate {:?} requires a mapped field argument",
155 field.name
156 )));
157 }
158 plan.aggregates.push(QueryAggregate {
159 function: *function,
160 field: aggregate_field,
161 alias: field.alias.clone(),
162 });
163 plan.source_evidence.push(evidence(
164 source,
165 format!("aggregate:{}", function.as_str()),
166 field.span,
167 ));
168 continue;
169 }
170 if !field.arguments.is_empty() {
171 return Err(GraphQlAdapterError::new(format!(
172 "non-aggregate projection {:?} cannot have arguments",
173 field.name
174 )));
175 }
176 if field.alias.is_some() {
177 return Err(GraphQlAdapterError::new(format!(
178 "projection alias on {:?} is not represented by the query plan",
179 field.name
180 )));
181 }
182 let canonical = mapping.mapped_field(&field.name)?;
183 if !plan.projection.contains(&canonical) {
184 plan.projection.push(canonical.clone());
185 }
186 plan.source_evidence.push(evidence(
187 source,
188 format!("projection:{canonical}"),
189 field.span,
190 ));
191 }
192 Ok(())
193}
194
195fn aggregate_field(
196 field: &Field,
197 mapping: &GraphQlRootMapping,
198) -> Result<Option<String>, GraphQlAdapterError> {
199 match field.arguments.as_slice() {
200 [] => Ok(None),
201 [argument] if argument.name == "field" => {
202 Ok(Some(mapping.mapped_symbol(symbol(&argument.value)?)?))
203 }
204 _ => Err(GraphQlAdapterError::new(format!(
205 "aggregate {:?} accepts only an optional field argument",
206 field.name
207 ))),
208 }
209}
210
211fn lower_filter(
212 value: &ValueNode,
213 mapping: &GraphQlRootMapping,
214) -> Result<QueryFilter, GraphQlAdapterError> {
215 let ValueKind::Object(fields) = &value.value else {
216 return Err(GraphQlAdapterError::new("filter arguments must be objects"));
217 };
218 let mut clauses = Vec::new();
219 for (name, value) in fields {
220 match name.as_str() {
221 "and" => clauses.push(QueryFilter::And(filter_list(value, mapping)?)),
222 "or" => clauses.push(QueryFilter::Or(filter_list(value, mapping)?)),
223 "not" => clauses.push(QueryFilter::Not(Box::new(lower_filter(value, mapping)?))),
224 _ => clauses.extend(field_filter(name, value, mapping)?),
225 }
226 }
227 collapse_and(clauses, "empty filter objects are unsupported")
228}
229
230fn filter_list(
231 value: &ValueNode,
232 mapping: &GraphQlRootMapping,
233) -> Result<Vec<QueryFilter>, GraphQlAdapterError> {
234 let ValueKind::List(values) = &value.value else {
235 return Err(GraphQlAdapterError::new(
236 "GraphQL and/or filter values must be lists",
237 ));
238 };
239 if values.is_empty() {
240 return Err(GraphQlAdapterError::new(
241 "GraphQL and/or filter lists must not be empty",
242 ));
243 }
244 values
245 .iter()
246 .map(|value| lower_filter(value, mapping))
247 .collect()
248}
249
250fn field_filter(
251 source_field: &str,
252 value: &ValueNode,
253 mapping: &GraphQlRootMapping,
254) -> Result<Vec<QueryFilter>, GraphQlAdapterError> {
255 let field = mapping.mapped_field(source_field)?;
256 let ValueKind::Object(operators) = &value.value else {
257 return Ok(vec![QueryFilter::Compare {
258 field,
259 operator: QueryComparisonOperator::Equal,
260 value: query_value(value)?,
261 }]);
262 };
263 if operators.is_empty() {
264 return Err(GraphQlAdapterError::new(
265 "empty comparison objects are unsupported",
266 ));
267 }
268 operators
269 .iter()
270 .map(|(name, value)| {
271 let operator = QueryComparisonOperator::from_graphql_key(name).ok_or_else(|| {
272 GraphQlAdapterError::new(format!("unsupported filter operator {name:?}"))
273 })?;
274 if operator == QueryComparisonOperator::IsNull
275 && !matches!(value.value, ValueKind::Boolean(_))
276 {
277 return Err(GraphQlAdapterError::new("isNull requires a boolean value"));
278 }
279 if matches!(
280 operator,
281 QueryComparisonOperator::In | QueryComparisonOperator::NotIn
282 ) && !matches!(value.value, ValueKind::List(_))
283 {
284 return Err(GraphQlAdapterError::new("in/notIn requires a list value"));
285 }
286 Ok(QueryFilter::Compare {
287 field: field.clone(),
288 operator,
289 value: query_value(value)?,
290 })
291 })
292 .collect()
293}
294
295fn collapse_and(
296 mut clauses: Vec<QueryFilter>,
297 empty_message: &str,
298) -> Result<QueryFilter, GraphQlAdapterError> {
299 match clauses.len() {
300 0 => Err(GraphQlAdapterError::new(empty_message)),
301 1 => Ok(clauses.remove(0)),
302 _ => Ok(QueryFilter::And(clauses)),
303 }
304}
305
306fn lower_order(
307 value: &ValueNode,
308 mapping: &GraphQlRootMapping,
309) -> Result<Vec<QueryOrder>, GraphQlAdapterError> {
310 let entries = match &value.value {
311 ValueKind::List(entries) => entries.as_slice(),
312 ValueKind::Object(_) => std::slice::from_ref(value),
313 _ => {
314 return Err(GraphQlAdapterError::new(
315 "order arguments must be objects or lists of objects",
316 ));
317 }
318 };
319 if entries.is_empty() {
320 return Err(GraphQlAdapterError::new("order lists must not be empty"));
321 }
322 entries
323 .iter()
324 .map(|entry| lower_order_entry(entry, mapping))
325 .collect()
326}
327
328fn lower_order_entry(
329 value: &ValueNode,
330 mapping: &GraphQlRootMapping,
331) -> Result<QueryOrder, GraphQlAdapterError> {
332 let ValueKind::Object(fields) = &value.value else {
333 return Err(GraphQlAdapterError::new(
334 "each order entry must be an object",
335 ));
336 };
337 let field_value = object_field(fields, "field")?;
338 let direction_value = object_field(fields, "direction")?;
339 if fields.len() != 2 {
340 return Err(GraphQlAdapterError::new(
341 "order entries support only field and direction",
342 ));
343 }
344 Ok(QueryOrder {
345 field: mapping.mapped_symbol(symbol(field_value)?)?,
346 direction: direction(direction_value)?,
347 })
348}
349
350fn lower_group(
351 value: &ValueNode,
352 mapping: &GraphQlRootMapping,
353) -> Result<Vec<String>, GraphQlAdapterError> {
354 let values = match &value.value {
355 ValueKind::List(values) => values.as_slice(),
356 ValueKind::Enum(_) | ValueKind::String(_) => std::slice::from_ref(value),
357 _ => return Err(GraphQlAdapterError::new("group arguments must name fields")),
358 };
359 if values.is_empty() {
360 return Err(GraphQlAdapterError::new("group lists must not be empty"));
361 }
362 values
363 .iter()
364 .map(|value| mapping.mapped_symbol(symbol(value)?))
365 .collect()
366}
367
368fn lower_mutation(
369 value: &ValueNode,
370 mapping: &GraphQlRootMapping,
371) -> Result<BTreeMap<String, QueryValue>, GraphQlAdapterError> {
372 let ValueKind::Object(fields) = &value.value else {
373 return Err(GraphQlAdapterError::new(
374 "mutation input arguments must be objects",
375 ));
376 };
377 if fields.is_empty() {
378 return Err(GraphQlAdapterError::new(
379 "mutation inputs must not be empty",
380 ));
381 }
382 fields
383 .iter()
384 .map(|(field, value)| Ok((mapping.mapped_field(field)?, query_value(value)?)))
385 .collect()
386}
387
388fn query_value(value: &ValueNode) -> Result<QueryValue, GraphQlAdapterError> {
389 match &value.value {
390 ValueKind::Null => Ok(QueryValue::Null),
391 ValueKind::Boolean(value) => Ok(QueryValue::Boolean(*value)),
392 ValueKind::Integer(value) => safe_integer(*value).map(QueryValue::Integer),
393 ValueKind::Float(value) => Ok(QueryValue::Float(*value)),
394 ValueKind::String(value) | ValueKind::Enum(value) => Ok(QueryValue::String(value.clone())),
395 ValueKind::List(values) => values
396 .iter()
397 .map(query_value)
398 .collect::<Result<Vec<_>, _>>()
399 .map(QueryValue::List),
400 ValueKind::Object(_) => Err(GraphQlAdapterError::new(
401 "nested GraphQL input objects require explicit field mappings",
402 )),
403 }
404}
405
406fn non_negative_integer(value: &ValueNode, role: &str) -> Result<u64, GraphQlAdapterError> {
407 let ValueKind::Integer(value) = value.value else {
408 return Err(GraphQlAdapterError::new(format!(
409 "GraphQL {role} must be a non-negative integer"
410 )));
411 };
412 safe_integer(value).and_then(|value| {
413 u64::try_from(value).map_err(|_| {
414 GraphQlAdapterError::new(format!("GraphQL {role} must be a non-negative integer"))
415 })
416 })
417}
418
419fn safe_integer(value: i64) -> Result<i64, GraphQlAdapterError> {
420 const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
421 if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
422 Ok(value)
423 } else {
424 Err(GraphQlAdapterError::new(
425 "GraphQL integer is outside the cross-language safe range",
426 ))
427 }
428}
429
430fn symbol(value: &ValueNode) -> Result<&str, GraphQlAdapterError> {
431 match &value.value {
432 ValueKind::Enum(value) | ValueKind::String(value) => Ok(value),
433 _ => Err(GraphQlAdapterError::new(
434 "canonical field references must be GraphQL enums or strings",
435 )),
436 }
437}
438
439fn direction(value: &ValueNode) -> Result<QuerySortDirection, GraphQlAdapterError> {
440 match symbol(value)?.to_ascii_lowercase().as_str() {
441 "asc" => Ok(QuerySortDirection::Ascending),
442 "desc" => Ok(QuerySortDirection::Descending),
443 other => Err(GraphQlAdapterError::new(format!(
444 "unsupported sort direction {other:?}"
445 ))),
446 }
447}
448
449fn object_field<'a>(
450 fields: &'a [(String, ValueNode)],
451 name: &str,
452) -> Result<&'a ValueNode, GraphQlAdapterError> {
453 fields
454 .iter()
455 .find_map(|(field, value)| (field == name).then_some(value))
456 .ok_or_else(|| GraphQlAdapterError::new(format!("missing object field {name:?}")))
457}
458
459fn validate_plan(plan: &QueryPlan) -> Result<(), GraphQlAdapterError> {
460 if plan.projection.is_empty() && plan.aggregates.is_empty() {
461 return Err(GraphQlAdapterError::new(
462 "a query plan requires a projection or aggregate",
463 ));
464 }
465 match plan.operation {
466 QueryOperation::Insert | QueryOperation::Update if plan.mutation.is_empty() => Err(
467 GraphQlAdapterError::new("insert/update mutations require a mapped mutation input"),
468 ),
469 QueryOperation::Select | QueryOperation::Delete if !plan.mutation.is_empty() => {
470 Err(GraphQlAdapterError::new(
471 "select/delete operations cannot contain mutation assignments",
472 ))
473 }
474 _ => Ok(()),
475 }
476}
477
478fn attach_plan_links(network: &mut LinkNetwork, plan: &QueryPlan) -> LinkId {
479 let cst_links = plan
480 .source_evidence
481 .iter()
482 .map(|evidence| closest_cst(network, evidence.span()))
483 .collect::<Vec<_>>();
484 let plan_concept = network.insert_point("executable-query-plan");
485 let mut references = vec![plan_concept];
486 for (evidence, cst) in plan.source_evidence.iter().zip(cst_links) {
487 let concept = network.insert_point(evidence.role());
488 let mut child_references = vec![concept];
489 if let Some(cst) = cst {
490 child_references.push(cst);
491 }
492 let child = network.insert_dynamic_link(
493 &child_references,
494 LinkMetadata::new()
495 .with_link_type(LinkType::Semantic)
496 .with_named(true)
497 .with_term(evidence.role())
498 .with_language("GraphQL")
499 .with_span(evidence.span()),
500 );
501 references.push(child);
502 }
503 if let Some(cst) = plan
504 .source_evidence
505 .first()
506 .and_then(|evidence| closest_cst(network, evidence.span()))
507 {
508 references.push(cst);
509 }
510 let root_span = plan
511 .source_evidence
512 .first()
513 .map(QuerySourceEvidence::span)
514 .expect("lowered plans always retain root evidence");
515 network.insert_dynamic_link(
516 &references,
517 LinkMetadata::new()
518 .with_link_type(LinkType::Semantic)
519 .with_named(true)
520 .with_term("executable-query-plan")
521 .with_language("GraphQL")
522 .with_span(root_span),
523 )
524}
525
526fn closest_cst(network: &LinkNetwork, span: SourceSpan) -> Option<LinkId> {
527 let target = span.byte_range();
528 network
529 .links()
530 .filter(|link| link.metadata().link_type() == Some(LinkType::Syntax))
531 .filter_map(|link| Some((link, link.metadata().span()?.byte_range())))
532 .filter(|(_, candidate)| {
533 candidate.start() <= target.start() && candidate.end() >= target.end()
534 })
535 .min_by_key(|(_, candidate)| candidate.end() - candidate.start())
536 .map(|(link, _)| Link::id(link))
537}
538
539fn evidence(source: &str, role: impl Into<String>, span: ByteSpan) -> QuerySourceEvidence {
540 QuerySourceEvidence::new(role, source_span(source, span))
541}
542
543fn source_span(source: &str, span: ByteSpan) -> SourceSpan {
544 SourceSpan::new(
545 ByteRange::new(span.start, span.end),
546 point_at(source, span.start),
547 point_at(source, span.end),
548 )
549}
550
551fn point_at(source: &str, offset: usize) -> Point {
552 let prefix = &source[..offset];
553 let row = prefix.bytes().filter(|byte| *byte == b'\n').count();
554 let column = prefix
555 .rsplit_once('\n')
556 .map_or(prefix.len(), |(_, line)| line.len());
557 Point::new(row, column)
558}