///|
/// Command handlers for gateway API
/// These handlers implement the business logic for CLI and HTTP operations
///
/// Write operations are async (WAL-before-state via PersistentVectorDB).
/// Read operations are sync (in-memory VectorDB).
///|
/// Handle search in a collection (sync — reads from in-memory engine)
pub fn[W, S] handle_search(
db : @vcdb.PersistentDB[W, S],
req : SearchRequest,
) -> Result[SearchResponse, GatewayError] {
let dim = db.dim()
if req.vector.length() != dim {
return Err(DimensionMismatch(expected=dim, got=req.vector.length()))
}
let filter = match req.filter {
Some(json) => @filter.parse_filter_json(json.stringify())
None => None
}
let hits = db.search(req.vector, req.limit, filter)
let results : Array[SearchHitResponse] = []
for hit in hits {
let payload = if req.with_payload { Some(hit.attrs) } else { None }
let vector = if req.with_vector {
match db.get(hit.id) {
Some(rec) => Some(rec.vector)
None => None
}
} else {
None
}
results.push({ id: hit.id, score: hit.score, payload, vector })
}
Ok({ result: results, time_ms: 0.0 })
}
///|
/// Handle get point by ID (sync)
pub fn[W, S] handle_get_point(
db : @vcdb.PersistentDB[W, S],
id : @types.VectorId,
) -> Result[PointResponse, GatewayError] {
match db.get(id) {
Some(record) => Ok({ id, payload: record.attrs, vector: record.vector })
None =>
Err(NotFound("Point with id " + id.to_display_string() + " not found"))
}
}
///|
/// Handle scrolling points in a collection (sync).
pub fn[W, S] handle_scroll(
db : @vcdb.PersistentDB[W, S],
req : ScrollRequest,
) -> Result[ScrollResponse, GatewayError] {
let vid_offset = match req.offset {
Some(o) => Some(@types.VectorId::from_int64(o))
None => None
}
// Use scroll_filtered (> offset) semantics directly for Bytes16Id compatibility
let entries = db.scroll_filtered(offset=vid_offset, limit=req.limit)
let points : Array[ScrollPointResponse] = []
for i, entry in entries {
let (id, record) = entry
points.push({
id,
payload: if req.with_payload {
Some(record.attrs)
} else {
None
},
vector: if req.with_vector {
Some(record.vector)
} else {
None
},
})
ignore(i)
}
let next_page_offset : @types.VectorId? = if entries.length() < req.limit {
None
} else {
match entries.last() {
Some((id, _)) => Some(id)
None => None
}
}
Ok({ points, next_page_offset })
}
///|
/// Handle collection creation (async — writes to storage)
pub async fn[S : @storage.AsyncStorage] handle_create_collection(
manager : CollectionManager[S],
config : CollectionConfig,
) -> Result[Unit, GatewayError] {
manager.create(config)
}
///|
/// Handle collection deletion (async — writes to storage)
pub async fn[S : @storage.AsyncStorage] handle_delete_collection(
manager : CollectionManager[S],
name : String,
) -> Result[Unit, GatewayError] {
manager.delete(name)
}
///|
/// Handle list collections (async — may load from storage)
pub async fn[S : @storage.AsyncStorage] handle_list_collections(
manager : CollectionManager[S],
) -> Result[CollectionsResponse, GatewayError] {
match manager.list_info() {
Ok(collections) => Ok({ collections, })
Err(err) => Err(err)
}
}
///|
/// Handle get collection info (async — may load from storage)
pub async fn[S : @storage.AsyncStorage] handle_get_collection(
manager : CollectionManager[S],
name : String,
) -> Result[CollectionInfo, GatewayError] {
manager.get_info(name)
}
///|
/// Convenience: Handle request with collection name (async)
pub async fn[S : @storage.AsyncStorage, T] with_collection(
manager : CollectionManager[S],
name : String,
handler : (@vcdb.PersistentDB[S, S]) -> Result[T, GatewayError],
) -> Result[T, GatewayError] {
match manager.get(name) {
Ok(db) => handler(db)
Err(err) => Err(err)
}
}
///|
/// Handle health check
pub fn handle_health() -> Json {
json_object([("status", "ok".to_json())])
}
///|
/// Handle service info
pub fn handle_service_info() -> Json {
json_object([
("name", "vcdb".to_json()),
("version", "0.1.0".to_json()),
("description", "MoonBit vector database".to_json()),
])
}
///|
/// Helper to format GatewayError to string
fn error_to_string(e : GatewayError) -> String {
match e {
NotFound(s) => "Not found: " + s
InvalidInput(s) => "Invalid input: " + s
DimensionMismatch(expected~, got~) =>
"Dimension mismatch: expected " +
expected.to_string() +
", got " +
got.to_string()
CollectionExists(s) => "Collection already exists: " + s
CollectionNotFound(s) => "Collection not found: " + s
StorageError(s) => "Storage error: " + s
}
}
///|
/// Parse and execute an API request, returning JSON response.
/// All operations are now async since collection loading and writes require I/O.
pub async fn[S : @storage.AsyncStorage] execute_request(
manager : CollectionManager[S],
http_method : String,
path : Array[String],
body : String,
) -> ApiResponse {
match (http_method, path) {
("GET", []) => ApiResponse::ok(handle_service_info())
("GET", ["healthz"]) => ApiResponse::ok(handle_health())
("GET", ["collections"]) =>
match handle_list_collections(manager) {
Ok(resp) => ApiResponse::ok(resp.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
("POST", ["collections", name]) => {
let config = match parse_json(body) {
Some(json) => {
let base = match CollectionConfig::from_json(json) {
Ok(parsed) => parsed
Err(msg) => return ApiResponse::err("Invalid request body: " + msg)
}
{ ..base, name, }
}
None => return ApiResponse::err("Invalid JSON")
}
match handle_create_collection(manager, config) {
Ok(_) => ApiResponse::ok(true.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
}
("GET", ["collections", name]) =>
match handle_get_collection(manager, name) {
Ok(info) => ApiResponse::ok(info.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
("DELETE", ["collections", name]) =>
match handle_delete_collection(manager, name) {
Ok(_) => ApiResponse::ok(true.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
("PUT", ["collections", name, "points"]) => {
let req = match parse_json(body) {
Some(json) =>
match UpsertRequest::from_json(json) {
Some(r) => r
None => return ApiResponse::err("Invalid request body")
}
None => return ApiResponse::err("Invalid JSON")
}
let points : Array[(@types.VectorId, Array[Double], @types.Attrs)] = req.points.map(fn(
p,
) {
(@types.VectorId::from_int64(p.id), p.vector, p.payload)
},
)
match manager.upsert_points(name, points) {
Ok(count) =>
ApiResponse::ok(
json_object([
("status", "ok".to_json()),
("upserted", count.to_double().to_json()),
]),
)
Err(e) => ApiResponse::err(error_to_string(e))
}
}
("POST", ["collections", name, "points", "search"]) => {
let req = match parse_json(body) {
Some(json) =>
match SearchRequest::from_json(json) {
Some(r) => r
None => return ApiResponse::err("Invalid request body")
}
None => return ApiResponse::err("Invalid JSON")
}
match with_collection(manager, name, fn(db) { handle_search(db, req) }) {
Ok(resp) => ApiResponse::ok(resp.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
}
("POST", ["collections", name, "points", "scroll"]) => {
let req = if body.length() == 0 {
{ offset: None, limit: 10, with_payload: true, with_vector: false }
} else {
match parse_json(body) {
Some(json) =>
match ScrollRequest::from_json(json) {
Some(r) => r
None => return ApiResponse::err("Invalid request body")
}
None => return ApiResponse::err("Invalid JSON")
}
}
match with_collection(manager, name, fn(db) { handle_scroll(db, req) }) {
Ok(resp) => ApiResponse::ok(resp.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
}
("GET", ["collections", name, "points", id_str]) => {
let parse_result : Result[Int64, _] = try? @string.parse_int64(id_str)
let id : @types.VectorId = match parse_result {
Ok(i) => @types.VectorId::from_int64(i)
Err(_) => return ApiResponse::err("Invalid point ID")
}
match
with_collection(manager, name, fn(db) { handle_get_point(db, id) }) {
Ok(point) => ApiResponse::ok(point.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
}
("DELETE", ["collections", name, "points", id_str]) => {
let parse_result : Result[Int64, _] = try? @string.parse_int64(id_str)
let id : Int64 = match parse_result {
Ok(i) => i
Err(_) => return ApiResponse::err("Invalid point ID")
}
match manager.remove_point(name, @types.VectorId::from_int64(id)) {
Ok(_) => ApiResponse::ok(true.to_json())
Err(e) => ApiResponse::err(error_to_string(e))
}
}
_ => ApiResponse::err("Not found")
}
}