fix(mysql): isolate compatible database-specific logic

This commit is contained in:
t8y2
2026-08-14 19:09:05 +08:00
parent 1ff24aaeb4
commit 6dcfdc6ae0
15 changed files with 484 additions and 887 deletions
+8 -21
View File
@@ -63,21 +63,8 @@ pub enum MysqlMode {
OceanBaseOracle,
}
fn is_oceanbase_mysql_config(config: &ConnectionConfig) -> bool {
config.db_type == DatabaseType::Mysql
&& config.driver_profile.as_deref().is_some_and(|profile| profile.eq_ignore_ascii_case("oceanbase"))
}
pub(crate) fn oceanbase_mysql_query_timeout_sql(config: &ConnectionConfig, timeout_secs: u64) -> Option<String> {
if !is_oceanbase_mysql_config(config) || timeout_secs == 0 {
return None;
}
let timeout_us = timeout_secs.saturating_mul(1_000_000);
Some(format!("SET ob_query_timeout = {timeout_us}"))
}
fn oceanbase_mysql_setup_queries(config: &ConnectionConfig) -> Vec<String> {
oceanbase_mysql_query_timeout_sql(config, config.query_timeout_secs).into_iter().collect()
db::oceanbase_mysql::query_timeout_sql(config, config.query_timeout_secs).into_iter().collect()
}
fn mysql_pool_setup_queries(config: &ConnectionConfig, url: &str) -> Vec<String> {
@@ -5246,12 +5233,12 @@ mod tests {
database_connection_config_with_catalog, gaussdb_identifier_quote_from_query_result,
gaussdb_m_jdbc_config_for_endpoint, gaussdb_uses_m_jdbc_driver, kafka_single_loopback_bootstrap_endpoint,
metadata_connection_config, mysql_metadata_fallback_url, mysql_pool_setup_queries,
oceanbase_mysql_query_timeout_sql, oceanbase_mysql_setup_queries, prestosql_jdbc_config_for_endpoint,
redacted_connection_url_for_endpoint, redis_sentinel_transport_id, redis_sentinel_transport_prefix,
sqlserver_legacy_agent_config, sqlserver_legacy_driver_error, sqlserver_uses_legacy_driver,
task_client_session_id, upsert_connection_url_param, uses_bare_mysql_pool, uses_tcp_probe,
validate_connection_url_params, validate_h2_database_path, AppState, MysqlMode, PoolKind,
GAUSSDB_M_JDBC_DRIVER_CLASS, GAUSSDB_M_JDBC_DRIVER_PROFILE, PRESTOSQL_JDBC_DRIVER_CLASS,
oceanbase_mysql_setup_queries, prestosql_jdbc_config_for_endpoint, redacted_connection_url_for_endpoint,
redis_sentinel_transport_id, redis_sentinel_transport_prefix, sqlserver_legacy_agent_config,
sqlserver_legacy_driver_error, sqlserver_uses_legacy_driver, task_client_session_id,
upsert_connection_url_param, uses_bare_mysql_pool, uses_tcp_probe, validate_connection_url_params,
validate_h2_database_path, AppState, MysqlMode, PoolKind, GAUSSDB_M_JDBC_DRIVER_CLASS,
GAUSSDB_M_JDBC_DRIVER_PROFILE, PRESTOSQL_JDBC_DRIVER_CLASS,
};
use crate::agent_connection::{
agent_connect_params, mongo_legacy_error_with_auth_hint, mongo_uses_legacy_driver,
@@ -5857,7 +5844,7 @@ mod tests {
config.driver_profile = Some("oceanbase".to_string());
assert_eq!(
oceanbase_mysql_query_timeout_sql(&config, 300_000),
db::oceanbase_mysql::query_timeout_sql(&config, 300_000),
Some("SET ob_query_timeout = 300000000000".to_string())
);
}
+27
View File
@@ -1,5 +1,32 @@
use crate::models::connection::{ConnectionConfig, DatabaseType};
pub use super::mysql_compatible::{
get_columns_show_from as get_catalog_columns, list_catalog_indexes, list_catalogs,
list_databases_show_from as list_catalog_databases, list_indexes_with_ddl_fallback as list_indexes,
list_tables_show_from as list_catalog_tables, show_create_table_ddl_from as get_catalog_table_ddl,
};
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::Doris
|| (*db_type == DatabaseType::Mysql
&& driver_profile
.is_some_and(|profile| matches!(profile.to_ascii_lowercase().as_str(), "doris" | "selectdb")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_matches_doris_and_selectdb_only() {
assert!(is_profile(&DatabaseType::Doris, None));
assert!(is_profile(&DatabaseType::Mysql, Some("DORIS")));
assert!(is_profile(&DatabaseType::Mysql, Some("selectdb")));
assert!(!is_profile(&DatabaseType::Mysql, Some("starrocks")));
assert!(!is_profile(&DatabaseType::Postgres, Some("doris")));
}
}
+19
View File
@@ -1,10 +1,21 @@
use mysql_async::prelude::*;
use std::collections::HashMap;
use crate::models::connection::{ConnectionConfig, DatabaseType};
use crate::types::{ColumnInfo, IndexInfo, ObjectInfo};
use super::mysql::{self, MySqlPool};
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::ManticoreSearch
|| (*db_type == DatabaseType::Mysql
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("manticoresearch")))
}
fn quote_identifier(value: &str) -> String {
format!("`{}`", value.replace('`', "``"))
}
@@ -205,6 +216,14 @@ fn index_info_from_row(row: &mysql_async::Row) -> IndexInfo {
mod tests {
use super::*;
#[test]
fn profile_matches_native_and_mysql_profile_connections() {
assert!(is_profile(&DatabaseType::ManticoreSearch, None));
assert!(is_profile(&DatabaseType::Mysql, Some("MANTICORESEARCH")));
assert!(!is_profile(&DatabaseType::Mysql, None));
assert!(!is_profile(&DatabaseType::Postgres, Some("manticoresearch")));
}
#[test]
fn show_metadata_uses_unqualified_table_names_when_database_is_empty() {
assert_eq!(show_columns_sql("", "idx", true), "SHOW FULL COLUMNS FROM `idx`");
+3
View File
@@ -24,6 +24,7 @@ pub mod mongo_driver;
pub mod mysql;
pub mod mysql_compatible;
pub mod ob_oracle;
pub mod oceanbase_mysql;
pub mod postgres;
pub mod proxy_tunnel;
pub mod questdb;
@@ -35,6 +36,8 @@ pub mod ssh_host_key;
pub mod ssh_prompt;
pub mod ssh_tunnel;
pub mod starrocks;
pub mod tdsql_mysql;
pub mod tidb;
pub mod transport_layer_tunnel;
pub mod turso_driver;
pub mod vector_driver;
+15 -752
View File
@@ -41,14 +41,12 @@ pub(crate) fn mysql_catalog_dialect(
db_type: DatabaseType,
driver_profile: Option<&str>,
) -> Option<MySqlCatalogDialect> {
match db_type {
DatabaseType::Doris => Some(MySqlCatalogDialect::Doris),
DatabaseType::StarRocks => Some(MySqlCatalogDialect::StarRocks),
_ => match driver_profile.map(str::to_ascii_lowercase).as_deref() {
Some("doris" | "selectdb") => Some(MySqlCatalogDialect::Doris),
Some("starrocks") => Some(MySqlCatalogDialect::StarRocks),
_ => None,
},
if super::doris::is_profile(&db_type, driver_profile) {
Some(MySqlCatalogDialect::Doris)
} else if super::starrocks::is_profile(&db_type, driver_profile) {
Some(MySqlCatalogDialect::StarRocks)
} else {
None
}
}
@@ -74,14 +72,11 @@ const MYSQL_RESULT_CELL_PREVIEW_MAX_BYTES: usize = 8 * 1024;
impl MySqlQueryDialect {
pub fn for_connection(db_type: DatabaseType, driver_profile: Option<&str>) -> Self {
let profile = driver_profile.map(str::to_ascii_lowercase);
Self {
supports_admin_show_results: matches!(
db_type,
DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::ManticoreSearch
) || profile.as_deref().is_some_and(|profile| {
matches!(profile, "doris" | "selectdb" | "starrocks" | "manticoresearch" | "tidb")
}),
supports_admin_show_results: super::doris::is_profile(&db_type, driver_profile)
|| super::starrocks::is_profile(&db_type, driver_profile)
|| super::manticoresearch::is_profile(&db_type, driver_profile)
|| super::tidb::is_profile(&db_type, driver_profile),
}
}
}
@@ -91,7 +86,7 @@ pub enum MySqlQueryStreamItem {
Row(Vec<serde_json::Value>),
}
fn quote_value(s: &str) -> String {
pub(super) fn quote_value(s: &str) -> String {
format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'"))
}
@@ -2655,7 +2650,7 @@ pub async fn get_table_comment(pool: &MySqlPool, database: &str, table: &str) ->
}
#[derive(Clone, Debug, Default)]
struct TableStatusMeta {
pub(super) struct TableStatusMeta {
comment: Option<String>,
created_at: Option<String>,
updated_at: Option<String>,
@@ -2889,7 +2884,7 @@ fn mysql_fallback_like_patterns(filter: &str) -> Vec<String> {
patterns
}
async fn list_tables_show_with_status(
pub(super) async fn list_tables_show_with_status(
pool: &MySqlPool,
database: &str,
) -> Result<(Vec<TableInfo>, HashMap<String, TableStatusMeta>), String> {
@@ -2943,116 +2938,6 @@ pub async fn list_tables_show(pool: &MySqlPool, database: &str) -> Result<Vec<Ta
list_tables_show_with_status(pool, database).await.map(|(tables, _)| tables)
}
fn starrocks_materialized_views_sql(database: &str) -> String {
format!(
"SELECT TABLE_NAME FROM information_schema.materialized_views WHERE TABLE_SCHEMA = {}",
quote_value(database)
)
}
/// Fallback DDL source for StarRocks materialized views when `SHOW CREATE
/// MATERIALIZED VIEW` fails (e.g. on versions predating starrocks/starrocks#73396,
/// merged 2026-05-19, which reject the statement for sync MVs with "Table not
/// found" because sync MVs are not registered as separate Tables).
///
/// `information_schema.materialized_views` is documented as the authoritative
/// list of all materialized views, with a column distinguishing SYNC from
/// ASYNC. See
/// https://docs.starrocks.io/docs/sql-reference/information_schema/materialized_views/.
///
/// Made `pub(super)` so the dispatch site in `schema::mysql_object_source` can
/// rely on it without rewriting the escape convention.
pub(crate) fn mysql_materialized_view_definition_sql(database: &str, name: &str) -> String {
format!(
"SELECT MATERIALIZED_VIEW_DEFINITION \
FROM information_schema.materialized_views \
WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \
LIMIT 1",
quote_value(database),
quote_value(name)
)
}
async fn list_starrocks_materialized_view_names(pool: &MySqlPool, database: &str) -> Result<HashSet<String>, String> {
let sql = starrocks_materialized_views_sql(database);
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
Ok(rows
.iter()
.filter_map(|row| {
let name = get_str_by_name(row, "TABLE_NAME").trim().to_string();
(!name.is_empty()).then_some(name)
})
.collect())
}
fn merge_starrocks_materialized_views(
tables: &mut Vec<TableInfo>,
materialized_view_names: Result<HashSet<String>, String>,
database: &str,
) {
let materialized_view_names = match materialized_view_names {
Ok(names) => names,
Err(err) => {
// Older StarRocks versions and restricted accounts may not expose this
// information_schema view; keep the base SHOW TABLES result usable.
log::warn!("Skipping materialized view classification for StarRocks database `{database}`: {err}");
return;
}
};
// Snapshot the names already returned by SHOW FULL TABLES so the second pass can
// append MVs that are absent from SHOW FULL TABLES without duplicating rows.
let known_names: HashSet<String> = tables.iter().map(|table| table.name.clone()).collect();
// Step 1 — reclassify: rows whose name appears in `information_schema.materialized_views`
// are MVs even when SHOW FULL TABLES labeled them as VIEW (sync MVs) or BASE TABLE
// (async MVs). See https://docs.starrocks.io/docs/sql-reference/information_schema/materialized_views/
// for the authoritative distinction between the two MV kinds.
for table in tables.iter_mut() {
if materialized_view_names.contains(&table.name) {
table.table_type = "MATERIALIZED_VIEW".to_string();
}
}
// Step 2 — union: on StarRocks versions predating starrocks/starrocks#73396 (merged
// 2026-05-19), sync MVs "are not registered as separate Tables" so SHOW FULL TABLES
// omits them entirely. Append those rows from the system view so they appear in the
// sidebar and the DDL source path has something to resolve. Sort names so that
// the resulting table order is deterministic across runs.
let mut materialized_view_names_sorted: Vec<&String> = materialized_view_names.iter().collect();
materialized_view_names_sorted.sort();
for name in materialized_view_names_sorted {
if !known_names.contains(name.as_str()) {
tables.push(TableInfo {
name: name.clone(),
table_type: "MATERIALIZED_VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
});
}
}
}
async fn list_starrocks_tables_with_status(
pool: &MySqlPool,
database: &str,
) -> Result<(Vec<TableInfo>, HashMap<String, TableStatusMeta>), String> {
let (tables, materialized_view_names) = tokio::join!(
list_tables_show_with_status(pool, database),
list_starrocks_materialized_view_names(pool, database)
);
let (mut tables, status) = tables?;
merge_starrocks_materialized_views(&mut tables, materialized_view_names, database);
Ok((tables, status))
}
pub async fn list_starrocks_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableInfo>, String> {
list_starrocks_tables_with_status(pool, database).await.map(|(tables, _)| tables)
}
fn requested_object_type(object_types: Option<&[String]>, object_type: &str) -> bool {
object_types.is_none_or(|types| {
types.is_empty() || types.iter().any(|candidate| candidate.eq_ignore_ascii_case(object_type))
@@ -3447,21 +3332,7 @@ fn filter_table_objects_fallback(
.collect()
}
pub async fn list_starrocks_table_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
let (tables, routines) =
tokio::join!(list_starrocks_tables_with_status(pool, database), list_routine_objects(pool, database));
let (tables, status) = tables?;
let mut objects = table_infos_to_objects(tables, &status, database);
match routines {
Ok(routines) => objects.extend(routines),
Err(err) => log::warn!("Skipping routines for database `{}` in object browser: {}", database, err),
}
Ok(objects)
}
fn table_infos_to_objects(
pub(super) fn table_infos_to_objects(
tables: Vec<TableInfo>,
status: &HashMap<String, TableStatusMeta>,
database: &str,
@@ -3497,7 +3368,7 @@ fn table_infos_to_objects(
.collect()
}
async fn list_routine_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
pub(super) async fn list_routine_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let rows = query_routine_rows(&mut conn, database, None, None, None).await?;
Ok(rows.iter().map(|row| row_to_object(row, database)).collect())
@@ -5302,438 +5173,6 @@ pub async fn show_create_table_ddl(pool: &MySqlPool, database: &str, table: &str
.ok_or_else(|| "Failed to read DDL".to_string())
}
// ---------------------------------------------------------------------------
// Doris / StarRocks multi-catalog support.
//
// These engines expose external catalogs (iceberg, hive, jdbc, ...) alongside
// the native `internal` catalog via `SHOW CATALOGS`. The functions below address
// objects in a specific catalog using 3-part qualified names
// (`<catalog>.<database>.<table>`), which the engines accept directly without
// needing to `SWITCH` the session catalog.
// ---------------------------------------------------------------------------
/// Build a 2-part qualified identifier `` `<catalog>`.`<database>` ``.
fn doris_catalog_database_ref(catalog: &str, database: &str) -> String {
format!("{}.{}", quote_identifier(catalog), quote_identifier(database))
}
/// Build a 3-part qualified identifier `` `<catalog>`.`<database>`.`<table>` ``.
fn doris_catalog_table_ref(catalog: &str, database: &str, table: &str) -> String {
format!("{}.{}.{}", quote_identifier(catalog), quote_identifier(database), quote_identifier(table))
}
/// `SHOW CATALOGS` → list of catalogs visible to the current user.
///
/// Column layouts differ between engines: Doris exposes `CatalogName` (with
/// `CatalogId`/`IsCurrent`/`CreateTime`/`LastUpdateTime`), while StarRocks
/// exposes `Catalog` (only `Type`/`Comment`, no `IsCurrent`). The name is read
/// from either column; missing trailing columns degrade gracefully to
/// empty/None. The built-in catalog is named `internal` in Doris and
/// `default_catalog` in StarRocks (both with `Type=internal`); detection is
/// type-based (see `CatalogInfo::is_internal`), not name-based.
pub async fn list_doris_catalogs(pool: &MySqlPool) -> Result<Vec<crate::db::CatalogInfo>, String> {
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let result = conn.query_iter("SHOW CATALOGS").await.map_err(|e| e.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
let catalogs: Vec<crate::db::CatalogInfo> = rows
.iter()
.filter_map(|row| {
// Doris column is `CatalogName`; StarRocks column is `Catalog`.
let name = first_nonempty_str_by_name(row, &["CatalogName", "Catalog"]).trim().to_string();
if name.is_empty() {
return None;
}
let catalog_type = get_str_by_name(row, "Type").trim().to_string();
let is_current = {
let value = get_str_by_name(row, "IsCurrent").trim().to_ascii_lowercase();
!value.is_empty() && value != "no" && value != "false" && value != "0"
};
let comment = get_opt_str(row, "Comment").map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
Some(crate::db::CatalogInfo { name, catalog_type, is_current, comment })
})
.collect();
Ok(normalize_doris_catalogs(catalogs))
}
/// Sort with the built-in catalog first, then the rest alphabetically by name.
/// The built-in catalog is identified by `CatalogInfo::is_internal` (type-based)
/// rather than by name, so StarRocks `default_catalog` sorts first just like
/// Doris `internal`. No synthetic catalog is injected: `SHOW CATALOGS` always
/// lists the built-in catalog on both engines, and a single-catalog result is
/// handled by the flat-sidebar fallback in the caller.
fn normalize_doris_catalogs(mut catalogs: Vec<crate::db::CatalogInfo>) -> Vec<crate::db::CatalogInfo> {
catalogs.sort_by(|a, b| match (a.is_internal(), b.is_internal()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
catalogs
}
/// `SHOW DATABASES FROM <catalog>` → databases in the given catalog.
pub async fn list_databases_show_from(pool: &MySqlPool, catalog: &str) -> Result<Vec<DatabaseInfo>, String> {
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let sql = format!("SHOW DATABASES FROM {}", quote_identifier(catalog));
let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
Ok(database_infos_from_names(rows.iter().map(|row| get_str(row, 0)), false))
}
/// `SHOW TABLES FROM <catalog>.<database>` → tables in an external catalog.
///
/// External catalogs do not support `SHOW TABLE STATUS`, so comments/status are
/// not fetched (the caller only needs names + types for browsing).
pub async fn list_tables_show_from(pool: &MySqlPool, catalog: &str, database: &str) -> Result<Vec<TableInfo>, String> {
let sql = format!("SHOW TABLES FROM {}", doris_catalog_database_ref(catalog, database));
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
let mut tables: Vec<TableInfo> = rows
.iter()
.filter_map(|row| {
let name = get_str(row, 0).trim().to_string();
if name.is_empty() {
return None;
}
// SHOW FULL TABLES exposes a type column; plain SHOW TABLES does not.
let table_type = get_str(row, 1);
Some(TableInfo {
name,
table_type: if table_type.trim().is_empty() { "TABLE".to_string() } else { table_type },
comment: None,
parent_schema: None,
parent_name: None,
})
})
.collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
Ok(tables)
}
/// `SHOW COLUMNS FROM <catalog>.<database>.<table>` → columns of an external
/// catalog table. Falls back to `DESCRIBE` if `SHOW COLUMNS` is rejected.
pub async fn get_columns_show_from(
pool: &MySqlPool,
catalog: &str,
database: &str,
table: &str,
) -> Result<Vec<ColumnInfo>, String> {
let qualified = doris_catalog_table_ref(catalog, database, table);
let full_sql = format!("SHOW FULL COLUMNS FROM {qualified}");
let plain_sql = format!("SHOW COLUMNS FROM {qualified}");
let describe_sql = format!("DESCRIBE {qualified}");
let mut conn = get_conn_with_health_check(pool).await?;
let rows: Vec<mysql_async::Row> = match conn.query_iter(&full_sql).await {
Ok(result) => result.collect_and_drop().await.map_err(|e| e.to_string())?,
Err(_) => match conn.query_iter(&plain_sql).await {
Ok(result) => result.collect_and_drop().await.map_err(|e| e.to_string())?,
Err(_) => {
let result = conn.query_iter(&describe_sql).await.map_err(|e| e.to_string())?;
result.collect_and_drop().await.map_err(|e| e.to_string())?
}
},
};
Ok(rows
.iter()
.filter_map(|row| {
let name = get_str_by_name(row, "Field").trim().to_string();
if name.is_empty() {
return None;
}
let key = get_str_by_name(row, "Key");
let collation = get_opt_str(row, "Collation").filter(|s| !s.is_empty());
Some(ColumnInfo {
name,
data_type: get_str_by_name(row, "Type"),
is_nullable: get_str_by_name(row, "Null").eq_ignore_ascii_case("YES"),
column_default: get_opt_str(row, "Default"),
is_primary_key: key.eq_ignore_ascii_case("PRI"),
is_unique: key.eq_ignore_ascii_case("UNI"),
extra: get_opt_str(row, "Extra"),
comment: get_opt_str(row, "Comment")
.map(|s| fix_potential_double_encoding(&s))
.filter(|s| !s.is_empty()),
numeric_precision: None,
numeric_scale: None,
character_maximum_length: None,
enum_values: None,
character_set: collation
.as_deref()
.and_then(|c| c.split_once('_').map(|(charset, _)| charset.to_string()))
.filter(|s| !s.is_empty()),
collation,
})
})
.collect())
}
/// `SHOW CREATE TABLE <catalog>.<database>.<table>` → DDL for an external
/// catalog table.
pub async fn show_create_table_ddl_from(
pool: &MySqlPool,
catalog: &str,
database: &str,
table: &str,
) -> Result<String, String> {
let sql = format!("SHOW CREATE TABLE {}", doris_catalog_table_ref(catalog, database, table));
let mut conn = get_conn_with_health_check(pool).await?;
let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
let row = rows.first().ok_or("DDL not found")?;
row.get_opt::<String, usize>(1)
.and_then(|result| result.ok())
.or_else(|| {
row.get_opt::<Vec<u8>, usize>(1)
.and_then(|result| result.ok())
.map(|b| String::from_utf8_lossy(&b).to_string())
})
.ok_or_else(|| "Failed to read DDL".to_string())
}
/// Best-effort index listing for an external catalog table. External catalogs
/// generally do not expose MySQL-style index metadata via `information_schema`
/// (that view is scoped to the internal catalog), so indexes are derived from
/// `SHOW CREATE TABLE` parsing. Returns empty on failure (graceful degradation
/// — indexes are informational for external tables).
pub async fn list_doris_catalog_indexes(
pool: &MySqlPool,
catalog: &str,
database: &str,
table: &str,
) -> Result<Vec<IndexInfo>, String> {
let ddl = show_create_table_ddl_from(pool, catalog, database, table).await?;
Ok(doris_indexes_from_create_table_ddl(&ddl))
}
fn doris_indexes_from_create_table_ddl(ddl: &str) -> Vec<IndexInfo> {
let mut indexes = Vec::new();
for raw_line in ddl.lines() {
let line = trim_ddl_definition_line(raw_line);
if line.is_empty() {
continue;
}
let upper = line.to_ascii_uppercase();
if upper.starts_with("PRIMARY KEY") {
if let Some(index) = doris_table_key_index("PRIMARY", line, true, true, "PRIMARY KEY") {
indexes.push(index);
}
} else if upper.starts_with("UNIQUE KEY") {
if let Some(index) = doris_table_key_index("UNIQUE KEY", line, true, false, "UNIQUE KEY") {
indexes.push(index);
}
} else if upper.starts_with("INDEX ") {
if let Some(index) = doris_secondary_index(line) {
indexes.push(index);
}
}
}
indexes
}
fn trim_ddl_definition_line(line: &str) -> &str {
let mut trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(',') {
trimmed = rest.trim_start();
}
while let Some(rest) = trimmed.strip_suffix(',') {
trimmed = rest.trim_end();
}
trimmed
}
fn doris_table_key_index(
name: &str,
line: &str,
is_unique: bool,
is_primary: bool,
index_type: &str,
) -> Option<IndexInfo> {
let columns = parse_mysql_index_columns(first_parenthesized_content(line)?);
if columns.is_empty() {
return None;
}
Some(IndexInfo {
name: name.to_string(),
columns,
is_unique,
is_primary,
filter: None,
index_type: Some(index_type.to_string()),
included_columns: None,
comment: None,
})
}
fn doris_secondary_index(line: &str) -> Option<IndexInfo> {
let (_, rest) = split_keyword_prefix(line, "INDEX")?;
let (name, after_name) = read_mysql_identifier(rest.trim_start())?;
let columns = parse_mysql_index_columns(first_parenthesized_content(after_name)?);
if columns.is_empty() {
return None;
}
Some(IndexInfo {
name,
columns,
is_unique: false,
is_primary: false,
filter: None,
index_type: mysql_keyword_argument(after_name, "USING").or_else(|| Some("INDEX".to_string())),
included_columns: None,
comment: mysql_quoted_string_argument(after_name, "COMMENT"),
})
}
fn split_keyword_prefix<'a>(line: &'a str, keyword: &str) -> Option<(&'a str, &'a str)> {
if line.len() < keyword.len() || !line[..keyword.len()].eq_ignore_ascii_case(keyword) {
return None;
}
let rest = &line[keyword.len()..];
if !rest.is_empty() && is_mysql_identifier_byte(rest.as_bytes()[0]) {
return None;
}
Some((&line[..keyword.len()], rest))
}
fn read_mysql_identifier(input: &str) -> Option<(String, &str)> {
let input = input.trim_start();
if input.is_empty() {
return None;
}
let bytes = input.as_bytes();
if bytes[0] == b'`' {
let mut i = 1;
let mut value = String::new();
while i < bytes.len() {
if bytes[i] == b'`' {
if i + 1 < bytes.len() && bytes[i + 1] == b'`' {
value.push('`');
i += 2;
continue;
}
return Some((value, &input[i + 1..]));
}
let ch = input[i..].chars().next()?;
value.push(ch);
i += ch.len_utf8();
}
return None;
}
let end = input.find(|ch: char| ch.is_whitespace() || matches!(ch, '(' | ')' | ',')).unwrap_or(input.len());
if end == 0 {
return None;
}
Some((input[..end].to_string(), &input[end..]))
}
fn first_parenthesized_content(input: &str) -> Option<&str> {
let bytes = input.as_bytes();
let mut depth = 0usize;
let mut start = None;
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\'' | b'"' | b'`' => {
i = skip_mysql_quoted(input, i, bytes[i]);
continue;
}
b'(' => {
if depth == 0 {
start = Some(i + 1);
}
depth += 1;
}
b')' if depth > 0 => {
depth -= 1;
if depth == 0 {
return start.map(|start| &input[start..i]);
}
}
_ => {}
}
i += 1;
}
None
}
fn split_top_level_csv(input: &str) -> Vec<&str> {
let bytes = input.as_bytes();
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\'' | b'"' | b'`' => {
i = skip_mysql_quoted(input, i, bytes[i]);
continue;
}
b'(' => depth += 1,
b')' if depth > 0 => depth -= 1,
b',' if depth == 0 => {
parts.push(input[start..i].trim());
start = i + 1;
}
_ => {}
}
i += 1;
}
parts.push(input[start..].trim());
parts
}
fn parse_mysql_index_columns(input: &str) -> Vec<String> {
split_top_level_csv(input)
.into_iter()
.filter_map(|part| read_mysql_identifier(part).map(|(column, _)| column))
.filter(|column| !column.is_empty())
.collect()
}
fn mysql_keyword_argument(input: &str, keyword: &str) -> Option<String> {
let bytes = input.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\'' | b'"' | b'`' => {
i = skip_mysql_quoted(input, i, bytes[i]);
continue;
}
_ if mysql_keyword_at(input, i, keyword) => {
return read_mysql_identifier(&input[i + keyword.len()..]).map(|(value, _)| value);
}
_ => i += 1,
}
}
None
}
fn mysql_quoted_string_argument(input: &str, keyword: &str) -> Option<String> {
let bytes = input.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\'' | b'"' | b'`' => {
i = skip_mysql_quoted(input, i, bytes[i]);
continue;
}
_ if mysql_keyword_at(input, i, keyword) => {
let rest = input[i + keyword.len()..].trim_start();
if rest.as_bytes().first().copied() != Some(b'\'') {
return None;
}
let end = skip_mysql_quoted(rest, 0, b'\'');
if end <= 1 || end > rest.len() {
return None;
}
return Some(rest[1..end - 1].replace("\\'", "'").replace("''", "'"));
}
_ => i += 1,
}
}
None
}
pub async fn list_foreign_keys(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
let column_sql = format!(
"SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_SCHEMA, \
@@ -6540,182 +5979,6 @@ mod tests {
assert_eq!(filtered.iter().map(|object| object.name.as_str()).collect::<Vec<_>>(), vec!["c_table"]);
}
#[test]
fn starrocks_materialized_views_are_classified_without_duplicating_tables() {
let mut tables = vec![
TableInfo {
name: "orders".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "orders_view".to_string(),
table_type: "VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "orders_mv".to_string(),
table_type: "VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
];
let materialized_views = HashSet::from(["orders_mv".to_string(), "orders_mv".to_string()]);
merge_starrocks_materialized_views(&mut tables, Ok(materialized_views), "analytics");
assert_eq!(tables.len(), 3);
assert_eq!(
tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::<Vec<_>>(),
vec![("orders", "BASE TABLE"), ("orders_view", "VIEW"), ("orders_mv", "MATERIALIZED_VIEW")]
);
}
#[test]
fn starrocks_async_materialized_views_reported_as_base_table_are_reclassified() {
// Async materialized views (StarRocks >= 2.5) appear as `BASE TABLE` in
// `SHOW FULL TABLES`. Classification must trust the
// `information_schema.materialized_views` source.
let mut tables = vec![
TableInfo {
name: "orders".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "orders_async_mv".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
];
let materialized_views = HashSet::from(["orders_async_mv".to_string()]);
merge_starrocks_materialized_views(&mut tables, Ok(materialized_views), "analytics");
assert_eq!(
tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::<Vec<_>>(),
vec![("orders", "BASE TABLE"), ("orders_async_mv", "MATERIALIZED_VIEW")]
);
}
#[test]
fn starrocks_materialized_view_lookup_failure_keeps_base_types() {
let mut tables = vec![TableInfo {
name: "orders_mv".to_string(),
table_type: "VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
}];
merge_starrocks_materialized_views(&mut tables, Err("permission denied".to_string()), "analytics");
assert_eq!(tables[0].table_type, "VIEW");
}
#[test]
fn starrocks_sync_mv_absent_from_show_full_tables_is_appended_from_information_schema() {
// StarRocks versions predating starrocks/starrocks#73396 (merged
// 2026-05-19) report sync MVs as "not registered as separate Tables",
// so SHOW FULL TABLES omits them. The merger must union names from
// information_schema.materialized_views so the sidebar and DDL path
// still resolve them.
let mut tables = vec![TableInfo {
name: "orders".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
}];
let materialized_views = HashSet::from([
"orders_mv".to_string(), // already present (reclassify path)
"daily_orders_mv".to_string(), // absent from SHOW FULL TABLES (union path)
]);
merge_starrocks_materialized_views(&mut tables, Ok(materialized_views), "analytics");
assert_eq!(tables.len(), 3);
assert_eq!(
tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::<Vec<_>>(),
vec![
("orders", "BASE TABLE"),
("daily_orders_mv", "MATERIALIZED_VIEW"),
("orders_mv", "MATERIALIZED_VIEW"),
]
);
}
#[test]
fn starrocks_materialized_view_query_is_scoped_to_database() {
let sql = starrocks_materialized_views_sql("tenant's analytics");
assert_eq!(
sql,
"SELECT TABLE_NAME FROM information_schema.materialized_views WHERE TABLE_SCHEMA = 'tenant\\'s analytics'"
);
}
#[test]
fn mysql_materialized_view_definition_fallback_is_scoped_to_db_and_name() {
// StarRocks predating PR 73396 (merged 2026-05-19) rejects
// `SHOW CREATE MATERIALIZED VIEW` for sync MVs with "Table not found"
// because sync MVs are not registered as separate Tables. The fallback
// path queries information_schema.materialized_views directly. The
// regression guards the SQL shape and the value escaping used by that
// fallback so the wire format isn't accidentally regressed.
assert_eq!(
mysql_materialized_view_definition_sql("shop", "daily_sales_mv"),
"SELECT MATERIALIZED_VIEW_DEFINITION FROM information_schema.materialized_views WHERE TABLE_SCHEMA = 'shop' AND TABLE_NAME = 'daily_sales_mv' LIMIT 1"
);
assert_eq!(
mysql_materialized_view_definition_sql("tenant's analytics", "weird'name"),
"SELECT MATERIALIZED_VIEW_DEFINITION FROM information_schema.materialized_views WHERE TABLE_SCHEMA = 'tenant\\'s analytics' AND TABLE_NAME = 'weird\\'name' LIMIT 1"
);
}
#[test]
fn starrocks_object_conversion_preserves_table_view_and_materialized_view_types() {
let tables = vec![
TableInfo {
name: "orders".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "orders_view".to_string(),
table_type: "VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "orders_mv".to_string(),
table_type: "MATERIALIZED_VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
];
let objects = table_infos_to_objects(tables, &HashMap::new(), "analytics");
assert_eq!(
objects.iter().map(|object| (object.name.as_str(), object.object_type.as_str())).collect::<Vec<_>>(),
vec![("orders", "TABLE"), ("orders_view", "VIEW"), ("orders_mv", "MATERIALIZED_VIEW")]
);
}
#[test]
fn mysql_table_comment_sql_targets_single_table() {
let sql = table_comment_sql("app", "users");
@@ -1,6 +1,7 @@
use mysql_async::prelude::*;
use std::collections::HashSet;
use crate::models::connection::ConnectionConfig;
use crate::types::{ColumnInfo, DatabaseInfo, IndexInfo, TableInfo};
use super::mysql::{
@@ -9,6 +10,14 @@ use super::mysql::{
list_indexes, mysql_keyword_at, quote_identifier, show_create_table_ddl, skip_mysql_quoted, MySqlPool,
};
pub fn uses_show_metadata(config: &ConnectionConfig) -> bool {
super::doris::is_config(config) || super::starrocks::is_config(config) || super::manticoresearch::is_config(config)
}
pub fn supports_external_catalogs(config: &ConnectionConfig) -> bool {
super::doris::is_config(config) || super::starrocks::is_config(config)
}
// Doris and StarRocks reuse the MySQL wire protocol, but catalog addressing and DDL/index
// metadata semantics are MySQL-compatible distributed behavior and stay isolated in this module.
pub async fn list_indexes_with_ddl_fallback(
+56
View File
@@ -0,0 +1,56 @@
use crate::models::connection::{ConnectionConfig, DatabaseType};
pub const DRIVER_PROFILE: &str = "oceanbase";
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::Mysql
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case(DRIVER_PROFILE))
}
pub fn query_timeout_sql(config: &ConnectionConfig, timeout_secs: u64) -> Option<String> {
if !is_config(config) || timeout_secs == 0 {
return None;
}
Some(format!("SET ob_query_timeout = {}", timeout_secs.saturating_mul(1_000_000)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_is_isolated_from_oracle_mode_and_standard_mysql() {
assert!(is_profile(&DatabaseType::Mysql, Some("OceanBase")));
assert!(!is_profile(&DatabaseType::Mysql, None));
assert!(!is_profile(&DatabaseType::OceanbaseOracle, Some("oceanbase")));
}
#[test]
fn query_timeout_uses_microseconds_and_saturates() {
let mut config = test_config();
assert_eq!(query_timeout_sql(&config, 30), Some("SET ob_query_timeout = 30000000".to_string()));
assert_eq!(query_timeout_sql(&config, u64::MAX), Some(format!("SET ob_query_timeout = {}", u64::MAX)));
config.driver_profile = None;
assert_eq!(query_timeout_sql(&config, 30), None);
}
fn test_config() -> ConnectionConfig {
serde_json::from_value(serde_json::json!({
"id": "oceanbase",
"name": "OceanBase",
"db_type": "mysql",
"driver_profile": "oceanbase",
"host": "127.0.0.1",
"port": 2883,
"username": "root",
"password": "",
"database": "test"
}))
.unwrap()
}
}
+204
View File
@@ -1,5 +1,209 @@
use mysql_async::prelude::*;
use std::collections::HashSet;
use crate::models::connection::{ConnectionConfig, DatabaseType};
use crate::types::{ObjectInfo, TableInfo};
use super::mysql::{
get_conn_with_timeout, get_str_by_name, list_routine_objects, list_tables_show_with_status, quote_value,
table_infos_to_objects, MySqlPool, TableStatusMeta,
};
pub use super::mysql_compatible::{
get_columns_show_from as get_catalog_columns, list_catalog_indexes, list_catalogs,
list_databases_show_from as list_catalog_databases, list_indexes_with_ddl_fallback as list_indexes,
list_tables_show_from as list_catalog_tables, show_create_table_ddl_from as get_catalog_table_ddl,
};
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::StarRocks
|| (*db_type == DatabaseType::Mysql
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("starrocks")))
}
fn materialized_views_sql(database: &str) -> String {
format!(
"SELECT TABLE_NAME FROM information_schema.materialized_views WHERE TABLE_SCHEMA = {}",
quote_value(database)
)
}
pub(crate) fn materialized_view_definition_sql(database: &str, name: &str) -> String {
format!(
"SELECT MATERIALIZED_VIEW_DEFINITION \
FROM information_schema.materialized_views \
WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \
LIMIT 1",
quote_value(database),
quote_value(name)
)
}
async fn list_materialized_view_names(pool: &MySqlPool, database: &str) -> Result<HashSet<String>, String> {
let sql = materialized_views_sql(database);
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
let result = conn.query_iter(&sql).await.map_err(|error| error.to_string())?;
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|error| error.to_string())?;
Ok(rows
.iter()
.filter_map(|row| {
let name = get_str_by_name(row, "TABLE_NAME").trim().to_string();
(!name.is_empty()).then_some(name)
})
.collect())
}
fn merge_materialized_views(
tables: &mut Vec<TableInfo>,
materialized_view_names: Result<HashSet<String>, String>,
database: &str,
) {
let materialized_view_names = match materialized_view_names {
Ok(names) => names,
Err(error) => {
log::warn!("Skipping materialized view classification for StarRocks database `{database}`: {error}");
return;
}
};
let known_names: HashSet<String> = tables.iter().map(|table| table.name.clone()).collect();
for table in tables.iter_mut() {
if materialized_view_names.contains(&table.name) {
table.table_type = "MATERIALIZED_VIEW".to_string();
}
}
let mut sorted_names: Vec<&String> = materialized_view_names.iter().collect();
sorted_names.sort();
for name in sorted_names {
if !known_names.contains(name.as_str()) {
tables.push(TableInfo {
name: name.clone(),
table_type: "MATERIALIZED_VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
});
}
}
}
async fn list_tables_with_status(
pool: &MySqlPool,
database: &str,
) -> Result<(Vec<TableInfo>, std::collections::HashMap<String, TableStatusMeta>), String> {
let (tables, materialized_view_names) =
tokio::join!(list_tables_show_with_status(pool, database), list_materialized_view_names(pool, database));
let (mut tables, status) = tables?;
merge_materialized_views(&mut tables, materialized_view_names, database);
Ok((tables, status))
}
pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableInfo>, String> {
list_tables_with_status(pool, database).await.map(|(tables, _)| tables)
}
pub async fn list_table_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
let (tables, routines) =
tokio::join!(list_tables_with_status(pool, database), list_routine_objects(pool, database));
let (tables, status) = tables?;
let mut objects = table_infos_to_objects(tables, &status, database);
match routines {
Ok(routines) => objects.extend(routines),
Err(error) => log::warn!("Skipping routines for database `{database}` in object browser: {error}"),
}
Ok(objects)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_matches_starrocks_only() {
assert!(is_profile(&DatabaseType::StarRocks, None));
assert!(is_profile(&DatabaseType::Mysql, Some("STARROCKS")));
assert!(!is_profile(&DatabaseType::Mysql, Some("doris")));
assert!(!is_profile(&DatabaseType::Postgres, Some("starrocks")));
}
#[test]
fn materialized_views_are_classified_without_duplicates() {
let mut tables = vec![table("orders", "BASE TABLE"), table("orders_view", "VIEW"), table("orders_mv", "VIEW")];
merge_materialized_views(&mut tables, Ok(HashSet::from(["orders_mv".to_string()])), "analytics");
assert_eq!(
tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::<Vec<_>>(),
vec![("orders", "BASE TABLE"), ("orders_view", "VIEW"), ("orders_mv", "MATERIALIZED_VIEW")]
);
}
#[test]
fn missing_materialized_views_are_appended_deterministically() {
let mut tables = vec![table("orders", "BASE TABLE")];
let names = HashSet::from(["orders_mv".to_string(), "daily_orders_mv".to_string()]);
merge_materialized_views(&mut tables, Ok(names), "analytics");
assert_eq!(
tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::<Vec<_>>(),
vec![
("orders", "BASE TABLE"),
("daily_orders_mv", "MATERIALIZED_VIEW"),
("orders_mv", "MATERIALIZED_VIEW"),
]
);
}
#[test]
fn lookup_failure_preserves_base_table_types() {
let mut tables = vec![table("orders_mv", "VIEW")];
merge_materialized_views(&mut tables, Err("permission denied".to_string()), "analytics");
assert_eq!(tables[0].table_type, "VIEW");
}
#[test]
fn materialized_view_queries_escape_database_and_name() {
assert_eq!(
materialized_views_sql("tenant's analytics"),
"SELECT TABLE_NAME FROM information_schema.materialized_views WHERE TABLE_SCHEMA = 'tenant\\'s analytics'"
);
assert_eq!(
materialized_view_definition_sql("tenant's analytics", "weird'name"),
"SELECT MATERIALIZED_VIEW_DEFINITION FROM information_schema.materialized_views WHERE TABLE_SCHEMA = 'tenant\\'s analytics' AND TABLE_NAME = 'weird\\'name' LIMIT 1"
);
}
#[test]
fn object_conversion_preserves_materialized_view_type() {
let objects = table_infos_to_objects(
vec![table("orders", "BASE TABLE"), table("orders_view", "VIEW"), table("orders_mv", "MATERIALIZED_VIEW")],
&std::collections::HashMap::new(),
"analytics",
);
assert_eq!(
objects.iter().map(|object| (object.name.as_str(), object.object_type.as_str())).collect::<Vec<_>>(),
vec![("orders", "TABLE"), ("orders_view", "VIEW"), ("orders_mv", "MATERIALIZED_VIEW")]
);
}
fn table(name: &str, table_type: &str) -> TableInfo {
TableInfo {
name: name.to_string(),
table_type: table_type.to_string(),
comment: None,
parent_schema: None,
parent_name: None,
}
}
}
+54
View File
@@ -0,0 +1,54 @@
use crate::models::connection::{ConnectionConfig, DatabaseType};
pub const DRIVER_PROFILE: &str = "tdsql";
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::Mysql
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case(DRIVER_PROFILE))
}
pub(crate) fn preserves_proxy_directive_for_database_type(db_type: DatabaseType) -> bool {
db_type == DatabaseType::Mysql
}
pub(crate) fn proxy_directive_start(statement: &str, executable_start: usize) -> Option<usize> {
let prefix = statement.get(..executable_start)?.trim_end();
prefix.strip_suffix("/*proxy*/").map(|before| before.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_is_isolated_from_standard_mysql() {
assert!(is_profile(&DatabaseType::Mysql, Some("TDSQL")));
assert!(!is_profile(&DatabaseType::Mysql, None));
assert!(!is_profile(&DatabaseType::Postgres, Some("tdsql")));
}
#[test]
fn proxy_directive_is_available_only_to_mysql_parsing() {
assert!(preserves_proxy_directive_for_database_type(DatabaseType::Mysql));
assert!(!preserves_proxy_directive_for_database_type(DatabaseType::Postgres));
}
#[test]
fn proxy_directive_must_be_exact_and_adjacent_to_executable_sql() {
let exact = "/*proxy*/ \n\tSHOW PROXY STATUS";
let executable_start = exact.find("SHOW").unwrap();
assert_eq!(proxy_directive_start(exact, executable_start), Some(0));
let ordinary = "/* proxy */\nSHOW PROXY STATUS";
let executable_start = ordinary.find("SHOW").unwrap();
assert_eq!(proxy_directive_start(ordinary, executable_start), None);
let separated = "/*proxy*/\n/* audit */\nSHOW PROXY STATUS";
let executable_start = separated.find("SHOW").unwrap();
assert_eq!(proxy_directive_start(separated, executable_start), None);
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::models::connection::{ConnectionConfig, DatabaseType};
pub const DRIVER_PROFILE: &str = "tidb";
pub fn is_config(config: &ConnectionConfig) -> bool {
is_profile(&config.db_type, config.driver_profile.as_deref())
}
pub fn is_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
*db_type == DatabaseType::Mysql
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case(DRIVER_PROFILE))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_is_isolated_from_standard_mysql() {
assert!(is_profile(&DatabaseType::Mysql, Some("TiDB")));
assert!(!is_profile(&DatabaseType::Mysql, None));
assert!(!is_profile(&DatabaseType::Postgres, Some("tidb")));
}
}
+1 -1
View File
@@ -1558,7 +1558,7 @@ async fn configured_operation_budget_for_pool_key(state: &AppState, pool_key: &s
fn oceanbase_mysql_session_timeout_sql(config: Option<&ConnectionConfig>, timeout_secs: Option<u64>) -> Option<String> {
let config = config?;
let timeout_secs = timeout_secs.unwrap_or(config.query_timeout_secs);
crate::connection::oceanbase_mysql_query_timeout_sql(config, timeout_secs)
crate::db::oceanbase_mysql::query_timeout_sql(config, timeout_secs)
}
async fn apply_oceanbase_mysql_session_timeout(
+41 -62
View File
@@ -365,7 +365,7 @@ pub async fn list_doris_catalogs_core(state: &AppState, connection_id: &str) ->
let db_config = connection_config(state, connection_id).await;
let connections = state.connections.read().await;
if let Some(PoolKind::Mysql(p, _)) = connections.get(&pool_key) {
return if db_config.as_ref().is_some_and(is_starrocks_config) {
return if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_catalogs(p).await
} else {
db::doris::list_catalogs(p).await
@@ -391,7 +391,7 @@ pub async fn list_doris_catalog_databases_core(
let PoolKind::Mysql(p, _) = pool else {
return Ok(vec![]);
};
let databases = if db_config.as_ref().is_some_and(is_starrocks_config) {
let databases = if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_catalog_databases(p, catalog).await
} else {
db::doris::list_catalog_databases(p, catalog).await
@@ -435,7 +435,7 @@ pub async fn list_doris_catalog_tables_core(
let PoolKind::Mysql(p, _) = pool else {
return Ok(vec![]);
};
let tables = if db_config.as_ref().is_some_and(is_starrocks_config) {
let tables = if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_catalog_tables(p, catalog, database).await
} else {
db::doris::list_catalog_tables(p, catalog, database).await
@@ -458,7 +458,7 @@ pub async fn get_doris_catalog_columns_core(
let PoolKind::Mysql(p, _) = pool else {
return Ok(vec![]);
};
let columns = if db_config.as_ref().is_some_and(is_starrocks_config) {
let columns = if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::get_catalog_columns(p, catalog, database, table).await
} else {
db::doris::get_catalog_columns(p, catalog, database, table).await
@@ -481,7 +481,7 @@ pub async fn get_doris_catalog_table_ddl_core(
let PoolKind::Mysql(p, _) = pool else {
return Err("DDL not supported for this connection".to_string());
};
if db_config.as_ref().is_some_and(is_starrocks_config) {
if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::get_catalog_table_ddl(p, catalog, database, table).await
} else {
db::doris::get_catalog_table_ddl(p, catalog, database, table).await
@@ -503,7 +503,7 @@ pub async fn list_doris_catalog_indexes_core(
let PoolKind::Mysql(p, _) = pool else {
return Ok(vec![]);
};
if db_config.as_ref().is_some_and(is_starrocks_config) {
if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_catalog_indexes(p, catalog, database, table).await
} else {
db::doris::list_catalog_indexes(p, catalog, database, table).await
@@ -559,7 +559,7 @@ pub async fn resolve_external_doris_catalog(
return None;
}
let config = connection_config(state, connection_id).await?;
if is_doris_family_catalog_capable_config(&config) {
if db::mysql_compatible::supports_external_catalogs(&config) {
Some(catalog.to_string())
} else {
None
@@ -620,7 +620,7 @@ async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Ve
{
db::dolt::list_databases(p).await
}
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata) => {
db::mysql::list_databases_show(p)
.await
.map(|databases| filter_mysql_system_databases_for_config(databases, db_config.as_ref()))
@@ -644,7 +644,10 @@ async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Ve
async fn list_database_metadata_once(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, String> {
let config = connection_config(state, connection_id).await;
if config.as_ref().is_some_and(|config| db::dolt::is_config(config) || is_doris_family_config(config)) {
if config
.as_ref()
.is_some_and(|config| db::dolt::is_config(config) || db::mysql_compatible::uses_show_metadata(config))
{
return list_databases_once(state, connection_id).await;
}
let connections = state.connections.read().await;
@@ -1080,8 +1083,8 @@ async fn get_table_comment_core_for_session(
match pool {
PoolKind::Mysql(p, mode)
if *mode != MysqlMode::OceanBaseOracle
&& !db_config.as_ref().is_some_and(is_doris_family_config)
&& !db_config.as_ref().is_some_and(is_manticoresearch_config) =>
&& !db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata)
&& !db_config.as_ref().is_some_and(db::manticoresearch::is_config) =>
{
db::mysql::get_table_comment(p, database, table).await
}
@@ -2149,12 +2152,12 @@ async fn list_tables_once(
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_starrocks_config) => {
db::mysql::list_starrocks_tables(p, database)
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::starrocks::is_config) => {
db::starrocks::list_tables(p, database)
.await
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types, table_name_filter))
}
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata) => {
db::mysql::list_tables_show(p, database)
.await
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types, table_name_filter))
@@ -4542,32 +4545,34 @@ for line in sys.stdin:
#[test]
fn doris_family_catalog_capable_matches_doris_and_starrocks_only() {
// Doris and StarRocks expose multi-catalog federation.
assert!(super::is_doris_family_catalog_capable_config(&test_connection_config(DatabaseType::Doris)));
assert!(super::is_doris_family_catalog_capable_config(&test_connection_config(DatabaseType::StarRocks)));
assert!(db::mysql_compatible::supports_external_catalogs(&test_connection_config(DatabaseType::Doris)));
assert!(db::mysql_compatible::supports_external_catalogs(&test_connection_config(DatabaseType::StarRocks)));
// Driver profiles for Doris/SelectDB/StarRocks also qualify.
let mut doris = test_connection_config(DatabaseType::Mysql);
doris.driver_profile = Some("doris".to_string());
assert!(super::is_doris_family_catalog_capable_config(&doris));
assert!(db::mysql_compatible::supports_external_catalogs(&doris));
let mut selectdb = test_connection_config(DatabaseType::Mysql);
selectdb.driver_profile = Some("selectdb".to_string());
assert!(super::is_doris_family_catalog_capable_config(&selectdb));
assert!(db::mysql_compatible::supports_external_catalogs(&selectdb));
let mut starrocks = test_connection_config(DatabaseType::Mysql);
starrocks.driver_profile = Some("starrocks".to_string());
assert!(super::is_doris_family_catalog_capable_config(&starrocks));
assert!(db::mysql_compatible::supports_external_catalogs(&starrocks));
// ManticoreSearch shares the MySQL code path but has no catalog concept.
assert!(!super::is_doris_family_catalog_capable_config(&test_connection_config(DatabaseType::ManticoreSearch)));
assert!(!db::mysql_compatible::supports_external_catalogs(&test_connection_config(
DatabaseType::ManticoreSearch
)));
let mut manticore = test_connection_config(DatabaseType::Mysql);
manticore.driver_profile = Some("manticoresearch".to_string());
assert!(!super::is_doris_family_catalog_capable_config(&manticore));
assert!(!db::mysql_compatible::supports_external_catalogs(&manticore));
// Plain MySQL / Postgres are not catalog-capable.
assert!(!super::is_doris_family_catalog_capable_config(&test_connection_config(DatabaseType::Mysql)));
assert!(!super::is_doris_family_catalog_capable_config(&test_connection_config(DatabaseType::Postgres)));
assert!(!db::mysql_compatible::supports_external_catalogs(&test_connection_config(DatabaseType::Mysql)));
assert!(!db::mysql_compatible::supports_external_catalogs(&test_connection_config(DatabaseType::Postgres)));
}
}
@@ -4991,7 +4996,7 @@ async fn list_object_statistics_once(
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
PoolKind::Mysql(p, mode) => {
if *mode == MysqlMode::OceanBaseOracle || db_config.as_ref().is_some_and(is_manticoresearch_config) {
if *mode == MysqlMode::OceanBaseOracle || db_config.as_ref().is_some_and(db::manticoresearch::is_config) {
Ok(vec![])
} else {
db::mysql::list_object_statistics(p, database).await
@@ -5191,11 +5196,11 @@ async fn list_objects_once(
// Note: mysql and ob_oracle take different second args (database vs schema)
if *mode == MysqlMode::OceanBaseOracle {
db::ob_oracle::list_objects(p, schema).await.map(unpaged_object_list)
} else if db_config.as_ref().is_some_and(is_manticoresearch_config) {
} else if db_config.as_ref().is_some_and(db::manticoresearch::is_config) {
db::manticoresearch::list_objects(p, database).await.map(unpaged_object_list)
} else if db_config.as_ref().is_some_and(is_starrocks_config) {
db::mysql::list_starrocks_table_objects(p, database).await.map(unpaged_object_list)
} else if db_config.as_ref().is_some_and(is_doris_family_config) {
} else if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_table_objects(p, database).await.map(unpaged_object_list)
} else if db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata) {
db::mysql::list_table_objects_show(p, database).await.map(unpaged_object_list)
} else {
db::mysql::list_objects(p, database, object_types, mysql_limit, mysql_offset)
@@ -5834,11 +5839,11 @@ async fn get_columns_core_for_session_inner(
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_manticoresearch_config) => {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::manticoresearch::is_config) => {
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
db::manticoresearch::get_columns(p, metadata_database, table).await.map(deduplicate_column_infos)
}
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata) => {
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
// Doris/StarRocks previously went straight to `SHOW COLUMNS` for
// speed (see perf(doris) commit), but `SHOW COLUMNS` reports the
@@ -6035,14 +6040,14 @@ async fn list_indexes_core_for_session(
match pool {
PoolKind::Mysql(p, mode) => {
if db_config.as_ref().is_some_and(is_manticoresearch_config) {
if db_config.as_ref().is_some_and(db::manticoresearch::is_config) {
return db::manticoresearch::list_indexes(p, table).await;
}
if *mode == MysqlMode::OceanBaseOracle {
db::ob_oracle::list_indexes(p, schema, table).await
} else if db_config.as_ref().is_some_and(is_starrocks_config) {
} else if db_config.as_ref().is_some_and(db::starrocks::is_config) {
db::starrocks::list_indexes(p, mysql_table_metadata_catalog(database, schema), table).await
} else if db_config.as_ref().is_some_and(is_doris_config) {
} else if db_config.as_ref().is_some_and(db::doris::is_config) {
db::doris::list_indexes(p, mysql_table_metadata_catalog(database, schema), table).await
} else {
db::mysql::list_indexes(p, mysql_table_metadata_catalog(database, schema), table).await
@@ -6693,34 +6698,8 @@ fn agent_paging_likely_applied(enabled: bool, limit: Option<usize>, returned_len
enabled && limit.is_some_and(|limit| returned_len <= limit)
}
fn is_doris_family_config(config: &ConnectionConfig) -> bool {
matches!(config.db_type, DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::ManticoreSearch)
|| matches!(config.driver_profile.as_deref(), Some("doris" | "selectdb" | "starrocks" | "manticoresearch"))
}
fn is_doris_config(config: &ConnectionConfig) -> bool {
config.db_type == DatabaseType::Doris || matches!(config.driver_profile.as_deref(), Some("doris" | "selectdb"))
}
fn is_starrocks_config(config: &ConnectionConfig) -> bool {
config.db_type == DatabaseType::StarRocks || matches!(config.driver_profile.as_deref(), Some("starrocks"))
}
/// Doris-family engines that support multi-catalog federation (`SHOW CATALOGS`).
/// Manticore Search is excluded — it shares the MySQL code path but has no
/// catalog concept.
pub fn is_doris_family_catalog_capable_config(config: &ConnectionConfig) -> bool {
matches!(config.db_type, DatabaseType::Doris | DatabaseType::StarRocks)
|| matches!(config.driver_profile.as_deref(), Some("doris" | "selectdb" | "starrocks"))
}
fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
matches!(config.db_type, DatabaseType::ManticoreSearch)
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
}
fn mysql_show_metadata_database_for_config<'a>(config: Option<&ConnectionConfig>, database: &'a str) -> &'a str {
if config.is_some_and(is_manticoresearch_config) {
if config.is_some_and(db::manticoresearch::is_config) {
""
} else {
database
@@ -6731,7 +6710,7 @@ fn filter_mysql_system_databases_for_config(
databases: Vec<db::DatabaseInfo>,
config: Option<&ConnectionConfig>,
) -> Vec<db::DatabaseInfo> {
if !config.is_some_and(is_manticoresearch_config) {
if !config.is_some_and(db::manticoresearch::is_config) {
return databases;
}
@@ -7308,7 +7287,7 @@ async fn mysql_object_source(
// sync MVs. Fall back to the persistent definition exposed by
// information_schema.materialized_views. The fallback returns a single
// column (MATERIALIZED_VIEW_DEFINITION) so the column index is always 0.
let fallback_sql = db::mysql::mysql_materialized_view_definition_sql(database, name);
let fallback_sql = db::starrocks::materialized_view_definition_sql(database, name);
read_mysql_object_source_row(&mut conn, &fallback_sql, 0).await.map_err(|fallback_err| {
format!(
"SHOW CREATE MATERIALIZED VIEW failed ({primary_err}); \
+11 -29
View File
@@ -7,7 +7,7 @@ pub(in crate::schema) async fn list_databases(
config: Option<&ConnectionConfig>,
) -> Result<Vec<db::DatabaseInfo>, String> {
match pool {
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => db::mysql::list_databases_show(p)
PoolKind::Mysql(p, _) if config.is_some_and(db::mysql_compatible::uses_show_metadata) => db::mysql::list_databases_show(p)
.await
.map(|databases| filter_mysql_system_databases_for_config(databases, config)),
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => db::ob_oracle::list_databases(p).await,
@@ -36,7 +36,7 @@ pub(in crate::schema) async fn list_tables(
schema: &str,
) -> Result<Vec<db::TableInfo>, String> {
match pool {
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::mysql_compatible::uses_show_metadata) => {
db::mysql::list_tables_show(p, database).await
}
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
@@ -85,10 +85,10 @@ pub(in crate::schema) async fn list_objects(
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
db::ob_oracle::list_objects(p, schema).await.map(Some)
}
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::manticoresearch::is_config) => {
db::manticoresearch::list_objects(p, database).await.map(Some)
}
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::mysql_compatible::uses_show_metadata) => {
db::mysql::list_table_objects_show(p, database).await.map(Some)
}
PoolKind::Mysql(p, _) => {
@@ -137,11 +137,11 @@ pub(in crate::schema) async fn get_columns(
table: &str,
) -> Result<Vec<db::ColumnInfo>, String> {
match pool {
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::manticoresearch::is_config) => {
let metadata_database = mysql_show_metadata_database_for_config(config, database);
db::manticoresearch::get_columns(p, metadata_database, table).await
}
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::mysql_compatible::uses_show_metadata) => {
let metadata_database = mysql_show_metadata_database_for_config(config, database);
db::mysql::get_columns(p, metadata_database, table).await
}
@@ -176,13 +176,13 @@ pub(in crate::schema) async fn list_indexes(
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
db::ob_oracle::list_indexes(p, schema, table).await
}
PoolKind::Mysql(p, _) if config.is_some_and(is_starrocks_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::starrocks::is_config) => {
db::starrocks::list_indexes(p, database, table).await
}
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::doris::is_config) => {
db::doris::list_indexes(p, database, table).await
}
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
PoolKind::Mysql(p, _) if config.is_some_and(db::manticoresearch::is_config) => {
db::mysql_compatible::list_indexes_with_ddl_fallback(p, database, table).await
}
PoolKind::Mysql(p, _) => db::mysql::list_indexes(p, schema, table).await,
@@ -343,26 +343,8 @@ fn is_cloudberry_config(config: &ConnectionConfig) -> bool {
matches!(config.driver_profile.as_deref(), Some("cloudberry"))
}
fn is_doris_family_config(config: &ConnectionConfig) -> bool {
matches!(config.db_type, DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::ManticoreSearch)
|| matches!(config.driver_profile.as_deref(), Some("doris" | "selectdb" | "starrocks" | "manticoresearch"))
}
fn is_doris_config(config: &ConnectionConfig) -> bool {
config.db_type == DatabaseType::Doris || matches!(config.driver_profile.as_deref(), Some("doris" | "selectdb"))
}
fn is_starrocks_config(config: &ConnectionConfig) -> bool {
config.db_type == DatabaseType::StarRocks || matches!(config.driver_profile.as_deref(), Some("starrocks"))
}
fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
matches!(config.db_type, DatabaseType::ManticoreSearch)
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
}
fn mysql_show_metadata_database_for_config<'a>(config: Option<&ConnectionConfig>, database: &'a str) -> &'a str {
if config.is_some_and(is_manticoresearch_config) {
if config.is_some_and(db::manticoresearch::is_config) {
""
} else {
database
@@ -373,7 +355,7 @@ fn filter_mysql_system_databases_for_config(
databases: Vec<db::DatabaseInfo>,
config: Option<&ConnectionConfig>,
) -> Vec<db::DatabaseInfo> {
if !config.is_some_and(is_manticoresearch_config) {
if !config.is_some_and(db::manticoresearch::is_config) {
return databases;
}
+8 -13
View File
@@ -143,7 +143,12 @@ impl Default for SqlDialectProfile {
impl SqlDialectProfile {
fn for_database_type(db_type: DatabaseType) -> Self {
if matches!(db_type, DatabaseType::Mysql) {
return Self::mysql();
return Self {
preserves_tdsql_proxy_directive: crate::db::tdsql_mysql::preserves_proxy_directive_for_database_type(
db_type,
),
..Self::mysql_compatible()
};
}
if matches!(db_type, DatabaseType::Gaussdb) {
@@ -178,10 +183,6 @@ impl SqlDialectProfile {
}
}
fn mysql() -> Self {
Self { preserves_tdsql_proxy_directive: true, ..Self::mysql_compatible() }
}
fn oracle_like() -> Self {
Self { supports_oracle_plsql_blocks: true, supports_slash_line_block_delimiter: true, ..Self::default() }
}
@@ -2530,18 +2531,13 @@ fn executable_sql_bounds(statement: &str, options: SqlParsingOptions) -> Option<
}
let executable_start = trimmed.len() - executable.len();
let start = if options.profile.preserves_tdsql_proxy_directive {
tdsql_proxy_directive_start(trimmed, executable_start).unwrap_or(executable_start)
crate::db::tdsql_mysql::proxy_directive_start(trimmed, executable_start).unwrap_or(executable_start)
} else {
executable_start
};
Some((start, trimmed_end))
}
fn tdsql_proxy_directive_start(statement: &str, executable_start: usize) -> Option<usize> {
let prefix = statement.get(..executable_start)?.trim_end();
prefix.strip_suffix("/*proxy*/").map(|before| before.len())
}
fn has_executable_sql_with_options(statement: &str, options: SqlParsingOptions) -> bool {
let chars = statement.chars().collect::<Vec<_>>();
let mut in_line_comment = false;
@@ -2700,7 +2696,7 @@ mod tests {
#[test]
fn dash_dash_requires_space_only_for_mysql_compatible_profiles() {
let mysql_profile = SqlDialectProfile::mysql();
let mysql_profile = SqlDialectProfile::for_database_type(DatabaseType::Mysql);
assert!(dash_dash_starts_line_comment(mysql_profile, Some(' ')));
assert!(dash_dash_starts_line_comment(mysql_profile, Some('\n')));
assert!(dash_dash_starts_line_comment(mysql_profile, Some('\u{1}')));
@@ -3391,7 +3387,6 @@ SELECT 2;";
assert!(!default.preserves_tdsql_proxy_directive);
let mysql = SqlDialectProfile::for_database_type(DatabaseType::Mysql);
assert_eq!(mysql, SqlDialectProfile::mysql());
assert!(mysql.supports_hash_line_comments);
assert!(mysql.supports_mysql_routine_blocks);
assert!(mysql.preserves_tdsql_proxy_directive);
+4 -9
View File
@@ -265,7 +265,7 @@ pub fn resolve_external_transfer_catalog_for_config<'a>(
config: &crate::models::connection::ConnectionConfig,
) -> Option<&'a str> {
let catalog = normalize_external_catalog_name(catalog)?;
if crate::schema::is_doris_family_catalog_capable_config(config) {
if db::mysql_compatible::supports_external_catalogs(config) {
Some(catalog)
} else {
None
@@ -3320,11 +3320,6 @@ fn max_transfer_write_rows(db_type: &DatabaseType, mode: &TransferMode) -> usize
}
}
fn is_oceanbase_mysql_profile(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool {
matches!(db_type, DatabaseType::Mysql)
&& driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("oceanbase"))
}
fn contains_oceanbase_mysql_table_options(sql: &str) -> bool {
let (sql_without_literals_or_comments, _) = protect_sql_literals(sql, true);
OCEANBASE_MYSQL_TABLE_OPTION_RE.is_match(&sql_without_literals_or_comments)
@@ -3383,8 +3378,8 @@ fn can_reuse_source_table_ddl(
target_driver_profile: Option<&str>,
preserves_target_table_name: bool,
) -> bool {
if is_oceanbase_mysql_profile(source_db_type, source_driver_profile)
&& !is_oceanbase_mysql_profile(target_db_type, target_driver_profile)
if db::oceanbase_mysql::is_profile(source_db_type, source_driver_profile)
&& !db::oceanbase_mysql::is_profile(target_db_type, target_driver_profile)
{
return false;
}
@@ -6593,7 +6588,7 @@ where
}
};
if contains_oceanbase_mysql_table_options(&source_ddl)
&& !is_oceanbase_mysql_profile(target_db_type, target_driver_profile.as_deref())
&& !db::oceanbase_mysql::is_profile(target_db_type, target_driver_profile.as_deref())
{
generate_create_table_ddl(
&columns,