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