feat(connection): centralize connection type registration
This commit is contained in:
114 files changed
+5284
-1342
No files matched your search
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user