fix(doris): qualify external catalog tables

This commit is contained in:
jischeng authored and GitHub committed 2026-07-13 23:56:53 +08:00
1 parent c9c2fddfe2
commit a9ca66a829
21 files changed
+222 -44

No files matched your search

+24 -8
View File
@@ -72,25 +72,41 @@ pub fn qualified_table_name(database_type: Option<DatabaseType>, schema: Option<
}
/// Like `qualified_table_name`, but prefixes a Doris/StarRocks external
/// catalog (`<catalog>.<schema>.<table>`) when `catalog` is present, non-empty,
/// and not the engine's `internal` catalog. The `internal` guard is defensive —
/// built-in-catalog tables never carry a catalog in the first place (the
/// sidebar routes them through the standard path), so this only ever prefixes
/// genuine external catalogs. Other engines ignore `catalog` (they have no
/// 3-part catalog naming).
/// catalog (`<catalog>.<database>.<table>`) when `catalog` is present,
/// non-empty, and not the engine's `internal` catalog. The middle segment is
/// the database — Doris/StarRocks have no separate schema concept, so
/// `schema` is only used when a caller passes it that way; otherwise `database`
/// fills the middle slot. When neither is set the name degrades to the 2-part
/// `<catalog>.<table>` form. The `internal` guard is defensive — built-in
/// catalog tables never carry a catalog in the first place (the sidebar routes
/// them through the standard path), so this only ever prefixes genuine
/// external catalogs. Other engines ignore `catalog` (they have no 3-part
/// catalog naming).
pub fn qualified_table_name_with_catalog(
database_type: Option<DatabaseType>,
catalog: Option<&str>,
schema: Option<&str>,
database: Option<&str>,
table_name: &str,
) -> String {
let table = qualified_table_name(database_type, schema, table_name);
let catalog = catalog.map(str::trim).filter(|catalog| !catalog.is_empty() && *catalog != "internal");
match (catalog, database_type) {
(Some(catalog), Some(DatabaseType::Doris | DatabaseType::StarRocks)) => {
let middle = schema
.map(str::trim)
.filter(|schema| !schema.is_empty())
.or_else(|| database.map(str::trim).filter(|database| !database.is_empty()));
let table = match middle {
Some(middle) => format!(
"{}.{}",
quote_table_identifier(database_type, middle),
quote_table_identifier(database_type, table_name)
),
None => quote_table_identifier(database_type, table_name),
};
format!("{}.{}", quote_table_identifier(database_type, catalog), table)
}
_ => table,
_ => qualified_table_name(database_type, schema, table_name),
}
}
@@ -33,6 +33,7 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
database_type,
options.catalog.as_deref(),
options.schema.as_deref(),
options.database.as_deref(),
&options.table_name,
)
};
@@ -458,13 +459,19 @@ pub(super) fn build_questdb_table_select_sql(
mod tests {
use super::*;
fn opts(database_type: DatabaseType, catalog: Option<&str>, table: &str) -> TableDataSelectSqlOptions {
fn opts(
database_type: DatabaseType,
catalog: Option<&str>,
database: Option<&str>,
table: &str,
) -> TableDataSelectSqlOptions {
TableDataSelectSqlOptions {
database_type: Some(database_type),
identifier_quote: None,
schema: None,
table_name: table.to_string(),
catalog: catalog.map(|c| c.to_string()),
database: database.map(|d| d.to_string()),
table_type: None,
primary_keys: Vec::new(),
columns: Vec::new(),
@@ -479,32 +486,42 @@ mod tests {
#[test]
fn doris_external_catalog_prefixes_from_clause() {
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), "orders"));
assert!(sql.contains("FROM `iceberg_catalog`.`orders`"), "sql was: {sql}");
let sql =
build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), Some("sales"), "orders"));
assert!(sql.contains("FROM `iceberg_catalog`.`sales`.`orders`"), "sql was: {sql}");
}
#[test]
fn starrocks_external_catalog_prefixes_from_clause() {
let sql = build_table_data_select_sql(opts(DatabaseType::StarRocks, Some("hive_catalog"), "orders"));
assert!(sql.contains("FROM `hive_catalog`.`orders`"), "sql was: {sql}");
let sql =
build_table_data_select_sql(opts(DatabaseType::StarRocks, Some("hive_catalog"), Some("sales"), "orders"));
assert!(sql.contains("FROM `hive_catalog`.`sales`.`orders`"), "sql was: {sql}");
}
#[test]
fn doris_external_catalog_without_database_degrades_to_two_part() {
// When neither schema nor database is provided the name degrades to the
// 2-part `catalog.table` form.
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), None, "orders"));
assert!(sql.contains("FROM `iceberg_catalog`.`orders`"), "sql was: {sql}");
}
#[test]
fn doris_internal_catalog_is_not_prefixed() {
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("internal"), "orders"));
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("internal"), None, "orders"));
assert!(!sql.contains("internal"), "sql was: {sql}");
assert!(sql.contains("FROM `orders`"), "sql was: {sql}");
}
#[test]
fn doris_empty_catalog_is_not_prefixed() {
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some(" "), "orders"));
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some(" "), None, "orders"));
assert!(sql.contains("FROM `orders`"), "sql was: {sql}");
}
#[test]
fn doris_no_catalog_is_not_prefixed() {
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, None, "orders"));
let sql = build_table_data_select_sql(opts(DatabaseType::Doris, None, None, "orders"));
assert!(sql.contains("FROM `orders`"), "sql was: {sql}");
}
@@ -512,7 +529,8 @@ mod tests {
fn external_catalog_is_ignored_for_non_doris_engines() {
// Postgres does not support the 3-part catalog naming; the catalog
// must be ignored to avoid emitting an invalid qualified name.
let sql = build_table_data_select_sql(opts(DatabaseType::Postgres, Some("iceberg_catalog"), "orders"));
let sql =
build_table_data_select_sql(opts(DatabaseType::Postgres, Some("iceberg_catalog"), Some("sales"), "orders"));
assert!(!sql.contains("iceberg_catalog"), "sql was: {sql}");
assert!(sql.contains("orders"), "sql was: {sql}");
}
+6 -1
View File
@@ -28,9 +28,14 @@ pub struct TableDataSelectSqlOptions {
pub table_name: String,
/// Doris / StarRocks multi-catalog: when set to a non-`internal` catalog,
/// the FROM clause is prefixed with the catalog
/// (`<catalog>.<schema>.<table>`).
/// (`<catalog>.<database>.<table>`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
/// Doris / StarRocks multi-catalog: the database under the external
/// catalog, used as the middle segment of the 3-part qualified name when
/// `schema` is absent (Doris/StarRocks have no separate schema concept).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_type: Option<String>,
#[serde(default)]