Skip to main content

meta_language/
sql_adapter.rs

1//! Registry-driven lowering from validated SQL statements to shared query plans.
2
3#[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
20/// SQL profiles whose common subset is normalized by the built-in adapter.
21pub 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/// A registered SQL vendor profile.
33#[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    /// Registry/language key used at the API boundary.
45    #[must_use]
46    pub const fn key(self) -> &'static str {
47        self.key
48    }
49
50    /// Human-readable vendor or standard name.
51    #[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/// One explicit SQL relation and field mapping.
65#[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    /// Creates a relation mapping. Fields remain unsupported until registered.
74    #[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    /// Adds a source-column to canonical-field mapping.
84    #[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    /// SQL relation name, optionally schema-qualified.
96    #[must_use]
97    pub fn source_relation(&self) -> &str {
98        &self.source_relation
99    }
100
101    /// Canonical resource name.
102    #[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/// Explicit, fail-closed schema registry used by SQL lowering.
129#[derive(Clone, Debug, Default, PartialEq, Eq)]
130pub struct SqlSchemaRegistry {
131    relations: BTreeMap<String, SqlRelationMapping>,
132}
133
134impl SqlSchemaRegistry {
135    /// Creates an empty registry.
136    #[must_use]
137    pub const fn new() -> Self {
138        Self {
139            relations: BTreeMap::new(),
140        }
141    }
142
143    /// Registers one relation mapping.
144    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    /// Loads the documented JSON registry shape used by shared parity fixtures.
165    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/// Error category for fail-closed SQL lowering.
212#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum SqlAdapterErrorKind {
214    UnsupportedLanguage,
215    InvalidConcreteSyntax,
216    Syntax,
217    Semantic,
218    Registry,
219}
220
221/// Fail-closed SQL adapter error.
222#[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    /// Error category.
247    #[must_use]
248    pub const fn kind(&self) -> SqlAdapterErrorKind {
249        self.kind
250    }
251
252    /// Optional source byte offset.
253    #[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
407/// Parses, validates, and lowers exactly one SQL statement and its provenance.
408pub 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
420/// Lowers one complete, clean SQL CST into the shared executable IR.
421pub 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}