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