mod dialect;
mod lexer;
pub use dialect::{first_unsupported, Construct, Dialect};
#[derive(Debug, Clone)]
pub struct Analyzed {
pub constructs: Vec<Construct>,
pub inline_override: Option<&'static str>,
pub rewrite: Rewrite,
}
#[derive(Debug, Clone)]
pub enum Rewrite {
Identity,
Replaced(String),
}
impl Rewrite {
pub fn apply(&self, original: &str) -> String {
match self {
Rewrite::Identity => original.to_string(),
Rewrite::Replaced(s) => s.clone(),
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct QueryAnalyzer;
impl QueryAnalyzer {
pub fn new() -> Self {
Self
}
pub fn analyze(&self, query: &str) -> Analyzed {
let (inline_override, remainder) = lexer::strip_dactyl_directive(query);
let tokens = lexer::tokenize(&remainder);
let constructs = detect_constructs(&tokens);
let rewrite = if constructs.is_empty() {
Rewrite::Identity
} else {
Rewrite::Replaced(remainder)
};
Analyzed {
constructs,
inline_override,
rewrite,
}
}
}
fn detect_constructs(tokens: &[lexer::Token]) -> Vec<Construct> {
let mut out = Vec::new();
let mut i = 0;
while i < tokens.len() {
if let lexer::Token::Word(w) = &tokens[i] {
match w.as_str() {
"json_each" => out.push(Construct::JsonEach),
"json_tree" => out.push(Construct::JsonTree),
"without" => {
if i + 1 < tokens.len() {
if let lexer::Token::Word(n) = &tokens[i + 1] {
if n == "rowid" {
out.push(Construct::WithoutRowId);
i += 1;
}
}
}
}
"strict" => out.push(Construct::Strict),
"jsonb" => out.push(Construct::Jsonb),
"returning" => out.push(Construct::Returning),
"ilike" => out.push(Construct::Ilike),
"gen_random_uuid" => out.push(Construct::GenRandomUuid),
"now" => out.push(Construct::NowFn),
_ => {}
}
} else if let lexer::Token::Op(o) = &tokens[i] {
match o.as_str() {
"->>" => out.push(Construct::JsonArrowText),
"->" => out.push(Construct::JsonArrow),
"@>" => out.push(Construct::JsonContains),
"<@" => out.push(Construct::JsonContained),
_ => {}
}
} else if matches!(tokens[i], lexer::Token::Question) {
out.push(Construct::JsonExists);
}
i += 1;
}
out
}
pub fn dialect_of(datastore: &str) -> Option<Dialect> {
match datastore {
"sqlite" => Some(Dialect::Sqlite),
"neon" | "postgres" | "postgresql" | "pg" => Some(Dialect::Postgres),
_ => None,
}
}