use super::column_list::matching_close;
use super::flags::flag_index;
use crate::ddl_ast::parse::clause::{ClauseCursor, Tok};
use crate::error::SqlError;
pub(super) fn read_with_options(
c: &mut ClauseCursor<'_, '_>,
src: &str,
) -> Result<Vec<(String, String)>, SqlError> {
if c.peek_punct('(') {
return c.paren_list("WITH options", |c| read_pair(c, src, false));
}
let mut pairs = vec![read_pair(c, src, true)?];
while c.eat_punct(',') {
pairs.push(read_pair(c, src, true)?);
}
Ok(pairs)
}
fn read_pair(
c: &mut ClauseCursor<'_, '_>,
src: &str,
bare: bool,
) -> Result<(String, String), SqlError> {
let key = c.name("WITH option name")?.to_lowercase();
c.expect_punct('=')?;
let value = read_value(c, src, &key, bare)?;
Ok((key, value))
}
fn ends_bare_clause(word: &str) -> bool {
["ENGINE", "BALANCED", "WITH"]
.iter()
.any(|kw| word.eq_ignore_ascii_case(kw))
|| flag_index(word).is_some()
}
fn read_value(
c: &mut ClauseCursor<'_, '_>,
src: &str,
key: &str,
bare: bool,
) -> Result<String, SqlError> {
let toks = c.remaining_tokens();
if c.peek_punct('[') {
let close = matching_close(toks, 0, '[', ']')
.ok_or_else(|| c.error(format!("WITH option {key}: unclosed '['")))?;
let inner = if close > 1 {
src[toks[1].span.start..toks[close - 1].span.end].trim()
} else {
""
};
c.skip(close + 1);
return Ok(inner.to_string());
}
let mut depth = 0i32;
let mut n = 0usize;
while let Some(tok) = toks.get(n) {
match &tok.tok {
Tok::Punct('(') => depth += 1,
Tok::Punct(')') if depth == 0 => break,
Tok::Punct(')') => depth -= 1,
Tok::Punct(',' | ';') if depth == 0 => break,
Tok::Word(w) if bare && depth == 0 && n > 0 && ends_bare_clause(w) => break,
_ => {}
}
n += 1;
}
if n == 0 {
return Err(c.error(format!(
"WITH option {key} needs a value — found {}",
c.describe_next()
)));
}
if depth != 0 {
return Err(c.error(format!("WITH option {key}: unbalanced parentheses")));
}
let value = match &toks[0].tok {
Tok::Str(s) | Tok::Ident(s) if n == 1 => s.to_string(),
_ => src[toks[0].span.start..toks[n - 1].span.end].to_string(),
};
c.skip(n);
Ok(value)
}
pub(super) fn read_balanced(c: &mut ClauseCursor<'_, '_>) -> Result<String, SqlError> {
c.expect_keyword("ON")?;
Ok(c.raw_paren("BALANCED ON")?.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ddl_ast::parse::clause::tokenize;
fn options(sql: &str) -> Result<Vec<(String, String)>, SqlError> {
let toks = tokenize(sql);
let mut c = ClauseCursor::new(sql, &toks, "CREATE COLLECTION");
c.expect_keyword("WITH")?;
let pairs = read_with_options(&mut c, sql)?;
c.finish()?;
Ok(pairs)
}
fn pair(k: &str, v: &str) -> (String, String) {
(k.to_string(), v.to_string())
}
#[test]
fn values_after_unicode_text_keep_their_offsets() {
assert_eq!(
options("WITH (note = 'ffff', ttl = 60)").expect("parses"),
vec![pair("note", "ffff"), pair("ttl", "60")]
);
}
#[test]
fn interval_value_keeps_its_literal() {
assert_eq!(
options("WITH (ttl = INTERVAL '15 minutes', capacity = 10)").expect("parses"),
vec![pair("ttl", "INTERVAL '15 minutes'"), pair("capacity", "10")]
);
assert_eq!(
options("WITH ttl = last_active + INTERVAL '1 hour'").expect("parses"),
vec![pair("ttl", "last_active + INTERVAL '1 hour'")]
);
}
#[test]
fn literal_comma_and_quote_stay_in_the_value() {
assert_eq!(
options("WITH (note = 'a, it''s)', engine = 'kv')").expect("parses"),
vec![pair("note", "a, it's)"), pair("engine", "kv")]
);
}
#[test]
fn list_value_is_its_bracket_interior() {
assert_eq!(
options("WITH (payload_indexes = ['a', 'b'])").expect("parses"),
vec![pair("payload_indexes", "'a', 'b'")]
);
}
#[test]
fn pair_without_equals_or_value_is_an_error() {
assert!(options("WITH (engine 'kv')").is_err());
assert!(options("WITH (engine = )").is_err());
assert!(options("WITH (payload_indexes = ['a'").is_err());
}
#[test]
fn balanced_clause_after_unicode_text_keeps_its_offsets() {
let sql = "BALANCED ON (group_key = 'ff', debit = tenant_id)";
let toks = tokenize(sql);
let mut c = ClauseCursor::new(sql, &toks, "CREATE COLLECTION");
c.expect_keyword("BALANCED").expect("BALANCED");
assert_eq!(
read_balanced(&mut c).expect("parses"),
"group_key = 'ff', debit = tenant_id"
);
}
}