///|
/// CollectionManager - manages multiple named PersistentVectorDB collections.
///
/// All mutations go through PersistentVectorDB, which enforces
/// WAL-before-state ordering. Read operations delegate directly.
///
/// Write operations are async (PersistentVectorDB I/O).
/// Read operations are sync (in-memory VectorDB).
///|
/// Collection entry — wraps VectorDB with its config.
pub struct Collection[S] {
config : CollectionConfig
db : @vcdb.PersistentDB[S, S]
}
///|
/// Collection manager with optional storage
pub struct CollectionManager[S] {
collections : Map[String, Collection[S]]
storage : S?
base_path : String
checkpoint_threshold : Int
}
///|
/// Create without storage (in-memory only)
pub fn[S] CollectionManager::new(
base_path? : String = "",
) -> CollectionManager[S] {
{ collections: {}, storage: None, base_path, checkpoint_threshold: 1000 }
}
///|
/// Create with storage
pub fn[S] CollectionManager::with_storage(
storage : S,
base_path? : String = "",
checkpoint_threshold? : Int = 1000,
) -> CollectionManager[S] {
{ collections: {}, storage: Some(storage), base_path, checkpoint_threshold }
}
///|
fn make_path(base_path : String, path : String) -> String {
if base_path.length() == 0 {
path
} else {
base_path + "/" + path
}
}
///|
fn invalid_collection_config_error(
name : String,
message : String,
) -> GatewayError {
StorageError("Invalid collection config for " + name + ": " + message)
}
///|
/// Persist config to storage
async fn[S : @storage.AsyncStorage] CollectionManager::persist_config(
self : CollectionManager[S],
name : String,
) -> Unit {
let storage = match self.storage {
Some(s) => s
None => return
}
let col = match self.collections.get(name) {
Some(c) => c
None => return
}
let config_path = make_path(
self.base_path,
@storage.collection_config_path(name),
)
let config_json = col.config.to_json().stringify()
let config_bytes = Bytes::from_array(
config_json.to_array().map(fn(c) { c.to_int().to_byte() }),
)
@storage.async_atomic_write(
storage,
config_path,
config_bytes,
@storage.Config,
)
}
///|
/// Checkpoint: write snapshot and truncate WAL
pub async fn[S : @storage.AsyncStorage] CollectionManager::checkpoint(
self : CollectionManager[S],
name : String,
) -> Unit {
self.persist_config(name)
match self.collections.get(name) {
Some(col) => col.db.checkpoint()
_ => ()
}
}
///|
/// Upsert points into a collection via PersistentVectorDB (WAL-before-state).
pub async fn[S : @storage.AsyncStorage] CollectionManager::upsert_points(
self : CollectionManager[S],
name : String,
points : Array[(@types.VectorId, Array[Double], @types.Attrs)],
timestamp_ns? : Int64 = 0L,
) -> Result[Int, GatewayError] {
let col = match self.collections.get(name) {
Some(c) => c
None => return Err(CollectionNotFound(name))
}
let dim = col.db.dim()
for p in points {
let (_, vector, _) = p
if vector.length() != dim {
return Err(DimensionMismatch(expected=dim, got=vector.length()))
}
}
col.db.upsert(points, timestamp_ns)
Ok(points.length())
}
///|
/// Remove a point from a collection via PersistentVectorDB (WAL-before-state).
pub async fn[S : @storage.AsyncStorage] CollectionManager::remove_point(
self : CollectionManager[S],
name : String,
id : @types.VectorId,
timestamp_ns? : Int64 = 0L,
) -> Result[Unit, GatewayError] {
let col = match self.collections.get(name) {
Some(c) => c
None => return Err(CollectionNotFound(name))
}
if !col.db.has(id) {
return Err(
NotFound("Point with id " + id.to_display_string() + " not found"),
)
}
let _ = col.db.remove(id, timestamp_ns)
Ok(())
}
///|
pub async fn[S : @storage.AsyncStorage] CollectionManager::create(
self : CollectionManager[S],
config : CollectionConfig,
) -> Result[Unit, GatewayError] {
try {
let storage = match self.storage {
Some(s) => s
None =>
// In-memory only — create a PersistentVectorDB with no-op persistence
// This case is not supported; storage is required for PersistentVectorDB.
return Err(StorageError("Storage backend required"))
}
let config_path = make_path(
self.base_path,
@storage.collection_config_path(config.name),
)
let already_exists = self.collections.contains(config.name) ||
@storage.async_exists(storage, config_path, @storage.Config)
if already_exists {
return Err(CollectionExists(config.name))
}
let db = @vcdb.PersistentDB::init(
storage,
storage,
self.base_path,
config.name,
config.dim,
1024,
metric=config.metric,
strategy=config.strategy,
checkpoint_threshold=self.checkpoint_threshold,
)
let collection : Collection[S] = { config, db }
self.collections[config.name] = collection
self.persist_config(config.name)
// Persist initial empty state
db.checkpoint()
Ok(())
} catch {
@storage.AsyncIOFailed(msg) => Err(StorageError(msg))
e => Err(StorageError(e.to_string()))
}
}
///|
/// Get VectorDB for read operations (sync)
pub fn[S] CollectionManager::get_db(
self : CollectionManager[S],
name : String,
) -> @vcdb.PersistentDB[S, S]? {
match self.collections.get(name) {
Some(c) => Some(c.db)
None => None
}
}
///|
/// Load collection from storage (async)
async fn[S : @storage.AsyncStorage] CollectionManager::load_collection_from_storage(
self : CollectionManager[S],
name : String,
) -> Result[Collection[S]?, GatewayError] {
try {
let storage = match self.storage {
Some(s) => s
None => return Ok(None)
}
// Load config
let config_path = make_path(
self.base_path,
@storage.collection_config_path(name),
)
if !@storage.async_exists(storage, config_path, @storage.Config) {
return Ok(None)
}
let config_bytes = @storage.async_read(
storage,
config_path,
@storage.Config,
)
let config_str = bytes_to_string(config_bytes)
let config = match parse_json(config_str) {
Some(json) =>
match CollectionConfig::from_json(json) {
Ok(parsed) => { ..parsed, name, }
Err(msg) => return Err(invalid_collection_config_error(name, msg))
}
None =>
return Err(
invalid_collection_config_error(name, "config is not valid JSON"),
)
}
// Load via VectorDB::init (handles WAL + snapshot replay)
let db = @vcdb.PersistentDB::init(
storage,
storage,
self.base_path,
name,
config.dim,
1024,
metric=config.metric,
strategy=config.strategy,
checkpoint_threshold=self.checkpoint_threshold,
)
Ok(Some({ config, db }))
} catch {
@storage.AsyncIOFailed(msg) => Err(StorageError(msg))
e => Err(StorageError(e.to_string()))
}
}
///|
fn bytes_to_string(bytes : Bytes) -> String {
let chars : Array[Char] = []
for i in 0.. Unit {
let storage = match self.storage {
Some(s) => s
None => return
}
// Remove config
let config_path = make_path(
self.base_path,
@storage.collection_config_path(name),
)
if @storage.async_exists(storage, config_path, @storage.Config) {
@storage.async_del(storage, config_path, @storage.Config)
}
// Remove data
let data_path = make_path(self.base_path, @storage.collection_data_path(name))
if @storage.async_exists(storage, data_path, @storage.Data) {
@storage.async_del(storage, data_path, @storage.Data)
}
// Remove WAL
let wal_path = make_path(self.base_path, @storage.collection_wal_path(name))
if @storage.async_exists(storage, wal_path, @storage.Data) {
@storage.async_del(storage, wal_path, @storage.Data)
}
}
///|
/// Get or load a collection (async for lazy loading from storage)
pub async fn[S : @storage.AsyncStorage] CollectionManager::get(
self : CollectionManager[S],
name : String,
) -> Result[@vcdb.PersistentDB[S, S], GatewayError] {
match self.collections.get(name) {
Some(c) => Ok(c.db)
None =>
match self.load_collection_from_storage(name) {
Ok(Some(col)) => {
let db = col.db
self.collections[name] = col
Ok(db)
}
Ok(None) => Err(CollectionNotFound(name))
Err(err) => Err(err)
}
}
}
///|
pub async fn[S : @storage.AsyncStorage] CollectionManager::get_info(
self : CollectionManager[S],
name : String,
) -> Result[CollectionInfo, GatewayError] {
match self.collections.get(name) {
Some(c) => Ok(collection_to_info(c))
None =>
match self.load_collection_from_storage(name) {
Ok(Some(col)) => {
let info = collection_to_info(col)
self.collections[name] = col
Ok(info)
}
Ok(None) => Err(CollectionNotFound(name))
Err(err) => Err(err)
}
}
}
///|
fn[S] collection_to_info(c : Collection[S]) -> CollectionInfo {
let metric_str = c.config.metric.to_name()
let strategy_str = c.config.strategy.to_name()
{
name: c.config.name,
vectors_count: c.db.size(),
dim: c.config.dim,
metric: metric_str,
strategy: strategy_str,
}
}
///|
pub async fn[S : @storage.AsyncStorage] CollectionManager::delete(
self : CollectionManager[S],
name : String,
) -> Result[Unit, GatewayError] {
if !self.collections.contains(name) {
match self.load_collection_from_storage(name) {
Ok(Some(col)) => self.collections[name] = col
Ok(None) => return Err(CollectionNotFound(name))
Err(err) => return Err(err)
}
}
try {
self.collections.remove(name)
self.remove_persisted_collection(name)
Ok(())
} catch {
@storage.AsyncIOFailed(msg) => Err(StorageError(msg))
e => Err(StorageError(e.to_string()))
}
}
///|
fn config_name_from_path(base_path : String, path : String) -> String? {
let relative_path = if base_path.length() == 0 {
path
} else {
let prefix = base_path + "/"
if path.has_prefix(prefix) {
path.unsafe_substring(start=prefix.length(), end=path.length())
} else {
path
}
}
let suffix = ".config.json"
if relative_path.has_suffix(suffix) {
Some(
relative_path.unsafe_substring(
start=0,
end=relative_path.length() - suffix.length(),
),
)
} else {
None
}
}
///|
pub async fn[S : @storage.AsyncStorage] CollectionManager::list(
self : CollectionManager[S],
) -> Result[Array[String], GatewayError] {
let names : Array[String] = self.collections.keys().collect()
let seen : Map[String, Bool] = {}
for name in names {
seen[name] = true
}
try {
match self.storage {
Some(storage) =>
for path in @storage.async_list(storage, @storage.Config) {
match config_name_from_path(self.base_path, path) {
Some(name) =>
if !seen.contains(name) {
names.push(name)
seen[name] = true
}
None => ()
}
}
None => ()
}
Ok(names)
} catch {
@storage.AsyncIOFailed(msg) => Err(StorageError(msg))
e => Err(StorageError(e.to_string()))
}
}
///|
pub async fn[S : @storage.AsyncStorage] CollectionManager::list_info(
self : CollectionManager[S],
) -> Result[Array[CollectionInfo], GatewayError] {
let infos : Array[CollectionInfo] = []
let names = match self.list() {
Ok(values) => values
Err(err) => return Err(err)
}
for name in names {
match self.get_info(name) {
Ok(info) => infos.push(info)
Err(err) => return Err(err)
}
}
Ok(infos)
}
///|
pub fn[S] CollectionManager::exists(
self : CollectionManager[S],
name : String,
) -> Bool {
self.collections.contains(name)
}
///|
pub fn[S] CollectionManager::count(self : CollectionManager[S]) -> Int {
self.collections.length()
}
///|
pub fn[S] CollectionManager::serialize_collection(
self : CollectionManager[S],
name : String,
) -> Bytes? {
match self.collections.get(name) {
Some(c) => Some(c.db.serialize_snapshot())
None => None
}
}
///|
pub fn[S] CollectionManager::load_collection(
self : CollectionManager[S],
config : CollectionConfig,
data : Bytes,
storage : S,
base_path : String,
) -> Result[Unit, GatewayError] {
if self.collections.contains(config.name) {
return Err(CollectionExists(config.name))
}
let db = @vcdb.PersistentDB::from_snapshot(
data,
storage,
base_path,
config.name,
)
let collection : Collection[S] = { config, db }
self.collections[config.name] = collection
Ok(())
}
///|
pub fn[S] CollectionManager::export_metadata(
self : CollectionManager[S],
) -> Json {
let collections : Array[Json] = []
for kv in self.collections {
let (_, c) = kv
collections.push(c.config.to_json())
}
json_object([
("version", 1.0.to_json()),
("collections", Json::array(collections)),
])
}