#[cfg(test)]
mod tests;
use crate::{
db::{
predicate::{CoercionId, CompareOp, ComparePredicate, Predicate},
reduced_sql::{Keyword, SqlParseError, SqlTokenCursor, TokenKind, tokenize_sql},
},
value::Value,
};
const DIRECT_STARTS_WITH_NON_FIELD_FEATURE: &str =
"STARTS_WITH first argument forms beyond plain or LOWER/UPPER field wrappers";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TextPredicateWrapper {
Lower,
Upper,
}
impl TextPredicateWrapper {
const fn unsupported_feature(self) -> &'static str {
match self {
Self::Lower => {
"LOWER(field) predicate forms beyond LIKE 'prefix%' or ordered text bounds"
}
Self::Upper => {
"UPPER(field) predicate forms beyond LIKE 'prefix%' or ordered text bounds"
}
}
}
}
#[derive(Debug, Eq, PartialEq)]
enum PredicateFieldOperand {
Plain(String),
Wrapped {
field: String,
wrapper: TextPredicateWrapper,
},
}
impl PredicateFieldOperand {
fn into_field_and_coercion(self) -> (String, CoercionId) {
match self {
Self::Plain(field) => (field, CoercionId::Strict),
Self::Wrapped { field, .. } => (field, CoercionId::TextCasefold),
}
}
}
pub(crate) fn parse_sql_predicate(sql: &str) -> Result<Predicate, SqlParseError> {
let tokens = tokenize_sql(sql)?;
let mut cursor = SqlTokenCursor::new(tokens);
let predicate = parse_predicate_from_cursor(&mut cursor)?;
if cursor.eat_semicolon() && !cursor.is_eof() {
return Err(SqlParseError::unsupported_feature(
"multi-statement SQL input",
));
}
if !cursor.is_eof() {
if let Some(feature) = cursor.peek_unsupported_feature() {
return Err(SqlParseError::unsupported_feature(feature));
}
return Err(SqlParseError::expected_end_of_input(cursor.peek_kind()));
}
Ok(predicate)
}
pub(in crate::db) fn parse_predicate_from_cursor(
cursor: &mut SqlTokenCursor,
) -> Result<Predicate, SqlParseError> {
parse_or_predicate(cursor)
}
fn parse_or_predicate(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
let mut left = parse_and_predicate(cursor)?;
while cursor.eat_keyword(Keyword::Or) {
let right = parse_and_predicate(cursor)?;
left = Predicate::Or(vec![left, right]);
}
Ok(left)
}
fn parse_and_predicate(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
let mut left = parse_not_predicate(cursor)?;
while cursor.eat_keyword(Keyword::And) {
let right = parse_not_predicate(cursor)?;
left = Predicate::And(vec![left, right]);
}
Ok(left)
}
fn parse_not_predicate(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
if cursor.eat_keyword(Keyword::Not) {
return Ok(Predicate::Not(Box::new(parse_not_predicate(cursor)?)));
}
parse_predicate_primary(cursor)
}
fn parse_predicate_primary(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
if cursor.eat_lparen() {
let predicate = parse_predicate_from_cursor(cursor)?;
cursor.expect_rparen()?;
return Ok(predicate);
}
if cursor.peek_identifier_keyword("STARTS_WITH")
&& matches!(cursor.peek_next_kind(), Some(TokenKind::LParen))
{
return parse_starts_with_predicate(cursor);
}
parse_field_predicate(cursor)
}
fn parse_field_predicate(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
let operand = parse_predicate_field_operand(cursor)?;
if cursor.eat_identifier_keyword("LIKE") {
return parse_like_prefix_predicate(cursor, operand);
}
match operand {
PredicateFieldOperand::Plain(field) => parse_plain_field_predicate(cursor, field),
PredicateFieldOperand::Wrapped { field, wrapper } => {
parse_wrapped_field_predicate(cursor, field, wrapper)
}
}
}
fn parse_plain_field_predicate(
cursor: &mut SqlTokenCursor,
field: String,
) -> Result<Predicate, SqlParseError> {
if cursor.eat_keyword(Keyword::Is) {
let is_not = cursor.eat_keyword(Keyword::Not);
cursor.expect_keyword(Keyword::Null)?;
return Ok(if is_not {
Predicate::IsNotNull { field }
} else {
Predicate::IsNull { field }
});
}
if cursor.eat_keyword(Keyword::Not) {
if cursor.eat_keyword(Keyword::In) {
return parse_in_predicate(cursor, field, true);
}
return Err(SqlParseError::expected("IN after NOT", cursor.peek_kind()));
}
if cursor.eat_keyword(Keyword::In) {
return parse_in_predicate(cursor, field, false);
}
if cursor.eat_keyword(Keyword::Between) {
return parse_between_predicate(cursor, field);
}
let op = cursor.parse_compare_operator()?;
let value = cursor.parse_literal()?;
Ok(predicate_compare(field, op, value))
}
fn parse_wrapped_field_predicate(
cursor: &mut SqlTokenCursor,
field: String,
wrapper: TextPredicateWrapper,
) -> Result<Predicate, SqlParseError> {
if cursor.eat_keyword(Keyword::Is)
|| cursor.eat_keyword(Keyword::Not)
|| cursor.eat_keyword(Keyword::In)
|| cursor.eat_keyword(Keyword::Between)
{
return Err(SqlParseError::unsupported_feature(
wrapper.unsupported_feature(),
));
}
let op = cursor.parse_compare_operator()?;
if !matches!(
op,
CompareOp::Gt | CompareOp::Gte | CompareOp::Lt | CompareOp::Lte
) {
return Err(SqlParseError::unsupported_feature(
wrapper.unsupported_feature(),
));
}
let value = cursor.parse_literal()?;
if !matches!(value, Value::Text(_)) {
return Err(SqlParseError::unsupported_feature(
wrapper.unsupported_feature(),
));
}
Ok(predicate_compare_with_coercion(
field,
op,
value,
CoercionId::TextCasefold,
))
}
fn parse_predicate_field_operand(
cursor: &mut SqlTokenCursor,
) -> Result<PredicateFieldOperand, SqlParseError> {
if cursor.peek_identifier_keyword("LOWER")
&& matches!(cursor.peek_next_kind(), Some(TokenKind::LParen))
{
return parse_wrapped_field_operand(cursor, TextPredicateWrapper::Lower);
}
if cursor.peek_identifier_keyword("UPPER")
&& matches!(cursor.peek_next_kind(), Some(TokenKind::LParen))
{
return parse_wrapped_field_operand(cursor, TextPredicateWrapper::Upper);
}
Ok(PredicateFieldOperand::Plain(cursor.expect_identifier()?))
}
fn parse_like_prefix_predicate(
cursor: &mut SqlTokenCursor,
operand: PredicateFieldOperand,
) -> Result<Predicate, SqlParseError> {
let Some(TokenKind::StringLiteral(pattern)) = cursor.bump() else {
return Err(SqlParseError::expected(
"string literal pattern after LIKE",
cursor.peek_kind(),
));
};
let Some(prefix) = like_prefix_from_pattern(pattern.as_str()) else {
return Err(SqlParseError::unsupported_feature(
"LIKE patterns beyond trailing '%' prefix form",
));
};
let (field, coercion) = operand.into_field_and_coercion();
Ok(Predicate::Compare(ComparePredicate::with_coercion(
field,
CompareOp::StartsWith,
Value::Text(prefix.to_string()),
coercion,
)))
}
fn parse_starts_with_predicate(cursor: &mut SqlTokenCursor) -> Result<Predicate, SqlParseError> {
let _ = cursor.eat_identifier_keyword("STARTS_WITH");
cursor.expect_lparen()?;
let operand = parse_predicate_field_operand(cursor)?;
if matches!(cursor.peek_kind(), Some(TokenKind::LParen)) {
return Err(SqlParseError::unsupported_feature(
DIRECT_STARTS_WITH_NON_FIELD_FEATURE,
));
}
expect_predicate_argument_comma(cursor, "',' between STARTS_WITH arguments")?;
let Some(TokenKind::StringLiteral(prefix)) = cursor.bump() else {
return Err(SqlParseError::expected(
"string literal second argument to STARTS_WITH",
cursor.peek_kind(),
));
};
cursor.expect_rparen()?;
let (field, coercion) = operand.into_field_and_coercion();
Ok(Predicate::Compare(ComparePredicate::with_coercion(
field,
CompareOp::StartsWith,
Value::Text(prefix),
coercion,
)))
}
fn parse_wrapped_field_operand(
cursor: &mut SqlTokenCursor,
wrapper: TextPredicateWrapper,
) -> Result<PredicateFieldOperand, SqlParseError> {
let _ = cursor.bump();
cursor.expect_lparen()?;
let field = cursor.expect_identifier()?;
cursor.expect_rparen()?;
Ok(PredicateFieldOperand::Wrapped { field, wrapper })
}
fn expect_predicate_argument_comma(
cursor: &mut SqlTokenCursor,
context: &'static str,
) -> Result<(), SqlParseError> {
if cursor.eat_comma() {
return Ok(());
}
Err(SqlParseError::expected(context, cursor.peek_kind()))
}
fn parse_in_predicate(
cursor: &mut SqlTokenCursor,
field: String,
negated: bool,
) -> Result<Predicate, SqlParseError> {
cursor.expect_lparen()?;
let mut values = Vec::new();
loop {
values.push(cursor.parse_literal()?);
if !cursor.eat_comma() {
break;
}
}
cursor.expect_rparen()?;
let op = if negated {
CompareOp::NotIn
} else {
CompareOp::In
};
Ok(Predicate::Compare(ComparePredicate::with_coercion(
field,
op,
Value::List(values),
CoercionId::Strict,
)))
}
fn parse_between_predicate(
cursor: &mut SqlTokenCursor,
field: String,
) -> Result<Predicate, SqlParseError> {
let lower = cursor.parse_literal()?;
cursor.expect_keyword(Keyword::And)?;
let upper = cursor.parse_literal()?;
Ok(Predicate::And(vec![
predicate_compare(field.clone(), CompareOp::Gte, lower),
predicate_compare(field, CompareOp::Lte, upper),
]))
}
fn like_prefix_from_pattern(pattern: &str) -> Option<&str> {
if !pattern.ends_with('%') {
return None;
}
let prefix = &pattern[..pattern.len() - 1];
if prefix.contains('%') || prefix.contains('_') {
return None;
}
Some(prefix)
}
fn predicate_compare(field: String, op: CompareOp, value: Value) -> Predicate {
let coercion = match op {
CompareOp::Lt | CompareOp::Lte | CompareOp::Gt | CompareOp::Gte => {
if matches!(value, Value::Text(_)) {
CoercionId::Strict
} else {
CoercionId::NumericWiden
}
}
_ => CoercionId::Strict,
};
predicate_compare_with_coercion(field, op, value, coercion)
}
fn predicate_compare_with_coercion(
field: String,
op: CompareOp,
value: Value,
coercion: CoercionId,
) -> Predicate {
Predicate::Compare(ComparePredicate::with_coercion(field, op, value, coercion))
}