use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Default, Clone)]
pub struct IndexCatalog {
by_name: BTreeMap<String, IndexDefinition>,
auto_seq: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndexDefinition {
pub name: String,
pub kind: StoredIndexKind,
pub entity: StoredIndexEntity,
pub label: Option<String>,
#[serde(default)]
pub additional_labels: Vec<String>,
pub properties: Vec<String>,
pub options: BTreeMap<String, IndexConfigValue>,
pub state: StoredIndexState,
}
impl IndexDefinition {
pub fn all_labels(&self) -> impl Iterator<Item = &str> {
self.label
.as_deref()
.into_iter()
.chain(self.additional_labels.iter().map(String::as_str))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StoredIndexKind {
Range,
Text,
Point,
Lookup,
Vector,
Fulltext,
}
impl StoredIndexKind {
pub const fn as_str(self) -> &'static str {
match self {
StoredIndexKind::Range => "RANGE",
StoredIndexKind::Text => "TEXT",
StoredIndexKind::Point => "POINT",
StoredIndexKind::Lookup => "LOOKUP",
StoredIndexKind::Vector => "VECTOR",
StoredIndexKind::Fulltext => "FULLTEXT",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StoredIndexEntity {
Node,
Relationship,
}
impl StoredIndexEntity {
pub const fn as_str(self) -> &'static str {
match self {
StoredIndexEntity::Node => "NODE",
StoredIndexEntity::Relationship => "RELATIONSHIP",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StoredIndexState {
Online,
Populating,
}
impl StoredIndexState {
pub const fn as_str(self) -> &'static str {
match self {
StoredIndexState::Online => "ONLINE",
StoredIndexState::Populating => "POPULATING",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum IndexConfigValue {
Number(f64),
Integer(i64),
String(String),
Bool(bool),
List(Vec<IndexConfigValue>),
Map(BTreeMap<String, IndexConfigValue>),
Null,
}
#[derive(Debug, Clone)]
pub enum CreateIndexOutcome {
Created(IndexDefinition),
NoOpExists(IndexDefinition),
}
#[derive(Debug, Clone, Error)]
pub enum CreateIndexError {
#[error("equivalent index already exists: {}", format_index_schema(.0))]
EquivalentIndexExists(IndexDefinition),
#[error("an index with the same name already exists: {}", .0.name)]
DuplicateName(IndexDefinition),
#[error("{0}")]
Unsupported(&'static str),
}
impl CreateIndexError {
pub const fn gql_status(&self) -> &'static str {
match self {
CreateIndexError::EquivalentIndexExists(_) => "22N70",
CreateIndexError::DuplicateName(_) => "22N71",
CreateIndexError::Unsupported(_) => "0A000",
}
}
}
#[derive(Debug, Clone)]
pub enum DropIndexOutcome {
Dropped(IndexDefinition),
NoOpMissing,
}
#[derive(Debug, Clone, Error)]
pub enum DropIndexError {
#[error("no index named `{0}` exists in the catalog")]
NotFound(String),
#[error("index `{index}` is owned by constraint `{constraint}` and cannot be dropped directly; use DROP CONSTRAINT instead")]
ConstraintOwned { index: String, constraint: String },
#[error("{0}")]
Unsupported(&'static str),
}
impl DropIndexError {
pub const fn gql_status(&self) -> &'static str {
match self {
DropIndexError::NotFound(_) => "42N51",
DropIndexError::ConstraintOwned { .. } => "22N73",
DropIndexError::Unsupported(_) => "0A000",
}
}
}
pub fn format_index_schema(def: &IndexDefinition) -> String {
let label_part = def
.label
.as_deref()
.map(|l| format!(":{l}"))
.unwrap_or_else(|| "*".to_string());
if def.properties.is_empty() {
format!("({label_part})")
} else {
format!("({label_part} {{{}}})", def.properties.join(", "))
}
}
impl IndexCatalog {
pub fn list(&self) -> Vec<IndexDefinition> {
self.by_name.values().cloned().collect()
}
pub fn get(&self, name: &str) -> Option<&IndexDefinition> {
self.by_name.get(name)
}
pub fn contains_name(&self, name: &str) -> bool {
self.by_name.contains_key(name)
}
pub fn find_equivalent(&self, request: &IndexRequest) -> Option<&IndexDefinition> {
self.by_name.values().find(|def| equivalent(def, request))
}
pub fn next_auto_name(&mut self, request: &IndexRequest) -> String {
let base = match &request.label {
Some(label) => format!(
"index_{}_{}_{}_{}",
request.kind.as_str().to_lowercase(),
request.entity.as_str().to_lowercase(),
label,
request.properties.join("_")
),
None => format!(
"index_{}_{}",
request.kind.as_str().to_lowercase(),
request.entity.as_str().to_lowercase()
),
};
let mut name = base.clone();
while self.by_name.contains_key(&name) {
self.auto_seq += 1;
name = format!("{base}_{}", self.auto_seq);
}
name
}
#[allow(clippy::result_large_err)]
pub fn try_create(
&mut self,
request: IndexRequest,
if_not_exists: bool,
) -> Result<CreateIndexOutcome, CreateIndexError> {
let provided_name = request.explicit_name.clone();
if let Some(name) = provided_name.as_ref() {
if let Some(existing) = self.by_name.get(name) {
let existing_clone = existing.clone();
if if_not_exists {
return Ok(CreateIndexOutcome::NoOpExists(existing_clone));
}
return Err(CreateIndexError::DuplicateName(existing_clone));
}
}
if let Some(existing) = self.find_equivalent(&request) {
let existing_clone = existing.clone();
if if_not_exists {
return Ok(CreateIndexOutcome::NoOpExists(existing_clone));
}
return Err(CreateIndexError::EquivalentIndexExists(existing_clone));
}
let name = match provided_name {
Some(name) => name,
None => self.next_auto_name(&request),
};
let def = IndexDefinition {
name: name.clone(),
kind: request.kind,
entity: request.entity,
label: request.label,
additional_labels: request.additional_labels,
properties: request.properties,
options: request.options,
state: StoredIndexState::Online,
};
self.by_name.insert(name, def.clone());
Ok(CreateIndexOutcome::Created(def))
}
pub fn try_drop(
&mut self,
name: &str,
if_exists: bool,
) -> Result<DropIndexOutcome, DropIndexError> {
match self.by_name.remove(name) {
Some(def) => Ok(DropIndexOutcome::Dropped(def)),
None if if_exists => Ok(DropIndexOutcome::NoOpMissing),
None => Err(DropIndexError::NotFound(name.to_string())),
}
}
}
fn equivalent(def: &IndexDefinition, request: &IndexRequest) -> bool {
if def.kind != request.kind || def.entity != request.entity {
return false;
}
match request.kind {
StoredIndexKind::Lookup => true, StoredIndexKind::Fulltext => {
def.label == request.label
&& def.additional_labels == request.additional_labels
&& def.properties == request.properties
}
_ => def.label == request.label && def.properties == request.properties,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndexRequest {
pub explicit_name: Option<String>,
pub kind: StoredIndexKind,
pub entity: StoredIndexEntity,
pub label: Option<String>,
#[serde(default)]
pub additional_labels: Vec<String>,
pub properties: Vec<String>,
pub options: BTreeMap<String, IndexConfigValue>,
}