1#[path = "query_plan/sql.rs"]
4mod parser;
5
6use std::collections::BTreeMap;
7use std::fmt;
8
9use serde_json::Value;
10
11use crate::configuration::ParseConfiguration;
12use crate::link_network::{LinkNetwork, LinkType};
13use crate::query_plan::{
14 attach_plan_links, LoweredQueryPlan, QueryAggregate, QueryAggregateFunction,
15 QueryComparisonOperator, QueryFilter, QueryOperation as CanonicalOperation, QueryOrder,
16 QueryPlan, QuerySortDirection, QuerySourceEvidence, QueryValue as CanonicalValue,
17};
18use crate::source::SourceSpan;
19
20pub const SQL_DIALECT_PROFILES: &[SqlDialectProfile] = &[
22 SqlDialectProfile::new("sql-ansi", "ANSI SQL"),
23 SqlDialectProfile::new("sql-postgres", "PostgreSQL"),
24 SqlDialectProfile::new("sql-mysql", "MySQL"),
25 SqlDialectProfile::new("sql-sqlite", "SQLite"),
26 SqlDialectProfile::new("sql-server", "SQL Server"),
27 SqlDialectProfile::new("sql-oracle", "Oracle"),
28 SqlDialectProfile::new("sql-bigquery", "BigQuery"),
29 SqlDialectProfile::new("sql-snowflake", "Snowflake"),
30];
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct SqlDialectProfile {
35 key: &'static str,
36 vendor: &'static str,
37}
38
39impl SqlDialectProfile {
40 const fn new(key: &'static str, vendor: &'static str) -> Self {
41 Self { key, vendor }
42 }
43
44 #[must_use]
46 pub const fn key(self) -> &'static str {
47 self.key
48 }
49
50 #[must_use]
52 pub const fn vendor(self) -> &'static str {
53 self.vendor
54 }
55
56 fn lookup(key: &str) -> Option<Self> {
57 SQL_DIALECT_PROFILES
58 .iter()
59 .copied()
60 .find(|profile| key.eq_ignore_ascii_case(profile.key))
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SqlRelationMapping {
67 source_relation: String,
68 resource: String,
69 fields: BTreeMap<String, String>,
70}
71
72impl SqlRelationMapping {
73 #[must_use]
75 pub fn new(source_relation: impl Into<String>, resource: impl Into<String>) -> Self {
76 Self {
77 source_relation: source_relation.into(),
78 resource: resource.into(),
79 fields: BTreeMap::new(),
80 }
81 }
82
83 #[must_use]
85 pub fn with_field(
86 mut self,
87 source_name: impl Into<String>,
88 canonical_field: impl Into<String>,
89 ) -> Self {
90 self.fields
91 .insert(source_name.into(), canonical_field.into());
92 self
93 }
94
95 #[must_use]
97 pub fn source_relation(&self) -> &str {
98 &self.source_relation
99 }
100
101 #[must_use]
103 pub fn resource(&self) -> &str {
104 &self.resource
105 }
106
107 fn mapped_field(&self, source: &str) -> Result<String, SqlAdapterError> {
108 let matches = self
109 .fields
110 .iter()
111 .filter(|(name, _)| name.eq_ignore_ascii_case(source))
112 .map(|(_, canonical)| canonical)
113 .collect::<Vec<_>>();
114 match matches.as_slice() {
115 [canonical] => Ok((*canonical).clone()),
116 [] => Err(SqlAdapterError::semantic(format!(
117 "unmapped SQL field {source:?} for relation {:?}",
118 self.source_relation
119 ))),
120 _ => Err(SqlAdapterError::registry(format!(
121 "ambiguous SQL field mapping {source:?} for relation {:?}",
122 self.source_relation
123 ))),
124 }
125 }
126}
127
128#[derive(Clone, Debug, Default, PartialEq, Eq)]
130pub struct SqlSchemaRegistry {
131 relations: BTreeMap<String, SqlRelationMapping>,
132}
133
134impl SqlSchemaRegistry {
135 #[must_use]
137 pub const fn new() -> Self {
138 Self {
139 relations: BTreeMap::new(),
140 }
141 }
142
143 pub fn register_relation(
145 &mut self,
146 mapping: SqlRelationMapping,
147 ) -> Result<(), SqlAdapterError> {
148 validate_mapping(&mapping)?;
149 if self
150 .relations
151 .keys()
152 .any(|name| name.eq_ignore_ascii_case(&mapping.source_relation))
153 {
154 return Err(SqlAdapterError::registry(format!(
155 "duplicate or case-ambiguous SQL relation mapping {:?}",
156 mapping.source_relation
157 )));
158 }
159 self.relations
160 .insert(mapping.source_relation.clone(), mapping);
161 Ok(())
162 }
163
164 pub fn from_json(value: &Value) -> Result<Self, SqlAdapterError> {
166 let relations = value
167 .get("relations")
168 .and_then(Value::as_array)
169 .ok_or_else(|| SqlAdapterError::registry("registry.relations must be an array"))?;
170 let mut registry = Self::new();
171 for relation in relations {
172 let mut mapping = SqlRelationMapping::new(
173 required_string(relation, "sourceRelation")?,
174 required_string(relation, "resource")?,
175 );
176 let fields = relation
177 .get("fields")
178 .and_then(Value::as_object)
179 .ok_or_else(|| SqlAdapterError::registry("relation.fields must be an object"))?;
180 for (source, canonical) in fields {
181 let canonical = canonical.as_str().ok_or_else(|| {
182 SqlAdapterError::registry("canonical SQL fields must be strings")
183 })?;
184 mapping = mapping.with_field(source, canonical);
185 }
186 registry.register_relation(mapping)?;
187 }
188 Ok(registry)
189 }
190
191 fn relation(&self, path: &[String]) -> Result<&SqlRelationMapping, SqlAdapterError> {
192 let source = path.join(".");
193 let matches = self
194 .relations
195 .iter()
196 .filter(|(name, _)| name.eq_ignore_ascii_case(&source))
197 .map(|(_, mapping)| mapping)
198 .collect::<Vec<_>>();
199 match matches.as_slice() {
200 [mapping] => Ok(*mapping),
201 [] => Err(SqlAdapterError::semantic(format!(
202 "unmapped SQL relation {source:?}"
203 ))),
204 _ => Err(SqlAdapterError::registry(format!(
205 "ambiguous SQL relation mapping {source:?}"
206 ))),
207 }
208 }
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum SqlAdapterErrorKind {
214 UnsupportedLanguage,
215 InvalidConcreteSyntax,
216 Syntax,
217 Semantic,
218 Registry,
219}
220
221#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct SqlAdapterError {
224 kind: SqlAdapterErrorKind,
225 message: String,
226 offset: Option<usize>,
227}
228
229impl SqlAdapterError {
230 fn new(kind: SqlAdapterErrorKind, message: impl Into<String>, offset: Option<usize>) -> Self {
231 Self {
232 kind,
233 message: message.into(),
234 offset,
235 }
236 }
237
238 fn semantic(message: impl Into<String>) -> Self {
239 Self::new(SqlAdapterErrorKind::Semantic, message, None)
240 }
241
242 fn registry(message: impl Into<String>) -> Self {
243 Self::new(SqlAdapterErrorKind::Registry, message, None)
244 }
245
246 #[must_use]
248 pub const fn kind(&self) -> SqlAdapterErrorKind {
249 self.kind
250 }
251
252 #[must_use]
254 pub const fn offset(&self) -> Option<usize> {
255 self.offset
256 }
257}
258
259impl fmt::Display for SqlAdapterError {
260 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261 if let Some(offset) = self.offset {
262 write!(formatter, "{} at byte {offset}", self.message)
263 } else {
264 formatter.write_str(&self.message)
265 }
266 }
267}
268
269impl std::error::Error for SqlAdapterError {}
270
271#[derive(Clone, Debug, PartialEq, Eq)]
272struct QuerySource {
273 path: Vec<String>,
274 alias: Option<String>,
275}
276
277#[derive(Clone, Debug, PartialEq, Eq)]
278struct Projection {
279 expression: QueryExpression,
280 alias: Option<String>,
281}
282
283#[derive(Clone, Debug, PartialEq, Eq)]
284struct Assignment {
285 column: Vec<String>,
286 value: QueryExpression,
287}
288
289#[derive(Clone, Debug, PartialEq, Eq)]
290struct SortExpression {
291 expression: QueryExpression,
292 direction: SortDirection,
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296enum SortDirection {
297 Ascending,
298 Descending,
299}
300
301#[derive(Clone, Debug, PartialEq, Eq)]
302enum QueryValue {
303 Null,
304 Boolean { value: bool },
305 Number { value: String },
306 String { value: String },
307}
308
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310enum AggregateFunction {
311 Count,
312 Sum,
313 Avg,
314 Min,
315 Max,
316 VariancePopulation,
317 StandardDeviationPopulation,
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
321enum UnaryOperator {
322 Not,
323 Negate,
324 Positive,
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328enum BinaryOperator {
329 Or,
330 And,
331 Equal,
332 NotEqual,
333 LessThan,
334 LessThanOrEqual,
335 GreaterThan,
336 GreaterThanOrEqual,
337 Is,
338 IsNot,
339 Like,
340 NotLike,
341 Add,
342 Subtract,
343 Multiply,
344 Divide,
345}
346
347#[derive(Clone, Debug, PartialEq, Eq)]
348enum QueryExpression {
349 Column {
350 path: Vec<String>,
351 },
352 Literal {
353 value: QueryValue,
354 },
355 Parameter {
356 name: String,
357 },
358 Wildcard,
359 Unary {
360 operator: UnaryOperator,
361 operand: Box<Self>,
362 },
363 Binary {
364 operator: BinaryOperator,
365 left: Box<Self>,
366 right: Box<Self>,
367 },
368 Aggregate {
369 function: AggregateFunction,
370 expression: Box<Self>,
371 distinct: bool,
372 },
373 Function {
374 name: String,
375 arguments: Vec<Self>,
376 },
377}
378
379#[derive(Clone, Debug, PartialEq, Eq)]
380enum QueryOperation {
381 Select {
382 distinct: bool,
383 projection: Vec<Projection>,
384 source: Option<QuerySource>,
385 predicate: Option<QueryExpression>,
386 group_by: Vec<QueryExpression>,
387 order_by: Vec<SortExpression>,
388 limit: Option<u64>,
389 offset: Option<u64>,
390 },
391 Insert {
392 into: QuerySource,
393 columns: Vec<String>,
394 rows: Vec<Vec<QueryExpression>>,
395 },
396 Update {
397 table: QuerySource,
398 assignments: Vec<Assignment>,
399 predicate: Option<QueryExpression>,
400 },
401 Delete {
402 source: QuerySource,
403 predicate: Option<QueryExpression>,
404 },
405}
406
407pub fn lower_sql(
409 source: &str,
410 language: &str,
411 registry: &SqlSchemaRegistry,
412) -> Result<LoweredQueryPlan, SqlAdapterError> {
413 ensure_profile(language)?;
414 let mut network = LinkNetwork::parse(source, language, ParseConfiguration::default());
415 let plan = lower_sql_cst(&network, language, registry)?;
416 let root_link = attach_plan_links(&mut network, &plan, language);
417 Ok(LoweredQueryPlan::new(plan, network, root_link))
418}
419
420pub fn lower_sql_cst(
422 network: &LinkNetwork,
423 language: &str,
424 registry: &SqlSchemaRegistry,
425) -> Result<QueryPlan, SqlAdapterError> {
426 ensure_profile(language)?;
427 if !network.verify_full_match(None).is_clean() {
428 return Err(SqlAdapterError::new(
429 SqlAdapterErrorKind::InvalidConcreteSyntax,
430 "SQL CST validation failed; semantic lowering was not attempted",
431 None,
432 ));
433 }
434 let source = network.reconstruct_text();
435 if source.trim().is_empty() {
436 return Err(SqlAdapterError::new(
437 SqlAdapterErrorKind::Syntax,
438 "SQL statement is empty",
439 Some(0),
440 ));
441 }
442 let operation = parser::parse(&source)?;
443 let operation_label = match &operation {
444 QueryOperation::Select { .. } => "select",
445 QueryOperation::Insert { .. } => "insert",
446 QueryOperation::Update { .. } => "update",
447 QueryOperation::Delete { .. } => "delete",
448 };
449 let mut plan = lower_operation(operation, registry)?;
450 let span = statement_span(network, language, operation_label).ok_or_else(|| {
451 SqlAdapterError::new(
452 SqlAdapterErrorKind::InvalidConcreteSyntax,
453 "SQL lowering requires grammar-backed CST syntax evidence",
454 None,
455 )
456 })?;
457 plan.add_source_evidence(QuerySourceEvidence::new(
458 format!("statement:{language}"),
459 span,
460 ));
461 Ok(plan)
462}
463
464fn lower_operation(
465 operation: QueryOperation,
466 registry: &SqlSchemaRegistry,
467) -> Result<QueryPlan, SqlAdapterError> {
468 match operation {
469 QueryOperation::Select {
470 distinct,
471 projection,
472 source,
473 predicate,
474 group_by,
475 order_by,
476 limit,
477 offset,
478 } => {
479 if distinct {
480 return Err(SqlAdapterError::semantic(
481 "SELECT DISTINCT requires an explicit query-plan extension",
482 ));
483 }
484 let source = source.ok_or_else(|| {
485 SqlAdapterError::semantic("SELECT without FROM has no canonical resource")
486 })?;
487 let mapping = registry.relation(&source.path)?;
488 let mut plan = QueryPlan::new(CanonicalOperation::Select, mapping.resource());
489 for item in projection {
490 lower_projection(item, mapping, &source, &mut plan)?;
491 }
492 if plan.projection.is_empty() && plan.aggregates.is_empty() {
493 return Err(SqlAdapterError::semantic(
494 "SELECT requires a mapped projection or aggregate",
495 ));
496 }
497 plan.filter = predicate
498 .as_ref()
499 .map(|value| lower_filter(value, mapping, &source))
500 .transpose()?;
501 plan.group_by = group_by
502 .iter()
503 .map(|value| mapped_column(value, mapping, &source))
504 .collect::<Result<Vec<_>, _>>()?;
505 plan.order = order_by
506 .iter()
507 .map(|order| {
508 Ok(QueryOrder::new(
509 mapped_column(&order.expression, mapping, &source)?,
510 match order.direction {
511 SortDirection::Ascending => QuerySortDirection::Ascending,
512 SortDirection::Descending => QuerySortDirection::Descending,
513 },
514 ))
515 })
516 .collect::<Result<Vec<_>, SqlAdapterError>>()?;
517 plan.set_pagination(limit, offset);
518 Ok(plan)
519 }
520 QueryOperation::Insert {
521 into,
522 columns,
523 rows,
524 } => {
525 let mapping = registry.relation(&into.path)?;
526 let [row] = rows.as_slice() else {
527 return Err(SqlAdapterError::semantic(
528 "multi-row INSERT requires an explicit query-plan extension",
529 ));
530 };
531 if columns.is_empty() {
532 return Err(SqlAdapterError::semantic(
533 "INSERT requires an explicit mapped column list",
534 ));
535 }
536 let mut plan = QueryPlan::new(CanonicalOperation::Insert, mapping.resource());
537 for (column, value) in columns.iter().zip(row) {
538 plan.set_mutation_value(mapping.mapped_field(column)?, lower_value(value)?);
539 }
540 Ok(plan)
541 }
542 QueryOperation::Update {
543 table,
544 assignments,
545 predicate,
546 } => {
547 let mapping = registry.relation(&table.path)?;
548 let mut plan = QueryPlan::new(CanonicalOperation::Update, mapping.resource());
549 for assignment in assignments {
550 let field = mapped_path(&assignment.column, mapping, &table)?;
551 plan.set_mutation_value(field, lower_value(&assignment.value)?);
552 }
553 plan.filter = predicate
554 .as_ref()
555 .map(|value| lower_filter(value, mapping, &table))
556 .transpose()?;
557 Ok(plan)
558 }
559 QueryOperation::Delete { source, predicate } => {
560 let mapping = registry.relation(&source.path)?;
561 let mut plan = QueryPlan::new(CanonicalOperation::Delete, mapping.resource());
562 plan.filter = predicate
563 .as_ref()
564 .map(|value| lower_filter(value, mapping, &source))
565 .transpose()?;
566 Ok(plan)
567 }
568 }
569}
570
571fn lower_projection(
572 projection: Projection,
573 mapping: &SqlRelationMapping,
574 source: &QuerySource,
575 plan: &mut QueryPlan,
576) -> Result<(), SqlAdapterError> {
577 match projection.expression {
578 QueryExpression::Column { path } => {
579 if projection.alias.is_some() {
580 return Err(SqlAdapterError::semantic(
581 "non-aggregate projection aliases are not represented by the query plan",
582 ));
583 }
584 let field = mapped_path(&path, mapping, source)?;
585 if !plan.projection.contains(&field) {
586 plan.add_projection(field);
587 }
588 }
589 QueryExpression::Aggregate {
590 function,
591 expression,
592 distinct,
593 } => {
594 if distinct {
595 return Err(SqlAdapterError::semantic(
596 "DISTINCT aggregates require an explicit query-plan extension",
597 ));
598 }
599 let field = match expression.as_ref() {
600 QueryExpression::Wildcard if function == AggregateFunction::Count => None,
601 expression => Some(mapped_column(expression, mapping, source)?),
602 };
603 if function != AggregateFunction::Count && field.is_none() {
604 return Err(SqlAdapterError::semantic(
605 "non-count aggregates require a mapped field",
606 ));
607 }
608 plan.add_aggregate(QueryAggregate::new(
609 aggregate_function(function),
610 field,
611 projection.alias,
612 ));
613 }
614 _ => {
615 return Err(SqlAdapterError::semantic(
616 "SQL projection expression is outside the shared query-plan subset",
617 ));
618 }
619 }
620 Ok(())
621}
622
623fn lower_filter(
624 expression: &QueryExpression,
625 mapping: &SqlRelationMapping,
626 source: &QuerySource,
627) -> Result<QueryFilter, SqlAdapterError> {
628 match expression {
629 QueryExpression::Unary {
630 operator: UnaryOperator::Not,
631 operand,
632 } => Ok(QueryFilter::Not(Box::new(lower_filter(
633 operand, mapping, source,
634 )?))),
635 QueryExpression::Binary {
636 operator: BinaryOperator::And,
637 left,
638 right,
639 } => Ok(QueryFilter::And(vec![
640 lower_filter(left, mapping, source)?,
641 lower_filter(right, mapping, source)?,
642 ])),
643 QueryExpression::Binary {
644 operator: BinaryOperator::Or,
645 left,
646 right,
647 } => Ok(QueryFilter::Or(vec![
648 lower_filter(left, mapping, source)?,
649 lower_filter(right, mapping, source)?,
650 ])),
651 QueryExpression::Binary {
652 operator,
653 left,
654 right,
655 } => {
656 let field = mapped_column(left, mapping, source)?;
657 let (operator, value) = comparison(*operator, right)?;
658 Ok(QueryFilter::Compare {
659 field,
660 operator,
661 value,
662 })
663 }
664 _ => Err(SqlAdapterError::semantic(
665 "SQL predicate is outside the shared query-plan subset",
666 )),
667 }
668}
669
670fn comparison(
671 operator: BinaryOperator,
672 right: &QueryExpression,
673) -> Result<(QueryComparisonOperator, CanonicalValue), SqlAdapterError> {
674 let canonical = match operator {
675 BinaryOperator::Equal => QueryComparisonOperator::Equal,
676 BinaryOperator::NotEqual => QueryComparisonOperator::NotEqual,
677 BinaryOperator::LessThan => QueryComparisonOperator::LessThan,
678 BinaryOperator::LessThanOrEqual => QueryComparisonOperator::LessThanOrEqual,
679 BinaryOperator::GreaterThan => QueryComparisonOperator::GreaterThan,
680 BinaryOperator::GreaterThanOrEqual => QueryComparisonOperator::GreaterThanOrEqual,
681 BinaryOperator::Like => QueryComparisonOperator::Like,
682 BinaryOperator::Is | BinaryOperator::IsNot => {
683 if !matches!(
684 right,
685 QueryExpression::Literal {
686 value: QueryValue::Null
687 }
688 ) {
689 return Err(SqlAdapterError::semantic(
690 "only IS NULL and IS NOT NULL are in the shared query-plan subset",
691 ));
692 }
693 return Ok((
694 QueryComparisonOperator::IsNull,
695 CanonicalValue::Boolean(operator == BinaryOperator::Is),
696 ));
697 }
698 BinaryOperator::NotLike
699 | BinaryOperator::Add
700 | BinaryOperator::Subtract
701 | BinaryOperator::Multiply
702 | BinaryOperator::Divide
703 | BinaryOperator::And
704 | BinaryOperator::Or => {
705 return Err(SqlAdapterError::semantic(
706 "SQL comparison requires an explicit query-plan extension",
707 ));
708 }
709 };
710 Ok((canonical, lower_value(right)?))
711}
712
713fn mapped_column(
714 expression: &QueryExpression,
715 mapping: &SqlRelationMapping,
716 source: &QuerySource,
717) -> Result<String, SqlAdapterError> {
718 let QueryExpression::Column { path } = expression else {
719 return Err(SqlAdapterError::semantic(
720 "query-plan fields must be direct mapped SQL columns",
721 ));
722 };
723 mapped_path(path, mapping, source)
724}
725
726fn mapped_path(
727 path: &[String],
728 mapping: &SqlRelationMapping,
729 source: &QuerySource,
730) -> Result<String, SqlAdapterError> {
731 let Some(field) = path.last() else {
732 return Err(SqlAdapterError::semantic("SQL column path is empty"));
733 };
734 if path.len() > 1 {
735 let qualifier = path[..path.len() - 1].join(".");
736 let source_name = source.path.join(".");
737 let source_tail = source.path.last().map(String::as_str).unwrap_or_default();
738 let qualifier_matches = qualifier.eq_ignore_ascii_case(&source_name)
739 || qualifier.eq_ignore_ascii_case(source_tail)
740 || source
741 .alias
742 .as_deref()
743 .is_some_and(|alias| qualifier.eq_ignore_ascii_case(alias));
744 if !qualifier_matches {
745 return Err(SqlAdapterError::semantic(format!(
746 "SQL column qualifier {qualifier:?} does not identify the mapped relation"
747 )));
748 }
749 }
750 mapping.mapped_field(field)
751}
752
753fn lower_value(expression: &QueryExpression) -> Result<CanonicalValue, SqlAdapterError> {
754 match expression {
755 QueryExpression::Literal { value } => match value {
756 QueryValue::Null => Ok(CanonicalValue::Null),
757 QueryValue::Boolean { value } => Ok(CanonicalValue::Boolean(*value)),
758 QueryValue::Number { value } => parse_number(value),
759 QueryValue::String { value } => Ok(CanonicalValue::String(value.clone())),
760 },
761 QueryExpression::Unary {
762 operator: UnaryOperator::Negate,
763 operand,
764 } => match lower_value(operand)? {
765 CanonicalValue::Integer(value) => {
766 safe_integer(value.checked_neg().ok_or_else(|| {
767 SqlAdapterError::semantic("SQL integer is outside the supported range")
768 })?)
769 }
770 CanonicalValue::Float(value) => Ok(CanonicalValue::Float(-value)),
771 _ => Err(SqlAdapterError::semantic(
772 "unary minus requires a numeric SQL literal",
773 )),
774 },
775 QueryExpression::Unary {
776 operator: UnaryOperator::Positive,
777 operand,
778 } => lower_value(operand),
779 _ => Err(SqlAdapterError::semantic(
780 "query-plan values must be bounded SQL literals",
781 )),
782 }
783}
784
785fn parse_number(value: &str) -> Result<CanonicalValue, SqlAdapterError> {
786 if value.contains('.') {
787 let value = value
788 .parse::<f64>()
789 .map_err(|_| SqlAdapterError::semantic("invalid SQL floating-point literal"))?;
790 if !value.is_finite() {
791 return Err(SqlAdapterError::semantic(
792 "SQL floating-point values must be finite",
793 ));
794 }
795 Ok(CanonicalValue::Float(value))
796 } else {
797 let value = value
798 .parse::<i64>()
799 .map_err(|_| SqlAdapterError::semantic("SQL integer is outside the supported range"))?;
800 safe_integer(value)
801 }
802}
803
804fn safe_integer(value: i64) -> Result<CanonicalValue, SqlAdapterError> {
805 const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
806 if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
807 Ok(CanonicalValue::Integer(value))
808 } else {
809 Err(SqlAdapterError::semantic(
810 "SQL integer is outside the cross-language safe range",
811 ))
812 }
813}
814
815const fn aggregate_function(function: AggregateFunction) -> QueryAggregateFunction {
816 match function {
817 AggregateFunction::Count => QueryAggregateFunction::Count,
818 AggregateFunction::Sum => QueryAggregateFunction::Sum,
819 AggregateFunction::Avg => QueryAggregateFunction::Average,
820 AggregateFunction::Min => QueryAggregateFunction::Minimum,
821 AggregateFunction::Max => QueryAggregateFunction::Maximum,
822 AggregateFunction::VariancePopulation => QueryAggregateFunction::PopulationVariance,
823 AggregateFunction::StandardDeviationPopulation => {
824 QueryAggregateFunction::PopulationStandardDeviation
825 }
826 }
827}
828
829fn ensure_profile(language: &str) -> Result<(), SqlAdapterError> {
830 if SqlDialectProfile::lookup(language).is_some() {
831 Ok(())
832 } else {
833 Err(SqlAdapterError::new(
834 SqlAdapterErrorKind::UnsupportedLanguage,
835 format!("unsupported SQL profile {language:?}"),
836 None,
837 ))
838 }
839}
840
841fn statement_span(network: &LinkNetwork, language: &str, operation: &str) -> Option<SourceSpan> {
842 network
843 .links()
844 .filter(|link| link.metadata().link_type() == Some(LinkType::Syntax))
845 .filter(|link| {
846 link.metadata()
847 .language()
848 .is_some_and(|value| value.eq_ignore_ascii_case(language))
849 })
850 .filter_map(|link| {
851 let span = link.metadata().span()?;
852 let term = link.metadata().term().unwrap_or_default();
853 let score = usize::from(term.eq_ignore_ascii_case(operation)) * 2
854 + usize::from(term.to_ascii_lowercase().contains(operation));
855 Some((span, score))
856 })
857 .max_by_key(|(span, score)| (span.byte_range().end() - span.byte_range().start(), *score))
858 .map(|(span, _)| span)
859}
860
861fn validate_mapping(mapping: &SqlRelationMapping) -> Result<(), SqlAdapterError> {
862 if mapping.source_relation.trim().is_empty() || mapping.resource.trim().is_empty() {
863 return Err(SqlAdapterError::registry(
864 "SQL relation and canonical resource names must not be empty",
865 ));
866 }
867 if mapping.fields.is_empty() {
868 return Err(SqlAdapterError::registry(
869 "SQL relation mappings require at least one explicit field",
870 ));
871 }
872 let names = mapping.fields.keys().collect::<Vec<_>>();
873 for (index, left) in names.iter().enumerate() {
874 if left.trim().is_empty() || mapping.fields[*left].trim().is_empty() {
875 return Err(SqlAdapterError::registry(
876 "SQL field mapping names must not be empty",
877 ));
878 }
879 if names[index + 1..]
880 .iter()
881 .any(|right| left.eq_ignore_ascii_case(right))
882 {
883 return Err(SqlAdapterError::registry(format!(
884 "case-ambiguous SQL field mapping {left:?}"
885 )));
886 }
887 }
888 Ok(())
889}
890
891fn required_string<'a>(value: &'a Value, name: &str) -> Result<&'a str, SqlAdapterError> {
892 value.get(name).and_then(Value::as_str).ok_or_else(|| {
893 SqlAdapterError::registry(format!("registry field {name:?} must be a string"))
894 })
895}