use crate::error::{NopalError, Result};
use std::collections::HashMap;
use std::time::SystemTime;
use crate::query::nql::parser::ast::{Statement, DeleteStmt, UpdateStmt, AddStmt, Query};
use crate::query::nql::executor::{Executor, UpdateResult};
use crate::query::nql::executor::result::QueryResult;
use crate::Transaction;
pub struct SketchManager {
sketches: HashMap<String, Sketch>,
}
#[derive(Debug, Clone)]
pub struct Sketch {
pub name: String,
pub statement: Statement,
pub created_at: SystemTime,
pub description: Option<String>,
preview_cache: Option<SketchPreview>,
}
#[derive(Debug, Clone)]
pub struct SketchPreview {
pub preview_type: PreviewType,
pub generated_at: SystemTime,
}
#[derive(Debug, Clone)]
pub enum PreviewType {
QueryResult(QueryResult),
DeletePreview {
nodes_affected: usize,
edges_affected: usize,
sample_nodes: Vec<String>, },
UpdatePreview {
nodes_affected: usize,
edges_affected: usize,
sample_changes: Vec<UpdateSample>,
},
AddPreview {
nodes_to_add: usize,
edges_to_add: usize,
},
Other(String),
}
#[derive(Debug, Clone)]
pub struct UpdateSample {
pub node_id: String,
pub property: String,
pub old_value: String,
pub new_value: String,
}
impl SketchManager {
pub fn new() -> Self {
Self {
sketches: HashMap::new(),
}
}
pub fn define(
&mut self,
name: String,
statement: Statement,
description: Option<String>,
) -> Result<()> {
self.validate_sketch(&statement)?;
let sketch = Sketch {
name: name.clone(),
statement,
created_at: SystemTime::now(),
description,
preview_cache: None,
};
self.sketches.insert(name, sketch);
Ok(())
}
pub async fn preview(
&mut self,
name: &str,
executor: &mut Executor<'_>,
) -> Result<SketchPreview> {
if let Some(sketch) = self.sketches.get(name)
&& let Some(cached) = &sketch.preview_cache {
return Ok(cached.clone());
}
let statement = self.sketches.get(name)
.ok_or_else(|| NopalError::SketchNotFound(name.to_string()))?
.statement
.clone();
let preview = match &statement {
Statement::Query(query) => {
self.preview_query(query, executor).await?
}
Statement::Delete(delete) => {
self.preview_delete(delete, executor).await?
}
Statement::Update(update) => {
self.preview_update(update, executor).await?
}
Statement::Add(add) => {
self.preview_add(add)?
}
Statement::CreateIndex(_) => {
SketchPreview{
preview_type: PreviewType::Other("CREATE INDEX operation".to_string()),
generated_at: SystemTime::now(),
}
}
Statement::DropIndex(_) => {
SketchPreview {
preview_type: PreviewType::Other("DROP INDEX operation".to_string()),
generated_at: SystemTime::now(),
}
}
Statement::Explain(_) => {
SketchPreview {
preview_type: PreviewType::Other("EXPLAIN query plan".to_string()),
generated_at: SystemTime::now(),
}
}
Statement::Profile(_) => {
SketchPreview {
preview_type: PreviewType::Other("PROFILE query execution".to_string()),
generated_at: SystemTime::now(),
}
}
Statement::Sketch(_) => {
return Err(NopalError::InvalidSketch(
"Cannot sketch a sketch".to_string()
));
}
Statement::Commit(_) => {
return Err(NopalError::InvalidSketch(
"Cannot sketch a commit".to_string()
));
}
};
if let Some(sketch) = self.sketches.get_mut(name) {
sketch.preview_cache = Some(preview.clone());
}
Ok(preview)
}
pub async fn commit(
&mut self,
name: &str,
executor: &mut Executor<'_>,
_tx: &mut Transaction,
) -> Result<CommitResult> {
let sketch = self.sketches.remove(name)
.ok_or_else(|| NopalError::SketchNotFound(name.to_string()))?;
let result = match sketch.statement {
Statement::Query(query) => {
let result = executor.execute(query.clone()).await?;
CommitResult::Query(result)
}
Statement::Delete(_delete) => {
return Err(NopalError::query_error("DELETE not yet implemented in executor"));
}
Statement::Update(_update) => {
return Err(NopalError::query_error("UPDATE not yet implemented in executor"));
}
Statement::CreateIndex(_) => {
return Err(NopalError::custom("CreateIndex not supported in sketches yet"));
}
Statement::DropIndex(_) => {
return Err(NopalError::custom("DropIndex not supported in sketches yet"));
}
Statement::Explain(_) => {
return Err(NopalError::custom("Explain not supported in sketches yet"));
}
Statement::Profile(_) => {
return Err(NopalError::custom("Profile not supported in sketches yet"));
}
Statement::Add(_add) => {
return Err(NopalError::query_error("ADD not yet implemented in executor"));
}
Statement::Sketch(_) | Statement::Commit(_) => {
return Err(NopalError::InvalidCommit(
"Cannot commit a sketch or commit statement".to_string()
));
}
};
Ok(result)
}
pub fn discard(&mut self, name: &str) -> Result<()> {
self.sketches.remove(name)
.ok_or_else(|| NopalError::SketchNotFound(name.to_string()))?;
Ok(())
}
pub fn list(&self) -> Vec<&Sketch> {
self.sketches.values().collect()
}
pub fn get(&self, name: &str) -> Option<&Sketch> {
self.sketches.get(name)
}
pub fn clear(&mut self) {
self.sketches.clear();
}
}
impl SketchManager {
async fn preview_query(
&self,
query: &Query,
executor: &mut Executor<'_>,
) -> Result<SketchPreview> {
let result = executor.execute(query.clone()).await?;
Ok(SketchPreview {
preview_type: PreviewType::QueryResult(result),
generated_at: SystemTime::now(),
})
}
async fn preview_delete(
&self,
delete: &DeleteStmt,
executor: &mut Executor<'_>,
) -> Result<SketchPreview> {
let matches = executor.match_pattern(&delete.pattern).await?;
let to_delete = if let Some(where_clause) = &delete.filter {
executor.filter_matches(matches, &where_clause.condition)?
} else {
matches
};
let to_delete: Vec<_> = if let Some(limit_clause) = &delete.limit {
to_delete.into_iter().take(limit_clause.limit).collect()
} else {
to_delete
};
let (nodes_affected, edges_affected) = executor.count_elements(&to_delete);
let sample_nodes = executor.sample_node_ids(&to_delete, 10);
Ok(SketchPreview {
preview_type: PreviewType::DeletePreview {
nodes_affected,
edges_affected,
sample_nodes,
},
generated_at: SystemTime::now(),
})
}
async fn preview_update(
&self,
update: &UpdateStmt,
executor: &mut Executor<'_>,
) -> Result<SketchPreview> {
let matches = executor.match_pattern(&update.pattern).await?;
let to_update = if let Some(where_clause) = &update.filter {
executor.filter_matches(matches, &where_clause.condition)?
} else {
matches
};
let to_update: Vec<_> = if let Some(limit_clause) = &update.limit {
to_update.into_iter().take(limit_clause.limit).collect()
} else {
to_update
};
let (nodes_affected, edges_affected) = executor.count_elements(&to_update);
let sample_changes = executor.sample_updates(
&to_update,
&update.assignments,
10
);
Ok(SketchPreview {
preview_type: PreviewType::UpdatePreview {
nodes_affected,
edges_affected,
sample_changes,
},
generated_at: SystemTime::now(),
})
}
fn preview_add(&self, add: &AddStmt) -> Result<SketchPreview> {
let mut nodes_to_add = 0;
let mut edges_to_add = 0;
for element in &add.pattern.elements {
match element {
crate::query::nql::parser::ast::PatternElement::Node(_) => {
nodes_to_add += 1;
}
crate::query::nql::parser::ast::PatternElement::Relationship(_) => {
edges_to_add += 1;
}
}
}
Ok(SketchPreview {
preview_type: PreviewType::AddPreview {
nodes_to_add,
edges_to_add,
},
generated_at: SystemTime::now(),
})
}
}
impl SketchManager {
fn validate_sketch(&self, statement: &Statement) -> Result<()> {
match statement {
Statement::Query(_) => Ok(()),
Statement::Delete(_) => Ok(()),
Statement::Update(_) => Ok(()),
Statement::CreateIndex(_) => Ok(()),
Statement::DropIndex(_) => Ok(()),
Statement::Explain(_) => Ok(()),
Statement::Profile(_) => Ok(()),
Statement::Add(_) => Ok(()),
Statement::Sketch(_) => {
Err(NopalError::InvalidSketch(
"Cannot sketch a sketch (no nested sketches)".to_string()
))
}
Statement::Commit(_) => {
Err(NopalError::InvalidSketch(
"Cannot sketch a commit".to_string()
))
}
}
}
}
#[derive(Debug)]
pub enum CommitResult {
Query(QueryResult),
Delete(DeleteResult),
Update(UpdateResult),
Add(AddResult),
}
#[derive(Debug)]
pub struct DeleteResult {
pub nodes_deleted: usize,
pub edges_deleted: usize,
}
#[derive(Debug)]
pub struct AddResult {
pub nodes_added: usize,
pub edges_added: usize,
}
impl std::fmt::Display for SketchPreview {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.preview_type {
PreviewType::QueryResult(result) => {
write!(f, "Query result: {} rows", result.rows.len())
}
PreviewType::DeletePreview { nodes_affected, edges_affected, .. } => {
write!(
f,
"Will delete: {} nodes, {} edges",
nodes_affected, edges_affected
)
}
PreviewType::UpdatePreview { nodes_affected, edges_affected, .. } => {
write!(
f,
"Will update: {} nodes, {} edges",
nodes_affected, edges_affected
)
}
PreviewType::AddPreview { nodes_to_add, edges_to_add } => {
write!(
f,
"Will add: {} nodes, {} edges",
nodes_to_add, edges_to_add
)
}
PreviewType::Other(desc) => {
write!(f, "Preview: {}", desc)
}
}
}
}
impl Default for SketchManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sketch_manager_basics() {
let manager = SketchManager::new();
assert_eq!(manager.list().len(), 0);
}
#[test]
fn test_sketch_validation() {
let _manager = SketchManager::new();
}
}