feat(connection): centralize connection type registration

This commit is contained in:
t8y2 committed 2026-08-23 16:38:06 +08:00
1 parent 38664e6aea
commit 0827478078
114 files changed
+5284 -1342

No files matched your search

+5
View File
@@ -94,3 +94,8 @@ windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32
mysql_common = { git = "https://github.com/t8y2/rust_mysql_common.git", rev = "47740d85a8277167a5717afac785d37b46a36296", default-features = false }
tokio = { version = "1", features = ["test-util"] }
tokio-openssl = "0.6"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml_ng = "0.10"
File diff suppressed because it is too large. Load diff
+100 -12
View File
@@ -1,29 +1,51 @@
use std::collections::HashSet;
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DatabaseDescriptor {
schema_version: u32,
order: u32,
db_type: String,
rust_variant: String,
}
fn main() {
let cargo_manifest_dir_str = env::var("CARGO_MANIFEST_DIR").unwrap();
let cargo_manifest_dir = Path::new(&cargo_manifest_dir_str);
let dialects_dir = cargo_manifest_dir.join("..").join("..").join("plugins").join("dialects");
let dialects_dir = std::fs::canonicalize(&dialects_dir).unwrap_or(dialects_dir);
let plugins_dir = cargo_manifest_dir.join("..").join("..").join("plugins");
let out_dir_str = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir_str).join("core_dialects.rs");
let out_dir = Path::new(&out_dir_str);
let mut entries: Vec<_> = std::fs::read_dir(&dialects_dir)
.expect("Cannot read plugins/dialects directory")
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "yaml" || ext == "yml"))
generate_core_dialects(&plugins_dir.join("dialects"), out_dir);
generate_connection_type_registry(&plugins_dir.join("connection-types"), out_dir);
}
fn yaml_entries(directory: &Path) -> Vec<PathBuf> {
let mut entries: Vec<_> = std::fs::read_dir(directory)
.unwrap_or_else(|_| panic!("Cannot read {} directory", directory.display()))
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "yaml" || extension == "yml"))
.collect();
entries.sort_by_key(|e| e.file_name());
entries.sort();
entries
}
fn generate_core_dialects(dialects_dir: &Path, out_dir: &Path) {
let dialects_dir = std::fs::canonicalize(dialects_dir).unwrap_or_else(|_| dialects_dir.to_path_buf());
let dest_path = out_dir.join("core_dialects.rs");
let entries = yaml_entries(&dialects_dir);
// Watch the directory itself so additions/removals of dialect files trigger a rebuild.
println!("cargo::rerun-if-changed={}", dialects_dir.to_str().unwrap());
let mut code = String::from("{\n");
for entry in &entries {
let path = entry.path();
for path in &entries {
let canonical = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
let file_name = path.file_stem().unwrap().to_str().unwrap();
let path_str = canonical.to_str().unwrap();
@@ -49,3 +71,69 @@ fn main() {
std::fs::write(&dest_path, code).expect("Failed to write core_dialects.rs");
}
fn generate_connection_type_registry(connection_types_dir: &Path, out_dir: &Path) {
let connection_types_dir =
std::fs::canonicalize(connection_types_dir).unwrap_or_else(|_| connection_types_dir.to_path_buf());
println!("cargo::rerun-if-changed={}", connection_types_dir.display());
let mut descriptors = Vec::new();
let mut db_types = HashSet::new();
let mut rust_variants = HashSet::new();
let mut orders = HashSet::new();
for path in yaml_entries(&connection_types_dir) {
println!("cargo::rerun-if-changed={}", path.display());
let content = std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("Cannot read {}", path.display()));
let descriptor: DatabaseDescriptor = serde_yaml_ng::from_str(&content)
.unwrap_or_else(|error| panic!("Invalid connection type descriptor {}: {error}", path.display()));
assert_eq!(descriptor.schema_version, 1, "{} must use schemaVersion 1", path.display());
assert!(db_types.insert(descriptor.db_type.clone()), "Duplicate database type {}", descriptor.db_type);
assert!(
rust_variants.insert(descriptor.rust_variant.clone()),
"Duplicate Rust database variant {}",
descriptor.rust_variant
);
assert!(orders.insert(descriptor.order), "Duplicate connection type descriptor order {}", descriptor.order);
let mut public_descriptor: serde_json::Value = serde_yaml_ng::from_str(&content)
.unwrap_or_else(|error| panic!("Invalid connection type descriptor {}: {error}", path.display()));
let public_object = public_descriptor.as_object_mut().expect("Database descriptor must be an object");
public_object.remove("schemaVersion");
public_object.remove("order");
public_object.remove("rustVariant");
descriptors.push((descriptor, public_descriptor));
}
descriptors.sort_by_key(|(descriptor, _)| descriptor.order);
let public_descriptors = descriptors.iter().map(|(_, descriptor)| descriptor.clone()).collect::<Vec<_>>();
let manifest = serde_json::json!({ "schemaVersion": 1, "drivers": public_descriptors });
std::fs::write(
out_dir.join("database_manifest.json"),
serde_json::to_string(&manifest).expect("Failed to serialize database manifest"),
)
.expect("Failed to write database_manifest.json");
let mut code = String::from(
"#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)]\n\
#[cfg_attr(feature = \"openapi\", derive(utoipa::ToSchema))]\n\
pub enum DatabaseType {\n",
);
for (descriptor, _) in &descriptors {
code.push_str(&format!(
" #[serde(rename = \"{}\")]\n {},\n",
descriptor.db_type, descriptor.rust_variant
));
}
code.push_str("}\n\nimpl DatabaseType {\n");
code.push_str(" pub const ALL: &'static [Self] = &[\n");
for (descriptor, _) in &descriptors {
code.push_str(&format!(" Self::{},\n", descriptor.rust_variant));
}
code.push_str(" ];\n\n pub const fn as_str(self) -> &'static str {\n match self {\n");
for (descriptor, _) in &descriptors {
code.push_str(&format!(" Self::{} => \"{}\",\n", descriptor.rust_variant, descriptor.db_type));
}
code.push_str(" }\n }\n}\n");
std::fs::write(out_dir.join("database_type.rs"), code).expect("Failed to write database_type.rs");
}
+57 -396
View File
@@ -1,425 +1,74 @@
use std::collections::HashSet;
use crate::database_manifest;
use crate::models::connection::DatabaseType;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AgentCatalogEntry {
pub db_type: DatabaseType,
pub key: &'static str,
pub label: &'static str,
pub store_visible: bool,
pub profiles: &'static [AgentDriverProfile],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentDriverProfile {
pub profile: &'static str,
pub key: &'static str,
pub label: &'static str,
pub store_visible: bool,
}
const ORACLE_PROFILES: &[AgentDriverProfile] = &[
AgentDriverProfile { profile: "oracle-legacy", key: "oracle", label: "Oracle", store_visible: false },
AgentDriverProfile { profile: "oracle-10g", key: "oracle", label: "Oracle", store_visible: false },
];
const GBASE_PROFILES: &[AgentDriverProfile] = &[
AgentDriverProfile { profile: "gbase8s", key: "gbase8s", label: "南大通用 GBase 8s", store_visible: true },
AgentDriverProfile { profile: "gbase8a", key: "gbase8a", label: "南大通用 GBase 8a", store_visible: true },
];
const MONGODB_PROFILES: &[AgentDriverProfile] = &[AgentDriverProfile {
profile: "mongodb-legacy",
key: "mongodb",
label: "MongoDB (Legacy)",
store_visible: false,
}];
const H2_PROFILES: &[AgentDriverProfile] = &[
AgentDriverProfile { profile: "h2-legacy", key: "h2", label: "H2 2.1 Legacy", store_visible: false },
AgentDriverProfile { profile: "h2-v1", key: "h2", label: "H2 1.x", store_visible: false },
AgentDriverProfile { profile: "h2-v2", key: "h2", label: "H2 2.0–2.1", store_visible: false },
AgentDriverProfile { profile: "h2-v3", key: "h2", label: "H2 2.2+", store_visible: false },
AgentDriverProfile { profile: "h2-custom", key: "h2", label: "H2 Custom JAR", store_visible: false },
];
const EXTRA_AGENT_LABELS: &[(&str, &str)] = &[
("duckdb", "DuckDB"),
("h2-legacy", "H2 2.1 Legacy"),
("kafka", "Apache Kafka"),
("rocketmq", "Apache RocketMQ"),
("rabbitmq", "RabbitMQ"),
("sqlserver-legacy", "SQL Server legacy compatibility component"),
];
const EXTRA_DRIVER_STORE_ENTRIES: &[(&str, &str)] = &[
("duckdb", "DuckDB"),
("kafka", "Apache Kafka"),
("rocketmq", "Apache RocketMQ"),
("rabbitmq", "RabbitMQ"),
("sqlserver-legacy", "SQL Server legacy compatibility component"),
];
const AGENT_CATALOG: &[AgentCatalogEntry] = &[
AgentCatalogEntry {
db_type: DatabaseType::Dameng,
key: "dameng",
label: "达梦 DM8",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Kingbase,
key: "kingbase",
label: "金仓KingbaseES",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Highgo,
key: "highgo",
label: "瀚高 HighGo",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Uxdb,
key: "uxdb",
label: "优炫 UXDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Vastbase,
key: "vastbase",
label: "海量 Vastbase",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Goldendb,
key: "goldendb",
label: "金篆 GoldenDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Databend,
key: "databend",
label: "Databend",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Databricks,
key: "databricks",
label: "Databricks SQL",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::SapHana,
key: "saphana",
label: "SAP HANA",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Teradata,
key: "teradata",
label: "Teradata",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Vertica,
key: "vertica",
label: "Vertica",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Firebird,
key: "firebird",
label: "Firebird",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Exasol,
key: "exasol",
label: "Exasol",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::OceanbaseOracle,
key: "oceanbase-oracle",
label: "OceanBase Oracle Mode",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Gbase,
key: "gbase8a",
label: "南大通用 GBase 8a",
store_visible: true,
profiles: GBASE_PROFILES,
},
AgentCatalogEntry {
db_type: DatabaseType::Access,
key: "access",
label: "Microsoft Access",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Oracle,
key: "oracle",
label: "Oracle",
store_visible: true,
profiles: ORACLE_PROFILES,
},
AgentCatalogEntry { db_type: DatabaseType::H2, key: "h2", label: "H2", store_visible: true, profiles: H2_PROFILES },
AgentCatalogEntry {
db_type: DatabaseType::Snowflake,
key: "snowflake",
label: "Snowflake",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Trino,
key: "trino",
label: "Trino",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Hive,
key: "hive",
label: "Apache Hive",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Kyuubi,
key: "hive",
label: "Apache Kyuubi",
store_visible: false,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Impala,
key: "hive",
label: "Apache Impala",
store_visible: false,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Spark,
key: "spark",
label: "Apache Spark",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry { db_type: DatabaseType::Db2, key: "db2", label: "IBM DB2", store_visible: true, profiles: &[] },
AgentCatalogEntry {
db_type: DatabaseType::Informix,
key: "informix",
label: "IBM Informix",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::InfluxDb,
key: "influxdb",
label: "InfluxDB",
store_visible: false,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Neo4j,
key: "neo4j",
label: "Neo4j",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Cassandra,
key: "cassandra",
label: "Apache Cassandra",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Bigquery,
key: "bigquery",
label: "Google BigQuery",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Spanner,
key: "spanner",
label: "Google Cloud Spanner",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Kylin,
key: "kylin",
label: "Apache Kylin",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Ignite,
key: "ignite",
label: "Apache Ignite",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Ignite3,
key: "ignite3",
label: "Apache Ignite 3",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Sundb,
key: "sundb",
label: "科蓝 SUNDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Oscar,
key: "oscar",
label: "神通 OSCAR",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Yashandb,
key: "yashandb",
label: "崖山 YashanDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Tdengine,
key: "tdengine",
label: "TDengine",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Xugu,
key: "xugu",
label: "虚谷 XuguDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::Iotdb,
key: "iotdb",
label: "Apache IoTDB",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry { db_type: DatabaseType::Etcd, key: "etcd", label: "etcd", store_visible: true, profiles: &[] },
AgentCatalogEntry {
db_type: DatabaseType::ZooKeeper,
key: "zookeeper",
label: "Apache ZooKeeper",
store_visible: true,
profiles: &[],
},
AgentCatalogEntry {
db_type: DatabaseType::MongoDb,
key: "mongodb",
label: "MongoDB (Legacy)",
store_visible: true,
profiles: MONGODB_PROFILES,
},
AgentCatalogEntry {
db_type: DatabaseType::Iris,
key: "iris",
label: "InterSystems IRIS",
store_visible: true,
profiles: &[],
},
];
pub fn entries() -> &'static [AgentCatalogEntry] {
AGENT_CATALOG
}
pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option<&'static str> {
if *db_type == DatabaseType::MessageQueue {
return match driver_profile {
Some("kafka") => Some("kafka"),
Some("rocketmq") => Some("rocketmq"),
Some("rabbitmq") => Some("rabbitmq"),
_ => None,
};
}
if *db_type == DatabaseType::SqlServer {
return driver_profile
.is_some_and(|profile| profile.eq_ignore_ascii_case("sqlserver-legacy"))
.then_some("sqlserver-legacy");
}
let entry = entry_for_db_type(db_type)?;
let entry = database_manifest::entry(db_type)?;
if let Some(driver_profile) = driver_profile {
if let Some(profile) = entry.profiles.iter().find(|profile| profile.profile == driver_profile) {
return Some(profile.key);
if let Some(profile) =
entry.driver_profiles.iter().find(|profile| profile.profile.eq_ignore_ascii_case(driver_profile))
{
return Some(profile.agent_key.as_str());
}
}
Some(entry.key)
entry.agent_key.as_deref()
}
pub fn is_agent_type(db_type: &DatabaseType) -> bool {
entry_for_db_type(db_type).is_some()
database_manifest::is_agent_runtime(db_type)
}
pub fn driver_store_entries() -> impl Iterator<Item = (&'static str, &'static str)> {
let mut seen = HashSet::new();
entries()
let mut entries = database_manifest::entries()
.iter()
.flat_map(move |entry| {
let base = entry.store_visible.then_some((entry.key, entry.label));
let profiles = entry
.profiles
.flat_map(|entry| {
let base = entry.driver_store_visible.then(|| {
entry
.agent_key
.as_deref()
.map(|key| (entry.driver_store_order.unwrap_or(u32::MAX), key, entry.label.as_str()))
});
let profiles = entry.driver_profiles.iter().filter(|profile| profile.store_visible).map(|profile| {
(
profile.store_order.unwrap_or(u32::MAX),
profile.package_key.as_deref().unwrap_or(profile.agent_key.as_str()),
profile.label.as_str(),
)
});
let managed = entry
.managed_drivers
.iter()
.filter(|profile| profile.store_visible)
.map(|profile| (profile.key, profile.label));
base.into_iter().chain(profiles)
.filter(|driver| driver.store_visible)
.map(|driver| (driver.store_order.unwrap_or(u32::MAX), driver.key.as_str(), driver.label.as_str()));
base.flatten().into_iter().chain(profiles).chain(managed)
})
.chain(EXTRA_DRIVER_STORE_ENTRIES.iter().copied())
.filter(move |(key, _)| seen.insert(*key))
.collect::<Vec<_>>();
entries.sort_by_key(|(order, _, _)| *order);
let mut seen = HashSet::new();
entries.into_iter().filter(move |(_, key, _)| seen.insert(*key)).map(|(_, key, label)| (key, label))
}
pub fn label_for_key(agent_key: &str) -> Option<&'static str> {
if let Some((_, label)) = EXTRA_AGENT_LABELS.iter().find(|(key, _)| *key == agent_key) {
return Some(label);
}
for entry in entries() {
if entry.key == agent_key {
return Some(entry.label);
for entry in database_manifest::entries() {
if entry.agent_key.as_deref() == Some(agent_key) {
return Some(entry.label.as_str());
}
if let Some(profile) = entry.profiles.iter().find(|profile| profile.key == agent_key) {
return Some(profile.label);
if let Some(profile) = entry
.driver_profiles
.iter()
.find(|profile| profile.package_key.as_deref() == Some(agent_key) || profile.agent_key == agent_key)
{
return Some(profile.label.as_str());
}
if let Some(driver) = entry.managed_drivers.iter().find(|driver| driver.key == agent_key) {
return Some(driver.label.as_str());
}
}
None
}
fn entry_for_db_type(db_type: &DatabaseType) -> Option<&'static AgentCatalogEntry> {
entries().iter().find(|entry| entry.db_type == *db_type)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -455,4 +104,16 @@ mod tests {
assert_eq!(agent_key(&DatabaseType::Kyuubi, None), Some("hive"));
assert_eq!(driver_store_entries().filter(|(key, _)| *key == "hive").count(), 1);
}
#[test]
fn manifest_agent_keys_match_catalog_defaults() {
for entry in database_manifest::entries().iter().filter(|entry| entry.agent_key.is_some()) {
assert_eq!(
agent_key(&entry.db_type, None),
entry.agent_key.as_deref(),
"agent key drift for {:?}",
entry.db_type
);
}
}
}
+29 -47
View File
@@ -1,4 +1,5 @@
use crate::agent_catalog;
use crate::database_manifest;
use crate::models::connection::DatabaseType;
pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option<&'static str> {
@@ -6,61 +7,19 @@ pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option
}
pub fn is_agent_type(db_type: &DatabaseType) -> bool {
agent_catalog::is_agent_type(db_type)
database_manifest::is_agent_runtime(db_type)
}
pub fn is_single_connection_pool(db_type: &DatabaseType) -> bool {
matches!(
db_type,
DatabaseType::Sqlite
| DatabaseType::DuckDb
| DatabaseType::Rqlite
| DatabaseType::Turso
| DatabaseType::CloudflareD1
| DatabaseType::MongoDb
| DatabaseType::DynamoDb
| DatabaseType::Hbase
| DatabaseType::Oracle
| DatabaseType::Dameng
| DatabaseType::Kingbase
| DatabaseType::Highgo
| DatabaseType::Uxdb
| DatabaseType::Vastbase
| DatabaseType::Goldendb
| DatabaseType::Yashandb
| DatabaseType::Oscar
| DatabaseType::Firebird
| DatabaseType::Iris
| DatabaseType::OceanbaseOracle
| DatabaseType::Access
| DatabaseType::Jdbc
| DatabaseType::VictoriaMetrics
)
database_manifest::entry(db_type).is_some_and(|entry| entry.single_connection_pool)
}
pub fn is_metadata_connection_scoped(db_type: &DatabaseType) -> bool {
// MySQL-protocol engines (incl. Doris/StarRocks) must not keep a configured
// default database when the caller asks for an unscoped metadata pool.
// External-catalog databases do not exist in the default catalog, so leaving
// `config.database` in the URL makes handshake/`USE` fail with Unknown database.
matches!(
db_type,
DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::ManticoreSearch
)
database_manifest::entry(db_type).is_some_and(|entry| entry.metadata_connection_scoped)
}
pub fn skips_tcp_probe(db_type: &DatabaseType) -> bool {
matches!(
db_type,
DatabaseType::Sqlite
| DatabaseType::DuckDb
| DatabaseType::Turso
| DatabaseType::CloudflareD1
| DatabaseType::Jdbc
| DatabaseType::MessageQueue
| DatabaseType::Mqtt
| DatabaseType::VictoriaMetrics
) || is_agent_type(db_type)
database_manifest::entry(db_type).is_some_and(|entry| entry.skip_tcp_probe) || is_agent_type(db_type)
}
/// Database types whose connection backs onto a single local file (or may, in the
@@ -68,7 +27,7 @@ pub fn skips_tcp_probe(db_type: &DatabaseType) -> bool {
/// manager" affordance. Whether the H2 connection is actually in file mode must
/// be determined separately by parsing the JDBC URL.
pub fn is_local_file_db_type(db_type: &DatabaseType) -> bool {
matches!(db_type, DatabaseType::Sqlite | DatabaseType::DuckDb | DatabaseType::Access | DatabaseType::H2)
database_manifest::entry(db_type).is_some_and(|entry| entry.local_file)
}
#[cfg(test)]
@@ -99,4 +58,27 @@ mod tests {
assert!(is_single_connection_pool(&DatabaseType::CloudflareD1));
assert!(skips_tcp_probe(&DatabaseType::CloudflareD1));
}
#[test]
fn manifest_connection_capabilities_are_used_for_every_registered_driver() {
for entry in database_manifest::entries() {
assert_eq!(
is_single_connection_pool(&entry.db_type),
entry.single_connection_pool,
"single connection pool drift for {:?}",
entry.db_type
);
assert_eq!(
is_metadata_connection_scoped(&entry.db_type),
entry.metadata_connection_scoped,
"metadata scope drift for {:?}",
entry.db_type
);
assert!(
skips_tcp_probe(&entry.db_type) || !is_agent_type(&entry.db_type),
"agent driver {:?} must skip the TCP probe",
entry.db_type
);
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use std::sync::OnceLock;
use serde::Deserialize;
use crate::models::connection::DatabaseType;
const DATABASE_MANIFEST_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/database_manifest.json"));
#[derive(Debug, Deserialize)]
struct DatabaseManifestFile {
drivers: Vec<DatabaseManifestEntry>,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseRuntimeMode {
Native,
File,
Agent,
External,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseDriverProfile {
pub profile: String,
pub agent_key: String,
#[serde(default)]
pub package_key: Option<String>,
pub label: String,
#[serde(default)]
pub store_visible: bool,
#[serde(default)]
pub store_order: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedDriverEntry {
pub key: String,
pub label: String,
#[serde(default)]
pub store_visible: bool,
#[serde(default)]
pub store_order: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseManifestEntry {
pub db_type: DatabaseType,
pub label: String,
#[serde(default)]
pub dialect: Option<String>,
pub runtime_mode: DatabaseRuntimeMode,
pub mcp_mode: String,
#[serde(default)]
pub agent_key: Option<String>,
#[serde(default)]
pub driver_store_visible: bool,
#[serde(default)]
pub driver_store_order: Option<u32>,
#[serde(default)]
pub driver_profiles: Vec<DatabaseDriverProfile>,
#[serde(default)]
pub managed_drivers: Vec<ManagedDriverEntry>,
#[serde(default)]
pub single_connection_pool: bool,
#[serde(default)]
pub metadata_connection_scoped: bool,
#[serde(default)]
pub skip_tcp_probe: bool,
#[serde(default)]
pub default_port: Option<u16>,
#[serde(default)]
pub local_file: bool,
#[serde(default)]
pub specialized_surface: bool,
}
static DATABASE_MANIFEST: OnceLock<DatabaseManifestFile> = OnceLock::new();
fn manifest() -> &'static DatabaseManifestFile {
DATABASE_MANIFEST.get_or_init(|| {
serde_json::from_str(DATABASE_MANIFEST_JSON).expect("database-drivers.manifest.json must be valid")
})
}
pub fn entries() -> &'static [DatabaseManifestEntry] {
&manifest().drivers
}
pub fn entry(db_type: &DatabaseType) -> Option<&'static DatabaseManifestEntry> {
entries().iter().find(|candidate| candidate.db_type == *db_type)
}
pub fn default_port(db_type: &DatabaseType) -> Option<u16> {
entry(db_type).and_then(|candidate| candidate.default_port.or(candidate.local_file.then_some(0)))
}
pub fn agent_key(db_type: &DatabaseType) -> Option<&'static str> {
entry(db_type).and_then(|candidate| candidate.agent_key.as_deref())
}
pub fn dialect_name(db_type: &DatabaseType) -> Option<&'static str> {
entry(db_type).and_then(|candidate| candidate.dialect.as_deref())
}
pub fn is_agent_runtime(db_type: &DatabaseType) -> bool {
entry(db_type).is_some_and(|candidate| candidate.runtime_mode == DatabaseRuntimeMode::Agent)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn manifest_entries_have_unique_database_types() {
let mut seen = HashSet::new();
assert!(entries().iter().all(|entry| seen.insert(entry.db_type)));
assert_eq!(seen.len(), DatabaseType::ALL.len());
assert!(DatabaseType::ALL.iter().all(|db_type| entry(db_type).is_some()));
}
#[test]
fn database_type_strings_match_serde_contract() {
for db_type in DatabaseType::ALL {
let serialized = serde_json::to_string(db_type).expect("database type must serialize");
assert_eq!(serialized, format!("\"{}\"", db_type.as_str()));
let deserialized =
serde_json::from_str::<DatabaseType>(&serialized).expect("database type must deserialize");
assert_eq!(deserialized, *db_type);
}
}
#[test]
fn manifest_contains_connection_runtime_defaults() {
assert_eq!(entry(&DatabaseType::Mysql).map(|entry| entry.default_port), Some(Some(3306)));
assert_eq!(entry(&DatabaseType::Postgres).map(|entry| entry.default_port), Some(Some(5432)));
assert_eq!(entry(&DatabaseType::Sqlite).map(|entry| entry.skip_tcp_probe), Some(true));
assert_eq!(entry(&DatabaseType::H2).map(|entry| entry.agent_key.as_deref()), Some(Some("h2")));
}
}
+1
View File
@@ -38,6 +38,7 @@ pub mod data_grid_extractors;
pub mod data_grid_sql;
pub mod database_capabilities;
pub mod database_export;
pub mod database_manifest;
pub mod database_search_sql;
pub mod db;
pub mod db_admin_sql;
+1 -120
View File
@@ -494,126 +494,7 @@ pub enum ProxyType {
Http,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum DatabaseType {
Mysql,
Postgres,
Sqlite,
Rqlite,
Redis,
#[serde(rename = "duckdb")]
DuckDb,
#[serde(rename = "clickhouse")]
ClickHouse,
#[serde(rename = "sqlserver")]
SqlServer,
#[serde(rename = "mongodb")]
MongoDb,
#[serde(rename = "dynamodb")]
DynamoDb,
#[serde(rename = "oracle")]
Oracle,
#[serde(rename = "elasticsearch")]
Elasticsearch,
#[serde(rename = "easysearch")]
Easysearch,
#[serde(rename = "meilisearch")]
Meilisearch,
Hbase,
#[serde(rename = "qdrant")]
Qdrant,
#[serde(rename = "milvus")]
Milvus,
#[serde(rename = "weaviate")]
Weaviate,
#[serde(rename = "chromadb")]
ChromaDb,
Doris,
#[serde(rename = "starrocks")]
StarRocks,
#[serde(rename = "manticoresearch")]
ManticoreSearch,
Databend,
Redshift,
Dameng,
Kingbase,
Highgo,
Uxdb,
Vastbase,
Goldendb,
Gaussdb,
Kwdb,
Yashandb,
Databricks,
#[serde(rename = "saphana")]
SapHana,
Teradata,
Vertica,
Firebird,
Exasol,
#[serde(rename = "opengauss")]
OpenGauss,
#[serde(rename = "oceanbase-oracle")]
OceanbaseOracle,
Gbase,
Access,
#[serde(rename = "h2")]
H2,
Snowflake,
Trino,
#[serde(rename = "prestosql")]
PrestoSql,
Hive,
Kyuubi,
Impala,
Spark,
#[serde(rename = "db2")]
Db2,
Informix,
#[serde(rename = "neo4j")]
Neo4j,
Cassandra,
#[serde(rename = "bigquery")]
Bigquery,
#[serde(rename = "spanner")]
Spanner,
Kylin,
Sundb,
Oscar,
Tdengine,
Xugu,
Iotdb,
Etcd,
#[serde(rename = "zookeeper")]
ZooKeeper,
Nacos,
Consul,
#[serde(rename = "iris")]
Iris,
#[serde(rename = "turso")]
Turso,
#[serde(rename = "cloudflare-d1")]
CloudflareD1,
#[serde(rename = "influxdb")]
InfluxDb,
#[serde(rename = "victoriametrics")]
VictoriaMetrics,
#[serde(rename = "questdb")]
Questdb,
Ignite,
Ignite3,
Jdbc,
/// Message queue admin connection (Pulsar / Kafka / RocketMQ). The specific
/// system is determined by `external_config.systemKind`.
#[serde(rename = "mq")]
MessageQueue,
/// MQTT broker connection. The broker address, client ID, and authentication
/// are stored in `external_config`.
#[serde(rename = "mqtt")]
Mqtt,
}
include!(concat!(env!("OUT_DIR"), "/database_type.rs"));
#[derive(Deserialize)]
struct ConnectionConfigData {
+1 -35
View File
@@ -19,41 +19,7 @@ pub enum DialectKind {
impl DialectKind {
pub fn from_database_type(db_type: DatabaseType) -> Self {
match db_type {
DatabaseType::Mysql
| DatabaseType::Doris
| DatabaseType::StarRocks
| DatabaseType::Goldendb
| DatabaseType::Sundb
| DatabaseType::Databend
| DatabaseType::Gbase => DialectKind::Mysql,
DatabaseType::Postgres
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
| DatabaseType::Highgo
| DatabaseType::Vastbase
| DatabaseType::Kingbase
| DatabaseType::Firebird
| DatabaseType::Redshift
| DatabaseType::Vertica
| DatabaseType::Exasol => DialectKind::Postgres,
DatabaseType::Sqlite | DatabaseType::Rqlite | DatabaseType::Turso => DialectKind::Sqlite,
DatabaseType::DuckDb => DialectKind::DuckDb,
DatabaseType::SqlServer | DatabaseType::Access => DialectKind::SqlServer,
DatabaseType::Oracle
| DatabaseType::Dameng
| DatabaseType::OceanbaseOracle
| DatabaseType::Iris
| DatabaseType::Yashandb
| DatabaseType::Xugu => DialectKind::Oracle,
DatabaseType::H2 => DialectKind::H2,
DatabaseType::ClickHouse => DialectKind::ClickHouse,
DatabaseType::ManticoreSearch => DialectKind::ManticoreSearch,
DatabaseType::Informix => DialectKind::Informix,
DatabaseType::Questdb => DialectKind::Questdb,
_ => DialectKind::Unsupported,
}
crate::database_manifest::dialect_name(&db_type).and_then(Self::from_label).unwrap_or(DialectKind::Unsupported)
}
pub fn to_database_type(self) -> Option<DatabaseType> {
@@ -77,9 +77,14 @@ impl DialectRegistry {
&self,
db_type: crate::models::connection::DatabaseType,
) -> Option<DialectCapabilityDescriptor> {
if let Some(dialect_name) = crate::database_manifest::dialect_name(&db_type) {
if let Some(loaded) = self.get(dialect_name) {
return Some(loaded.descriptor);
}
}
let kind = DialectKind::from_database_type(db_type);
let label = kind.label();
let db_name = format!("{db_type:?}").to_ascii_lowercase();
let db_name = db_type.as_str();
self.get(&db_name).or_else(|| self.get(label)).map(|ld| ld.descriptor)
}
@@ -29,6 +29,8 @@ struct DriverManifestEntry {
#[serde(default)]
skip_tcp_probe: bool,
#[serde(default)]
specialized_surface: bool,
#[serde(default)]
driver_profiles: Vec<DriverProfileEntry>,
}
@@ -235,18 +237,14 @@ fn driver_manifest_matches_core_database_capabilities() {
let support_levels = ["connect", "browse", "understand", "operate"];
for driver in &manifest.drivers {
// MQ is a message queue, not a database — skip database capability checks
if driver.db_type == DatabaseType::MessageQueue {
continue;
}
assert!(
support_levels.contains(&driver.support_level.as_str()),
"invalid support level for {:?}",
driver.db_type
);
assert!(
driver.capabilities.any_enabled(),
"database {:?} should declare at least one product capability",
driver.capabilities.any_enabled() || driver.specialized_surface,
"database {:?} should declare a product capability or a specialized surface",
driver.db_type
);
assert_eq!(
+2 -19
View File
@@ -14,6 +14,7 @@ use crate::mongo::{self, MongoCommand, MongoSafetyError};
use crate::session::{McpSession, McpSessionStore};
use dbx_core::{
agent_tools::{format_query_result_as_text, QueryCellWindow},
database_manifest,
db::redis_driver::{classify_command, parse_command_argv, RedisCommandResult, RedisCommandSafety},
models::connection::DatabaseType,
production_safety::{
@@ -655,7 +656,7 @@ impl DbxMcpServer {
Ok(db_type) => db_type,
Err(error) => return tool_error("INVALID_CONNECTION_TYPE", error),
};
let port = match request.port.or_else(|| default_port(&request.db_type)) {
let port = match request.port.or_else(|| database_manifest::default_port(&db_type)) {
Some(port) => port,
None => return text("Port is required for this database type."),
};
@@ -1207,24 +1208,6 @@ fn scoped_connection_ids(value: Option<&str>) -> Vec<String> {
ids
}
fn default_port(db_type: &str) -> Option<u16> {
match db_type.trim().to_ascii_lowercase().as_str() {
"mysql" | "doris" | "starrocks" | "manticoresearch" => Some(3306),
"postgres" | "redshift" | "highgo" | "kingbase" | "opengauss" | "gaussdb" => Some(5432),
"uxdb" => Some(52025),
"redis" => Some(6379),
"mongodb" => Some(27017),
"rqlite" => Some(4001),
"kwdb" => Some(26257),
"cloudflare-d1" => Some(443),
"tdengine" => Some(6041),
"iotdb" => Some(6667),
"xugu" => Some(5138),
"sqlite" | "duckdb" | "access" => Some(0),
_ => None,
}
}
fn ambiguous_connections(name: &str, connections: &[dbx_core::models::connection::ConnectionConfig]) -> String {
let lines = connections
.iter()