fix(schema): fall back to SHOW CREATE MATERIALIZED VIEW on Doris

This commit is contained in:
zipg authored and GitHub committed 2026-09-25 10:32:54 +08:00
1 parent ade47c9f3e
commit eac2f14bfc
1 file changed
+123
+123
View File
@@ -12975,6 +12975,91 @@ mod ddl_tests {
);
}
/// Doris answers `SHOW CREATE TABLE <mv>` with a pointer to
/// `SHOW CREATE MATERIALIZED VIEW`; the DDL request must follow that hint.
#[tokio::test]
async fn mysql_ddl_falls_back_to_materialized_view_after_a_doris_refusal() {
let mut executor = FakeMysqlDdlExecutor {
outcomes: [
Err(mysql_server_error(
1105,
"errCode = 2, detailMessage = not support async materialized view, please use `show create materialized view`",
)),
Ok("CREATE MATERIALIZED VIEW `mv_daily` (id)".to_string()),
]
.into(),
executed: Vec::new(),
};
let ddl = mysql_ddl_with_executor(&mut executor, "dbx_test", "mv_daily").await.unwrap();
assert_eq!(ddl, "CREATE MATERIALIZED VIEW `mv_daily` (id);");
assert_eq!(
executor.executed,
["SHOW CREATE TABLE `dbx_test`.`mv_daily`", "SHOW CREATE MATERIALIZED VIEW `dbx_test`.`mv_daily`"]
);
}
/// Without a database the materialized view statement stays unqualified,
/// matching the qualifier used for the table probe.
#[tokio::test]
async fn mysql_ddl_falls_back_to_materialized_view_without_a_database() {
let mut executor = FakeMysqlDdlExecutor {
outcomes: [
Err(mysql_server_error(
1105,
"not support async materialized view, please use `show create materialized view`",
)),
Ok("CREATE MATERIALIZED VIEW `mv_daily` (id)".to_string()),
]
.into(),
executed: Vec::new(),
};
let ddl = mysql_ddl_with_executor(&mut executor, "", "mv_daily").await.unwrap();
assert_eq!(ddl, "CREATE MATERIALIZED VIEW `mv_daily` (id);");
assert_eq!(executor.executed, ["SHOW CREATE TABLE `mv_daily`", "SHOW CREATE MATERIALIZED VIEW `mv_daily`"]);
}
/// Engines without `SHOW CREATE MATERIALIZED VIEW` answer the retry with a
/// syntax error; the original table error is what the user should still see.
#[tokio::test]
async fn mysql_ddl_preserves_the_table_error_when_the_materialized_view_probe_fails() {
let refusal = mysql_server_error(
1105,
"errCode = 2, detailMessage = not support async materialized view, please use `show create materialized view`",
);
let expected = refusal.to_string();
let mut executor = FakeMysqlDdlExecutor {
outcomes: [Err(refusal), Err(mysql_server_error(1064, "You have an error in your SQL syntax"))].into(),
executed: Vec::new(),
};
let error = mysql_ddl_with_executor(&mut executor, "app", "missing").await.unwrap_err();
assert_eq!(error, expected);
assert_eq!(
executor.executed,
["SHOW CREATE TABLE `app`.`missing`", "SHOW CREATE MATERIALIZED VIEW `app`.`missing`"]
);
}
/// Unrelated failures must not trigger an extra probe, even when the server
/// reports them with the same generic error code Doris uses.
#[tokio::test]
async fn mysql_ddl_does_not_probe_materialized_views_for_unrelated_failures() {
let mut executor = FakeMysqlDdlExecutor {
outcomes: [Err(mysql_server_error(1105, "errCode = 2, detailMessage = table is broken"))].into(),
executed: Vec::new(),
};
let error = mysql_ddl_with_executor(&mut executor, "app", "missing").await.unwrap_err();
assert_eq!(error, "Server error: `ERROR 1105 (HY000): errCode = 2, detailMessage = table is broken'");
assert_eq!(executor.executed, ["SHOW CREATE TABLE `app`.`missing`"]);
}
#[tokio::test]
async fn mysql_ddl_preserves_qualified_error_when_fallback_fails() {
let first_error = mysql_server_error(1146, "qualified table doesn't exist");
@@ -13048,6 +13133,21 @@ impl MysqlDdlQueryError {
fn is_no_such_table(&self) -> bool {
matches!(self, Self::Query(mysql_async::Error::Server(error)) if error.code == 1146)
}
/// Doris exposes asynchronous materialized views as base tables and then
/// refuses `SHOW CREATE TABLE` / `SHOW CREATE VIEW` on them with a pointer
/// to another statement:
/// `ERROR 1105 (HY000): errCode = 2, detailMessage = not support async
/// materialized view, please use `show create materialized view``.
/// Recognize that refusal so the DDL request can be retried with the
/// statement the server asked for instead of failing outright.
fn is_materialized_view_ddl_refusal(&self) -> bool {
matches!(
self,
Self::Query(mysql_async::Error::Server(error))
if error.message.to_ascii_lowercase().contains("materialized view")
)
}
}
impl std::fmt::Display for MysqlDdlQueryError {
@@ -13095,6 +13195,10 @@ async fn mysql_ddl_with_executor(
Ok(ddl) => return Ok(normalize_mysql_display_ddl(ddl)),
Err(error) => error,
};
if qualified_error.is_materialized_view_ddl_refusal() {
let name = mysql_qualified_name(database, table);
return mysql_materialized_view_ddl(executor, &name, qualified_error).await;
}
if database.trim().is_empty() || !qualified_error.is_no_such_table() {
return Err(qualified_error.to_string());
}
@@ -13108,6 +13212,25 @@ async fn mysql_ddl_with_executor(
}
}
/// Reads the definition of a materialized view that the server refused to
/// describe as a table.
///
/// The fallback only replaces the original error when the statement actually
/// returns a definition: engines without `SHOW CREATE MATERIALIZED VIEW`
/// (plain MySQL, MariaDB) answer with a syntax error, so the caller still sees
/// the error it would have seen before this fallback existed.
async fn mysql_materialized_view_ddl(
executor: &mut impl MysqlDdlQueryExecutor,
qualified_name: &str,
original_error: MysqlDdlQueryError,
) -> Result<String, String> {
let sql = format!("SHOW CREATE MATERIALIZED VIEW {qualified_name}");
match executor.execute(&sql).await {
Ok(ddl) => Ok(normalize_mysql_display_ddl(ddl)),
Err(_) => Err(original_error.to_string()),
}
}
pub async fn mysql_ddl(pool: &db::mysql::MySqlPool, database: &str, table: &str) -> Result<String, String> {
// Use the health-checked getter so a stale pooled connection (server closed
// it after an idle timeout, NAT/firewall dropped the TCP state, etc.) is