diff --git a/crates/dbx-core/src/schema/mod.rs b/crates/dbx-core/src/schema/mod.rs index a3e07a5659..6812543ccc 100644 --- a/crates/dbx-core/src/schema/mod.rs +++ b/crates/dbx-core/src/schema/mod.rs @@ -12975,6 +12975,91 @@ mod ddl_tests { ); } + /// Doris answers `SHOW CREATE TABLE ` 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 { + 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 { // 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