modelvault-core 0.16.0

Core engine for ModelVault — application-focused embedded storage with model schemas, validation, and migrations.
Documentation
use crate::error::{DbError, QueryError};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Tok {
    Ident(String),
    Number(usize),
    Star,
    Comma,
    Dot,
    LParen,
    RParen,
    QMark,
    Eq,
    Lt,
    Lte,
    Gt,
    Gte,
}

fn err(msg: impl Into<String>) -> DbError {
    DbError::Query(QueryError {
        message: msg.into(),
    })
}

fn is_ident_start(c: char) -> bool {
    // Non-short-circuit `|` so both sides are counted by llvm-cov (unlike `||`).
    c.is_ascii_alphabetic() | (c == '_')
}
fn is_ident_cont(c: char) -> bool {
    c.is_ascii_alphanumeric() | (c == '_')
}

pub(crate) fn lex(input: &str) -> Result<Vec<Tok>, DbError> {
    let mut out = Vec::new();
    let mut chars = input.chars().peekable();
    while let Some(c) = chars.peek().copied() {
        if c.is_whitespace() {
            chars.next();
            continue;
        }
        match c {
            _ if c.is_ascii_digit() => {
                let mut s = String::new();
                while let Some(n) = chars.peek().copied() {
                    if n.is_ascii_digit() {
                        s.push(chars.next().unwrap());
                    } else {
                        break;
                    }
                }
                let n = s
                    .parse::<usize>()
                    .map_err(|_| err("invalid integer literal"))?;
                out.push(Tok::Number(n));
            }
            '*' => {
                chars.next();
                out.push(Tok::Star);
            }
            ',' => {
                chars.next();
                out.push(Tok::Comma);
            }
            '.' => {
                chars.next();
                out.push(Tok::Dot);
            }
            '(' => {
                chars.next();
                out.push(Tok::LParen);
            }
            ')' => {
                chars.next();
                out.push(Tok::RParen);
            }
            '?' => {
                chars.next();
                out.push(Tok::QMark);
            }
            '=' => {
                chars.next();
                out.push(Tok::Eq);
            }
            '<' => {
                chars.next();
                if chars.peek().copied() == Some('=') {
                    chars.next();
                    out.push(Tok::Lte);
                } else {
                    out.push(Tok::Lt);
                }
            }
            '>' => {
                chars.next();
                if chars.peek().copied() == Some('=') {
                    chars.next();
                    out.push(Tok::Gte);
                } else {
                    out.push(Tok::Gt);
                }
            }
            _ if is_ident_start(c) => {
                let mut s = String::new();
                s.push(chars.next().unwrap());
                while let Some(n) = chars.peek().copied() {
                    if is_ident_cont(n) {
                        s.push(chars.next().unwrap());
                    } else {
                        break;
                    }
                }
                out.push(Tok::Ident(s));
            }
            _ => return Err(err(format!("unsupported character in SQL: {c:?}"))),
        }
    }
    Ok(out)
}

pub(crate) fn ident_eq(a: &str, b: &str) -> bool {
    a.eq_ignore_ascii_case(b)
}