use crate::parser;
use crate::Result;
pub struct Validated {
sql: String,
visual: String,
has_visual: bool,
tree: Option<tree_sitter::Tree>,
valid: bool,
errors: Vec<ValidationError>,
warnings: Vec<ValidationWarning>,
}
impl Validated {
pub fn has_visual(&self) -> bool {
self.has_visual
}
pub fn sql(&self) -> &str {
&self.sql
}
pub fn visual(&self) -> &str {
&self.visual
}
pub fn tree(&self) -> Option<&tree_sitter::Tree> {
self.tree.as_ref()
}
pub fn valid(&self) -> bool {
self.valid
}
pub fn errors(&self) -> &[ValidationError] {
&self.errors
}
pub fn warnings(&self) -> &[ValidationWarning] {
&self.warnings
}
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub message: String,
pub location: Option<Location>,
}
#[derive(Debug, Clone)]
pub struct ValidationWarning {
pub message: String,
pub location: Option<Location>,
}
#[derive(Debug, Clone)]
pub struct Location {
pub line: usize,
pub column: usize,
}
pub fn validate(query: &str) -> Result<Validated> {
let mut errors = Vec::new();
let warnings = Vec::new();
let source_tree = match parser::SourceTree::new(query) {
Ok(st) => st,
Err(e) => {
errors.push(ValidationError {
message: e.to_string(),
location: None,
});
return Ok(Validated {
sql: String::new(),
visual: String::new(),
has_visual: false,
tree: None,
valid: false,
errors,
warnings,
});
}
};
let sql_part = source_tree.extract_sql().unwrap_or_default();
let viz_part = source_tree.extract_visualise().unwrap_or_default();
let root = source_tree.root();
let has_visual = source_tree
.find_node(&root, "(visualise_statement) @viz")
.is_some();
if !has_visual {
return Ok(Validated {
sql: sql_part,
visual: viz_part,
has_visual: false,
tree: None,
valid: true,
errors,
warnings,
});
}
if let Err(e) = source_tree.validate() {
errors.push(ValidationError {
message: e.to_string(),
location: None,
});
return Ok(Validated {
sql: sql_part,
visual: viz_part,
has_visual: true,
tree: Some(source_tree.tree),
valid: false,
errors,
warnings,
});
}
let plots = match parser::build_ast(&source_tree) {
Ok(p) => p,
Err(e) => {
errors.push(ValidationError {
message: e.to_string(),
location: None,
});
return Ok(Validated {
sql: sql_part,
visual: viz_part,
has_visual,
tree: Some(source_tree.tree),
valid: false,
errors,
warnings,
});
}
};
if let Some(plot) = plots.first() {
for (layer_idx, layer) in plot.layers.iter().enumerate() {
let context = format!("Layer {}", layer_idx + 1);
if !layer.mappings.wildcard {
if let Err(e) = layer.validate_required_aesthetics() {
errors.push(ValidationError {
message: format!("{}: {}", context, e),
location: None,
});
}
}
if let Err(e) = layer.validate_settings() {
errors.push(ValidationError {
message: format!("{}: {}", context, e),
location: None,
});
}
}
}
Ok(Validated {
sql: sql_part,
visual: viz_part,
has_visual,
tree: Some(source_tree.tree),
valid: errors.is_empty(),
errors,
warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_with_visual() {
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y").unwrap();
assert!(validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().starts_with("VISUALISE"));
assert!(validated.tree().is_some());
assert!(validated.valid());
}
#[test]
fn test_validate_without_visual() {
let validated = validate("SELECT 1 as x, 2 as y").unwrap();
assert!(!validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().is_empty());
assert!(validated.tree().is_none());
assert!(validated.valid());
}
#[test]
fn test_validate_valid_query() {
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y").unwrap();
assert!(
validated.valid(),
"Expected valid query: {:?}",
validated.errors()
);
assert!(validated.errors().is_empty());
}
#[test]
fn test_validate_missing_required_aesthetic() {
let validated =
validate("SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x").unwrap();
assert!(!validated.valid());
assert!(!validated.errors().is_empty());
assert!(validated.errors()[0].message.contains("y"));
}
#[test]
fn test_validate_syntax_error() {
let validated = validate("SELECT 1 VISUALISE DRAW invalidgeom").unwrap();
assert!(!validated.valid());
assert!(!validated.errors().is_empty());
}
#[test]
fn test_validate_sql_and_visual_content() {
let query = "SELECT 1 as x, 2 as y VISUALISE DRAW point MAPPING x AS x, y AS y DRAW line MAPPING x AS x, y AS y";
let validated = validate(query).unwrap();
assert!(validated.has_visual());
assert_eq!(validated.sql(), "SELECT 1 as x, 2 as y");
assert!(validated.visual().contains("DRAW point"));
assert!(validated.visual().contains("DRAW line"));
assert!(validated.valid());
}
#[test]
fn test_validate_sql_only() {
let query = "SELECT 1 as x, 2 as y";
let validated = validate(query).unwrap();
assert!(validated.valid());
assert!(validated.errors().is_empty());
}
}