Skip to content

Commit a3708e7

Browse files
committed
feat: automatically include EXPLAIN plan in AI analysis
Enhanced AI-powered query analysis by automatically including query execution plan (EXPLAIN) in the context sent to AI providers. This significantly improves the quality of AI recommendations as it can see actual execution details. Changes: 1. Updated AiAnalysisService::analyzeQuery(): - Added optional ExplainAnalysis parameter - Passes explain plan to buildQueryContext() for AI context 2. Updated AiCommand::analyze(): - Now executes EXPLAIN plan before sending to AI - Includes execution plan in AI context for better analysis - Gracefully handles EXPLAIN failures (continues without plan) 3. Updated AiCommand::query(): - Passes baseAnalysis (which includes EXPLAIN plan) to AI service - AI now receives both base analysis and execution plan 4. Updated SelectQueryBuilder::explainAiAdvice(): - Passes baseAnalysis to analyzeQuery() for better AI analysis 5. Updated MCP tools: - AnalyzeQueryTool: Gets EXPLAIN plan and includes it in AI context - ExplainPlanTool: Passes baseAnalysis to AI service Benefits: - AI can see actual execution plan (table scans, index usage, access types) - More accurate optimization recommendations - Better understanding of query performance bottlenecks - No user action required - works automatically for all AI analysis commands Both 'pdodb ai analyze' and 'pdodb ai query' commands now automatically include EXPLAIN plan in AI context, providing more accurate and actionable recommendations.
1 parent e380095 commit a3708e7

File tree

5 files changed

+52
-8
lines changed

5 files changed

+52
-8
lines changed

src/ai/AiAnalysisService.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use tommyknocker\pdodb\ai\providers\YandexProvider;
1414
use tommyknocker\pdodb\exceptions\QueryException;
1515
use tommyknocker\pdodb\PdoDb;
16+
use tommyknocker\pdodb\query\analysis\ExplainAnalysis;
1617

1718
/**
1819
* Service for AI-powered database analysis.
@@ -100,10 +101,11 @@ protected function createProvider(string $providerName): AiProviderInterface
100101
* @param string|null $tableName Table name
101102
* @param string|null $provider Provider name
102103
* @param array<string, mixed> $options Additional options
104+
* @param ExplainAnalysis|null $explainAnalysis Optional explain analysis to include in context
103105
*
104106
* @return string AI analysis
105107
*/
106-
public function analyzeQuery(string $sql, ?string $tableName = null, ?string $provider = null, array $options = []): string
108+
public function analyzeQuery(string $sql, ?string $tableName = null, ?string $provider = null, array $options = [], ?ExplainAnalysis $explainAnalysis = null): string
107109
{
108110
$aiProvider = $this->getProvider($provider);
109111

@@ -120,7 +122,7 @@ public function analyzeQuery(string $sql, ?string $tableName = null, ?string $pr
120122
$aiProvider->setTimeout((int)$options['timeout']);
121123
}
122124

123-
$context = $this->contextBuilder->buildQueryContext($sql, $tableName);
125+
$context = $this->contextBuilder->buildQueryContext($sql, $tableName, $explainAnalysis);
124126

125127
return $aiProvider->analyzeQuery($sql, $context);
126128
}

src/ai/mcp/tools/AnalyzeQueryTool.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use tommyknocker\pdodb\ai\AiAnalysisService;
88
use tommyknocker\pdodb\PdoDb;
9+
use tommyknocker\pdodb\query\analysis\ExplainAnalyzer;
910

