use crate::error::LiteError;
use crate::query::ddl::tokens::Token;
use super::cursor::{check_ident, describe, syntax};
use super::statement::SortedIndexStatement;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ReadFn {
Rank,
TopK,
Range,
SortedCount,
}
impl ReadFn {
pub(super) fn from_name(name: &str, from_form: bool) -> Option<Self> {
let upper = name.to_ascii_uppercase();
match (upper.as_str(), from_form) {
("RANK", false) => Some(Self::Rank),
("TOPK", _) => Some(Self::TopK),
("RANGE", true) => Some(Self::Range),
("SORTED_COUNT", false) => Some(Self::SortedCount),
_ => None,
}
}
pub(super) fn shares_sql_name(self) -> bool {
matches!(self, Self::Rank | Self::Range)
}
fn name(self) -> &'static str {
match self {
Self::Rank => "RANK",
Self::TopK => "TOPK",
Self::Range => "RANGE",
Self::SortedCount => "SORTED_COUNT",
}
}
}
pub(super) fn parse_read(
func: ReadFn,
tokens: &[Token<'_>],
name_at: usize,
) -> Option<Result<SortedIndexStatement, LiteError>> {
let (args, after) = match call_args(tokens, name_at + 1) {
Ok(found) => found,
Err(error) => return Some(Err(error)),
};
let trailing = match &tokens[after..] {
[] | [Token::Semicolon] => None,
[first, ..] => Some(first),
};
if let Some(first) = trailing {
if func.shares_sql_name() {
return None;
}
return Some(Err(syntax(format!(
"unexpected trailing input '{}' after {}(...): remove it or split the statement",
describe(first),
func.name()
))));
}
Some(statement(func, &args))
}
fn call_args<'t, 'a>(
tokens: &'t [Token<'a>],
open_at: usize,
) -> Result<(Vec<&'t Token<'a>>, usize), LiteError> {
if !matches!(tokens.get(open_at), Some(Token::Open)) {
return Err(syntax("expected '(' in function call"));
}
let mut args = Vec::new();
let mut offset = open_at + 1;
if matches!(tokens.get(offset), Some(Token::Close)) {
return Ok((args, offset + 1));
}
loop {
match tokens.get(offset) {
Some(token @ (Token::Bare(_) | Token::Identifier(_) | Token::String(_))) => {
args.push(token);
}
None | Some(Token::Semicolon) => {
return Err(syntax("expected ')' in function call"));
}
Some(other) => {
return Err(syntax(format!(
"expected a literal or identifier argument, got '{}'",
describe(other)
)));
}
}
offset += 1;
match tokens.get(offset) {
Some(Token::Comma) => offset += 1,
Some(Token::Close) => return Ok((args, offset + 1)),
None | Some(Token::Semicolon) => {
return Err(syntax("expected ')' in function call"));
}
Some(other) => {
return Err(syntax(format!(
"expected ',' or ')' between arguments, got '{}'",
describe(other)
)));
}
}
}
}
fn statement(func: ReadFn, args: &[&Token<'_>]) -> Result<SortedIndexStatement, LiteError> {
for arg in args {
if let Token::Bare(raw) = arg
&& raw.starts_with('$')
{
return Err(LiteError::Unsupported {
detail: format!(
"bound parameter {raw} in {}(): inline the value as a literal",
func.name()
),
});
}
}
match func {
ReadFn::Rank => {
let [index, key] = args else {
return Err(syntax("RANK requires 2 arguments: (index_name, key_value)"));
};
Ok(SortedIndexStatement::Rank {
index: index_name(func, index)?,
key: key_bytes(key)?,
})
}
ReadFn::TopK => {
let [index, k] = args else {
return Err(syntax("TOPK requires 2 arguments: (index_name, k)"));
};
let k = match k {
Token::Bare(raw) => raw.parse::<u32>().ok(),
_ => None,
}
.ok_or_else(|| {
syntax(format!(
"TOPK: k must be a positive integer, got '{}'",
describe(k)
))
})?;
Ok(SortedIndexStatement::TopK {
index: index_name(func, index)?,
k,
})
}
ReadFn::Range => {
let [index, min, max] = args else {
return Err(syntax(
"RANGE requires 3 arguments: (index_name, score_min, score_max)",
));
};
Ok(SortedIndexStatement::Range {
index: index_name(func, index)?,
min: score_bound(min)?,
max: score_bound(max)?,
})
}
ReadFn::SortedCount => {
let [index] = args else {
return Err(syntax("SORTED_COUNT requires 1 argument: (index_name)"));
};
Ok(SortedIndexStatement::Count {
index: index_name(func, index)?,
})
}
}
}
fn index_name(func: ReadFn, token: &Token<'_>) -> Result<String, LiteError> {
match token {
Token::String(name) => Ok(name.clone()),
Token::Bare(raw) | Token::Identifier(raw) => check_ident(raw),
other => Err(syntax(format!(
"{}: index name must be an identifier or a string literal, got '{}'",
func.name(),
describe(other)
))),
}
}
fn key_bytes(token: &Token<'_>) -> Result<Vec<u8>, LiteError> {
match token {
Token::String(text) => Ok(text.as_bytes().to_vec()),
Token::Bare(raw) => Ok(raw.as_bytes().to_vec()),
other => Err(syntax(format!(
"RANK: key_value must be a string literal or a number, got '{}'",
describe(other)
))),
}
}
fn score_bound(token: &Token<'_>) -> Result<Option<f64>, LiteError> {
let text = match token {
Token::Bare(raw) => (*raw).to_string(),
Token::String(text) => text.trim().to_string(),
other => {
return Err(syntax(format!(
"RANGE: score bound must be a number, NULL, NONE or *, got '{}'",
describe(other)
)));
}
};
if text.eq_ignore_ascii_case("NULL") || text.eq_ignore_ascii_case("NONE") || text == "*" {
return Ok(None);
}
match text.parse::<f64>() {
Ok(score) if score.is_finite() => Ok(Some(score)),
Ok(_) => Err(syntax(format!(
"RANGE: score bound '{text}' must be a finite number"
))),
Err(_) => Err(LiteError::Unsupported {
detail: format!(
"RANGE string score bound '{text}': Lite sorted indexes rank by one numeric \
score column; pass a number, NULL, NONE or *"
),
}),
}
}