use crate::{eval_scalar, ScalarEvalContext};
use uqa_core::{ScoredEntry, Value};
use uqa_graph::{GraphLabelInfo, LabelKind};
use uqa_sql::{
expr::EngineHook,
semantics::graph_commands::{
age_error, eval_age_bool_with, eval_age_graph_name_with, eval_age_name_with,
graph_create_name, graph_drop_name, require_age_arity, validate_graph_drop_cascade,
AGE_DEPENDENT_OBJECTS_STILL_EXIST, AGE_DUPLICATE_SCHEMA, AGE_FEATURE_NOT_SUPPORTED,
AGE_INVALID_PARAMETER_VALUE, AGE_UNDEFINED_SCHEMA, AGE_UNDEFINED_TABLE,
},
SQLError, SQLParam, ScalarExpr,
};
use uqa_storage::StorageBackendResult;
pub trait GraphLifecycle {
fn has_graph(&self, name: &str) -> StorageBackendResult<bool>;
fn has_namespace(&self, name: &str) -> StorageBackendResult<bool>;
fn create_graph(&self, name: String) -> StorageBackendResult<bool>;
fn drop_graph(&self, name: &str) -> StorageBackendResult<bool>;
fn list_graph_labels(&self, graph: &str) -> StorageBackendResult<Option<Vec<GraphLabelInfo>>>;
fn create_graph_label(
&self,
graph: &str,
label: &str,
kind: LabelKind,
) -> StorageBackendResult<bool>;
fn drop_graph_label(&self, graph: &str, label: &str) -> StorageBackendResult<bool>;
fn graph_label_relation_dependents(
&self,
graph: &str,
label: &str,
) -> StorageBackendResult<Vec<String>>;
fn rename_graph(&self, from: &str, to: &str) -> StorageBackendResult<bool>;
}
pub fn run_graph_create(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
params: &[SQLParam],
hook: &dyn EngineHook,
) -> Result<Vec<ScoredEntry>, SQLError> {
let ctx = ScalarEvalContext::new(None, params).with_function_hook(hook);
run_graph_create_with_evaluator(runtime, args, &mut |expr| eval_scalar(expr, &ctx))?;
Ok(Vec::new())
}
pub fn run_graph_create_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<bool, SQLError> {
let name = graph_create_name(args, evaluate)?;
runtime
.create_graph(name)
.map_err(|err| SQLError::Internal(format!("create graph: {err}")))
}
pub fn run_graph_drop(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
params: &[SQLParam],
hook: &dyn EngineHook,
) -> Result<Vec<ScoredEntry>, SQLError> {
let ctx = ScalarEvalContext::new(None, params).with_function_hook(hook);
run_graph_drop_with_evaluator(runtime, args, &mut |expr| eval_scalar(expr, &ctx))?;
Ok(Vec::new())
}
pub fn run_graph_drop_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<bool, SQLError> {
let name = graph_drop_name(args, evaluate)?;
let graph_exists = runtime
.has_graph(&name)
.map_err(|err| SQLError::Internal(format!("read graph catalog: {err}")))?;
validate_graph_drop_cascade(&name, graph_exists, args.get(1), evaluate)?;
runtime
.drop_graph(&name)
.map_err(|err| SQLError::Internal(format!("drop graph: {err}")))
}
fn age_graph_catalog_error(err: impl std::fmt::Display) -> SQLError {
SQLError::Internal(format!("read graph catalog: {err}"))
}
pub fn run_age_create_graph_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity("create_graph", args, 1..=1)?;
let name = eval_age_graph_name_with(&args[0], evaluate)?;
if !uqa_graph::age_names::is_valid_graph_name(&name) {
return Err(age_error(
AGE_INVALID_PARAMETER_VALUE,
"graph name is invalid",
));
}
if runtime.has_graph(&name).map_err(age_graph_catalog_error)? {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("graph \"{name}\" already exists"),
));
}
if runtime
.has_namespace(&name)
.map_err(age_graph_catalog_error)?
{
return Err(age_error(
AGE_DUPLICATE_SCHEMA,
format!("schema \"{name}\" already exists"),
));
}
runtime
.create_graph(name)
.map_err(|err| SQLError::Internal(format!("create graph: {err}")))?;
Ok(Value::Null)
}
pub fn run_age_drop_graph_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity("drop_graph", args, 1..=2)?;
let name = eval_age_graph_name_with(&args[0], evaluate)?;
if !runtime.has_graph(&name).map_err(age_graph_catalog_error)? {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("graph \"{name}\" does not exist"),
));
}
let cascade = match args.get(1) {
Some(expr) => eval_age_bool_with(expr, "drop_graph.cascade", evaluate)?,
None => false,
};
if !cascade {
let labels = runtime
.list_graph_labels(&name)
.map_err(age_graph_catalog_error)?
.unwrap_or_default();
if !labels.is_empty() {
return Err(age_error(
AGE_DEPENDENT_OBJECTS_STILL_EXIST,
format!("cannot drop schema {name} because other objects depend on it"),
));
}
}
runtime
.drop_graph(&name)
.map_err(|err| SQLError::Internal(format!("drop graph: {err}")))?;
Ok(Value::Null)
}
pub fn run_age_graph_exists_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity("graph_exists", args, 1..=1)?;
let name = eval_age_graph_name_with(&args[0], evaluate)?;
let exists = runtime.has_graph(&name).map_err(age_graph_catalog_error)?;
Ok(Value::Str(uqa_core::agtype::render(&Value::Bool(exists))))
}
fn run_age_create_label_with_evaluator(
runtime: &dyn GraphLifecycle,
function_name: &str,
kind: uqa_graph::LabelKind,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity(function_name, args, 2..=2)?;
let graph = eval_age_name_with(&args[0], "graph name must not be NULL", evaluate)?;
let label = eval_age_name_with(&args[1], "label name must not be NULL", evaluate)?;
if !uqa_graph::age_names::is_valid_graph_name(&graph) {
return Err(age_error(
AGE_INVALID_PARAMETER_VALUE,
"graph name is invalid",
));
}
if !uqa_graph::age_names::is_valid_label_name(&label) {
return Err(age_error(
AGE_INVALID_PARAMETER_VALUE,
"label name is invalid",
));
}
if !runtime.has_graph(&graph).map_err(age_graph_catalog_error)? {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("graph \"{graph}\" does not exist."),
));
}
let labels = runtime
.list_graph_labels(&graph)
.map_err(age_graph_catalog_error)?
.unwrap_or_default();
if !labels
.iter()
.any(|entry| entry.name == kind.default_label_name())
{
return Err(age_error(
AGE_UNDEFINED_TABLE,
format!(
"relation \"{graph}.{}\" does not exist",
kind.default_label_name()
),
));
}
let created = runtime
.create_graph_label(&graph, &label, kind)
.map_err(|err| SQLError::Internal(format!("create label: {err}")))?;
if !created {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("label \"{label}\" already exists"),
));
}
Ok(Value::Null)
}
pub fn run_age_create_vlabel_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
run_age_create_label_with_evaluator(
runtime,
"create_vlabel",
uqa_graph::LabelKind::Vertex,
args,
evaluate,
)
}
pub fn run_age_create_elabel_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
run_age_create_label_with_evaluator(
runtime,
"create_elabel",
uqa_graph::LabelKind::Edge,
args,
evaluate,
)
}
pub fn run_age_drop_label_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity("drop_label", args, 2..=3)?;
let graph = eval_age_name_with(&args[0], "graph name must not be NULL", evaluate)?;
let label = eval_age_name_with(&args[1], "label name must not be NULL", evaluate)?;
let force = match args.get(2) {
Some(expr) => eval_age_bool_with(expr, "drop_label.force", evaluate)?,
None => false,
};
if !runtime.has_graph(&graph).map_err(age_graph_catalog_error)? {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("graph \"{graph}\" does not exist"),
));
}
let labels = runtime
.list_graph_labels(&graph)
.map_err(age_graph_catalog_error)?
.unwrap_or_default();
let Some(entry) = labels.iter().find(|entry| entry.name == label) else {
return Err(age_error(
AGE_UNDEFINED_TABLE,
format!("label \"{label}\" does not exist"),
));
};
if force {
return Err(age_error(
AGE_FEATURE_NOT_SUPPORTED,
"force option is not supported yet",
));
}
let dependent_views = runtime
.graph_label_relation_dependents(&graph, &label)
.map_err(|error| SQLError::Internal(format!("inspect label dependencies: {error}")))?;
if !dependent_views.is_empty() {
return Err(age_error(
AGE_DEPENDENT_OBJECTS_STILL_EXIST,
format!("cannot drop table {graph}.{label} because other objects depend on it"),
));
}
if (entry.id == uqa_graph::VERTEX_DEFAULT_LABEL_ID
|| entry.id == uqa_graph::EDGE_DEFAULT_LABEL_ID)
&& labels.iter().any(|candidate| {
candidate.id >= uqa_graph::FIRST_USER_LABEL_ID && candidate.kind == entry.kind
})
{
return Err(age_error(
AGE_DEPENDENT_OBJECTS_STILL_EXIST,
format!("cannot drop table {graph}.{label} because other objects depend on it"),
));
}
runtime
.drop_graph_label(&graph, &label)
.map_err(|err| SQLError::Internal(format!("drop label: {err}")))?;
Ok(Value::Null)
}
pub fn run_age_alter_graph_with_evaluator(
runtime: &dyn GraphLifecycle,
args: &[ScalarExpr],
evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
) -> Result<Value, SQLError> {
require_age_arity("alter_graph", args, 3..=3)?;
let graph = eval_age_name_with(&args[0], "graph_name must not be NULL", evaluate)?;
let operation = eval_age_name_with(&args[1], "operation must not be NULL", evaluate)?;
let new_value = eval_age_name_with(&args[2], "new_value must not be NULL", evaluate)?;
if !operation.eq_ignore_ascii_case("RENAME") {
return Err(age_error(
AGE_INVALID_PARAMETER_VALUE,
format!("invalid operation \"{operation}\""),
));
}
if !uqa_graph::age_names::is_valid_graph_name(&new_value) {
return Err(age_error(
AGE_INVALID_PARAMETER_VALUE,
"new graph name is invalid",
));
}
if !runtime.has_graph(&graph).map_err(age_graph_catalog_error)? {
return Err(age_error(
AGE_UNDEFINED_SCHEMA,
format!("graph \"{graph}\" does not exist"),
));
}
if runtime
.has_namespace(&new_value)
.map_err(age_graph_catalog_error)?
{
return Err(age_error(
AGE_DUPLICATE_SCHEMA,
format!("schema \"{new_value}\" already exists"),
));
}
runtime
.rename_graph(&graph, &new_value)
.map_err(|err| SQLError::Internal(format!("rename graph: {err}")))?;
Ok(Value::Null)
}