1011
/**
1112
* MCP tool for analyzing SQL queries with AI.
@@ -62,7 +63,25 @@ public function execute(array $arguments): string|array
6263
$provider = $arguments['provider'] ?? null;
6364

6465
try {
65-
$analysis = $this->aiService->analyzeQuery($sql, $tableName, $provider);
66+
// Get EXPLAIN plan for better AI analysis
67+
$explainAnalysis = null;
68+
try {
69+
$connection = $this->db->connection;
70+
$dialect = $connection->getDialect();
71+
$pdo = $connection->getPdo();
72+
$explainResults = $dialect->executeExplain($pdo, $sql, []);
73+
$queryBuilder = $this->db->find();
74+
$reflection = new \ReflectionClass($queryBuilder);
75+
$executionEngineProperty = $reflection->getProperty('executionEngine');
76+
$executionEngineProperty->setAccessible(true);
77+
$executionEngine = $executionEngineProperty->getValue($queryBuilder);
78+
$analyzer = new ExplainAnalyzer($dialect, $executionEngine);
79+
$explainAnalysis = $analyzer->analyze($explainResults, $tableName);
80+
} catch (\Throwable $e) {
81+
// If EXPLAIN fails, continue without it
82+
}
83+
84+
$analysis = $this->aiService->analyzeQuery($sql, $tableName, $provider, [], $explainAnalysis);
6685

6786
return [
6887
'sql' => $sql,

src/ai/mcp/tools/ExplainPlanTool.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ public function execute(array $arguments): string|array
8080
$analyzer = new ExplainAnalyzer($dialect, $executionEngine);
8181
$baseAnalysis = $analyzer->analyze($explainResults, $tableName);
8282

83-
// Then get AI analysis
84-
$aiAnalysis = $this->aiService->analyzeQuery($sql, $tableName, $provider);
83+
// Then get AI analysis (pass baseAnalysis which includes EXPLAIN plan)
84+
$aiAnalysis = $this->aiService->analyzeQuery($sql, $tableName, $provider, [], $baseAnalysis);
8585
$aiProvider = $this->aiService->getProvider($provider);
8686
$model = $aiProvider->getModel();
8787

src/cli/commands/AiCommand.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ protected function analyze(): int
6363
$tableName = $this->getOption('table');
6464

6565
try {
66+
// Get EXPLAIN plan for better AI analysis
67+
static::info('Analyzing query execution plan...');
68+
$connection = $db->connection;
69+
$dialect = $connection->getDialect();
70+
$pdo = $connection->getPdo();
71+
72+
$explainAnalysis = null;
73+
try {
74+
$explainResults = $dialect->executeExplain($pdo, $sql, []);
75+
$queryBuilder = $db->find();
76+
$reflection = new \ReflectionClass($queryBuilder);
77+
$executionEngineProperty = $reflection->getProperty('executionEngine');
78+
$executionEngineProperty->setAccessible(true);
79+
$executionEngine = $executionEngineProperty->getValue($queryBuilder);
80+
$analyzer = new ExplainAnalyzer($dialect, $executionEngine);
81+
$explainAnalysis = $analyzer->analyze($explainResults, $tableName);
82+
} catch (\Throwable $e) {
83+
// If EXPLAIN fails, continue without it (e.g., invalid SQL or unsupported)
84+
static::info('Note: Could not get execution plan: ' . $e->getMessage());
85+
}
86+
6687
static::info('Initializing AI service...');
6788
$aiService = new AiAnalysisService($db);
6889
$actualProvider = $aiService->getProvider($provider);
@@ -72,7 +93,7 @@ protected function analyze(): int
7293
static::info("Model: {$model}");
7394
static::loading('Sending request to AI API');
7495

75-
$analysis = $aiService->analyzeQuery($sql, $tableName, $provider, $options);
96+
$analysis = $aiService->analyzeQuery($sql, $tableName, $provider, $options, $explainAnalysis);
7697

7798
if (getenv('PHPUNIT') === false) {
7899
echo "\r" . str_repeat(' ', 80) . "\r"; // Clear loading line
@@ -152,7 +173,8 @@ protected function query(): int
152173
static::info("Model: {$model}");
153174
static::loading('Sending request to AI API');
154175

155-
$aiAnalysis = $aiService->analyzeQuery($sql, $tableName, $provider, $options);
176+
// Pass baseAnalysis (which includes EXPLAIN plan) to AI for better analysis
177+
$aiAnalysis = $aiService->analyzeQuery($sql, $tableName, $provider, $options, $baseAnalysis);
156178

157179
if (getenv('PHPUNIT') === false) {
158180
echo "\r" . str_repeat(' ', 80) . "\r"; // Clear loading line

src/query/SelectQueryBuilder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,8 @@ public function explainAiAdvice(?string $tableName = null, ?string $provider = n
991991
$targetTable = $tableName ?? $this->table;
992992

993993
try {
994-
$aiAnalysis = $aiService->analyzeQuery($sqlData['sql'], $targetTable, $provider, $options);
994+
// Pass baseAnalysis (which includes EXPLAIN plan) to AI for better analysis
995+
$aiAnalysis = $aiService->analyzeQuery($sqlData['sql'], $targetTable, $provider, $options, $baseAnalysis);
995996
$aiProvider = $aiService->getProvider($provider);
996997
$model = $aiProvider->getModel();
997998
} catch (\Throwable $e) {

0 commit comments

Comments
 (0)