pub mod plugin;
pub mod plugins;
pub use plugin::{
Backend, BackendConformanceReport, BackendPluginContract, BackendPluginSurface,
BackendSupportState, all_plugins, has_runtime_implementation, plugin_for, plugin_for_kind,
support_state_for_kind, support_state_for_token,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackendKind {
Postgres,
Mysql,
Sqlite,
Mssql,
Clickhouse,
Redis,
Memcached,
Qdrant,
Weaviate,
Pinecone,
Minio,
S3,
AzureBlob,
Gcs,
Mongodb,
Elasticsearch,
Neo4j,
Cassandra,
}
impl BackendKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::Postgres => "postgres",
Self::Mysql => "mysql",
Self::Sqlite => "sqlite",
Self::Mssql => "sqlserver",
Self::Clickhouse => "clickhouse",
Self::Redis => "redis",
Self::Memcached => "memcached",
Self::Qdrant => "qdrant",
Self::Weaviate => "weaviate",
Self::Pinecone => "pinecone",
Self::Minio => "minio",
Self::S3 => "s3",
Self::AzureBlob => "azureblob",
Self::Gcs => "gcs",
Self::Mongodb => "mongodb",
Self::Elasticsearch => "elasticsearch",
Self::Neo4j => "neo4j",
Self::Cassandra => "cassandra",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token.trim().to_ascii_lowercase().as_str() {
"postgres" => Some(Self::Postgres),
"mysql" => Some(Self::Mysql),
"sqlite" => Some(Self::Sqlite),
"sqlserver" => Some(Self::Mssql),
"clickhouse" => Some(Self::Clickhouse),
"redis" => Some(Self::Redis),
"memcached" => Some(Self::Memcached),
"qdrant" => Some(Self::Qdrant),
"weaviate" => Some(Self::Weaviate),
"pinecone" => Some(Self::Pinecone),
"minio" => Some(Self::Minio),
"s3" => Some(Self::S3),
"azureblob" => Some(Self::AzureBlob),
"gcs" => Some(Self::Gcs),
"mongodb" => Some(Self::Mongodb),
"elasticsearch" => Some(Self::Elasticsearch),
"neo4j" => Some(Self::Neo4j),
"cassandra" => Some(Self::Cassandra),
_ => None,
}
}
pub fn tier(&self) -> BackendTier {
match self {
Self::Postgres | Self::Mysql | Self::Sqlite | Self::Mssql | Self::Clickhouse => {
BackendTier::Sql
}
Self::Redis | Self::Memcached => BackendTier::Cache,
Self::Qdrant | Self::Weaviate | Self::Pinecone => BackendTier::Vector,
Self::Minio | Self::S3 | Self::AzureBlob | Self::Gcs => BackendTier::Object,
Self::Mongodb | Self::Elasticsearch => BackendTier::Document,
Self::Neo4j => BackendTier::Graph,
Self::Cassandra => BackendTier::Column,
}
}
pub fn role(&self) -> BackendRole {
match self {
Self::Postgres | Self::Mysql | Self::Sqlite => BackendRole::Canonical,
Self::Mssql
| Self::Mongodb
| Self::Clickhouse
| Self::Neo4j
| Self::Redis
| Self::Memcached
| Self::Qdrant
| Self::Weaviate
| Self::Pinecone
| Self::Minio
| Self::S3
| Self::AzureBlob
| Self::Gcs
| Self::Elasticsearch
| Self::Cassandra => BackendRole::Projection,
}
}
pub fn capabilities(&self) -> BackendCapability {
match self {
Self::Postgres => BackendCapability {
supports_sql_ddl: true,
supports_transactions: true,
supports_xa: false,
supports_two_phase_commit: true,
supports_rls: true,
supports_vector_search: false,
supports_streaming: true,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: true,
supports_idempotency: true,
supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "strong".into(),
},
Self::Mysql => BackendCapability {
supports_sql_ddl: true,
supports_transactions: true,
supports_xa: true,
supports_two_phase_commit: true,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: true, supports_idempotency: true,
supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "strong".into(),
},
Self::Mssql => BackendCapability {
supports_sql_ddl: true,
supports_transactions: true, supports_xa: false, supports_two_phase_commit: false,
supports_rls: false, supports_vector_search: false,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true, supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "strong".into(),
},
Self::Sqlite => BackendCapability {
supports_sql_ddl: true,
supports_transactions: true,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: true, supports_idempotency: true,
supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "strong".into(),
},
Self::Clickhouse => BackendCapability {
supports_sql_ddl: true,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: true,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: false,
supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "eventual".into(),
},
Self::Redis => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: true,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: false,
max_payload_bytes: 536_870_912, consistency_model: "eventual".into(),
},
Self::Memcached => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false, supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: false,
supports_ttl: true, is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true, supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: false, max_payload_bytes: 1_048_576, consistency_model: "eventual".into(),
},
Self::Qdrant => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: true,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: true,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "eventual".into(),
},
Self::Weaviate => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true,
supports_vector_search: true,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: true,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "eventual".into(),
},
Self::Pinecone => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: true,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: true, supports_resource_lifecycle: true,
max_payload_bytes: 2_097_152, consistency_model: "eventual".into(),
},
Self::Minio | Self::S3 => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: true,
is_object_store: true,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 5_368_709_120, consistency_model: "strong".into(),
},
Self::AzureBlob | Self::Gcs => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: true,
is_object_store: true,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 5_368_709_120, consistency_model: "strong".into(),
},
Self::Mongodb => BackendCapability {
supports_sql_ddl: false,
supports_transactions: true,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: true,
supports_ttl: true,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true,
supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 16_777_216, consistency_model: "causal".into(),
},
Self::Elasticsearch => BackendCapability {
supports_sql_ddl: false,
supports_transactions: false, supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: true, supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true, supports_schema_migration: false,
supports_hybrid_search: true, supports_resource_lifecycle: true,
max_payload_bytes: 104_857_600, consistency_model: "eventual".into(),
},
Self::Cassandra => BackendCapability {
supports_sql_ddl: true,
supports_transactions: false, supports_xa: false,
supports_two_phase_commit: false,
supports_rls: false, supports_vector_search: false,
supports_streaming: false,
supports_ttl: true, is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: true, supports_schema_migration: true,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "tunable".into(), },
Self::Neo4j => BackendCapability {
supports_sql_ddl: false,
supports_transactions: true,
supports_xa: false,
supports_two_phase_commit: false,
supports_rls: true, supports_vector_search: false,
supports_streaming: false,
supports_ttl: false,
is_object_store: false,
is_migration_ledger_capable: false,
supports_idempotency: false,
supports_schema_migration: false,
supports_hybrid_search: false,
supports_resource_lifecycle: true,
max_payload_bytes: 0,
consistency_model: "strong".into(),
},
}
}
pub fn default_env_key(&self) -> &'static str {
match self {
Self::Postgres => "UDB_SQL_DSN",
Self::Mysql => "UDB_MYSQL_DSN",
Self::Sqlite => "UDB_SQLITE_DSN",
Self::Mssql => "UDB_MSSQL_DSN",
Self::Clickhouse => "UDB_COLUMN_DSN",
Self::Redis => "UDB_CACHE_DSN",
Self::Memcached => "UDB_MEMCACHED_DSN",
Self::Qdrant => "UDB_VECTOR_DSN",
Self::Weaviate => "UDB_WEAVIATE_DSN",
Self::Pinecone => "UDB_PINECONE_DSN",
Self::Minio => "UDB_OBJECT_DSN",
Self::S3 => "UDB_S3_DSN",
Self::AzureBlob => "UDB_AZUREBLOB_DSN",
Self::Gcs => "UDB_GCS_DSN",
Self::Mongodb => "UDB_NOSQL_DSN",
Self::Elasticsearch => "UDB_ELASTIC_DSN",
Self::Neo4j => "UDB_GRAPH_DSN",
Self::Cassandra => "UDB_CASSANDRA_DSN",
}
}
pub fn dsn_scheme(&self) -> String {
format!("udb+{}+{}", self.tier().as_str(), self.as_str())
}
pub fn from_store_kind(store_kind: &str, backend_hint: &str) -> Option<Self> {
if !backend_hint.is_empty() {
match backend_hint.to_lowercase().as_str() {
"postgres" | "postgresql" => return Some(Self::Postgres),
"mysql" | "mariadb" => return Some(Self::Mysql),
"sqlite" => return Some(Self::Sqlite),
"sqlserver" | "mssql" => return Some(Self::Mssql),
"clickhouse" => return Some(Self::Clickhouse),
"redis" => return Some(Self::Redis),
"memcached" => return Some(Self::Memcached),
"qdrant" => return Some(Self::Qdrant),
"weaviate" => return Some(Self::Weaviate),
"pinecone" => return Some(Self::Pinecone),
"minio" => return Some(Self::Minio),
"s3" => return Some(Self::S3),
"azureblob" | "azure" => return Some(Self::AzureBlob),
"gcs" => return Some(Self::Gcs),
"mongodb" | "mongo" => return Some(Self::Mongodb),
"elasticsearch" | "elastic" => return Some(Self::Elasticsearch),
"neo4j" => return Some(Self::Neo4j),
"cassandra" | "scylla" => return Some(Self::Cassandra),
_ => {}
}
}
match store_kind {
"sql" | "relational" => Some(Self::Postgres),
"cache" | "kv" | "key_value" | "key-value" | "keyvalue" => Some(Self::Redis),
"vector" => Some(Self::Qdrant),
"storage" | "object" | "blob" => Some(Self::Minio),
"nosql" | "document" => Some(Self::Mongodb),
"graph" => Some(Self::Neo4j),
"timeseries" | "time-series" | "column" | "columnar" | "wide-column" => {
Some(Self::Clickhouse)
}
"search" => Some(Self::Elasticsearch),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackendTier {
Sql,
Cache,
Vector,
Object,
Document,
Graph,
Column,
}
impl BackendTier {
pub fn as_str(&self) -> &'static str {
match self {
Self::Sql => "sql",
Self::Cache => "cache",
Self::Vector => "vector",
Self::Object => "object",
Self::Document => "document",
Self::Graph => "graph",
Self::Column => "column",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct BackendCapability {
pub supports_sql_ddl: bool,
pub supports_transactions: bool,
pub supports_xa: bool,
pub supports_two_phase_commit: bool,
pub supports_rls: bool,
pub supports_vector_search: bool,
pub supports_streaming: bool,
pub supports_ttl: bool,
pub is_object_store: bool,
pub is_migration_ledger_capable: bool,
pub supports_idempotency: bool,
pub supports_schema_migration: bool,
pub supports_hybrid_search: bool,
pub supports_resource_lifecycle: bool,
pub max_payload_bytes: u64,
pub consistency_model: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackendRole {
Canonical,
Projection,
Both,
}
impl BackendRole {
pub fn as_str(self) -> &'static str {
match self {
Self::Canonical => "canonical",
Self::Projection => "projection",
Self::Both => "both",
}
}
pub fn can_host_system_tables(self) -> bool {
matches!(self, Self::Canonical | Self::Both)
}
pub fn can_be_durability_anchor(self) -> bool {
matches!(self, Self::Canonical | Self::Both)
}
pub fn can_receive_projections(self) -> bool {
matches!(self, Self::Projection | Self::Both)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackendCapabilityMatrixEntry {
pub backend: String,
pub tier: String,
pub operations: Vec<String>,
pub unsupported_error_code: String,
pub consistency_model: String,
pub max_payload_bytes: u64,
pub supports_xa: bool,
pub supports_two_phase_commit: bool,
#[serde(default = "default_backend_role")]
pub role: BackendRole,
}
fn default_backend_role() -> BackendRole {
BackendRole::Projection
}
const OP_PING: &str = "ping";
const OP_PROBE: &str = "probe";
const OP_QUERY: &str = "query";
const OP_MUTATE: &str = "mutate";
const OP_TRANSACTION: &str = "transaction";
const OP_SEARCH: &str = "search";
const OP_GET_OBJECT: &str = "get_object";
const OP_PUT_OBJECT: &str = "put_object";
const OP_ENSURE_RESOURCE: &str = "ensure_resource";
const OP_DROP_RESOURCE: &str = "drop_resource";
const OP_LIST_RESOURCES: &str = "list_resources";
pub const UNSUPPORTED_OPERATION_CODE: &str = "UDB_UNSUPPORTED_OPERATION";
impl BackendKind {
pub fn all_known() -> &'static [BackendKind] {
const ALL: &[BackendKind] = &[
BackendKind::Postgres,
BackendKind::Mysql,
BackendKind::Sqlite,
BackendKind::Mssql,
BackendKind::Clickhouse,
BackendKind::Redis,
BackendKind::Memcached,
BackendKind::Qdrant,
BackendKind::Weaviate,
BackendKind::Pinecone,
BackendKind::Minio,
BackendKind::S3,
BackendKind::AzureBlob,
BackendKind::Gcs,
BackendKind::Mongodb,
BackendKind::Elasticsearch,
BackendKind::Neo4j,
BackendKind::Cassandra,
];
ALL
}
pub fn supported_operations(&self) -> Vec<&'static str> {
let cap = self.capabilities();
let mut ops = vec![OP_PING, OP_PROBE];
if cap.supports_resource_lifecycle {
ops.extend([OP_ENSURE_RESOURCE, OP_DROP_RESOURCE, OP_LIST_RESOURCES]);
}
match self {
Self::Postgres
| Self::Mysql
| Self::Sqlite
| Self::Mssql
| Self::Clickhouse
| Self::Redis
| Self::Memcached
| Self::Mongodb
| Self::Elasticsearch
| Self::Neo4j
| Self::Cassandra
| Self::Weaviate
| Self::Pinecone => ops.push(OP_QUERY),
_ => {}
}
match self {
Self::Postgres
| Self::Mysql
| Self::Sqlite
| Self::Mssql
| Self::Clickhouse
| Self::Redis
| Self::Memcached
| Self::Mongodb
| Self::Neo4j
| Self::Qdrant
| Self::Elasticsearch
| Self::Cassandra
| Self::Weaviate
| Self::Pinecone => ops.push(OP_MUTATE),
_ => {}
}
if cap.supports_transactions {
ops.push(OP_TRANSACTION);
}
if cap.supports_vector_search || cap.supports_hybrid_search {
ops.push(OP_SEARCH);
}
if cap.is_object_store {
ops.extend([OP_GET_OBJECT, OP_PUT_OBJECT]);
}
ops.sort_unstable();
ops.dedup();
ops
}
pub fn supports_operation(&self, operation: &str) -> bool {
match operation {
OP_PING | OP_PROBE | OP_ENSURE_RESOURCE | OP_DROP_RESOURCE | OP_LIST_RESOURCES
| OP_QUERY | OP_MUTATE | OP_TRANSACTION | OP_SEARCH | OP_GET_OBJECT | OP_PUT_OBJECT => {
self.supported_operations().contains(&operation)
}
_ => false,
}
}
pub fn capability_matrix_entry(&self) -> BackendCapabilityMatrixEntry {
let cap = self.capabilities();
BackendCapabilityMatrixEntry {
backend: self.as_str().to_string(),
tier: self.tier().as_str().to_string(),
operations: self
.supported_operations()
.into_iter()
.map(ToString::to_string)
.collect(),
unsupported_error_code: UNSUPPORTED_OPERATION_CODE.to_string(),
consistency_model: cap.consistency_model,
max_payload_bytes: cap.max_payload_bytes,
supports_xa: cap.supports_xa,
supports_two_phase_commit: cap.supports_two_phase_commit,
role: self.role(),
}
}
}
pub fn capability_matrix() -> Vec<BackendCapabilityMatrixEntry> {
all_plugins()
.into_iter()
.map(|plugin| plugin.kind().capability_matrix_entry())
.collect()
}
pub const CORE_BACKENDS: [BackendKind; 4] = [
BackendKind::Postgres,
BackendKind::Redis,
BackendKind::Qdrant,
BackendKind::Minio,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn core_backends_have_correct_tiers() {
assert_eq!(BackendKind::Postgres.tier(), BackendTier::Sql);
assert_eq!(BackendKind::Redis.tier(), BackendTier::Cache);
assert_eq!(BackendKind::Qdrant.tier(), BackendTier::Vector);
assert_eq!(BackendKind::Minio.tier(), BackendTier::Object);
}
#[test]
fn only_postgres_is_ledger_capable() {
for b in &CORE_BACKENDS {
let cap = b.capabilities();
if *b == BackendKind::Postgres {
assert!(
cap.is_migration_ledger_capable,
"postgres must be ledger capable"
);
} else {
assert!(
!cap.is_migration_ledger_capable,
"{} must NOT be ledger capable",
b.as_str()
);
}
}
}
#[test]
fn dsn_scheme_follows_udb_convention() {
assert_eq!(BackendKind::Postgres.dsn_scheme(), "udb+sql+postgres");
assert_eq!(BackendKind::Redis.dsn_scheme(), "udb+cache+redis");
assert_eq!(BackendKind::Qdrant.dsn_scheme(), "udb+vector+qdrant");
assert_eq!(BackendKind::Minio.dsn_scheme(), "udb+object+minio");
}
#[test]
fn from_store_kind_falls_back_to_tier_default() {
assert_eq!(
BackendKind::from_store_kind("sql", ""),
Some(BackendKind::Postgres)
);
assert_eq!(
BackendKind::from_store_kind("cache", ""),
Some(BackendKind::Redis)
);
assert_eq!(
BackendKind::from_store_kind("vector", ""),
Some(BackendKind::Qdrant)
);
assert_eq!(
BackendKind::from_store_kind("storage", ""),
Some(BackendKind::Minio)
);
}
#[test]
fn from_store_kind_respects_backend_hint() {
assert_eq!(
BackendKind::from_store_kind("vector", "weaviate"),
Some(BackendKind::Weaviate)
);
assert_eq!(
BackendKind::from_store_kind("object", "s3"),
Some(BackendKind::S3)
);
}
#[test]
fn wired_vector_backends_support_vector_search() {
for b in [
BackendKind::Qdrant,
BackendKind::Elasticsearch,
BackendKind::Weaviate,
BackendKind::Pinecone,
] {
assert!(
b.capabilities().supports_vector_search,
"{} must support vector search",
b.as_str()
);
}
}
#[test]
fn metadata_only_backends_advertise_no_capabilities() {
use crate::backend::BackendKind;
for b in [
BackendKind::Postgres,
BackendKind::Mysql,
BackendKind::Sqlite,
BackendKind::Mssql,
BackendKind::Clickhouse,
BackendKind::Redis,
BackendKind::Memcached,
BackendKind::Qdrant,
BackendKind::Weaviate,
BackendKind::Pinecone,
BackendKind::Minio,
BackendKind::S3,
BackendKind::AzureBlob,
BackendKind::Gcs,
BackendKind::Mongodb,
BackendKind::Elasticsearch,
BackendKind::Neo4j,
BackendKind::Cassandra,
] {
let caps = b.capabilities();
assert_ne!(
caps.consistency_model,
"metadata_only",
"{} should NOT be metadata_only — all 18 backends wired in C9",
b.as_str()
);
}
}
#[test]
fn object_backends_are_object_stores() {
for b in [
BackendKind::Minio,
BackendKind::S3,
BackendKind::AzureBlob,
BackendKind::Gcs,
] {
assert!(
b.capabilities().is_object_store,
"{} must be object store",
b.as_str()
);
}
}
#[test]
fn test_capability_rejection() {
let postgres = BackendKind::Postgres;
assert!(postgres.capabilities().supports_sql_ddl);
let redis = BackendKind::Redis;
assert!(!redis.capabilities().supports_sql_ddl);
assert!(!redis.capabilities().supports_vector_search);
let qdrant = BackendKind::Qdrant;
assert!(qdrant.capabilities().supports_vector_search);
assert!(!qdrant.capabilities().supports_sql_ddl);
}
#[test]
fn capability_matrix_lists_generic_dispatch_operations() {
let matrix = capability_matrix();
#[cfg(feature = "redis")]
{
let redis = matrix
.iter()
.find(|entry| entry.backend == "redis")
.expect("redis matrix entry");
assert!(redis.operations.contains(&"query".to_string()));
assert!(redis.operations.contains(&"mutate".to_string()));
assert_eq!(redis.unsupported_error_code, UNSUPPORTED_OPERATION_CODE);
}
#[cfg(feature = "qdrant")]
{
let qdrant = matrix
.iter()
.find(|entry| entry.backend == "qdrant")
.expect("qdrant matrix entry");
assert!(qdrant.operations.contains(&"search".to_string()));
assert!(!qdrant.operations.contains(&"query".to_string()));
}
}
#[test]
fn advertised_generic_dispatch_operations_are_admitted() {
for backend in ALL_KINDS {
for operation in backend.supported_operations() {
assert!(
backend.supports_operation(operation),
"{} advertises '{operation}' but rejects it",
backend.as_str()
);
}
}
}
#[test]
fn generic_dispatch_operation_table_pins_known_drift_cases() {
for backend in [
BackendKind::Mysql,
BackendKind::Sqlite,
BackendKind::Mssql,
BackendKind::Memcached,
BackendKind::Elasticsearch,
BackendKind::Cassandra,
BackendKind::Weaviate,
BackendKind::Pinecone,
] {
assert!(
backend.supported_operations().contains(&OP_QUERY),
"{} has a real generic query executor",
backend.as_str()
);
assert!(
backend.supported_operations().contains(&OP_MUTATE),
"{} has a real generic mutate executor",
backend.as_str()
);
}
for backend in [
BackendKind::Qdrant,
BackendKind::AzureBlob,
BackendKind::Gcs,
] {
assert!(
!backend.supports_operation(OP_QUERY),
"{} query is an unsupported fallback and must not be admitted",
backend.as_str()
);
}
}
const ALL_KINDS: [BackendKind; 18] = [
BackendKind::Postgres,
BackendKind::Mysql,
BackendKind::Sqlite,
BackendKind::Mssql,
BackendKind::Clickhouse,
BackendKind::Redis,
BackendKind::Memcached,
BackendKind::Qdrant,
BackendKind::Weaviate,
BackendKind::Pinecone,
BackendKind::Minio,
BackendKind::S3,
BackendKind::AzureBlob,
BackendKind::Gcs,
BackendKind::Mongodb,
BackendKind::Elasticsearch,
BackendKind::Neo4j,
BackendKind::Cassandra,
];
fn serde_token(b: &BackendKind) -> String {
serde_json::to_string(b)
.unwrap()
.trim_matches('"')
.to_string()
}
#[test]
fn as_str_tokens_are_pinned() {
let expected: [(BackendKind, &str); 18] = [
(BackendKind::Postgres, "postgres"),
(BackendKind::Mysql, "mysql"),
(BackendKind::Sqlite, "sqlite"),
(BackendKind::Mssql, "sqlserver"),
(BackendKind::Clickhouse, "clickhouse"),
(BackendKind::Redis, "redis"),
(BackendKind::Memcached, "memcached"),
(BackendKind::Qdrant, "qdrant"),
(BackendKind::Weaviate, "weaviate"),
(BackendKind::Pinecone, "pinecone"),
(BackendKind::Minio, "minio"),
(BackendKind::S3, "s3"),
(BackendKind::AzureBlob, "azureblob"),
(BackendKind::Gcs, "gcs"),
(BackendKind::Mongodb, "mongodb"),
(BackendKind::Elasticsearch, "elasticsearch"),
(BackendKind::Neo4j, "neo4j"),
(BackendKind::Cassandra, "cassandra"),
];
for (kind, token) in expected {
assert_eq!(kind.as_str(), token, "as_str token changed for {kind:?}");
}
}
#[test]
fn serde_round_trips_for_every_variant() {
for kind in ALL_KINDS {
let json = serde_json::to_string(&kind).unwrap();
let back: BackendKind = serde_json::from_str(&json).unwrap();
assert_eq!(back, kind, "serde round-trip failed for {kind:?}");
}
}
#[test]
fn config_backend_tokens_agree_across_as_str_and_serde() {
for kind in [
BackendKind::Postgres,
BackendKind::Redis,
BackendKind::Qdrant,
BackendKind::Clickhouse,
BackendKind::Mongodb,
BackendKind::Neo4j,
BackendKind::Minio,
BackendKind::S3,
] {
assert_eq!(
kind.as_str(),
serde_token(&kind),
"as_str() and serde token diverged for config backend {kind:?}"
);
}
}
#[test]
fn from_token_is_exact_inverse_of_as_str() {
for kind in ALL_KINDS {
assert_eq!(
BackendKind::from_token(kind.as_str()),
Some(kind.clone()),
"from_token(as_str()) must round-trip for {kind:?}"
);
}
}
#[test]
fn from_token_is_case_insensitive_and_rejects_unknown() {
assert_eq!(
BackendKind::from_token("POSTGRES"),
Some(BackendKind::Postgres)
);
assert_eq!(
BackendKind::from_token(" Qdrant "),
Some(BackendKind::Qdrant)
);
assert_eq!(
BackendKind::from_token("sqlserver"),
Some(BackendKind::Mssql)
);
assert_eq!(BackendKind::from_token("mssql"), None);
assert_eq!(BackendKind::from_token("not_a_backend"), None);
assert_eq!(BackendKind::from_token(""), None);
}
#[test]
fn known_as_str_vs_serde_divergences_are_locked() {
assert_eq!(BackendKind::Mssql.as_str(), "sqlserver");
assert_eq!(serde_token(&BackendKind::Mssql), "mssql");
assert_eq!(BackendKind::AzureBlob.as_str(), "azureblob");
assert_eq!(serde_token(&BackendKind::AzureBlob), "azure_blob");
}
}