#[derive(Debug, Clone, PartialEq)]
pub enum BindQualifier {
None,
Input,
Bind,
Capture,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LvalueSpec {
Wildcard,
Explicit(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum BindPoint {
Reference {
name: String,
qualifier: BindQualifier,
lvalue_spec: Option<LvalueSpec>,
},
InlineDefinition(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldType {
Static,
BindRef(String),
Template(Vec<BindPoint>),
}
pub fn classify_field(value: &str) -> FieldType {
let bind_points = extract_bind_points(value);
if bind_points.is_empty() {
FieldType::Static
} else if bind_points.len() == 1
&& value.starts_with('{')
&& !value.starts_with("{{")
&& value.ends_with('}')
{
match &bind_points[0] {
BindPoint::Reference { name, .. } => FieldType::BindRef(name.clone()),
_ => FieldType::Template(bind_points),
}
} else {
FieldType::Template(bind_points)
}
}
pub fn extract_bind_points(value: &str) -> Vec<BindPoint> {
let mut points = Vec::new();
let chars: Vec<char> = value.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '{' {
if i + 1 < chars.len() && chars[i + 1] == '{' {
i += 2;
let start = i;
while i + 1 < chars.len() && !(chars[i] == '}' && chars[i + 1] == '}') {
i += 1;
}
if i + 1 < chars.len() {
let expr: String = chars[start..i].iter().collect();
points.push(BindPoint::InlineDefinition(expr.trim().to_string()));
i += 2; }
} else {
let next_nonspace = chars[i + 1..].iter().find(|c| !c.is_whitespace()).copied();
if matches!(next_nonspace, Some(c) if is_literal_start(c)) {
i += 1;
continue;
}
i += 1;
let start = i;
let mut depth = 1u32;
while i < chars.len() {
if chars[i] == '{' {
depth += 1;
}
if chars[i] == '}' {
depth -= 1;
if depth == 0 {
break;
}
}
i += 1;
}
if i < chars.len() {
let raw: String = chars[start..i].iter().collect();
let raw = raw.trim();
if is_literal_content(raw) {
i += 1;
} else if let Some(expr) = raw.strip_prefix(":=") {
let expr = expr.strip_suffix(":=").unwrap_or(expr).trim();
points.push(BindPoint::InlineDefinition(expr.to_string()));
i += 1;
} else if let Some((name_part, spec)) = extract_lvalue_spec(raw) {
let (qualifier, name) = parse_qualified_ref(name_part);
points.push(BindPoint::Reference {
name,
qualifier,
lvalue_spec: Some(spec),
});
i += 1;
} else if is_expression(raw) {
points.push(BindPoint::InlineDefinition(raw.to_string()));
i += 1;
} else {
let (qualifier, name) = parse_qualified_ref(raw);
points.push(BindPoint::Reference {
name,
qualifier,
lvalue_spec: None,
});
i += 1;
}
}
}
} else {
i += 1;
}
}
points
}
pub fn is_expression_public(s: &str) -> bool {
is_expression(s)
}
pub fn split_lvalue_spec(body: &str) -> (&str, Option<LvalueSpec>) {
match extract_lvalue_spec(body) {
Some((name_part, spec)) => (name_part, Some(spec)),
None => (body, None),
}
}
fn extract_lvalue_spec(raw: &str) -> Option<(&str, LvalueSpec)> {
let (name_part, spec_part) = raw.rsplit_once(':')?;
let name_part = name_part.trim();
let spec_part = spec_part.trim();
if !is_bare_identifier(name_part) {
return None;
}
let lower = name_part.to_lowercase();
if matches!(
lower.as_str(),
"input" | "coord" | "coordinate" | "bind" | "capture"
) {
return None;
}
if spec_part == "*" {
Some((name_part, LvalueSpec::Wildcard))
} else if is_polydat_type_name(spec_part) {
Some((name_part, LvalueSpec::Explicit(spec_part.to_string())))
} else {
None
}
}
fn is_polydat_type_name(s: &str) -> bool {
matches!(
s,
"u64"
| "f64"
| "u32"
| "i32"
| "i64"
| "f32"
| "bool"
| "str"
| "bytes"
| "json"
| "vec_f32"
| "vec_i32"
)
}
pub(crate) fn is_bare_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_expression(s: &str) -> bool {
s.contains('(') || s.contains(')') ||
s.contains('+') || s.contains('*') || s.contains('/') ||
s.contains('%') || s.contains('^') || s.contains('&') ||
s.contains('|') || s.contains('!') || s.contains('<') ||
s.contains('>') ||
s.starts_with(|c: char| c.is_ascii_digit()) ||
(s.starts_with('-') && s.len() > 1 && s.as_bytes()[1].is_ascii_digit())
}
fn is_literal_content(s: &str) -> bool {
s.starts_with('\'') || s.starts_with('"')
}
fn is_literal_start(c: char) -> bool {
c == '\'' || c == '"'
}
fn parse_qualified_ref(raw: &str) -> (BindQualifier, String) {
if let Some((prefix, name)) = raw.split_once(':') {
let qualifier = match prefix.trim().to_lowercase().as_str() {
"input" | "coord" | "coordinate" => BindQualifier::Input,
"bind" => BindQualifier::Bind,
"capture" => BindQualifier::Capture,
_ => return (BindQualifier::None, raw.to_string()), };
(qualifier, name.trim().to_string())
} else {
(BindQualifier::None, raw.to_string())
}
}
pub fn referenced_bindings(value: &str) -> Vec<String> {
extract_bind_points(value)
.into_iter()
.filter_map(|bp| match bp {
BindPoint::Reference { name, .. } => Some(name),
_ => None,
})
.collect()
}
pub fn replace_bind_points_with_markers(value: &str) -> String {
let names = referenced_bindings(value);
let mut result = value.to_string();
for name in &names {
let quoted = format!("'{{{name}}}'");
if let Some(pos) = result.find("ed) {
result.replace_range(pos..pos + quoted.len(), "?");
continue;
}
let bare = format!("{{{name}}}");
if let Some(pos) = result.find(&bare) {
result.replace_range(pos..pos + bare.len(), "?");
}
}
result
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum CaptureAgg {
Min(String),
Max(String),
Sum(String),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CapturePoint {
pub source_name: String,
pub as_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cast_type: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub slurp: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub count: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agg: Option<CaptureAgg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub row_filter: Option<(String, String)>,
}
#[derive(Debug, Clone)]
pub struct CaptureParseResult {
pub raw_template: String,
pub captures: Vec<CapturePoint>,
}
pub fn parse_capture_points(template: &str) -> CaptureParseResult {
let mut captures = Vec::new();
let mut raw = String::with_capacity(template.len());
let chars: Vec<char> = template.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' {
let bracket_start = i;
let attempt_start = i + 1;
i = attempt_start;
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
let cast_type = if i < chars.len() && chars[i] == '(' {
i += 1;
let cast_start = i;
while i < chars.len() && chars[i] != ')' {
i += 1;
}
let cast: String = chars[cast_start..i].iter().collect();
if i < chars.len() {
i += 1;
} while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
Some(cast.trim().to_string())
} else {
None
};
let slurp = if i < chars.len() && chars[i] == '@' {
i += 1;
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
true
} else {
false
};
let name_start = i;
if i < chars.len() {
let first = chars[i];
let is_valid_first = first.is_ascii_alphabetic() || first == '_' || first == '*';
if !is_valid_first {
raw.push('[');
i = attempt_start;
continue;
}
}
while i < chars.len()
&& (chars[i].is_alphanumeric()
|| chars[i] == '_'
|| chars[i] == '-'
|| chars[i] == '.'
|| chars[i] == '*')
{
i += 1;
}
let source_name: String = chars[name_start..i].iter().collect();
if source_name.is_empty() {
raw.push('[');
i = attempt_start;
continue;
}
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
let as_name = if i + 2 < chars.len()
&& (chars[i] == 'a' || chars[i] == 'A')
&& (chars[i + 1] == 's' || chars[i + 1] == 'S')
&& chars[i + 2].is_whitespace()
{
i += 2; while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
let alias_start = i;
while i < chars.len()
&& (chars[i].is_alphanumeric()
|| chars[i] == '_'
|| chars[i] == '-'
|| chars[i] == '.')
{
i += 1;
}
let alias: String = chars[alias_start..i].iter().collect();
alias
} else {
source_name.clone()
};
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
if i < chars.len() && chars[i] == ']' {
i += 1;
captures.push(CapturePoint {
row_filter: None,
source_name: source_name.clone(),
as_name,
cast_type,
slurp,
path: None,
count: false,
agg: None,
});
raw.push_str(&source_name);
} else {
let _ = bracket_start;
raw.push('[');
i = attempt_start;
}
} else {
raw.push(chars[i]);
i += 1;
}
}
CaptureParseResult {
raw_template: raw,
captures,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_expression_detects_function_calls() {
assert!(is_expression("hash(cycle)"));
assert!(is_expression("mod(x, 100)"));
}
#[test]
fn is_expression_detects_operators() {
assert!(is_expression("x + 1"));
assert!(is_expression("a * b"));
assert!(is_expression("x & 0xFF"));
}
#[test]
fn is_expression_rejects_simple_names() {
assert!(!is_expression("cycle"));
assert!(!is_expression("my_var"));
assert!(!is_expression("user_id"));
}
#[test]
fn is_expression_rejects_hyphenated_names() {
assert!(!is_expression("my-variable"));
assert!(!is_expression("some-long-name"));
}
#[test]
fn is_expression_detects_numeric_literals() {
assert!(is_expression("42"));
assert!(is_expression("3.14"));
assert!(is_expression("-5"));
}
#[test]
fn static_field() {
assert_eq!(classify_field("plain text"), FieldType::Static);
assert_eq!(classify_field("42"), FieldType::Static);
assert_eq!(classify_field(""), FieldType::Static);
}
#[test]
fn pure_bind_ref() {
assert_eq!(
classify_field("{userid}"),
FieldType::BindRef("userid".into())
);
}
#[test]
fn template_with_bind_points() {
let ft = classify_field("SELECT * FROM t WHERE id={id} AND name={name}");
match ft {
FieldType::Template(points) => {
assert_eq!(points.len(), 2);
assert_eq!(
points[0],
BindPoint::Reference {
name: "id".into(),
qualifier: BindQualifier::None,
lvalue_spec: None
}
);
assert_eq!(
points[1],
BindPoint::Reference {
name: "name".into(),
qualifier: BindQualifier::None,
lvalue_spec: None
}
);
}
_ => panic!("expected Template"),
}
}
#[test]
fn inline_definition() {
let ft = classify_field("value is {{Template('user-{}', ToString())}}");
match ft {
FieldType::Template(points) => {
assert_eq!(points.len(), 1);
match &points[0] {
BindPoint::InlineDefinition(expr) => {
assert!(expr.contains("Template"));
}
_ => panic!("expected InlineDefinition"),
}
}
_ => panic!("expected Template"),
}
}
#[test]
fn mixed_references_and_literals() {
let refs = referenced_bindings("INSERT INTO t (a, b) VALUES ({col_a}, {col_b})");
assert_eq!(refs, vec!["col_a", "col_b"]);
}
#[test]
fn no_bind_points() {
let refs = referenced_bindings("just a plain string");
assert!(refs.is_empty());
}
#[test]
fn qualified_coord() {
let points = extract_bind_points("{coord:cycle}");
assert_eq!(points.len(), 1);
assert_eq!(
points[0],
BindPoint::Reference {
name: "cycle".into(),
qualifier: BindQualifier::Input,
lvalue_spec: None,
}
);
}
#[test]
fn qualified_capture() {
let points = extract_bind_points("{capture:balance}");
assert_eq!(points.len(), 1);
assert_eq!(
points[0],
BindPoint::Reference {
name: "balance".into(),
qualifier: BindQualifier::Capture,
lvalue_spec: None,
}
);
}
#[test]
fn qualified_bind() {
let points = extract_bind_points("{bind:user_id}");
assert_eq!(points.len(), 1);
assert_eq!(
points[0],
BindPoint::Reference {
name: "user_id".into(),
qualifier: BindQualifier::Bind,
lvalue_spec: None,
}
);
}
#[test]
fn unknown_qualifier_becomes_unqualified() {
let points = extract_bind_points("{port:auth_token}");
assert_eq!(points.len(), 1);
assert_eq!(
points[0],
BindPoint::Reference {
name: "port:auth_token".into(),
qualifier: BindQualifier::None,
lvalue_spec: None,
}
);
}
#[test]
fn unqualified_still_works() {
let points = extract_bind_points("{user_id}");
assert_eq!(
points[0],
BindPoint::Reference {
name: "user_id".into(),
qualifier: BindQualifier::None,
lvalue_spec: None,
}
);
}
#[test]
fn qualified_referenced_bindings_returns_bare_name() {
let refs = referenced_bindings("VALUES ({coord:cycle}, {capture:balance}, {user_id})");
assert_eq!(refs, vec!["cycle", "balance", "user_id"]);
}
#[test]
fn coordinate_long_form() {
let points = extract_bind_points("{coordinate:row}");
assert_eq!(
points[0],
BindPoint::Reference {
name: "row".into(),
qualifier: BindQualifier::Input,
lvalue_spec: None,
}
);
}
#[test]
fn capture_simple() {
let result = parse_capture_points("select [username] from users where id={id}");
assert_eq!(result.captures.len(), 1);
assert_eq!(result.captures[0].source_name, "username");
assert_eq!(result.captures[0].as_name, "username");
assert_eq!(
result.raw_template,
"select username from users where id={id}"
);
}
#[test]
fn capture_with_alias() {
let result = parse_capture_points("select [username as u1] from users");
assert_eq!(result.captures.len(), 1);
assert_eq!(result.captures[0].source_name, "username");
assert_eq!(result.captures[0].as_name, "u1");
assert_eq!(result.raw_template, "select username from users");
}
#[test]
fn capture_with_type_cast() {
let result = parse_capture_points("select [(List) items] from orders");
assert_eq!(result.captures.len(), 1);
assert_eq!(result.captures[0].source_name, "items");
assert_eq!(result.captures[0].cast_type, Some("List".into()));
}
#[test]
fn capture_wildcard() {
let result = parse_capture_points("select [*] from users");
assert_eq!(result.captures.len(), 1);
assert_eq!(result.captures[0].source_name, "*");
}
#[test]
fn capture_empty_brackets_pass_through() {
let result = parse_capture_points(r#"{"arguments":["foo",[]]}"#);
assert!(
result.captures.is_empty(),
"no capture should be extracted: {:?}",
result.captures
);
assert_eq!(result.raw_template, r#"{"arguments":["foo",[]]}"#);
}
#[test]
fn capture_jni_array_signature_pass_through() {
let template = r#""operation":"forceKeyspaceFlush(java.lang.String,[Ljava.lang.String;)""#;
let result = parse_capture_points(template);
assert!(
result.captures.is_empty(),
"JNI signature should not parse as capture: {:?}",
result.captures
);
assert_eq!(result.raw_template, template);
}
#[test]
fn capture_json_array_with_string_pass_through() {
let result = parse_capture_points(r#"["alpha","beta"]"#);
assert!(result.captures.is_empty());
assert_eq!(result.raw_template, r#"["alpha","beta"]"#);
}
#[test]
fn capture_json_array_with_number_pass_through() {
let result = parse_capture_points("[42]");
assert!(result.captures.is_empty());
assert_eq!(result.raw_template, "[42]");
}
#[test]
fn capture_cql_collection_literal_pass_through() {
let template = "INSERT INTO t (vals) VALUES ([1, 2, 3])";
let result = parse_capture_points(template);
assert!(result.captures.is_empty());
assert_eq!(result.raw_template, template);
}
#[test]
fn capture_multiple() {
let result = parse_capture_points("select [a], [b as x] from t where id={id}");
assert_eq!(result.captures.len(), 2);
assert_eq!(result.captures[0].source_name, "a");
assert_eq!(result.captures[0].as_name, "a");
assert_eq!(result.captures[1].source_name, "b");
assert_eq!(result.captures[1].as_name, "x");
}
#[test]
fn capture_no_captures() {
let result = parse_capture_points("select * from users where id={id}");
assert!(result.captures.is_empty());
assert_eq!(result.raw_template, "select * from users where id={id}");
}
#[test]
fn capture_mixed_with_bind_points() {
let result =
parse_capture_points("select [name], [age as user_age] from users where id={userid}");
assert_eq!(result.captures.len(), 2);
assert!(result.raw_template.contains("{userid}"));
assert!(!result.raw_template.contains('['));
assert!(!result.raw_template.contains(']'));
}
}