use super::cache::global_cache;
use super::CommandResult;
use crate::cli::error::helpers as error_helpers;
use crate::cli::logging::QueryLogger;
use crate::cli::syntax_highlighting::{highlight_sparql, HighlightConfig};
use crate::cli::validation::MultiValidator;
use crate::cli::validation::{dataset_validation, query_validation};
use crate::cli::{progress::helpers, ArgumentValidator, CliContext};
use oxirs_core::rdf_store::RdfStore;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
pub async fn run(dataset: String, query: String, file: bool, output: String) -> CommandResult {
let ctx = CliContext::new();
let mut validator = MultiValidator::new();
validator.add(
ArgumentValidator::new("dataset", Some(&dataset))
.required()
.custom(|d| !d.trim().is_empty(), "Dataset name cannot be empty"),
);
if !PathBuf::from(&dataset).exists() {
dataset_validation::validate_dataset_name(&dataset)?;
}
validator.add(
ArgumentValidator::new("output", Some(&output))
.required()
.custom(
is_supported_output_format,
"Output format must be one of: json, csv, tsv, table, xml",
),
);
if file {
let query_path = PathBuf::from(&query);
validator.add(
ArgumentValidator::new("query_file", Some(query_path.to_str().unwrap_or("")))
.required()
.is_file(),
);
}
validator.finish()?;
ctx.info(&format!("Executing SPARQL query on dataset '{dataset}'"));
let sparql_query = if file {
let query_path = PathBuf::from(&query);
let pb = helpers::file_progress(1);
pb.set_message("Reading query file");
let content = fs::read_to_string(&query_path)?;
pb.finish_with_message("Query file loaded");
content
} else {
query
};
query_validation::validate_sparql_syntax(&sparql_query)?;
let complexity = query_validation::estimate_query_complexity(&sparql_query);
if complexity >= 7 {
ctx.warn(&format!(
"Complex query detected (complexity: {}/10) - {}",
complexity,
query_validation::complexity_description(complexity)
));
}
if ctx.should_show_verbose() {
ctx.info("Query:");
let highlight_config = HighlightConfig::default();
let highlighted_query = highlight_sparql(&sparql_query, &highlight_config);
println!("{}", highlighted_query);
ctx.verbose(&format!(
"Query complexity: {}/10 - {}",
complexity,
query_validation::complexity_description(complexity)
));
}
let dataset_dir = PathBuf::from(&dataset);
let dataset_path = if dataset_dir.join("oxirs.toml").exists() {
let dataset_name = dataset_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&dataset);
let (storage_path, _config) =
crate::config::load_named_dataset(&dataset_dir, dataset_name)?;
storage_path
} else {
dataset_dir
};
let store = if dataset_path.is_dir() {
RdfStore::open(&dataset_path).map_err(|e| format!("Failed to open dataset: {e}"))?
} else {
return Err(error_helpers::dataset_not_found_error(&dataset));
};
let start_time = Instant::now();
let mut query_logger = QueryLogger::new("sparql_query", &dataset);
query_logger.add_query_text(&sparql_query);
let cache_enabled = env::var("OXIRS_DISABLE_CACHE").unwrap_or_default() != "1";
if cache_enabled {
let cache = global_cache();
if let Some(_cached_json) = cache.get(&dataset, &sparql_query) {
if ctx.should_show_verbose() {
ctx.verbose("✨ Result served from cache (full support coming soon)");
}
}
}
let query_progress = helpers::query_progress();
query_progress.set_message("Executing SPARQL query");
let results = match store.query(&sparql_query) {
Ok(res) => {
let binding_count = res.len();
query_logger.complete(binding_count);
let duration_ms = start_time.elapsed().as_secs_f64() * 1000.0;
if let Err(e) = super::history::record_query(
&dataset,
&sparql_query,
Some(duration_ms),
Some(binding_count),
true,
None,
) {
if ctx.should_show_verbose() {
ctx.verbose(&format!("Failed to record query history: {}", e));
}
}
if cache_enabled && is_cacheable_query(&sparql_query) && ctx.should_show_verbose() {
ctx.verbose("💾 Query is cacheable (caching will be enabled in future release)");
}
res
}
Err(e) => {
query_logger.error(&e.to_string());
let duration_ms = start_time.elapsed().as_secs_f64() * 1000.0;
if let Err(hist_err) = super::history::record_query(
&dataset,
&sparql_query,
Some(duration_ms),
None,
false,
Some(e.to_string()),
) {
if ctx.should_show_verbose() {
ctx.verbose(&format!("Failed to record query history: {}", hist_err));
}
}
return Err(format!("Query execution failed: {e}").into());
}
};
let duration = start_time.elapsed();
query_progress
.finish_with_message(format!("Query completed in {:.3}s", duration.as_secs_f64()));
ctx.info("Query Results");
ctx.info(&format!(
"Execution time: {:.3} seconds",
duration.as_secs_f64()
));
ctx.info(&format!("Result count: {} solutions", results.len()));
format_results_enhanced(&results, &output, &ctx)?;
Ok(())
}
fn is_supported_output_format(format: &str) -> bool {
matches!(
format,
"json" | "csv" | "tsv" | "table" | "xml" | "html" | "markdown" | "md"
)
}
fn is_cacheable_query(query: &str) -> bool {
let query_upper = query.to_uppercase();
for line in query_upper.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with("PREFIX") {
continue;
}
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("SELECT") || trimmed.starts_with("ASK") {
return true;
}
if trimmed.starts_with("CONSTRUCT")
|| trimmed.starts_with("DESCRIBE")
|| trimmed.starts_with("INSERT")
|| trimmed.starts_with("DELETE")
|| trimmed.starts_with("CLEAR")
|| trimmed.starts_with("DROP")
{
return false;
}
}
false
}
fn format_results_enhanced(
results: &oxirs_core::rdf_store::OxirsQueryResults,
output_format: &str,
_ctx: &crate::cli::CliContext,
) -> Result<(), Box<dyn std::error::Error>> {
use crate::cli::formatters::{create_formatter, Binding, QueryResults, RdfTerm};
use oxirs_core::rdf_store::QueryResults as CoreQueryResults;
use std::io;
let formatter_results = match results.results() {
CoreQueryResults::Bindings(bindings) => {
let variables = results.variables();
QueryResults {
variables: variables.to_vec(),
bindings: bindings
.iter()
.map(|var_binding| {
let values: Vec<Option<RdfTerm>> = variables
.iter()
.map(|var| var_binding.get(var).map(term_to_rdf_term))
.collect();
Binding { values }
})
.collect(),
}
}
CoreQueryResults::Boolean(value) => {
QueryResults {
variables: vec!["result".to_string()],
bindings: vec![Binding {
values: vec![Some(RdfTerm::Literal {
value: value.to_string(),
lang: None,
datatype: Some("http://www.w3.org/2001/XMLSchema#boolean".to_string()),
})],
}],
}
}
CoreQueryResults::Graph(quads) => {
QueryResults {
variables: vec![
"subject".to_string(),
"predicate".to_string(),
"object".to_string(),
],
bindings: quads
.iter()
.map(|quad| Binding {
values: vec![
Some(subject_to_rdf_term(quad.subject())),
Some(predicate_to_rdf_term(quad.predicate())),
Some(object_to_rdf_term(quad.object())),
],
})
.collect(),
}
}
};
if let Some(formatter) = create_formatter(output_format) {
let mut stdout = io::stdout();
formatter.format(&formatter_results, &mut stdout)?;
} else {
return Err(format!("Unsupported output format: {output_format}").into());
}
Ok(())
}
fn term_to_rdf_term(term: &oxirs_core::model::Term) -> crate::cli::formatters::RdfTerm {
use crate::cli::formatters::RdfTerm;
use oxirs_core::model::Term;
match term {
Term::NamedNode(node) => RdfTerm::Uri {
value: node.as_str().to_string(),
},
Term::BlankNode(bnode) => RdfTerm::Bnode {
value: bnode.as_str().to_string(),
},
Term::Literal(lit) => RdfTerm::Literal {
value: lit.value().to_string(),
lang: lit.language().map(|l| l.to_string()),
datatype: Some(lit.datatype().as_str().to_string()),
},
Term::Variable(var) => {
RdfTerm::Literal {
value: format!("?{}", var.name()),
lang: None,
datatype: None,
}
}
Term::QuotedTriple(triple) => {
RdfTerm::Literal {
value: format!(
"<<{} {} {}>>",
triple.subject(),
triple.predicate(),
triple.object()
),
lang: None,
datatype: Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement".to_string()),
}
}
}
}
fn subject_to_rdf_term(subject: &oxirs_core::model::Subject) -> crate::cli::formatters::RdfTerm {
use crate::cli::formatters::RdfTerm;
use oxirs_core::model::Subject;
match subject {
Subject::NamedNode(node) => RdfTerm::Uri {
value: node.as_str().to_string(),
},
Subject::BlankNode(bnode) => RdfTerm::Bnode {
value: bnode.as_str().to_string(),
},
Subject::Variable(var) => {
RdfTerm::Literal {
value: format!("?{}", var.name()),
lang: None,
datatype: None,
}
}
Subject::QuotedTriple(triple) => {
RdfTerm::Literal {
value: format!(
"<<{} {} {}>>",
triple.subject(),
triple.predicate(),
triple.object()
),
lang: None,
datatype: Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement".to_string()),
}
}
}
}
fn predicate_to_rdf_term(
predicate: &oxirs_core::model::Predicate,
) -> crate::cli::formatters::RdfTerm {
use crate::cli::formatters::RdfTerm;
use oxirs_core::model::Predicate;
match predicate {
Predicate::NamedNode(node) => RdfTerm::Uri {
value: node.as_str().to_string(),
},
Predicate::Variable(var) => {
RdfTerm::Literal {
value: format!("?{}", var.name()),
lang: None,
datatype: None,
}
}
}
}
fn object_to_rdf_term(object: &oxirs_core::model::Object) -> crate::cli::formatters::RdfTerm {
use crate::cli::formatters::RdfTerm;
use oxirs_core::model::Object;
match object {
Object::NamedNode(node) => RdfTerm::Uri {
value: node.as_str().to_string(),
},
Object::BlankNode(bnode) => RdfTerm::Bnode {
value: bnode.as_str().to_string(),
},
Object::Literal(lit) => RdfTerm::Literal {
value: lit.value().to_string(),
lang: lit.language().map(|l| l.to_string()),
datatype: Some(lit.datatype().as_str().to_string()),
},
Object::Variable(var) => {
RdfTerm::Literal {
value: format!("?{}", var.name()),
lang: None,
datatype: None,
}
}
Object::QuotedTriple(triple) => {
RdfTerm::Literal {
value: format!(
"<<{} {} {}>>",
triple.subject(),
triple.predicate(),
triple.object()
),
lang: None,
datatype: Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement".to_string()),
}
}
}
}