Skip to main content

velesdb_server/handlers/
indexes.rs

1//! Index management handlers (EPIC-009 Propagation).
2
3use axum::{
4    extract::{Path, State},
5    http::StatusCode,
6    response::IntoResponse,
7    Json,
8};
9use std::sync::Arc;
10
11use crate::types::{CreateIndexRequest, ErrorResponse, IndexResponse, ListIndexesResponse};
12use crate::AppState;
13
14use super::helpers::{auto_core_error_response, error_response, get_vector_collection_or_404};
15
16/// Create a property index on a graph collection.
17#[utoipa::path(
18    post,
19    path = "/collections/{name}/indexes",
20    tag = "indexes",
21    request_body = CreateIndexRequest,
22    params(
23        ("name" = String, Path, description = "Collection name")
24    ),
25    responses(
26        (status = 201, description = "Index created", body = IndexResponse),
27        (status = 400, description = "Invalid request", body = ErrorResponse),
28        (status = 404, description = "Collection not found", body = ErrorResponse)
29    )
30)]
31pub async fn create_index(
32    State(state): State<Arc<AppState>>,
33    Path(name): Path<String>,
34    Json(req): Json<CreateIndexRequest>,
35) -> impl IntoResponse {
36    let collection = match get_vector_collection_or_404(&state, &name) {
37        Ok(c) => c,
38        Err(resp) => return resp,
39    };
40
41    let result = match dispatch_index_creation(&collection, &req) {
42        Ok(r) => r,
43        Err(resp) => return resp,
44    };
45
46    match result {
47        Ok(()) => {
48            // Retrieve real cardinality/memory_bytes from the freshly-created index.
49            let (cardinality, memory_bytes) = collection
50                .list_indexes()
51                .into_iter()
52                .find(|i| i.label == req.label && i.property == req.property)
53                .map_or((0, 0), |i| (i.cardinality, i.memory_bytes));
54
55            (
56                StatusCode::CREATED,
57                Json(IndexResponse {
58                    label: req.label,
59                    property: req.property,
60                    index_type: req.index_type,
61                    cardinality,
62                    memory_bytes,
63                }),
64            )
65                .into_response()
66        }
67        Err(e) => auto_core_error_response(&e),
68    }
69}
70
71/// Dispatch index creation by type.
72#[allow(clippy::result_large_err)]
73fn dispatch_index_creation(
74    collection: &velesdb_core::collection::VectorCollection,
75    req: &CreateIndexRequest,
76) -> Result<velesdb_core::error::Result<()>, axum::response::Response> {
77    match req.index_type.to_lowercase().as_str() {
78        "hash" => Ok(collection.create_property_index(&req.label, &req.property)),
79        "range" => Ok(collection.create_range_index(&req.label, &req.property)),
80        _ => Err(error_response(
81            StatusCode::BAD_REQUEST,
82            format!("Invalid index_type: {}. Valid: hash, range", req.index_type),
83        )),
84    }
85}
86
87/// List all indexes on a collection.
88#[utoipa::path(
89    get,
90    path = "/collections/{name}/indexes",
91    tag = "indexes",
92    params(
93        ("name" = String, Path, description = "Collection name")
94    ),
95    responses(
96        (status = 200, description = "List of indexes", body = ListIndexesResponse),
97        (status = 404, description = "Collection not found", body = ErrorResponse)
98    )
99)]
100pub async fn list_indexes(
101    State(state): State<Arc<AppState>>,
102    Path(name): Path<String>,
103) -> impl IntoResponse {
104    let collection = match get_vector_collection_or_404(&state, &name) {
105        Ok(c) => c,
106        Err(resp) => return resp,
107    };
108
109    let core_indexes = collection.list_indexes();
110    let indexes: Vec<IndexResponse> = core_indexes
111        .into_iter()
112        .map(|i| IndexResponse {
113            label: i.label,
114            property: i.property,
115            index_type: i.index_type,
116            cardinality: i.cardinality,
117            memory_bytes: i.memory_bytes,
118        })
119        .collect();
120    let total = indexes.len();
121
122    Json(ListIndexesResponse { indexes, total }).into_response()
123}
124
125/// Delete a property index.
126#[utoipa::path(
127    delete,
128    path = "/collections/{name}/indexes/{label}/{property}",
129    tag = "indexes",
130    params(
131        ("name" = String, Path, description = "Collection name"),
132        ("label" = String, Path, description = "Node label"),
133        ("property" = String, Path, description = "Property name")
134    ),
135    responses(
136        (status = 200, description = "Index deleted", body = Object),
137        (status = 404, description = "Index or collection not found", body = ErrorResponse)
138    )
139)]
140pub async fn delete_index(
141    State(state): State<Arc<AppState>>,
142    Path((name, label, property)): Path<(String, String, String)>,
143) -> impl IntoResponse {
144    let collection = match get_vector_collection_or_404(&state, &name) {
145        Ok(c) => c,
146        Err(resp) => return resp,
147    };
148
149    match collection.drop_index(&label, &property) {
150        Ok(dropped) => {
151            if dropped {
152                Json(serde_json::json!({
153                    "message": "Index deleted",
154                    "label": label,
155                    "property": property
156                }))
157                .into_response()
158            } else {
159                error_response(
160                    StatusCode::NOT_FOUND,
161                    format!("Index on {}.{} not found", label, property),
162                )
163            }
164        }
165        Err(e) => auto_core_error_response(&e),
166    }
167}