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("e @ (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}"), "e.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 : }
|