refactor(plan): converge estimated plan capability and strategy

This commit is contained in:
jearhe authored and GitHub committed 2026-09-24 17:21:38 +08:00
1 parent c5eb0a7a4a
commit 78e717bea6
2 files changed
+270 -135

No files matched your search

+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),