#[cfg(test)]
mod tests;
use super::field_list::is_valid_field_name;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedIndexAttribute {
pub fields: Vec<String>,
pub using: Option<String>,
pub opclass: Option<String>,
}
pub fn parse_index_attribute(raw: &str) -> Result<ParsedIndexAttribute, String> {
let inner = raw
.strip_prefix("@@index(")
.and_then(|value| value.strip_suffix(')'))
.ok_or_else(|| format!("unsupported index attribute `{raw}`"))?;
let mut entries = split_top_level_commas(inner);
if entries.is_empty() {
return Err(format!(
"index attribute `{raw}` must list fields as `@@index([field1, field2])`"
));
}
let fields = parse_bracketed_field_list(raw, entries.remove(0))?;
if fields.is_empty() {
return Err(format!(
"index attribute `{raw}` must list at least one field"
));
}
let mut using = None;
let mut opclass = None;
for entry in entries {
let (key, value) = entry
.split_once(':')
.ok_or_else(|| format!("index attribute `{raw}` has invalid entry `{entry}`"))?;
let value = value.trim();
match key.trim() {
"using" if using.is_none() => using = Some(parse_using_value(raw, value)?),
"using" => {
return Err(format!(
"index attribute `{raw}` declares `using` more than once"
));
}
"opclass" if opclass.is_none() => opclass = Some(parse_opclass_value(raw, value)?),
"opclass" => {
return Err(format!(
"index attribute `{raw}` declares `opclass` more than once"
));
}
other => {
return Err(format!(
"index attribute `{raw}` has unsupported key `{other}`; expected `using` or `opclass`"
));
}
}
}
Ok(ParsedIndexAttribute {
fields,
using,
opclass,
})
}
fn parse_bracketed_field_list(raw: &str, entry: &str) -> Result<Vec<String>, String> {
let list = entry
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.ok_or_else(|| {
format!("index attribute `{raw}` must list fields as `@@index([field1, field2])`")
})?;
let mut fields = Vec::new();
for part in list.split(',').map(str::trim) {
if part.is_empty() {
continue;
}
if !is_valid_field_name(part) {
return Err(format!(
"index attribute `{raw}` lists invalid field name `{part}`"
));
}
if fields.contains(&part.to_owned()) {
return Err(format!(
"index attribute `{raw}` lists field `{part}` more than once"
));
}
fields.push(part.to_owned());
}
Ok(fields)
}
fn parse_using_value(raw: &str, value: &str) -> Result<String, String> {
if !is_valid_identifier(value) {
return Err(format!(
"index attribute `{raw}` has invalid `using` value `{value}`; expected a bare \
access method name like `ivfflat`"
));
}
Ok(value.to_owned())
}
fn parse_opclass_value(raw: &str, value: &str) -> Result<String, String> {
let inner = value
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.ok_or_else(|| {
format!(
"index attribute `{raw}` has invalid `opclass` value `{value}`; expected a \
quoted string like `\"vector_l2_ops\"`"
)
})?;
if !is_valid_identifier(inner) {
return Err(format!(
"index attribute `{raw}` has invalid `opclass` value `\"{inner}\"`"
));
}
Ok(inner.to_owned())
}
fn is_valid_identifier(value: &str) -> bool {
let mut chars = value.chars();
matches!(chars.next(), Some(first) if first.is_ascii_alphabetic() || first == '_')
&& chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
fn split_top_level_commas(input: &str) -> Vec<&str> {
let mut entries = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (index, ch) in input.char_indices() {
match ch {
'[' | '(' => depth += 1,
']' | ')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
entries.push(input[start..index].trim());
start = index + ch.len_utf8();
}
_ => {}
}
}
let tail = input[start..].trim();
if !tail.is_empty() {
entries.push(tail);
}
entries
.into_iter()
.filter(|entry| !entry.is_empty())
.collect()
}