LCOV - code coverage report
Current view: top level - dashql-core/src/analyzer - clfn_calls.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 94.1 % 426 401
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! Calls in a namespace with client functions: check a `CALL` against the signature of its function, and describe it for the
       2              : //! runtime of the host.
       3              : //!
       4              : //! `CALL` is a generic statement. A spec shadows its namespace on a connection, every other call goes to the backend as
       5              : //! written and the checks don't apply to it. They walk the value of the script, so recursive schemas need no care. See
       6              : //! `docs/design/client_functions.md`.
       7              : 
       8              : use super::state::AnalysisState;
       9              : use super::AnalyzedScript;
      10              : use crate::catalog::clfn::*;
      11              : use crate::catalog::{ClientFunctionSpec, CatalogSnapshot};
      12              : use crate::utils::unquote_string_literal;
      13              : use dashql_buffers::analyzer::{AnalyzerErrorSeverity, AnalyzerErrorType};
      14              : use dashql_buffers::parser::{AttributeKey as K, Node, NodeType};
      15              : use dashql_parser::attrs;
      16              : use dashql_parser::parser::context::resolve_text_span;
      17              : use dashql_parser::parser::ParsedScript;
      18              : use dashql_parser::scanner::{ScannedScript, Symbol};
      19              : use dashql_parser::symbol_kind::*;
      20              : use serde_json::{json, Map, Number, Value};
      21              : use std::sync::Arc;
      22              : 
      23              : /// A `CALL` in a namespace with client functions, it never reaches the backend
      24              : #[derive(Clone, Debug)]
      25              : pub struct ClientFunctionCall {
      26              :     /// The statement
      27              :     pub ast_statement_id: u32,
      28              :     /// The call, the root of the statement or the body of its EXPLAIN
      29              :     pub ast_node_id: u32,
      30              :     /// Prints the request, sends nothing?
      31              :     pub explain: bool,
      32              :     /// The namespace
      33              :     pub namespace: Arc<str>,
      34              :     /// The function, None if the spec does not know it
      35              :     pub function: Option<Arc<str>>,
      36              :     /// The effect of the function, a write if it is unknown
      37              :     pub effect: ClientFunctionEffect,
      38              : }
      39              : 
      40              : impl ClientFunctionCall {
      41              :     /// Can another script read the result? A call of a known function that reads, not its EXPLAIN.
      42           16 :     pub fn readable(&self) -> bool {
      43           16 :         !self.explain && self.function.is_some() && self.effect == ClientFunctionEffect::Read
      44           16 :     }
      45              : }
      46              : 
      47              : /// Does a statement start with a call in a namespace with client functions? This comes from the tokens, so it also holds
      48              : /// for a call that doesn't parse, which the analysis never sees. A host that sends text to a backend asks this first.
      49              : /// An EXPLAIN may put anything before the call: `EXPLAIN (analyze) CALL` doesn't parse, and no backend may get it either.
      50            0 : pub fn starts_client_function_call(scanned: &ScannedScript, catalog: &CatalogSnapshot) -> bool {
      51            0 :     let starts_call = |symbols: &[Symbol]| {
      52            0 :         let [call, namespace, dot, ..] = symbols else { return false };
      53            0 :         if call.kind != S_CALL_P || (dot.kind != S_DOT && dot.kind != S_DOT_TRAILING) {
      54            0 :             return false;
      55            0 :         }
      56            0 :         let location = namespace.location;
      57            0 :         let name = &scanned.text.as_bytes()[location.offset as usize..(location.offset + location.length) as usize];
      58            0 :         catalog.client_function_spec(&String::from_utf8_lossy(name).trim_matches('"').to_ascii_lowercase()).is_some()
      59            0 :     };
      60            0 :     scanned.symbols.split(|symbol| symbol.kind == S_SEMICOLON).any(|statement| match statement.split_first() {
      61            0 :         Some((first, rest)) if first.kind == S_EXPLAIN => (0..rest.len()).any(|i| starts_call(&rest[i..])),
      62            0 :         _ => starts_call(statement),
      63            0 :     })
      64            0 : }
      65              : 
      66              : /// The call of a statement: the root or the body of an EXPLAIN
      67          239 : fn call_of_statement(parsed: &ParsedScript, root: u32) -> Option<(u32, bool)> {
      68          239 :     let node = &parsed.nodes[root as usize];
      69          239 :     match node.node_type() {
      70          170 :         NodeType::OBJECT_SQL_CALL => Some((root, false)),
      71              :         NodeType::OBJECT_EXT_EXPLAIN => {
      72           22 :             let [body] = attrs!(parsed.children(node), [K::EXT_EXPLAIN_STATEMENT]);
      73           22 :             let body = body?;
      74           22 :             (body.node_type() == NodeType::OBJECT_SQL_CALL).then(|| (node_id(parsed, body), true))
      75              :         }
      76           47 :         _ => None,
      77              :     }
      78          239 : }
      79              : 
      80          259 : fn node_id(parsed: &ParsedScript, node: &Node) -> u32 {
      81          259 :     ((node as *const Node as usize - parsed.nodes.as_ptr() as usize) / std::mem::size_of::<Node>()) as u32
      82          259 : }
      83              : 
      84              : /// The source text of a node
      85         1293 : fn source<'a>(parsed: &'a ParsedScript, node: &Node) -> &'a str {
      86         1293 :     let scanned = &parsed.scanned;
      87         1293 :     let span = resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
      88         1293 :     let begin = (span.offset as usize).min(scanned.text.len());
      89         1293 :     let end = (begin + span.length as usize).min(scanned.text.len());
      90         1293 :     &scanned.text[begin..end]
      91         1293 : }
      92              : 
      93              : /// A name in a script, a key of an option or attribute
      94              : struct KeyName {
      95              :     /// The spelling in the script, without the quotes
      96              :     text: String,
      97              :     /// Was it quoted? A quoted name means what it says.
      98              :     quoted: bool,
      99              : }
     100              : 
     101              : /// Read a key: a name or a string
     102         1004 : fn read_key(parsed: &ParsedScript, key: &Node) -> KeyName {
     103         1004 :     let text = source(parsed, key);
     104         1004 :     if key.node_type() == NodeType::LITERAL_STRING {
     105            0 :         return KeyName { text: unquote_string_literal(text).unwrap_or_else(|| text.to_string()), quoted: true };
     106         1004 :     }
     107         1004 :     match text.as_bytes().first() {
     108            9 :         Some(&quote @ (b'"' | b'`')) if text.len() >= 2 => {
     109            9 :             let quote = quote as char;
     110            9 :             let inner = &text[1..text.len() - 1];
     111            9 :             KeyName { text: inner.replace(&format!("{quote}{quote}"), &quote.to_string()), quoted: true }
     112              :         }
     113          995 :         _ => KeyName { text: text.to_string(), quoted: false },
     114              :     }
     115         1004 : }
     116              : 
     117              : /// The segments of a call name, without trailing dots. Unquoted names are lower case already.
     118          191 : fn read_name_path(parsed: &ParsedScript, name: &Node) -> Vec<String> {
     119          191 :     parsed
     120          191 :         .children(name)
     121          191 :         .iter()
     122          383 :         .filter(|c| c.node_type() == NodeType::NAME)
     123          377 :         .map(|c| {
     124          377 :             let key = read_key(parsed, c);
     125          377 :             if key.quoted { key.text } else { key.text.to_ascii_lowercase() }
     126          377 :         })
     127          191 :         .collect()
     128          191 : }
     129              : 
     130              : /// The function that a call name selects
     131              : struct Callee<'a> {
     132              :     namespace: &'a Arc<str>,
     133              :     spec: &'a ClientFunctionSpec,
     134              :     /// What follows the namespace
     135              :     path: Vec<String>,
     136              : }
     137              : 
     138          191 : fn resolve_callee<'a>(parsed: &ParsedScript, specs: &'a CatalogSnapshot, call: &Node) -> Option<Callee<'a>> {
     139          191 :     let [name] = attrs!(parsed.children(call), [K::SQL_CALL_NAME]);
     140          191 :     let mut path = read_name_path(parsed, name?).into_iter();
     141              :     // A namespace is a plain lower-case name, also if a script quotes it, like `starts_client_function_call` and completion read it
     142          191 :     let namespace = path.next()?.to_ascii_lowercase();
     143          191 :     let (namespace, spec) = specs.client_function_specs().get_key_value(namespace.as_str())?;
     144          182 :     Some(Callee { namespace, spec, path: path.collect() })
     145          191 : }
     146              : 
     147              : /// The fields of an object: the key and value of every field
     148           71 : fn fields_of<'p>(parsed: &'p ParsedScript, object: &Node) -> Vec<(&'p Node, &'p Node)> {
     149           71 :     parsed
     150           71 :         .children(object)
     151           71 :         .iter()
     152          113 :         .filter_map(|field| {
     153          113 :             let [key, value] = attrs!(parsed.children(field), [K::EXT_VARARG_FIELD_KEY, K::EXT_VARARG_FIELD_VALUE]);
     154          113 :             Some((key?, value?))
     155          113 :         })
     156           71 :         .collect()
     157           71 : }
     158              : 
     159              : /// Is the `method` option of a call of the escape hatch the string literal GET?
     160           13 : fn raw_method_is_get(parsed: &ParsedScript, args: Option<&Node>) -> bool {
     161           28 :     args.map(|a| parsed.children(a)).unwrap_or(&[]).iter().any(|arg| {
     162           28 :         let [key, value] = attrs!(parsed.children(arg), [K::SQL_FUNCTION_ARG_NAME, K::SQL_FUNCTION_ARG_VALUE]);
     163           28 :         let (Some(key), Some(value)) = (key, value) else { return false };
     164           28 :         let name = read_key(parsed, key);
     165           28 :         let is_method = if name.quoted { name.text == "method" } else { name.text.eq_ignore_ascii_case("method") };
     166           28 :         is_method && value.node_type() == NodeType::LITERAL_STRING && string_text(source(parsed, value)).eq_ignore_ascii_case("GET")
     167           28 :     })
     168           13 : }
     169              : 
     170              : /// A field as it decides the variant of a union
     171           51 : fn read_field(parsed: &ParsedScript, key: &Node, value: &Node) -> ClientFunctionField {
     172           51 :     let name = read_key(parsed, key);
     173           51 :     let text = (value.node_type() == NodeType::LITERAL_STRING).then(|| string_text(source(parsed, value)));
     174           51 :     ClientFunctionField { name: name.text, quoted: name.quoted, text }
     175           51 : }
     176              : 
     177              : /// Add a suggestion to a message
     178           60 : fn with_suggestion(message: String, suggestion: Option<&str>) -> String {
     179           60 :     match suggestion {
     180            6 :         Some(name) => format!("{message}, did you mean {name}?"),
     181           54 :         None => message,
     182              :     }
     183           60 : }
     184              : 
     185              : /// Check the calls of a script and record those in a namespace with a spec
     186         4003 : pub(super) fn analyze(state: &mut AnalysisState) {
     187         4003 :     let parsed = state.parsed;
     188         4003 :     let snapshot = state.out.snapshot.clone();
     189         4003 :     if snapshot.client_function_specs().is_empty() {
     190         3799 :         return;
     191          204 :     }
     192          273 :     for (statement_id, statement) in parsed.statements.iter().enumerate() {
     193          239 :         let Some((call_id, explain)) = call_of_statement(parsed, statement.root) else { continue };
     194          191 :         let call = &parsed.nodes[call_id as usize];
     195          191 :         let Some(callee) = resolve_callee(parsed, &snapshot, call) else { continue };
     196          182 :         let [name, args] = attrs!(parsed.children(call), [K::SQL_CALL_NAME, K::SQL_CALL_ARGS]);
     197          182 :         let name_id = name.map(|n| node_id(parsed, n)).unwrap_or(call_id);
     198          182 :         let spec = callee.spec;
     199          182 :         let function = match callee.path.as_slice() {
     200          176 :             [function_name] => spec.function(function_name),
     201            6 :             _ => None,
     202              :         };
     203           13 :         match function {
     204          169 :             Some(function) => Checker { state, spec, function }.check_options(args, call_id),
     205              :             // A name that is still typed, the parser reports the trailing dot
     206           13 :             None if callee.path.is_empty() => {}
     207              :             // The spec shadows its namespace, the backend never gets this call
     208              :             None => {
     209            8 :                 let function_name = callee.path.join(".");
     210           72 :                 let suggestion = closest_name(&function_name, spec.functions.iter().map(|f| f.name.as_str()));
     211            8 :                 let message = with_suggestion(format!("unknown function {}.{function_name}", callee.namespace), suggestion);
     212            8 :                 state.add_error(AnalyzerErrorType::CLIENT_FUNCTION_UNKNOWN_FUNCTION, AnalyzerErrorSeverity::ERROR, name_id, message);
     213              :             }
     214              :         }
     215          182 :         state.out.client_function_calls.push(ClientFunctionCall {
     216          182 :             ast_statement_id: statement_id as u32,
     217          182 :             ast_node_id: call_id,
     218          182 :             explain,
     219          182 :             namespace: callee.namespace.clone(),
     220          182 :             function: function.map(|f| Arc::from(f.name.as_str())),
     221          169 :             effect: match function {
     222              :                 // The escape hatch reads if its method is the literal GET
     223          169 :                 Some(function) if function.raw && raw_method_is_get(parsed, args) => ClientFunctionEffect::Read,
     224          163 :                 Some(function) => function.effect(),
     225           13 :                 None => ClientFunctionEffect::Write,
     226              :             },
     227              :         });
     228              :     }
     229         4003 : }
     230              : 
     231              : /// The checks of the call of one function
     232              : struct Checker<'s, 'a, 'c> {
     233              :     state: &'s mut AnalysisState<'a>,
     234              :     spec: &'c ClientFunctionSpec,
     235              :     function: &'c ClientFunction,
     236              : }
     237              : 
     238              : impl Checker<'_, '_, '_> {
     239           26 :     fn error(&mut self, error_type: AnalyzerErrorType, node: &Node, message: String) {
     240           26 :         let id = node_id(self.state.parsed, node);
     241           26 :         self.state.add_error(error_type, AnalyzerErrorSeverity::ERROR, id, message);
     242           26 :     }
     243              : 
     244              :     /// Check the options of the call
     245          169 :     fn check_options(&mut self, args: Option<&Node>, call_id: u32) {
     246          169 :         let parsed = self.state.parsed;
     247          169 :         let function = self.function;
     248          169 :         let mut seen: Vec<Arc<str>> = Vec::new();
     249          169 :         let args: &[Node] = args.map(|a| parsed.children(a)).unwrap_or(&[]);
     250          169 :         let standard = function.standard_options(self.spec);
     251          169 :         let mut used_standard: Vec<&str> = Vec::new();
     252          314 :         for arg in args {
     253          308 :             let [key, value] = attrs!(parsed.children(arg), [K::SQL_FUNCTION_ARG_NAME, K::SQL_FUNCTION_ARG_VALUE]);
     254          308 :             let (Some(key), Some(value)) = (key, value) else { continue };
     255          308 :             let name = read_key(parsed, key);
     256              :             // The standard options are not sent, the runtime applies them
     257          308 :             if !name.quoted && name.text.starts_with('_') {
     258           17 :                 let lower = name.text.to_ascii_lowercase();
     259           22 :                 match standard.iter().find(|s| **s == lower) {
     260           13 :                     Some(&option) => {
     261           13 :                         if used_standard.contains(&option) {
     262            0 :                             self.error(AnalyzerErrorType::CLIENT_FUNCTION_DUPLICATE_OPTION, key, format!("duplicate option {option}"));
     263           13 :                         }
     264           13 :                         used_standard.push(option);
     265           13 :                         self.check_standard_value(option, value);
     266              :                     }
     267            4 :                     None => {
     268            4 :                         let message = format!("{}.{} has no option {}", self.spec.namespace, function.name, name.text);
     269            4 :                         self.error(AnalyzerErrorType::CLIENT_FUNCTION_UNSUPPORTED_OPTION, key, message);
     270            4 :                     }
     271              :                 }
     272           17 :                 continue;
     273          291 :             }
     274          291 :             let attribute = find_attribute(&function.options, &name.text, name.quoted);
     275          291 :             let canonical: Arc<str> = Arc::from(attribute.map_or(name.text.as_str(), |a| a.name.as_str()));
     276          291 :             if seen.contains(&canonical) {
     277            1 :                 self.error(AnalyzerErrorType::CLIENT_FUNCTION_DUPLICATE_OPTION, key, format!("duplicate option {canonical}"));
     278          290 :             }
     279          291 :             seen.push(canonical);
     280          291 :             if attribute.is_none() && !name.quoted {
     281           47 :                 let suggestion = closest_name(&name.text, function.options.iter().map(|o| o.name.as_str()));
     282           45 :                 let message = format!("{}.{} has no option {}", self.spec.namespace, function.name, name.text);
     283           45 :                 self.report_unknown(function.unknown_options(), key, with_suggestion(message, suggestion));
     284          246 :             }
     285          291 :             self.check_value(value, attribute);
     286              :         }
     287              :         // `_next` sets an option of the function
     288          169 :         let paging = function.paging.as_ref();
     289          169 :         if used_standard.contains(&OPTION_NEXT) {
     290            4 :             if let Some(option) = paging.and_then(|p| p.option.as_ref()).filter(|o| seen.iter().any(|s| **s == ***o)) {
     291            1 :                 let call = &parsed.nodes[call_id as usize];
     292            1 :                 let message = format!("{OPTION_NEXT} sets the option {option}, use one of them");
     293            1 :                 self.error(AnalyzerErrorType::CLIENT_FUNCTION_DUPLICATE_OPTION, call, message);
     294            2 :             }
     295          166 :         }
     296          169 :         if used_standard.contains(&OPTION_NEXT) && used_standard.contains(&OPTION_POLL) {
     297            0 :             let call = &parsed.nodes[call_id as usize];
     298            0 :             let message = format!("{OPTION_NEXT} and {OPTION_POLL} continue different things, use one of them");
     299            0 :             self.error(AnalyzerErrorType::CLIENT_FUNCTION_UNSUPPORTED_OPTION, call, message);
     300          169 :         }
     301              :         // A poll and a next URL carry the whole request
     302          169 :         let next_url = used_standard.contains(&OPTION_NEXT) && paging.is_some_and(|p| p.kind == "url");
     303          169 :         if !used_standard.contains(&OPTION_POLL) && !next_url {
     304          179 :             for option in function.options.iter().filter(|o| o.required) {
     305          211 :                 if !seen.iter().any(|s| **s == *option.name) {
     306            9 :                     let call = &parsed.nodes[call_id as usize];
     307            9 :                     let message = format!("{}.{} needs the option {}", self.spec.namespace, function.name, option.name);
     308            9 :                     self.error(AnalyzerErrorType::CLIENT_FUNCTION_MISSING_OPTION, call, message);
     309          163 :                 }
     310              :             }
     311            2 :         }
     312          169 :     }
     313              : 
     314              :     /// `_pages` takes a positive number, and `_wait` a number of seconds, which can be its default 0
     315           13 :     fn check_standard_value(&mut self, option: &str, value: &Node) {
     316           13 :         if !matches!(option, OPTION_PAGES | OPTION_WAIT) {
     317            4 :             return;
     318            9 :         }
     319            9 :         match value.node_type() {
     320            0 :             NodeType::OBJECT_SQL_PARAMETER_REF => {}
     321            8 :             NodeType::LITERAL_INTEGER if source(self.state.parsed, value).parse::<u64>().is_ok_and(|n| n > 0 || option == OPTION_WAIT) => {}
     322              :             _ => {
     323            2 :                 let expected = if option == OPTION_WAIT { "a number of seconds" } else { "a positive number" };
     324            2 :                 self.error(AnalyzerErrorType::CLIENT_FUNCTION_INVALID_VALUE, value, format!("{option} takes {expected}"));
     325              :             }
     326              :         }
     327           13 :     }
     328              : 
     329           54 :     fn report_unknown(&mut self, policy: ClientFunctionUnknownPolicy, node: &Node, message: String) {
     330           54 :         let severity = match policy {
     331           11 :             ClientFunctionUnknownPolicy::Error => AnalyzerErrorSeverity::ERROR,
     332            2 :             ClientFunctionUnknownPolicy::Warning => AnalyzerErrorSeverity::WARNING,
     333           41 :             ClientFunctionUnknownPolicy::Allow => return,
     334              :         };
     335           13 :         let id = node_id(self.state.parsed, node);
     336           13 :         self.state.add_error(AnalyzerErrorType::CLIENT_FUNCTION_UNKNOWN_OPTION, severity, id, message);
     337           54 :     }
     338              : 
     339              :     /// Check the value of an option or attribute
     340          342 :     fn check_value(&mut self, value: &Node, attribute: Option<&ClientFunctionAttribute>) {
     341          342 :         self.check_nested_value(value, attribute, attribute.is_some_and(|a| a.map));
     342          342 :     }
     343              : 
     344              :     /// Check a value. `map` says that an object is the map of the attribute: its keys are free, and the attribute holds for
     345              :     /// its values.
     346          364 :     fn check_nested_value(&mut self, value: &Node, attribute: Option<&ClientFunctionAttribute>, map: bool) {
     347          364 :         let parsed = self.state.parsed;
     348          364 :         match value.node_type() {
     349            4 :             NodeType::ARRAY if map => {
     350            4 :                 let mut seen: Vec<KeyName> = Vec::new();
     351            7 :                 for (key, item) in fields_of(parsed, value) {
     352            7 :                     let name = read_key(parsed, key);
     353            7 :                     if seen.iter().any(|s| s.text == name.text) {
     354            1 :                         self.error(AnalyzerErrorType::CLIENT_FUNCTION_DUPLICATE_OPTION, key, format!("duplicate key {}", name.text));
     355            6 :                     }
     356            7 :                     seen.push(name);
     357            7 :                     self.check_nested_value(item, attribute, false);
     358              :                 }
     359              :             }
     360              :             // An object
     361              :             NodeType::ARRAY => {
     362           33 :                 let spec = self.spec;
     363           33 :                 let schema = attribute.and_then(|a| a.schema.as_ref()).and_then(|s| spec.schema(s));
     364           33 :                 self.check_fields(value, schema);
     365              :             }
     366              :             NodeType::OBJECT_EXT_VARARG_ARRAY => {
     367            7 :                 let [values] = attrs!(parsed.children(value), [K::EXT_VARARG_ARRAY_VALUES]);
     368           15 :                 for item in values.map(|v| parsed.children(v)).unwrap_or(&[]) {
     369           15 :                     self.check_nested_value(item, attribute, map);
     370           15 :                 }
     371              :             }
     372              :             NodeType::LITERAL_NULL
     373              :             | NodeType::LITERAL_INTEGER
     374              :             | NodeType::LITERAL_FLOAT
     375              :             | NodeType::LITERAL_STRING
     376              :             | NodeType::BOOL
     377              :             | NodeType::OBJECT_SQL_NARY_EXPRESSION
     378              :             | NodeType::OBJECT_SQL_PARAMETER_REF
     379          317 :             | NodeType::OBJECT_SQL_SELECT_EXPRESSION => {}
     380              :             // A function, such as now() or cast(...)
     381            3 :             _ => {
     382            3 :                 let message = "a value can't call a function".to_string();
     383            3 :                 self.error(AnalyzerErrorType::CLIENT_FUNCTION_INVALID_VALUE, value, message);
     384            3 :             }
     385              :         }
     386          364 :     }
     387              : 
     388              :     /// Check the fields of an object against its schema. Without a schema any key goes. An object of a union has to fit
     389              :     /// one variant.
     390           33 :     fn check_fields(&mut self, object: &Node, schema: Option<&ClientFunctionSchema>) {
     391           33 :         let parsed = self.state.parsed;
     392           33 :         let spec = self.spec;
     393           33 :         let fields = fields_of(parsed, object);
     394           51 :         let read: Vec<ClientFunctionField> = fields.iter().map(|(key, value)| read_field(parsed, key, value)).collect();
     395           33 :         let fit = schema.map(|s| spec.fit(s, &read));
     396           33 :         let mut seen: Vec<Arc<str>> = Vec::new();
     397           51 :         for (i, (key, value)) in fields.iter().copied().enumerate() {
     398           51 :             let name = &read[i];
     399           51 :             let attribute = schema.and_then(|s| spec.attribute_of(s, &name.name, name.quoted));
     400           51 :             let canonical: Arc<str> = Arc::from(attribute.map_or(name.name.as_str(), |a| a.name.as_str()));
     401           51 :             if seen.contains(&canonical) {
     402            0 :                 self.error(AnalyzerErrorType::CLIENT_FUNCTION_DUPLICATE_OPTION, key, format!("duplicate attribute {canonical}"));
     403           51 :             }
     404           51 :             seen.push(canonical);
     405           51 :             if let Some(fit) = fit.as_ref() {
     406           38 :                 if attribute.is_none() && !name.quoted {
     407           12 :                     let candidates = fit.variants.iter().flat_map(|v| v.attributes.iter()).map(|a| a.name.as_str());
     408            7 :                     let message = with_suggestion(format!("no attribute {} here", name.name), closest_name(&name.name, candidates));
     409            7 :                     self.report_unknown(fit.unknown(), key, message);
     410           31 :                 } else if fit.conflicts.contains(&i) {
     411            2 :                     let message = match fit.tag_field.map(|t| &read[t]) {
     412            1 :                         Some(tag) => format!("no attribute {} when {} is '{}'", name.name, tag.name, tag.text.as_deref().unwrap_or_default()),
     413            1 :                         None => format!("{} does not fit the attributes before it", name.name),
     414              :                     };
     415            2 :                     self.report_unknown(fit.unknown(), key, message);
     416           29 :                 }
     417           13 :             }
     418           51 :             self.check_value(value, attribute);
     419              :         }
     420              :         // An object only has to satisfy one variant, the closest ones say what is missing
     421           33 :         let missing: Vec<Vec<&str>> = fit
     422           33 :             .iter()
     423           33 :             .flat_map(|f| f.variants.iter())
     424           33 :             .map(|variant| {
     425           48 :                 let is_missing = |a: &&ClientFunctionAttribute| a.required && !seen.iter().any(|s| **s == *a.name);
     426           24 :                 variant.attributes.iter().filter(is_missing).map(|a| a.name.as_str()).collect()
     427           24 :             })
     428           33 :             .collect();
     429           33 :         let fewest = missing.iter().map(Vec::len).min().unwrap_or(0);
     430           33 :         if fewest == 0 {
     431           28 :             return;
     432            5 :         }
     433            6 :         let closest: Vec<&Vec<&str>> = missing.iter().filter(|m| m.len() == fewest).collect();
     434              :         // What all of them need is certain, the rest is a choice between them
     435           13 :         let needed_by_all = |name: &str| closest.iter().all(|m| m.contains(&name));
     436            5 :         for name in closest[0].iter().copied().filter(|name| needed_by_all(name)) {
     437            4 :             self.error(AnalyzerErrorType::CLIENT_FUNCTION_MISSING_OPTION, object, format!("needs the attribute {name}"));
     438            4 :         }
     439            6 :         let mut choice: Vec<&str> = closest.iter().flat_map(|m| m.iter().copied()).filter(|name| !needed_by_all(name)).collect();
     440            5 :         choice.sort_unstable();
     441            5 :         choice.dedup();
     442            5 :         if !choice.is_empty() {
     443            1 :             let message = format!("needs one of the attributes {}", choice.join(", "));
     444            1 :             self.error(AnalyzerErrorType::CLIENT_FUNCTION_MISSING_OPTION, object, message);
     445            4 :         }
     446           33 :     }
     447              : }
     448              : 
     449              : // -------------------------------------------------------------------------------------------------
     450              : // The description of a call
     451              : // -------------------------------------------------------------------------------------------------
     452              : 
     453              : /// A value that the runtime fills in: a JSON pointer into the options and a parameter
     454           17 : fn binding(pointer: String, name: String) -> Value {
     455           17 :     json!({ "pointer": pointer, "param": name })
     456           17 : }
     457              : 
     458              : /// A segment of a JSON pointer
     459          233 : fn pointer_segment(segment: &str) -> String {
     460          233 :     segment.replace('~', "~0").replace('/', "~1")
     461          233 : }
     462              : 
     463              : /// Does a parenthesis around all of these tokens match? A query can start and end with parentheses of its own, as in
     464              : /// `(select 1) union (select 2)`.
     465           42 : fn is_parenthesized(symbols: &[Symbol]) -> bool {
     466           42 :     if symbols.first().is_none_or(|s| s.kind != S_LRB) {
     467           10 :         return false;
     468           32 :     }
     469           32 :     let mut depth = 0;
     470          262 :     for (i, symbol) in symbols.iter().enumerate() {
     471          262 :         match symbol.kind {
     472           66 :             S_LRB => depth += 1,
     473           66 :             S_RRB => depth -= 1,
     474          130 :             _ => {}
     475              :         }
     476          262 :         if depth == 0 {
     477           32 :             return i + 1 == symbols.len();
     478          230 :         }
     479              :     }
     480            0 :     false
     481           42 : }
     482              : 
     483              : /// The SQL text of a query value, without the parentheses around it. The tokens tell, since a string can hold a parenthesis.
     484           16 : fn select_text<'a>(parsed: &'a ParsedScript, node: &Node) -> &'a str {
     485           16 :     let scanned = &parsed.scanned;
     486           16 :     let span = node.symbol_span();
     487           16 :     let mut symbols = &scanned.symbols[span.offset() as usize..(span.offset() + span.length()) as usize];
     488           42 :     while is_parenthesized(symbols) {
     489           26 :         symbols = &symbols[1..symbols.len() - 1];
     490           26 :     }
     491           16 :     match (symbols.first(), symbols.last()) {
     492           16 :         (Some(first), Some(last)) => &scanned.text[first.location.offset as usize..last.location.end() as usize],
     493            0 :         _ => "",
     494              :     }
     495           16 : }
     496              : 
     497              : /// The number of a literal, as exact as JSON allows
     498           54 : pub(crate) fn number(text: &str) -> Value {
     499           69 :     let compact: String = text.chars().filter(|c| !c.is_whitespace()).collect();
     500           54 :     if let Ok(n) = compact.parse::<i64>() {
     501           53 :         return Value::Number(n.into());
     502            1 :     }
     503            1 :     if let Ok(n) = compact.parse::<u64>() {
     504            0 :         return Value::Number(n.into());
     505            1 :     }
     506            1 :     compact.parse::<f64>().ok().and_then(Number::from_f64).map(Value::Number).unwrap_or(Value::Null)
     507           54 : }
     508              : 
     509              : /// The text of a string literal
     510          145 : fn string_text(text: &str) -> String {
     511          145 :     if let Some(inner) = text.strip_prefix("$$").and_then(|t| t.strip_suffix("$$")) {
     512            1 :         return inner.to_string();
     513          144 :     }
     514          144 :     unquote_string_literal(text).unwrap_or_else(|| text.to_string())
     515          145 : }
     516              : 
     517              : struct Describer<'a> {
     518              :     analyzed: &'a AnalyzedScript,
     519              :     spec: &'a ClientFunctionSpec,
     520              :     bindings: Vec<Value>,
     521              : }
     522              : 
     523              : impl Describer<'_> {
     524              :     /// The value of an option or attribute
     525          226 :     fn value(&mut self, node: &Node, attribute: Option<&ClientFunctionAttribute>, pointer: &str) -> Value {
     526          226 :         self.nested_value(node, attribute, attribute.is_some_and(|a| a.map), pointer)
     527          226 :     }
     528              : 
     529              :     /// The value of a node, a parameter leaves a null and a binding. `map` says that an object is the map of the attribute:
     530              :     /// its keys stay as written, and the attribute holds for its values.
     531          248 :     fn nested_value(&mut self, node: &Node, attribute: Option<&ClientFunctionAttribute>, map: bool, pointer: &str) -> Value {
     532          248 :         let parsed = self.analyzed.parsed.clone();
     533          248 :         let text = source(&parsed, node);
     534          248 :         match node.node_type() {
     535            1 :             NodeType::LITERAL_NULL => Value::Null,
     536            7 :             NodeType::BOOL => Value::Bool(node.children_begin_or_value() != 0),
     537           51 :             NodeType::LITERAL_INTEGER | NodeType::LITERAL_FLOAT | NodeType::OBJECT_SQL_NARY_EXPRESSION => number(text),
     538          112 :             NodeType::LITERAL_STRING => Value::String(string_text(text)),
     539           16 :             NodeType::OBJECT_SQL_SELECT_EXPRESSION => Value::String(select_text(&parsed, node).to_string()),
     540              :             NodeType::OBJECT_SQL_PARAMETER_REF => {
     541           17 :                 let id = node_id(&parsed, node);
     542           29 :                 let name = self.analyzed.parameters.iter().find(|p| p.ast_node_id == id).and_then(|p| p.name.clone());
     543           17 :                 self.bindings.push(binding(pointer.to_string(), name.as_deref().unwrap_or("").to_string()));
     544           17 :                 Value::Null
     545              :             }
     546              :             NodeType::OBJECT_EXT_VARARG_ARRAY => {
     547            7 :                 let [values] = attrs!(parsed.children(node), [K::EXT_VARARG_ARRAY_VALUES]);
     548            7 :                 let items = values.map(|v| parsed.children(v)).unwrap_or(&[]);
     549           15 :                 Value::Array(items.iter().enumerate().map(|(i, item)| self.nested_value(item, attribute, map, &format!("{pointer}/{i}"))).collect())
     550              :             }
     551              :             NodeType::ARRAY => {
     552           34 :                 let spec = self.spec;
     553           34 :                 let schema = attribute.and_then(|a| a.schema.as_ref()).and_then(|s| spec.schema(s));
     554           34 :                 let mut object = Map::new();
     555           55 :                 for (key, value) in fields_of(&parsed, node) {
     556           55 :                     let name = read_key(&parsed, key);
     557           55 :                     if map {
     558            7 :                         let field_pointer = format!("{pointer}/{}", pointer_segment(&name.text));
     559            7 :                         let field_value = self.nested_value(value, attribute, false, &field_pointer);
     560            7 :                         object.insert(name.text, field_value);
     561            7 :                         continue;
     562           48 :                     }
     563           48 :                     let field_attribute = schema.and_then(|s| spec.attribute_of(s, &name.text, name.quoted));
     564           48 :                     let field_name = field_attribute.map_or(name.text, |a| a.name.clone());
     565           48 :                     let field_pointer = format!("{pointer}/{}", pointer_segment(&field_name));
     566           48 :                     let field_value = self.value(value, field_attribute, &field_pointer);
     567           48 :                     object.insert(field_name, field_value);
     568              :                 }
     569           34 :                 Value::Object(object)
     570              :             }
     571            3 :             _ => Value::Null,
     572              :         }
     573          248 :     }
     574              : }
     575              : 
     576              : impl AnalyzedScript {
     577              :     /// Does the script call a client function that writes? A call of an unknown function counts as one, its EXPLAIN
     578              :     /// sends nothing. Such a script runs only on request, like one with a `@manual` tag.
     579         1641 :     pub fn calls_writes(&self) -> bool {
     580         1641 :         self.client_function_calls.iter().any(|c| !c.explain && c.effect == ClientFunctionEffect::Write)
     581         1641 :     }
     582              : 
     583              :     /// The call of a script that is a single call in a namespace with client functions
     584          449 :     pub fn single_client_function_call(&self) -> Option<&ClientFunctionCall> {
     585          449 :         match &self.parsed.statements[..] {
     586          449 :             [_] => self.client_function_calls.iter().find(|c| c.ast_statement_id == 0),
     587           16 :             _ => None,
     588              :         }
     589          449 :     }
     590              : 
     591              :     /// The result columns of a call of a known function, in the order of the runtime
     592            1 :     pub fn client_function_result_columns(&self, call: &ClientFunctionCall) -> Option<Vec<Arc<str>>> {
     593            1 :         let spec = self.snapshot.client_function_spec(&call.namespace)?;
     594            1 :         let function = spec.function(call.function.as_deref()?)?;
     595            1 :         Some(spec.result_columns(function).into_iter().map(Arc::from).collect())
     596            1 :     }
     597              : 
     598              :     /// Describe a `CALL` of a function of a spec for the runtime of the host: plain JSON over the names of the spec.
     599              :     /// A known name has the spelling of the spec, any other the spelling of the script. Strings are unescaped, and a
     600              :     /// parameter is a null with an entry in `bindings`. None for any other statement.
     601           93 :     pub fn describe_client_function_call(&self, statement_id: u32) -> Option<Value> {
     602          135 :         let record = self.client_function_calls.iter().find(|c| c.ast_statement_id == statement_id)?;
     603           93 :         let function_name = record.function.as_ref()?;
     604           87 :         let spec = self.snapshot.client_function_spec(&record.namespace)?;
     605           87 :         let function = spec.function(function_name)?;
     606           87 :         let parsed = self.parsed.clone();
     607           87 :         let call = &parsed.nodes[record.ast_node_id as usize];
     608           87 :         let [args] = attrs!(parsed.children(call), [K::SQL_CALL_ARGS]);
     609           87 :         let mut describer = Describer { analyzed: self, spec, bindings: Vec::new() };
     610           87 :         let mut options = Map::new();
     611          179 :         for arg in args.map(|a| parsed.children(a)).unwrap_or(&[]) {
     612          178 :             let [key, value] = attrs!(parsed.children(arg), [K::SQL_FUNCTION_ARG_NAME, K::SQL_FUNCTION_ARG_VALUE]);
     613          178 :             let (Some(key), Some(value)) = (key, value) else { continue };
     614          178 :             let name = read_key(&parsed, key);
     615          178 :             let attribute = find_attribute(&function.options, &name.text, name.quoted);
     616          178 :             let option_name = match attribute {
     617          143 :                 Some(attribute) => attribute.name.clone(),
     618              :                 // The standard options fold their case like any other known name
     619           35 :                 None if !name.quoted && name.text.starts_with('_') => name.text.to_ascii_lowercase(),
     620           20 :                 None => name.text,
     621              :             };
     622          178 :             let pointer = format!("/{}", pointer_segment(&option_name));
     623          178 :             let described = describer.value(value, attribute, &pointer);
     624          178 :             options.insert(option_name, described);
     625              :         }
     626           87 :         let mut out = json!({
     627           87 :             "namespace": &*record.namespace,
     628           87 :             "function": &**function_name,
     629           87 :             "effect": if record.effect == ClientFunctionEffect::Read { "read" } else { "write" },
     630           87 :             "explain": record.explain,
     631           87 :             "options": options,
     632              :         });
     633           87 :         if !describer.bindings.is_empty() {
     634           12 :             out["bindings"] = Value::Array(describer.bindings);
     635           75 :         }
     636           87 :         Some(out)
     637           93 :     }
     638              : }
        

Generated by: LCOV version 2.0-1