orql 0.1.0

A toy SQL parser for a subset of the Oracle dialect.
Documentation
use std::{borrow::Cow, fmt::Display};

use super::Location;
use crate::scanner::{self, Comment, CommentStyle, NationalStyle, Token, TokenType};

/// The [parsing](super) `Result` type
pub type Result<T> = std::result::Result<T, Error>;

/// Parsing releated errors
#[derive(Debug)]
pub enum Error {
    /// An underlying error during scanning the parsed text
    Scanner(scanner::Error),

    /// General purpose error during the encounter of something unexpected
    Unexpected {
        unexpected: Cow<'static, str>,
        expected: &'static str,
        loc: Location,
    },

    /// Unabalanced paranthesis error
    Unbalanced {
        /// Location of the closing parenthesis missing an opening counterpart
        loc: Location,
    },

    /// Unsupported full partition outer join
    FullPartitionedOuterJoin {
        /// Location of the JOIN not being supported
        loc: Location,
    },

    /// Usage of an aggregate function in an invalid context
    AggregateFunctionNotAllowed {
        /// location of the function not allowed to
        /// be called in the parsed context
        loc: Location,
    },

    /// Usage of an analytical function in an invalid context
    AnalyticFunctionNotAllowed {
        /// location of the function not allowed to
        /// be called in the parsed context
        loc: Location,
    },

    InvalidNumArgs {
        /// location of the function call (or order by clause) to have an
        /// invalid number of arguments
        loc: Location,
    },

    InvalidArgName {
        /// location of the invalid argument name
        loc: Location,
    },
}

impl Error {
    /// Creates [Error::Unexpected] for an unexpectadly encountered token
    pub(super) fn unexpected_token<'s, T: AsRef<Token<'s>>>(t: T, expected: &'static str) -> Self {
        let t = t.as_ref();
        Self::Unexpected {
            unexpected: unexpected_token_display(t),
            expected,
            loc: t.loc,
        }
    }
}

fn unexpected_token_display(t: &Token<'_>) -> Cow<'static, str> {
    fn push_truncated(out: &mut String, mut max: usize, s: &str) {
        if !s.is_empty() {
            let starts_with_whitespace =
                s.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
            let mut splits = s.split_whitespace().enumerate();

            while max > 0
                && let Some((i, split)) = splits.next()
            {
                if i == 0 {
                    if starts_with_whitespace {
                        out.push(' ');
                        max -= 1;
                    }
                } else {
                    out.push(' ');
                    max -= 1;
                }

                if split.len() > max {
                    out.push_str(&split[..max]);
                    out.push('');
                    return;
                } else {
                    out.push_str(split);
                    max -= split.len();
                }
            }
            if splits.next().is_some() {
                out.push('');
            }
        }
    }
    match &t.ttype {
        TokenType::LeftParen => "`(`".into(),
        TokenType::RightParen => "`)`".into(),
        TokenType::Dot => "`.`".into(),
        TokenType::Comma => "`,`".into(),
        TokenType::Semicolon => "`;`".into(),
        TokenType::Plus => "`+`".into(),
        TokenType::Minus => "`-`".into(),
        TokenType::Star => "`*`".into(),
        TokenType::Slash => "`/`".into(),
        TokenType::At => "`@`".into(),
        TokenType::PipePipe => "`||`".into(),
        TokenType::Equal => "`=`".into(),
        TokenType::EqualGreater => "`=>`".into(),
        TokenType::CaretEqual => "`^=`".into(),
        TokenType::Less => "`<`".into(),
        TokenType::LessEqual => "`<=`".into(),
        TokenType::LessGreater => "`<>`".into(),
        TokenType::Greater => "`>`".into(),
        TokenType::GreaterEqual => "`>=`".into(),
        TokenType::BangEqual => "`!=`".into(),
        TokenType::QuestionMark => "`?`".into(),
        TokenType::Comment(Comment(text, style)) => {
            let (prefix, suffix) = match style {
                CommentStyle::Line => ("`--", "`"),
                CommentStyle::Block => ("`/*", "*/`"),
            };
            const COMMENT_MAX_LEN: usize = 20;
            // ~ one char for the potential ellipsis when truncating
            let mut out = String::with_capacity(COMMENT_MAX_LEN + 1 + prefix.len() + suffix.len());
            out.push_str(prefix);
            push_truncated(&mut out, COMMENT_MAX_LEN, text);
            out.push_str(suffix);
            out.into()
        }
        TokenType::Integer(lexem) | TokenType::Float(lexem) => format!("`{lexem}`").into(),
        TokenType::Text(text, style) => {
            let (prefix, s, suffix): (&str, &str, &str) = match (text, style) {
                (scanner::Text::Regular(s), NationalStyle::None) => ("`'", s, "'`"),
                (scanner::Text::Regular(s), NationalStyle::National) => ("`N'", s, "'`"),
                (scanner::Text::Quoted(s), NationalStyle::None) => {
                    ("`Q'", s.as_str_with_delimiters(), "'`")
                }
                (scanner::Text::Quoted(s), NationalStyle::National) => {
                    ("`NQ'", s.as_str_with_delimiters(), "'`")
                }
            };
            const TEXT_MAX_LEN: usize = 20;
            let mut out = String::with_capacity(TEXT_MAX_LEN + 1 + prefix.len() + suffix.len());
            out.push_str(prefix);
            // XXX expand `'` (for "regular" text only) to `''` here
            push_truncated(&mut out, TEXT_MAX_LEN, s);
            out.push_str(suffix);
            out.into()
        }
        TokenType::Placeholder(ident) => format!("':{ident}'").into(),
        TokenType::Identifier(ident, _) => format!("'{ident}'").into(),
        TokenType::Keyword(kw) => kw.as_str().into(),
    }
}

impl From<scanner::Error> for Error {
    fn from(value: scanner::Error) -> Self {
        Self::Scanner(value)
    }
}

impl From<&scanner::Error> for Error {
    fn from(value: &scanner::Error) -> Self {
        value.clone().into()
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Scanner(e) => e.fmt(f),
            Error::Unexpected {
                unexpected,
                expected,
                loc,
            } => {
                write!(f, "Unexpected {unexpected}; expected {expected} {loc}")
            }
            Error::Unbalanced { loc } => {
                write!(f, "Unbalanced parentheses {loc}",)
            }
            Error::FullPartitionedOuterJoin { loc } => {
                write!(
                    f,
                    "FULL PARTITIONED OUTER JOIN is not supported (ORA-39754) {loc}"
                )
            }
            Error::AggregateFunctionNotAllowed { loc } => {
                write!(f, "Aggregate functions not allowed here {loc}")
            }
            Error::AnalyticFunctionNotAllowed { loc } => {
                write!(f, "Analytic functions not allowed here {loc}")
            }
            Error::InvalidNumArgs { loc } => write!(f, "Invalid number of arguments {loc}"),
            Error::InvalidArgName { loc } => write!(
                f,
                "Invalid argument name, only simple identifiers allowed {loc}"
            ),
        }
    }
}

impl std::error::Error for Error {}