Skip to main content

surql_parser/
params.rs

1use crate::{Result, parse};
2
3/// Extract all `$param` names used in a SurrealQL query.
4///
5/// Parses the input, then scans for parameter tokens. Returns a sorted,
6/// deduplicated list of parameter names (without the `$` prefix).
7///
8/// Parameters inside `DEFINE FUNCTION` signatures are excluded —
9/// only "free" parameters (query-level bindings) are returned.
10///
11/// # Example
12///
13/// ```
14/// let params = surql_parser::extract_params(
15///     "SELECT * FROM user WHERE age > $min AND name = $name"
16/// ).unwrap();
17/// assert_eq!(params, vec!["min", "name"]);
18/// ```
19pub fn extract_params(input: &str) -> Result<Vec<String>> {
20	parse(input)?;
21	Ok(scan_params(input))
22}
23
24/// Scan a (known-valid) SurrealQL string for `$param` tokens.
25///
26/// Skips string literals and comments. Returns sorted, deduplicated names.
27pub(crate) fn scan_params(input: &str) -> Vec<String> {
28	let mut params = std::collections::BTreeSet::new();
29	let bytes = input.as_bytes();
30	let len = bytes.len();
31	let mut i = 0;
32
33	while i < len {
34		match bytes[i] {
35			// Skip single-quoted strings: 'text''s escaped'
36			b'\'' => {
37				i += 1;
38				while i < len {
39					if bytes[i] == b'\'' {
40						i += 1;
41						if i < len && bytes[i] == b'\'' {
42							i += 1; // escaped ''
43							continue;
44						}
45						break;
46					}
47					i += 1;
48				}
49			}
50			// Skip double-quoted strings: "text\"s escaped"
51			b'"' => {
52				i += 1;
53				while i < len {
54					if bytes[i] == b'\\' {
55						i += 2;
56						continue;
57					}
58					if bytes[i] == b'"' {
59						i += 1;
60						break;
61					}
62					i += 1;
63				}
64			}
65			// Skip backtick-quoted identifiers: `field name`
66			b'`' => {
67				i += 1;
68				while i < len {
69					if bytes[i] == b'`' {
70						i += 1;
71						break;
72					}
73					i += 1;
74				}
75			}
76			// Skip line comments: -- ...
77			b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
78				i += 2;
79				while i < len && bytes[i] != b'\n' {
80					i += 1;
81				}
82			}
83			// Skip block comments: /* ... */
84			b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
85				i += 2;
86				let mut depth = 1u32;
87				while i + 1 < len && depth > 0 {
88					if bytes[i] == b'/' && bytes[i + 1] == b'*' {
89						depth += 1;
90						i += 2;
91					} else if bytes[i] == b'*' && bytes[i + 1] == b'/' {
92						depth -= 1;
93						i += 2;
94					} else {
95						i += 1;
96					}
97				}
98			}
99			// Collect $param
100			b'$' => {
101				i += 1;
102				let start = i;
103				while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
104					i += 1;
105				}
106				if i > start {
107					let name = &input[start..i];
108					params.insert(name.to_string());
109				}
110			}
111			_ => i += 1,
112		}
113	}
114
115	params.into_iter().collect()
116}