use selene_core::{DbString, Value};
use selene_graph::{GraphError, TypedIndexKind};
use super::meta::{StaticOutputColumn, StaticParameter};
use super::unit_result;
use crate::procedure_registry::ProcedureError;
use crate::{GqlType, MutationContext, ProcedureOutputColumn, ProcedureParameter, ProcedureResult};
static CREATE_INDEX_PARAMS: [StaticParameter; 3] = [
StaticParameter::new("label", GqlType::String, false).with_description("Node label."),
StaticParameter::new("property", GqlType::String, false).with_description("Property name."),
StaticParameter::new("kind", GqlType::String, false).with_description("Index value kind."),
];
static CREATE_INDEX_OUTPUTS: [StaticOutputColumn; 0] = [];
pub(super) fn signature() -> Vec<ProcedureParameter> {
CREATE_INDEX_PARAMS
.iter()
.cloned()
.map(StaticParameter::into_parameter)
.collect()
}
pub(super) fn output_columns() -> Vec<ProcedureOutputColumn> {
CREATE_INDEX_OUTPUTS
.iter()
.cloned()
.map(StaticOutputColumn::into_output_column)
.collect()
}
pub(super) fn execute(
ctx: &mut MutationContext<'_, '_>,
args: &[Value],
) -> Result<ProcedureResult, ProcedureError> {
if args.len() != 3 {
return Err(invalid_arg(
"selene.create_index expects exactly 3 arguments",
));
}
let label = string_arg(&args[0], "label")?;
let property = string_arg(&args[1], "property")?;
let kind = parse_kind(string_arg(&args[2], "kind")?)?;
match ctx
.mutator()
.create_property_index(label.clone(), property.clone(), kind)
{
Ok(()) => Ok(unit_result()),
Err(GraphError::PropertyIndexAlreadyExists { .. }) => Err(invalid_arg(format!(
"index for ({label}, {property}) already exists"
))),
Err(GraphError::IndexValueRejected { .. }) => Err(invalid_arg(
"existing nodes contain values incompatible with the requested index kind",
)),
Err(other) => Err(ProcedureError::Internal {
detail: format!("unexpected graph error during index creation: {other}"),
}),
}
}
fn string_arg(value: &Value, name: &'static str) -> Result<DbString, ProcedureError> {
let Value::String(value) = value else {
return Err(invalid_arg(format!(
"selene.create_index {name} must be a non-empty STRING"
)));
};
if value.as_str().is_empty() {
return Err(invalid_arg(format!(
"selene.create_index {name} must be a non-empty STRING"
)));
}
Ok(value.clone())
}
fn parse_kind(value: DbString) -> Result<TypedIndexKind, ProcedureError> {
let raw = value.as_str();
match raw.to_ascii_lowercase().as_str() {
"bool" | "boolean" => Ok(TypedIndexKind::Bool),
"i64" | "integer" | "int" => Ok(TypedIndexKind::I64),
"u64" | "uint" | "uint64" => Ok(TypedIndexKind::U64),
"i128" | "int128" => Ok(TypedIndexKind::I128),
"u128" | "uint128" => Ok(TypedIndexKind::U128),
"decimal" | "dec" => Ok(TypedIndexKind::Decimal),
"f32" | "float32" => Ok(TypedIndexKind::F32),
"f64" | "float" => Ok(TypedIndexKind::F64),
"string" => Ok(TypedIndexKind::String),
"date" => Ok(TypedIndexKind::Date),
"local_datetime" | "localdatetime" => Ok(TypedIndexKind::LocalDateTime),
"zoned_datetime" | "zoneddatetime" => Ok(TypedIndexKind::ZonedDateTime),
"local_time" | "localtime" => Ok(TypedIndexKind::LocalTime),
"zoned_time" | "zonedtime" => Ok(TypedIndexKind::ZonedTime),
"duration" => Ok(TypedIndexKind::Duration),
"uuid" => Ok(TypedIndexKind::Uuid),
_ => Err(invalid_arg(format!(
"unknown index kind '{raw}'; expected one of bool, i64, u64, i128, u128, decimal, f32, f64, string, date, local_datetime, zoned_datetime, local_time, zoned_time, duration, uuid"
))),
}
}
fn invalid_arg(detail: impl Into<String>) -> ProcedureError {
ProcedureError::InvalidArgument {
detail: detail.into(),
}
}