refactor(plan): converge estimated plan capability and strategy

This commit is contained in:
jearhe
2026-09-24 17:21:38 +08:00
committed by GitHub
parent c5eb0a7a4a
commit 78e717bea6
2 changed files with 270 additions and 135 deletions
+84 -57
View File
@@ -25,8 +25,8 @@ use crate::connection::AppState;
use crate::db::QueryResult;
use crate::models::connection::{ConnectionConfig, DatabaseType};
use crate::query_execution_sql::{
build_explain_sql, estimated_plan_format, supports_explain_plan, EstimatedPlanFormat, ExplainFormat,
ExplainSqlOptions,
build_explain_sql, estimated_plan_strategy, supports_explain_plan, EstimatedPlanAcquisition, EstimatedPlanFormat,
EstimatedPlanStrategy, ExplainFormat, ExplainSqlOptions,
};
use super::{execute_sql_statement_with_options_typed, QueryExecutionMode, QueryExecutionOptions};
@@ -166,33 +166,35 @@ pub async fn explain_estimated_plan(state: &AppState, request: PluginPlanRequest
let config = connection_config(state, &request.connection_id).await?;
require_open_connection(state, &request.connection_id).await?;
let database_type = explain_database_type(&config);
if !supports_explain_plan(Some(database_type)) {
return Err(unsupported_dialect_message(database_type));
}
let strategy =
estimated_plan_strategy(Some(database_type)).ok_or_else(|| unsupported_dialect_message(database_type))?;
let timeout_secs = plugin_plan_timeout_secs(request.timeout_ms, &config);
let (plan_text, rows_truncated) = if uses_driver_native_plan(database_type) {
// Dameng and Oracle hand back a native plan listing from their driver;
// Oracle's `EXPLAIN PLAN FOR` alone only fills `PLAN_TABLE`. This reuses
// the core the AI and command explain paths already use, pinned to
// `mode = "explain"` so Dameng autotrace stays unreachable.
let text = get_agent_explain_info_core(
state,
&request.connection_id,
request.database.as_deref(),
request.schema.as_deref(),
&request.sql,
Some(AGENT_EXPLAIN_MODE),
Some(timeout_secs),
)
.await?;
(text, false)
} else {
native_estimated_plan(state, &request, database_type, timeout_secs).await?
let (plan_text, rows_truncated) = match strategy.acquisition() {
EstimatedPlanAcquisition::DriverNative => {
// Dameng and Oracle hand back a native plan listing from their driver;
// Oracle's `EXPLAIN PLAN FOR` alone only fills `PLAN_TABLE`. This reuses
// the core the AI and command explain paths already use, pinned to
// `mode = "explain"` so Dameng autotrace stays unreachable.
let text = get_agent_explain_info_core(
state,
&request.connection_id,
request.database.as_deref(),
request.schema.as_deref(),
&request.sql,
Some(AGENT_EXPLAIN_MODE),
Some(timeout_secs),
)
.await?;
(text, false)
}
EstimatedPlanAcquisition::GeneratedSql | EstimatedPlanAcquisition::SqlServerShowPlanSession => {
native_estimated_plan(state, &request, database_type, strategy, timeout_secs).await?
}
};
let plan_text = non_empty_plan_text(plan_text)?;
let mut payload = finalize_plan_payload(database_type, plan_text)?;
let mut payload = finalize_plan_payload(strategy.format(), plan_text)?;
if rows_truncated {
payload.truncated = true;
payload.warnings.push(WARNING_PLAN_ROWS_TRUNCATED.to_string());
@@ -208,16 +210,11 @@ pub async fn explain_estimated_plan(state: &AppState, request: PluginPlanRequest
})
}
/// Dialects whose plan comes from the driver/oracle-side native plan listing
/// instead of a generated `EXPLAIN` statement.
fn uses_driver_native_plan(database_type: DatabaseType) -> bool {
matches!(database_type, DatabaseType::Dameng | DatabaseType::Oracle)
}
async fn native_estimated_plan(
state: &AppState,
request: &PluginPlanRequest,
database_type: DatabaseType,
strategy: EstimatedPlanStrategy,
timeout_secs: u64,
) -> Result<(String, bool), String> {
let built = build_explain_sql(ExplainSqlOptions {
@@ -235,8 +232,14 @@ async fn native_estimated_plan(
return Err("The host did not build an EXPLAIN statement".to_string());
};
if database_type == DatabaseType::SqlServer {
return sqlserver_estimated_plan(state, request, timeout_secs).await;
match strategy.acquisition() {
EstimatedPlanAcquisition::GeneratedSql => {}
EstimatedPlanAcquisition::SqlServerShowPlanSession => {
return sqlserver_estimated_plan(state, request, timeout_secs).await;
}
EstimatedPlanAcquisition::DriverNative => {
return Err("Driver-native plans must use the shared driver explain path".to_string());
}
}
let database = request.database.as_deref().unwrap_or_default();
@@ -364,8 +367,8 @@ fn non_empty_plan_text(text: String) -> Result<String, String> {
/// Decodes the acquired text into the shape advertised by `format`, downgrades
/// an unparseable JSON answer to text instead of lying about it, and enforces
/// the payload cap.
fn finalize_plan_payload(database_type: DatabaseType, text: String) -> Result<PlanPayload, String> {
let mut format = estimated_plan_format(Some(database_type));
fn finalize_plan_payload(plan_format: EstimatedPlanFormat, text: String) -> Result<PlanPayload, String> {
let mut format = plan_format;
let mut warnings = Vec::new();
let mut raw_plan = Value::String(text);
@@ -657,14 +660,14 @@ mod tests {
#[test]
fn parses_json_plans_and_downgrades_other_payloads_to_text() {
let payload =
finalize_plan_payload(DatabaseType::Postgres, r#"[{"Plan": {"Node Type": "Seq Scan"}}]"#.to_string())
finalize_plan_payload(EstimatedPlanFormat::Json, r#"[{"Plan": {"Node Type": "Seq Scan"}}]"#.to_string())
.unwrap();
assert_eq!(payload.format, EstimatedPlanFormat::Json);
assert_eq!(payload.raw_plan[0]["Plan"]["Node Type"], Value::String("Seq Scan".to_string()));
assert!(!payload.truncated);
assert!(payload.warnings.is_empty());
let payload = finalize_plan_payload(DatabaseType::Postgres, "Seq Scan on users".to_string()).unwrap();
let payload = finalize_plan_payload(EstimatedPlanFormat::Json, "Seq Scan on users".to_string()).unwrap();
assert_eq!(payload.format, EstimatedPlanFormat::Text);
assert_eq!(payload.raw_plan, Value::String("Seq Scan on users".to_string()));
assert_eq!(payload.warnings, vec![WARNING_PLAN_NOT_JSON.to_string()]);
@@ -672,12 +675,12 @@ mod tests {
#[test]
fn keeps_text_and_xml_plans_as_strings() {
let text = finalize_plan_payload(DatabaseType::Oracle, "1 #NSET2: [0, 1, 0]".to_string()).unwrap();
let text = finalize_plan_payload(EstimatedPlanFormat::Text, "1 #NSET2: [0, 1, 0]".to_string()).unwrap();
assert_eq!(text.format, EstimatedPlanFormat::Text);
assert_eq!(text.raw_plan, Value::String("1 #NSET2: [0, 1, 0]".to_string()));
let xml =
finalize_plan_payload(DatabaseType::SqlServer, "<ShowPlanXML><BatchSequence/></ShowPlanXML>".to_string())
finalize_plan_payload(EstimatedPlanFormat::Xml, "<ShowPlanXML><BatchSequence/></ShowPlanXML>".to_string())
.unwrap();
assert_eq!(xml.format, EstimatedPlanFormat::Xml);
assert_eq!(xml.raw_plan, Value::String("<ShowPlanXML><BatchSequence/></ShowPlanXML>".to_string()));
@@ -687,7 +690,7 @@ mod tests {
#[test]
fn truncates_oversized_text_plans_and_rejects_oversized_json_plans() {
let oversized = "x".repeat(MAX_PLUGIN_PLAN_BYTES + 1_024);
let payload = finalize_plan_payload(DatabaseType::Doris, oversized.clone()).unwrap();
let payload = finalize_plan_payload(EstimatedPlanFormat::Text, oversized.clone()).unwrap();
assert!(payload.truncated);
assert_eq!(payload.warnings, vec![WARNING_PLAN_TRUNCATED.to_string()]);
let Value::String(text) = &payload.raw_plan else {
@@ -698,7 +701,7 @@ mod tests {
// A JSON document must not be handed over half-parsed.
let oversized_json = format!(r#"[{{"Plan": "{}"}}]"#, "y".repeat(MAX_PLUGIN_PLAN_BYTES + 1_024));
let error = finalize_plan_payload(DatabaseType::Postgres, oversized_json).unwrap_err();
let error = finalize_plan_payload(EstimatedPlanFormat::Json, oversized_json).unwrap_err();
assert!(error.contains("host limit"), "{error}");
}
@@ -758,15 +761,23 @@ mod tests {
}
#[test]
fn plans_native_dialects_through_the_driver_and_the_rest_through_explain_sql() {
assert!(uses_driver_native_plan(DatabaseType::Dameng));
assert!(uses_driver_native_plan(DatabaseType::Oracle));
fn plugin_acquisition_paths_match_estimated_plan_strategy() {
use EstimatedPlanAcquisition::{DriverNative, GeneratedSql, SqlServerShowPlanSession};
for database_type in
[DatabaseType::Postgres, DatabaseType::Mysql, DatabaseType::SqlServer, DatabaseType::OceanbaseOracle]
{
assert!(!uses_driver_native_plan(database_type));
assert!(supports_explain_plan(Some(database_type)));
for (database_type, expected_acquisition) in [
(DatabaseType::Mysql, GeneratedSql),
(DatabaseType::Doris, GeneratedSql),
(DatabaseType::Postgres, GeneratedSql),
(DatabaseType::Questdb, GeneratedSql),
(DatabaseType::Dameng, DriverNative),
(DatabaseType::Oracle, DriverNative),
(DatabaseType::OceanbaseOracle, GeneratedSql),
(DatabaseType::SqlServer, SqlServerShowPlanSession),
] {
let strategy = estimated_plan_strategy(Some(database_type))
.unwrap_or_else(|| panic!("{database_type:?} must have an acquisition strategy"));
assert!(supports_explain_plan(Some(database_type)), "{database_type:?}");
assert_eq!(strategy.acquisition(), expected_acquisition, "{database_type:?}");
}
}
@@ -853,15 +864,31 @@ mod tests {
}
#[tokio::test]
async fn capabilities_report_doris_for_the_mysql_doris_profile() {
let mut doris = config(DatabaseType::Mysql, 30);
doris.driver_profile = Some("doris".to_string());
let (state, _dir) = saved_connection_state(std::slice::from_ref(&doris)).await;
open_connection(&state, &doris.id).await;
async fn capabilities_resolve_mysql_profiles_before_selecting_plan_strategy() {
let cases = [
("mysql-doris", Some("doris"), "doris"),
("mysql-selectdb", Some("selectdb"), "doris"),
("mysql-native", None, "mysql"),
("mysql-profile", Some("mysql"), "mysql"),
("mysql-starrocks", Some("starrocks"), "mysql"),
];
let configs = cases
.iter()
.map(|(id, profile, _)| {
let mut config = config_for(DatabaseType::Mysql, id, 30);
config.driver_profile = profile.map(|profile| profile.to_string());
config
})
.collect::<Vec<_>>();
let (state, _dir) = saved_connection_state(&configs).await;
let capabilities = plugin_plan_capabilities(&state, &doris.id).await.unwrap();
let capabilities = serde_json::to_value(capabilities).unwrap();
assert_eq!(capabilities["dbType"], "doris");
assert!(capabilities["supports"]["estimatedPlan"].as_bool().unwrap_or_default());
for ((_, _, expected_db_type), config) in cases.iter().zip(configs.iter()) {
open_connection(&state, &config.id).await;
let capabilities = plugin_plan_capabilities(&state, &config.id)
.await
.unwrap_or_else(|error| panic!("capabilities should be available for {}: {error}", config.id));
assert_eq!(capabilities.db_type, *expected_db_type, "{}", config.id);
assert!(capabilities.supports.estimated_plan, "{}", config.id);
}
}
}
+186 -78
View File
@@ -42,7 +42,7 @@ pub struct ExplainSqlBuildResult {
/// Callers that hand a raw plan to a consumer which cannot inspect the SQL
/// (the plugin Host API) need to know how to decode it. Keep this in sync with
/// the generated statements in [`build_explain_sql`];
/// `estimated_plan_format_matches_generated_sql` guards the pairing.
/// `estimated_plan_strategy_matches_capability_sql_format_and_acquisition` guards the pairing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EstimatedPlanFormat {
@@ -64,6 +64,51 @@ impl EstimatedPlanFormat {
}
}
/// The execution contract used to acquire an estimated plan.
///
/// This describes the acquisition path only; connection and driver execution
/// remain owned by `dbx-core`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EstimatedPlanAcquisition {
/// Execute the SQL generated by `build_explain_sql`.
GeneratedSql,
/// Ask the connected driver's existing explain facility for a plan.
DriverNative,
/// Capture SHOWPLAN on a dedicated SQL Server client session.
SqlServerShowPlanSession,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EstimatedPlanSqlGeneration {
Postgres,
Mysql,
Plain,
Oracle,
SqlServer,
Json,
}
/// One complete Estimated Plan contract for a database dialect.
///
/// Capability, SQL generation, payload encoding and Plugin Host acquisition
/// all derive from this mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EstimatedPlanStrategy {
sql_generation: EstimatedPlanSqlGeneration,
acquisition: EstimatedPlanAcquisition,
format: EstimatedPlanFormat,
}
impl EstimatedPlanStrategy {
pub const fn acquisition(self) -> EstimatedPlanAcquisition {
self.acquisition
}
pub const fn format(self) -> EstimatedPlanFormat {
self.format
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DroppedFilePreviewSqlOptions {
@@ -73,9 +118,9 @@ pub struct DroppedFilePreviewSqlOptions {
}
pub fn build_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult {
if !supports_explain_plan(options.database_type) {
let Some(strategy) = estimated_plan_strategy(options.database_type) else {
return explain_err("unsupported");
}
};
let source = strip_trailing_semicolons(options.sql.trim());
if source.is_empty() {
@@ -96,33 +141,30 @@ pub fn build_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult {
return explain_err("unsafe");
}
let sql = match options.database_type {
// ANALYZE executes the statement; is_safe_explain_sql has already limited
// the source to SELECT/WITH/TABLE/VALUES. MongoDb shares the plain arm only.
Some(DatabaseType::Postgres) if options.analyze == Some(true) => {
let sql = match strategy.sql_generation {
// ANALYZE executes the statement; the safety gate above has already
// limited the source to the existing read-only subset.
EstimatedPlanSqlGeneration::Postgres if options.analyze == Some(true) => {
format!("EXPLAIN (ANALYZE, FORMAT JSON) {source}")
}
Some(DatabaseType::Postgres | DatabaseType::MongoDb) => {
format!("EXPLAIN (FORMAT JSON) {source}")
}
Some(DatabaseType::Dameng | DatabaseType::Questdb) => {
format!("EXPLAIN {source}")
}
Some(DatabaseType::Doris) => format!("EXPLAIN {source}"),
Some(DatabaseType::Oracle) => format!("EXPLAIN PLAN FOR {source}"),
EstimatedPlanSqlGeneration::Postgres => format!("EXPLAIN (FORMAT JSON) {source}"),
EstimatedPlanSqlGeneration::Plain => format!("EXPLAIN {source}"),
EstimatedPlanSqlGeneration::Oracle => format!("EXPLAIN PLAN FOR {source}"),
// STATISTICS XML returns the same ShowPlanXML document plus per-operator
// runtime counters, at the price of actually running the statement.
Some(DatabaseType::SqlServer) if options.analyze == Some(true) => {
EstimatedPlanSqlGeneration::SqlServer if options.analyze == Some(true) => {
format!("SET STATISTICS XML ON;\nGO\n{source}\nGO\nSET STATISTICS XML OFF;")
}
Some(DatabaseType::SqlServer) => {
EstimatedPlanSqlGeneration::SqlServer => {
format!("SET SHOWPLAN_XML ON;\nGO\n{source}\nGO\nSET SHOWPLAN_XML OFF;")
}
Some(DatabaseType::Mysql) if options.format == Some(ExplainFormat::Standard) => {
EstimatedPlanSqlGeneration::Mysql if options.format == Some(ExplainFormat::Standard) => {
// MySQL 8.0.32+ may otherwise inherit TREE or JSON from the session-level explain_format.
format!("EXPLAIN FORMAT=TRADITIONAL {source}")
}
_ => format!("EXPLAIN FORMAT=JSON {source}"),
EstimatedPlanSqlGeneration::Mysql | EstimatedPlanSqlGeneration::Json => {
format!("EXPLAIN FORMAT=JSON {source}")
}
};
ExplainSqlBuildResult { ok: true, sql: Some(sql), reason: None }
}
@@ -146,34 +188,51 @@ pub fn build_dropped_file_preview_sql(options: DroppedFilePreviewSqlOptions) ->
None
}
/// How a consumer should decode the estimated plan `build_explain_sql` asks
/// the server for. Dameng and Oracle plans reach DBX through the driver's
/// native plan text rather than a generated `EXPLAIN` statement, and every
/// agent plan is text.
pub fn estimated_plan_format(database_type: Option<DatabaseType>) -> EstimatedPlanFormat {
match database_type {
Some(DatabaseType::SqlServer) => EstimatedPlanFormat::Xml,
Some(DatabaseType::Dameng | DatabaseType::Questdb | DatabaseType::Doris | DatabaseType::Oracle) => {
EstimatedPlanFormat::Text
/// The estimated-plan contract for a database, if one is currently supported.
pub fn estimated_plan_strategy(database_type: Option<DatabaseType>) -> Option<EstimatedPlanStrategy> {
use EstimatedPlanAcquisition::{DriverNative, GeneratedSql, SqlServerShowPlanSession};
use EstimatedPlanFormat::{Json, Text, Xml};
use EstimatedPlanSqlGeneration::{Mysql, Oracle, Plain, Postgres, SqlServer};
let strategy = match database_type? {
DatabaseType::Mysql => EstimatedPlanStrategy { sql_generation: Mysql, acquisition: GeneratedSql, format: Json },
DatabaseType::Doris => EstimatedPlanStrategy { sql_generation: Plain, acquisition: GeneratedSql, format: Text },
DatabaseType::Postgres => {
EstimatedPlanStrategy { sql_generation: Postgres, acquisition: GeneratedSql, format: Json }
}
_ => EstimatedPlanFormat::Json,
}
DatabaseType::Questdb => {
EstimatedPlanStrategy { sql_generation: Plain, acquisition: GeneratedSql, format: Text }
}
DatabaseType::Dameng => {
EstimatedPlanStrategy { sql_generation: Plain, acquisition: DriverNative, format: Text }
}
DatabaseType::Oracle => {
EstimatedPlanStrategy { sql_generation: Oracle, acquisition: DriverNative, format: Text }
}
DatabaseType::OceanbaseOracle => EstimatedPlanStrategy {
sql_generation: EstimatedPlanSqlGeneration::Json,
acquisition: GeneratedSql,
format: Json,
},
DatabaseType::SqlServer => {
EstimatedPlanStrategy { sql_generation: SqlServer, acquisition: SqlServerShowPlanSession, format: Xml }
}
// The former MongoDB SQL-generation arm was unreachable behind the old
// capability gate; it remains unsupported and has no Estimated Plan strategy.
_ => return None,
};
Some(strategy)
}
/// Payload encoding defined by the estimated-plan strategy for this dialect.
/// Keep the historical JSON fallback for unsupported/unspecified dialects;
/// callers must check capability separately before using it.
pub fn estimated_plan_format(database_type: Option<DatabaseType>) -> EstimatedPlanFormat {
estimated_plan_strategy(database_type).map_or(EstimatedPlanFormat::Json, EstimatedPlanStrategy::format)
}
pub fn supports_explain_plan(database_type: Option<DatabaseType>) -> bool {
matches!(
database_type,
Some(
DatabaseType::Mysql
| DatabaseType::Doris
| DatabaseType::Postgres
| DatabaseType::Questdb
| DatabaseType::Dameng
| DatabaseType::Oracle
| DatabaseType::OceanbaseOracle
| DatabaseType::SqlServer
)
)
estimated_plan_strategy(database_type).is_some()
}
pub fn is_safe_explain_sql(sql: &str) -> bool {
@@ -1398,9 +1457,11 @@ mod tests {
for (db_type, expected) in [
(DatabaseType::MongoDb, "EXPLAIN (FORMAT JSON) SELECT 1"),
(DatabaseType::Mysql, "EXPLAIN FORMAT=JSON SELECT 1"),
(DatabaseType::Dameng, "EXPLAIN SELECT 1"),
(DatabaseType::Doris, "EXPLAIN SELECT 1"),
(DatabaseType::Questdb, "EXPLAIN SELECT 1"),
(DatabaseType::Dameng, "EXPLAIN SELECT 1"),
(DatabaseType::Oracle, "EXPLAIN PLAN FOR SELECT 1"),
(DatabaseType::OceanbaseOracle, "EXPLAIN FORMAT=JSON SELECT 1"),
] {
let analyzed = build_explain_sql(ExplainSqlOptions {
database_type: Some(db_type),
@@ -1416,8 +1477,8 @@ mod tests {
});
assert_eq!(analyzed, plain, "{db_type:?} must ignore the analyze flag");
// MongoDb is rejected earlier by supports_explain_plan, so it never
// reaches the match arm it nominally shares with Postgres.
// MongoDb is rejected by the strategy capability gate; its historical
// SQL-generation arm remains unreachable.
if db_type != DatabaseType::MongoDb {
assert_eq!(analyzed.sql, Some(expected.to_string()), "{db_type:?}");
} else {
@@ -1570,44 +1631,71 @@ mod tests {
}
#[test]
fn estimated_plan_format_matches_generated_sql() {
// Every dialect `supports_explain_plan` advertises must declare a payload
// format that the statement `build_explain_sql` generates really produces.
for database_type in
DatabaseType::ALL.iter().copied().filter(|database_type| supports_explain_plan(Some(*database_type)))
{
let explain_sql = build_explain_sql(ExplainSqlOptions {
fn estimated_plan_strategy_matches_capability_sql_format_and_acquisition() {
use EstimatedPlanAcquisition::{DriverNative, GeneratedSql, SqlServerShowPlanSession};
use EstimatedPlanFormat::{Json, Text, Xml};
let cases = [
(DatabaseType::Mysql, "EXPLAIN FORMAT=JSON SELECT 1", Json, GeneratedSql),
(DatabaseType::Doris, "EXPLAIN SELECT 1", Text, GeneratedSql),
(DatabaseType::Postgres, "EXPLAIN (FORMAT JSON) SELECT 1", Json, GeneratedSql),
(DatabaseType::Questdb, "EXPLAIN SELECT 1", Text, GeneratedSql),
(DatabaseType::Dameng, "EXPLAIN SELECT 1", Text, DriverNative),
(DatabaseType::Oracle, "EXPLAIN PLAN FOR SELECT 1", Text, DriverNative),
(DatabaseType::OceanbaseOracle, "EXPLAIN FORMAT=JSON SELECT 1", Json, GeneratedSql),
(
DatabaseType::SqlServer,
"SET SHOWPLAN_XML ON;\nGO\nSELECT 1\nGO\nSET SHOWPLAN_XML OFF;",
Xml,
SqlServerShowPlanSession,
),
];
for (database_type, expected_sql, expected_format, expected_acquisition) in cases {
let strategy = estimated_plan_strategy(Some(database_type))
.unwrap_or_else(|| panic!("{database_type:?} must have a complete Estimated Plan strategy"));
assert!(supports_explain_plan(Some(database_type)), "{database_type:?}");
assert_eq!(strategy.acquisition(), expected_acquisition, "{database_type:?}");
assert_eq!(strategy.format(), expected_format, "{database_type:?}");
assert_eq!(estimated_plan_format(Some(database_type)), expected_format, "{database_type:?}");
let result = build_explain_sql(ExplainSqlOptions {
database_type: Some(database_type),
format: None,
analyze: None,
sql: "SELECT 1".to_string(),
})
.sql
.expect("a supported dialect always builds EXPLAIN SQL");
match estimated_plan_format(Some(database_type)) {
EstimatedPlanFormat::Json => assert!(
explain_sql.contains("FORMAT JSON") || explain_sql.contains("FORMAT=JSON"),
"{database_type:?} declares a JSON plan but generated '{explain_sql}'"
),
EstimatedPlanFormat::Xml => assert!(
explain_sql.contains("SHOWPLAN_XML"),
"{database_type:?} declares an XML plan but generated '{explain_sql}'"
),
EstimatedPlanFormat::Text => assert!(
explain_sql.starts_with("EXPLAIN ") && !explain_sql.contains("FORMAT"),
"{database_type:?} declares a text plan but generated '{explain_sql}'"
),
}
});
assert!(result.ok, "{database_type:?}: {:?}", result.reason);
assert_eq!(result.sql.as_deref(), Some(expected_sql), "{database_type:?}");
}
assert_eq!(estimated_plan_format(None), EstimatedPlanFormat::Json);
assert_eq!(estimated_plan_format(Some(DatabaseType::Dameng)), EstimatedPlanFormat::Text);
assert_eq!(estimated_plan_format(Some(DatabaseType::Questdb)), EstimatedPlanFormat::Text);
assert_eq!(estimated_plan_format(Some(DatabaseType::Doris)), EstimatedPlanFormat::Text);
assert_eq!(estimated_plan_format(Some(DatabaseType::Oracle)), EstimatedPlanFormat::Text);
assert_eq!(estimated_plan_format(Some(DatabaseType::SqlServer)), EstimatedPlanFormat::Xml);
assert_eq!(estimated_plan_format(Some(DatabaseType::Mysql)), EstimatedPlanFormat::Json);
for database_type in [
None,
Some(DatabaseType::Redis),
Some(DatabaseType::MongoDb),
Some(DatabaseType::Gaussdb),
Some(DatabaseType::StarRocks),
] {
assert!(estimated_plan_strategy(database_type).is_none(), "{database_type:?}");
assert!(!supports_explain_plan(database_type), "{database_type:?}");
assert_eq!(
build_explain_sql(ExplainSqlOptions {
database_type,
format: None,
analyze: None,
sql: "SELECT 1".to_string(),
})
.reason
.as_deref(),
Some("unsupported"),
"{database_type:?}"
);
}
// Preserve the legacy fallback for callers that ask for a format without
// first checking support; it does not enable plan capability.
assert_eq!(estimated_plan_format(None), Json);
assert_eq!(estimated_plan_format(Some(DatabaseType::Redis)), Json);
}
#[test]
@@ -1697,6 +1785,26 @@ mod tests {
ExplainSqlBuildResult { ok: false, sql: None, reason: Some("unsafe".to_string()) }
);
for sql in [
"DROP TABLE users;",
"DELETE FROM users;",
"INSERT INTO users (id) VALUES (1);",
"UPDATE users SET name = 'x';",
"SELECT 1; DROP TABLE users;",
"CREATE INDEX idx ON users (id);",
] {
assert_eq!(
build_explain_sql(ExplainSqlOptions {
database_type: Some(DatabaseType::Mysql),
format: None,
analyze: None,
sql: sql.to_string(),
}),
ExplainSqlBuildResult { ok: false, sql: None, reason: Some("unsafe".to_string()) },
"{sql} must remain blocked by the explain safety gate"
);
}
assert_eq!(
build_explain_sql(ExplainSqlOptions {
database_type: Some(DatabaseType::Mysql),